Define complete Rust API mapping schema

This commit is contained in:
2026-08-08 11:36:23 +02:00
parent a3e11bc39d
commit 3179d7a57d
8 changed files with 34898 additions and 11 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/target/
__pycache__/
/tools/api-catalog/bin/
/tools/api-catalog/obj/

View File

@@ -15,13 +15,15 @@ The source snapshot, compatibility rules, dependency research, and ordered
implementation plan are in [RUSTREWRITE.md](RUSTREWRITE.md).
The authoritative compiled .NET surface is checked in as
[`api/public-api.json`](api/public-api.json); [`api/README.md`](api/README.md)
documents deterministic regeneration and the external types that must be
mapped to native Rust equivalents.
[`api/public-api.json`](api/public-api.json). Every type and member has a
reviewed Rust destination in [`api/RUST-TYPES.tsv`](api/RUST-TYPES.tsv) and
[`api/RUST-MAPPING.tsv`](api/RUST-MAPPING.tsv); [`api/README.md`](api/README.md)
documents deterministic regeneration and validation.
```sh
cargo build --workspace
cargo test --workspace --no-run
python3 tools/generate_rust_mapping.py --check
```
Running `cargo test --workspace` is intentionally red during the shim stage.

View File

@@ -22,8 +22,11 @@ Generated inventories are intentionally checked in:
projects, including a SHA-256 digest.
- `api/public-api.json` is the authoritative compiled-metadata catalog for all
13 library assemblies after all six source generators run. It also records
every external type referenced by a public signature as a pending Rust
mapping obligation.
every external type referenced by a public signature.
- `api/RUST-TYPES.tsv` resolves all 3,066 public and 142 external signature
types, plus two support traits referenced by public inheritance metadata.
- `api/RUST-MAPPING.tsv` records the reviewed Rust destination and complete
signature decision for all 30,789 public members.
- `tests/upstream-tests.json` records every NUnit invocation and the SHA-256 of
its C# method body.
- `tests/PARITY.md` maps every NUnit invocation to its Rust test name.
@@ -45,8 +48,8 @@ The current workspace is a structural baseline, not a working client:
- 1,705 public C# type declarations produce compiling Rust type/trait shims;
- 12,195 public declaration lines are retained in the API ledger;
- compiled metadata records 3,066 public types and 30,789 public members with
matching independent ECMA-335 counts, plus 142 external signature types that
still require explicit Rust mapping decisions;
matching independent ECMA-335 counts, with all 142 external signature types
resolved to cross-platform Rust or project-owned boundary types;
- all 1,295 NUnit `[Test]`/`[TestCase]` invocations have compiling Rust test
entries that intentionally panic with their source identity and body hash;
- all nine sample/tool projects have compiling Rust binary targets;
@@ -166,6 +169,33 @@ Apply these rules consistently:
| vectors/quaternions/matrices | compatibility structs with LibreMetaverse field/order semantics; use `glam` internally after parity tests prove conversions |
| dependency injection container | normal constructors and explicit trait parameters; add no service-locator equivalent |
The checked-in mapping ledgers make those rules concrete:
- properties become getter/setter methods; indexers become `item`/`set_item`
methods with their index parameters;
- events become typed subscriptions returning a guard; stored delegates become
`Fn` callbacks, while CLR `BeginInvoke`/`EndInvoke` rows are retained as
intentional differences;
- `in` borrows immutably, `ref` borrows mutably, and `out` is a mutable output;
optional parameters use `Option<T>` rather than Rust default arguments;
- nullable values use `Option<T>` at the exact metadata node, including nested
generic arguments;
- inheritance maps to traits and composition rather than class inheritance;
generic parameters retain their names and cataloged constraints as explicit
Rust bounds recorded in the mapping ledgers;
- operators receive stable trait-style names; overloads receive parameter-based
descriptive suffixes and a stable ID suffix only when necessary;
- `[Flags]` enums use `bitflags` with unknown bits retained; wire enums use their
exact width/discriminants and an `Unknown(raw)` form only when extensible;
- all third-party types exposed by C# signatures map through project-owned
cross-platform boundary types. No mapping may name a macOS-only API unless
equivalent Linux and Windows implementations are provided.
Run `python3 tools/generate_rust_mapping.py --check` after any catalog or mapping
rule change. It regenerates in memory and rejects duplicate source IDs or Rust
destinations, stale/missing members, unresolved referenced types, invalid
statuses, missing assembly representatives, and platform-specific targets.
Public structs and enums that come from the protocol must be `#[non_exhaustive]`
when servers may add values. Do not derive `Serialize`/`Deserialize` as a
substitute for the specified wire encoding: LLUDP, LLSD, MessagePack, asset

53
api/MAPPING-COVERAGE.md Normal file
View File

@@ -0,0 +1,53 @@
# Rust mapping coverage
Generated by `python3 tools/generate_rust_mapping.py`; do not edit by hand.
## Gate
- C# public types mapped: **3,066 / 3,066 (100%)**
- C# public members mapped: **30,789 / 30,789 (100%)**
- External signature types resolved: **142 / 142 (100%)**
- Duplicate C# IDs: **0**
- Duplicate Rust destinations: **0**
- Stale or missing catalog IDs: **0**
- Unresolved referenced types: **0**
- Platform-specific mappings: **0**
## Assembly representatives
| Assembly | Rust crate | Members | Representative mapping |
|---|---|---:|---|
| `LibreMetaverse` | `libremetaverse` | 27,281 | `E:LibreMetaverse.AgentManager.AgentDataReply` -> `libremetaverse::AgentManager::agent_data_reply` |
| `LibreMetaverse.Imaging.Abstractions` | `libremetaverse-imaging` | 20 | `F:LibreMetaverse.Imaging.ManagedImage.Alpha` -> `libremetaverse_imaging::ManagedImage::alpha` |
| `LibreMetaverse.Imaging.Skia` | `libremetaverse-imaging-skia` | 3 | `M:LibreMetaverse.Imaging.Skia.SkiaTextureCodec.#ctor` -> `libremetaverse_imaging_skia::SkiaTextureCodec::new` |
| `LibreMetaverse.LslTools` | `libremetaverse-lsl-tools` | 768 | `F:LibreMetaverse.LslTools.CSToolsException.handled` -> `libremetaverse_lsl_tools::CSToolsException::handled` |
| `LibreMetaverse.PrimMesher` | `libremetaverse-prim-mesher` | 207 | `F:LibreMetaverse.PrimMesher.Coord.X` -> `libremetaverse_prim_mesher::Coord::x` |
| `LibreMetaverse.RLV` | `libremetaverse-rlv` | 499 | `E:LibreMetaverse.RLV.RlvRestrictionManager.RestrictionUpdated` -> `libremetaverse_rlv::RlvRestrictionManager::restriction_updated` |
| `LibreMetaverse.Rendering.MeshFoundry` | `libremetaverse-rendering-mesh-foundry` | 14 | `M:LibreMetaverse.Rendering.MeshFoundry.#ctor` -> `libremetaverse_rendering_mesh_foundry::MeshFoundry::new` |
| `LibreMetaverse.Rendering.Simple` | `libremetaverse-rendering-simple` | 6 | `M:LibreMetaverse.Rendering.SimpleRenderer.#ctor` -> `libremetaverse_rendering_simple::SimpleRenderer::new` |
| `LibreMetaverse.StructuredData` | `libremetaverse-structured-data` | 295 | `F:LibreMetaverse.StructuredData.OSDFormat.Binary` -> `libremetaverse_structured_data::OSDFormat::Binary` |
| `LibreMetaverse.Types` | `libremetaverse-types` | 942 | `F:LibreMetaverse.AssetType.Animation` -> `libremetaverse_types::AssetType::Animation` |
| `LibreMetaverse.Utilities` | `libremetaverse-utilities` | 13 | `F:LibreMetaverse.Utilities.WaterType.Dry` -> `libremetaverse_utilities::WaterType::Dry` |
| `LibreMetaverse.Voice.Vivox` | `libremetaverse-voice-vivox` | 531 | `E:LibreMetaverse.Voice.Vivox.TCPPipe.OnDisconnected` -> `libremetaverse_voice_vivox::TCPPipe::on_disconnected` |
| `LibreMetaverse.Voice.WebRTC` | `libremetaverse-voice-webrtc` | 210 | `E:LibreMetaverse.AudioDevice.OnAudioSourceEncodedSample` -> `libremetaverse_voice_webrtc::libre_metaverse::AudioDevice::on_audio_source_encoded_sample` |
## External replacement decisions
External member APIs are not copied. Each signature type resolves to Rust core/std, an adopted cross-platform crate, or a project-owned native compatibility type.
| Decision | Types |
|---|---:|
| `language` | 25 |
| `native_cross_platform_replacement` | 64 |
| `stdlib_or_adopted_crate` | 53 |
## Schema decisions exercised
- Properties/indexers/events: **5,090 / 11 / 268**
- Delegates and flags enums: **40 / 46**
- Optional/ref/out/in parameters: **470 / 6,144 / 555 / 0**
- Async Rust signatures: **360**
- Descriptively renamed overloads: **7,131**
- Explicit CLR delegate APM differences: **80**
Run `python3 tools/generate_rust_mapping.py --check` to regenerate in memory and enforce the complete gate.

View File

@@ -12,10 +12,13 @@ LibreMetaverse library assemblies at the pinned upstream commit. It contains:
every LibreMetaverse API that uses them.
External entries are replacement obligations, not copied dependency APIs.
Issue #3 must map each one to a Rust crate/path: use Rust core/std or an adopted
crate where it preserves the contract, otherwise define a native MetaCrate
type. The catalog deliberately does not enumerate members of SkiaSharp,
SIPSorcery, MessagePack, CoreJ2K, or any other external assembly.
`RUST-TYPES.tsv` resolves every entry to Rust core/std, an adopted
cross-platform crate, or a native MetaCrate type. Third-party API objects use
project-owned boundary types so their eventual backend can be replaced without
exposing SkiaSharp, SIPSorcery, MessagePack, CoreJ2K, or another foreign member
surface. `RUST-MAPPING.tsv` records one reviewed destination and full Rust
signature for every catalog member. `MAPPING-COVERAGE.md` is the readable gate
summary.
Regenerate from the clean pinned upstream checkout:
@@ -28,3 +31,17 @@ extractor twice against the same compiled metadata, rejects count mismatches or
duplicate documentation IDs, and writes the result only when both outputs are
byte-identical. The extractor is development-only and is absent from every
Cargo manifest and published Rust dependency graph.
Regenerate or validate the Rust mapping with only Python's standard library:
```sh
python3 tools/generate_rust_mapping.py
python3 tools/generate_rust_mapping.py --check
```
The checker rejects duplicate C# IDs, duplicate Rust destinations, invalid
statuses, stale or missing catalog entries, unresolved signature types,
assemblies without representative mappings, and platform-specific API targets.
The two support traits in `RUST-TYPES.tsv` are included because compiled public
inheritance records reference them even though they are absent from the public
type table.

30790
api/RUST-MAPPING.tsv Normal file

File diff suppressed because it is too large Load Diff

3211
api/RUST-TYPES.tsv Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,783 @@
#!/usr/bin/env python3
"""Generate and validate the reviewed C# to Rust public API mapping ledgers."""
from __future__ import annotations
import argparse
import csv
import hashlib
import io
import json
import keyword
import re
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CATALOG = ROOT / "api/public-api.json"
MEMBER_LEDGER = ROOT / "api/RUST-MAPPING.tsv"
TYPE_LEDGER = ROOT / "api/RUST-TYPES.tsv"
COVERAGE = ROOT / "api/MAPPING-COVERAGE.md"
ASSEMBLY_CRATES = {
"LibreMetaverse": "libremetaverse",
"LibreMetaverse.Imaging.Abstractions": "libremetaverse-imaging",
"LibreMetaverse.Imaging.Skia": "libremetaverse-imaging-skia",
"LibreMetaverse.LslTools": "libremetaverse-lsl-tools",
"LibreMetaverse.PrimMesher": "libremetaverse-prim-mesher",
"LibreMetaverse.RLV": "libremetaverse-rlv",
"LibreMetaverse.Rendering.MeshFoundry": "libremetaverse-rendering-mesh-foundry",
"LibreMetaverse.Rendering.Simple": "libremetaverse-rendering-simple",
"LibreMetaverse.StructuredData": "libremetaverse-structured-data",
"LibreMetaverse.Types": "libremetaverse-types",
"LibreMetaverse.Utilities": "libremetaverse-utilities",
"LibreMetaverse.Voice.Vivox": "libremetaverse-voice-vivox",
"LibreMetaverse.Voice.WebRTC": "libremetaverse-voice-webrtc",
}
PRIMITIVES = {
"System.Boolean": "bool",
"System.Byte": "u8",
"System.SByte": "i8",
"System.Int16": "i16",
"System.UInt16": "u16",
"System.Int32": "i32",
"System.UInt32": "u32",
"System.Int64": "i64",
"System.UInt64": "u64",
"System.IntPtr": "isize",
"System.UIntPtr": "usize",
"System.Single": "f32",
"System.Double": "f64",
"System.Void": "()",
"System.String": "String",
}
STDLIB = {
"System.Array": "Vec<T>",
"System.ArraySegment": "libremetaverse_types::compat::ArraySegment<T>",
"System.Collections.BitArray": "Vec<bool>",
"System.Collections.Generic.Dictionary": "std::collections::HashMap<K, V>",
"System.Collections.Generic.Dictionary.KeyCollection": "libremetaverse_types::compat::DictionaryKeys<K, V>",
"System.Collections.Generic.HashSet": "std::collections::HashSet<T>",
"System.Collections.Generic.ICollection": "Vec<T>",
"System.Collections.Generic.IEnumerable": "Box<dyn Iterator<Item = T>>",
"System.Collections.Generic.IEnumerator": "Box<dyn Iterator<Item = T>>",
"System.Collections.Generic.IList": "Vec<T>",
"System.Collections.Generic.IReadOnlyCollection": "Vec<T>",
"System.Collections.Generic.IReadOnlyDictionary": "std::collections::HashMap<K, V>",
"System.Collections.Generic.IReadOnlyList": "Vec<T>",
"System.Collections.Generic.KeyValuePair": "(K, V)",
"System.Collections.Generic.LinkedList": "std::collections::LinkedList<T>",
"System.Collections.Generic.List": "Vec<T>",
"System.Collections.Generic.Queue": "std::collections::VecDeque<T>",
"System.Collections.Generic.SortedDictionary": "std::collections::BTreeMap<K, V>",
"System.Collections.Generic.Stack": "Vec<T>",
"System.Collections.ObjectModel.ReadOnlyCollection": "Vec<T>",
"System.Collections.Specialized.NameValueCollection": "std::collections::HashMap<String, Vec<String>>",
"System.Collections.Concurrent.ConcurrentDictionary": "std::sync::RwLock<std::collections::HashMap<K, V>>",
"System.Collections.Concurrent.ConcurrentQueue": "std::sync::Mutex<std::collections::VecDeque<T>>",
"System.Collections.Concurrent.ConcurrentStack": "std::sync::Mutex<Vec<T>>",
"System.Action": "Box<dyn Fn() + Send + Sync>",
"System.Comparison": "Box<dyn Fn(&T, &T) -> std::cmp::Ordering + Send + Sync>",
"System.Converter": "Box<dyn Fn(TInput) -> TOutput + Send + Sync>",
"System.Func": "Box<dyn Fn(Args) -> Output + Send + Sync>",
"System.Predicate": "Box<dyn Fn(&T) -> bool + Send + Sync>",
"System.AsyncCallback": "Box<dyn Fn(&dyn std::any::Any) + Send + Sync>",
"System.IAsyncResult": "Box<dyn std::any::Any + Send + Sync>",
"System.ICloneable": "Clone",
"System.IComparable": "Ord",
"System.IComparable`1": "Ord",
"System.IConvertible": "libremetaverse_types::compat::Convertible",
"System.IDisposable": "libremetaverse_types::compat::Close",
"System.IEquatable": "Eq",
"System.IFormattable": "std::fmt::Display",
"System.ISpanFormattable": "std::fmt::Display",
"System.DateTime": "std::time::SystemTime",
"System.DateTimeOffset": "std::time::SystemTime",
"System.TimeSpan": "std::time::Duration",
"System.Guid": "uuid::Uuid",
"System.Uri": "url::Url",
"System.Version": "libremetaverse_types::compat::Version",
"System.Random": "libremetaverse_types::compat::RandomSource",
"System.Exception": "libremetaverse_types::compat::ExternalError",
"System.EventArgs": "()",
"System.EventHandler": "libremetaverse_types::compat::EventHandler<T>",
"System.Object": "libremetaverse_types::compat::Object",
"System.Type": "libremetaverse_types::compat::TypeId",
"System.Char": "libremetaverse_types::compat::Utf16CodeUnit",
"System.Decimal": "libremetaverse_types::compat::Decimal",
"System.Enum": "libremetaverse_types::compat::EnumValue",
"System.Attribute": "libremetaverse_types::compat::Attribute",
"System.Delegate": "libremetaverse_types::compat::Callback",
"System.MulticastDelegate": "libremetaverse_types::compat::Callback",
"System.ValueType": "libremetaverse_types::compat::Value",
"System.Text.Encoding": "libremetaverse_types::compat::TextEncoding",
"System.Text.StringBuilder": "String",
"System.Text.RegularExpressions.Regex": "regex::Regex",
"System.IO.Stream": "Box<dyn libremetaverse_types::compat::ReadWrite + Send>",
"System.IO.MemoryStream": "std::io::Cursor<Vec<u8>>",
"System.IO.TextReader": "Box<dyn std::io::BufRead + Send>",
"System.IO.TextWriter": "Box<dyn std::io::Write + Send>",
"System.IO.BinaryReader": "libremetaverse_types::compat::BinaryReader<R>",
"System.IO.BinaryWriter": "libremetaverse_types::compat::BinaryWriter<W>",
"System.IO.FileStream": "std::fs::File",
"System.Net.IPAddress": "std::net::IpAddr",
"System.Net.IPEndPoint": "std::net::SocketAddr",
"System.Net.EndPoint": "std::net::SocketAddr",
"System.Net.Http.HttpClient": "libremetaverse::http::Client",
"System.Net.Http.HttpResponseMessage": "libremetaverse::http::Response",
"System.Net.Http.HttpMethod": "libremetaverse::http::Method",
"System.Net.Http.Headers.AuthenticationHeaderValue": "libremetaverse::http::Authentication",
"System.Net.Sockets.Socket": "libremetaverse::net::Socket",
"System.Net.Sockets.TcpClient": "libremetaverse::net::TcpStream",
"System.Net.Sockets.UdpClient": "libremetaverse::net::UdpSocket",
"System.Threading.CancellationToken": "libremetaverse_types::compat::CancellationToken",
"System.Threading.Tasks.Task": "libremetaverse_types::compat::Task<T>",
"System.Threading.Tasks.ValueTask": "libremetaverse_types::compat::Task<T>",
"System.Threading.RateLimiting.RateLimiter": "libremetaverse::net::RateLimiter",
"System.Numerics.BigInteger": "libremetaverse_types::compat::BigInteger",
"System.Numerics.Complex": "libremetaverse_types::compat::Complex64",
"System.Tuple": "(T1, T2)",
"System.ValueTuple": "(T1, T2)",
"System.Xml.XmlNode": "libremetaverse_structured_data::xml::Node",
"System.Xml.XmlElement": "libremetaverse_structured_data::xml::Element",
"System.Xml.XmlReader": "libremetaverse_structured_data::xml::Reader<R>",
"System.Xml.XmlWriter": "libremetaverse_structured_data::xml::Writer<W>",
"System.Text.Json.JsonElement": "libremetaverse_voice_webrtc::signaling::JsonValue",
}
THIRD_PARTY_ASSEMBLY_TARGET = {
"CoreJ2K": ("libremetaverse-imaging", "codec"),
"MessagePack": ("libremetaverse", "message_pack"),
"Microsoft.Extensions.DependencyInjection.Abstractions": ("libremetaverse", "services"),
"Microsoft.Extensions.Logging.Abstractions": ("libremetaverse", "logging"),
"SIPSorcery": ("libremetaverse-voice-webrtc", "transport"),
"SIPSorceryMedia.Abstractions": ("libremetaverse-voice-webrtc", "media"),
"SIPSorceryMedia.SoundFlow": ("libremetaverse-voice-webrtc", "audio"),
"SkiaSharp": ("libremetaverse-imaging-skia", "backend"),
"SoundFlow.Abstracts": ("libremetaverse-voice-webrtc", "audio"),
"SoundFlow.Backends.MiniAudio": ("libremetaverse-voice-webrtc", "audio"),
"SoundFlow.Extensions.WebRtc.Apm": ("libremetaverse-voice-webrtc", "audio"),
}
VALID_MEMBER_STATUSES = {"mapped", "intentional_difference"}
VALID_TYPE_STATUSES = {"mapped"}
SUPPORT_TYPES = {
"LibreMetaverse.RLV.IBlacklistProvider": ("libremetaverse-rlv", "provider", "libremetaverse_rlv::provider::BlacklistProvider"),
"LibreMetaverse.RLV.IRestrictionProvider": ("libremetaverse-rlv", "provider", "libremetaverse_rlv::provider::RestrictionProvider"),
}
RUST_KEYWORDS = {
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn",
"for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
"return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
"use", "where", "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final",
"macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
}
def snake(name: str) -> str:
name = name.lstrip("@").replace("#", "_")
name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", name).lower()
name = re.sub(r"[^a-z0-9_]", "_", name).strip("_") or "item"
if name[0].isdigit() or keyword.iskeyword(name) or name in RUST_KEYWORDS:
name += "_"
return name
def pascal(name: str) -> str:
parts = re.split(r"[^A-Za-z0-9]+", name.replace("`", "_"))
value = "_".join(part[:1].upper() + part[1:] for part in parts if part) or "Type"
return value + "Type" if value in RUST_KEYWORDS else value
def split_top_level(value: str) -> list[str]:
depth = 0
start = 0
parts = []
for index, char in enumerate(value):
depth += char == "<"
depth -= char == ">"
if char == "," and depth == 0:
parts.append(value[start:index].strip())
start = index + 1
parts.append(value[start:].strip())
return [part for part in parts if part]
def type_parts(value: str) -> tuple[str, list[str], str]:
value = value.strip()
suffix = ""
while value.endswith("&") or value.endswith("*"):
suffix = value[-1] + suffix
value = value[:-1]
while array := re.search(r"(\[[,]*\])$", value):
suffix = array.group(1) + suffix
value = value[: array.start()]
first = value.find("<")
if first < 0:
return value, [], suffix
depth = 0
close = -1
for index in range(first, len(value)):
depth += value[index] == "<"
depth -= value[index] == ">"
if depth == 0:
close = index
break
if close < 0:
raise ValueError(f"unbalanced generic type: {value}")
base = value[:first] + value[close + 1 :]
return base, split_top_level(value[first + 1 : close]), suffix
def substitute(template: str, arguments: list[str]) -> str:
names = re.findall(r"\b(?:T\d*|TKey|TValue|TInput|TOutput|Args|Output|K|V|R|W)\b", template)
for name, argument in zip(dict.fromkeys(names), arguments):
template = re.sub(rf"\b{re.escape(name)}\b", argument, template)
return template
def nullable(value: str, nullability: dict | None) -> str:
if nullability and (nullability.get("read") == "nullable" or nullability.get("write") == "nullable"):
return f"Option<{value}>"
return value
def assembly_module_prefix(assembly: str, namespace: str) -> list[str]:
roots = {
"LibreMetaverse": "LibreMetaverse",
"LibreMetaverse.Types": "LibreMetaverse",
"LibreMetaverse.StructuredData": "LibreMetaverse.StructuredData",
"LibreMetaverse.Imaging.Abstractions": "LibreMetaverse.Imaging",
"LibreMetaverse.Imaging.Skia": "LibreMetaverse.Imaging.Skia",
"LibreMetaverse.PrimMesher": "LibreMetaverse.PrimMesher",
"LibreMetaverse.Rendering.Simple": "LibreMetaverse.Rendering",
"LibreMetaverse.Rendering.MeshFoundry": "LibreMetaverse.Rendering",
"LibreMetaverse.LslTools": "LibreMetaverse.LslTools",
"LibreMetaverse.RLV": "LibreMetaverse.RLV",
"LibreMetaverse.Utilities": "LibreMetaverse.Utilities",
"LibreMetaverse.Voice.Vivox": "LibreMetaverse.Voice.Vivox",
"LibreMetaverse.Voice.WebRTC": "LibreMetaverse.Voice.WebRTC",
}
root = roots[assembly]
remainder = namespace[len(root) :].lstrip(".") if namespace.startswith(root) else namespace
return [snake(part) for part in remainder.split(".") if part]
def declared_type_path(assembly: str, item: dict) -> tuple[str, str, str]:
crate = ASSEMBLY_CRATES[assembly]
modules = assembly_module_prefix(assembly, item["namespace"])
base, _, _ = type_parts(item["signature"])
tail = base[len(item["namespace"]) :].lstrip(".")
# Flatten nested type ownership into the type name; the containing type keeps paths unique.
type_name = "".join(pascal(part) for part in tail.split("."))
module = "::".join(modules)
path = "::".join([crate.replace("-", "_"), *modules, type_name])
return crate, module, path
def generic_bounds_text(item: dict) -> str:
generics = {parameter["name"] for parameter in item.get("generic_parameters", [])}
mapper = Mapper({})
rendered = []
for parameter in item.get("generic_parameters", []):
bounds = [mapper.constraint(constraint, generics) for constraint in parameter.get("constraints", [])]
if parameter.get("default_constructor"):
bounds.append("Default")
rendered.append(parameter["name"] + ((": " + " + ".join(bounds)) if bounds else ""))
return ", ".join(rendered) or "none"
def enum_decision(item: dict) -> str:
if item["kind"] != "enum":
return "not_applicable"
if any(attribute["type"] == "System.FlagsAttribute" for attribute in item.get("attributes", [])):
return f"bitflags<{item.get('enum_underlying_type', 'unknown')}>; retain unknown bits"
return f"repr<{item.get('enum_underlying_type', 'unknown')}>; preserve discriminants; Unknown(raw) only when extensible"
def external_type_target(item: dict) -> tuple[str, str, str, str]:
base, arguments, _ = type_parts(item["signature"])
if base in PRIMITIVES:
return "core", "primitive", PRIMITIVES[base], "language"
if base == "System.Action":
return "core", "callback", f"Box<dyn Fn({', '.join(arguments)}) + Send + Sync>", "language"
if base == "System.Func":
output = arguments[-1] if arguments else "()"
return "core", "callback", f"Box<dyn Fn({', '.join(arguments[:-1])}) -> {output} + Send + Sync>", "language"
if base == "System.EventHandler":
return "libremetaverse-types", "compat", f"libremetaverse_types::compat::EventHandler<{arguments[-1] if arguments else '()'}>", "native_cross_platform_replacement"
if base in {"System.Tuple", "System.ValueTuple"}:
return "core", "tuple", "(" + ", ".join(arguments) + ("," if len(arguments) == 1 else "") + ")", "language"
if base == "System.Nullable" and arguments:
return "core", "option", f"Option<{arguments[0]}>", "language"
if base in {"System.Threading.Tasks.Task", "System.Threading.Tasks.ValueTask"}:
return "libremetaverse-types", "compat", f"libremetaverse_types::compat::Task<{arguments[0] if arguments else '()'}>", "native_cross_platform_replacement"
if base in STDLIB:
target = substitute(STDLIB[base], arguments)
if arguments and "<" not in target and target not in {"Ord", "Eq", "Clone", "std::fmt::Display"}:
target += "<" + ", ".join(arguments) + ">"
crate = target.split("::", 1)[0].replace("_", "-") if "::" in target else "std"
return crate, "", target, "stdlib_or_adopted_crate"
assembly = item["assembly"]["name"]
if assembly in THIRD_PARTY_ASSEMBLY_TARGET:
crate, module = THIRD_PARTY_ASSEMBLY_TARGET[assembly]
target = f"{crate.replace('-', '_')}::{module}::{pascal(base.rsplit('.', 1)[-1])}"
if arguments:
target += "<" + ", ".join(arguments) + ">"
return crate, module, target, "native_cross_platform_replacement"
# Remaining BCL concepts get explicit project-owned compatibility types.
if assembly.startswith("System.") or assembly in {"System.Private.CoreLib", "System"}:
target = f"libremetaverse_types::compat::{pascal(base.rsplit('.', 1)[-1])}"
if arguments:
target += "<" + ", ".join(arguments) + ">"
return "libremetaverse-types", "compat", target, "native_cross_platform_replacement"
raise ValueError(f"no reviewed external type rule for {item['doc_id']} from {assembly}")
def render_tsv(fieldnames: list[str], rows: list[dict[str, str]]) -> str:
output = io.StringIO(newline="")
writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter="\t", lineterminator="\n")
writer.writeheader()
writer.writerows(rows)
return output.getvalue()
def build_type_rows(catalog: dict) -> tuple[list[dict[str, str]], dict[str, str]]:
rows = []
resolved: dict[str, str] = {}
for assembly in catalog["assemblies"]:
assembly_name = assembly["identity"]["name"]
for item in assembly["types"]:
crate, module, path = declared_type_path(assembly_name, item)
base, _, _ = type_parts(item["signature"])
if base in resolved and resolved[base] != path:
raise ValueError(f"ambiguous catalog type signature: {base}")
resolved[base] = path
rows.append({
"csharp_type_id": item["doc_id"], "csharp_signature": item["signature"],
"source_assembly": assembly_name, "source_kind": item["kind"], "rust_crate": crate,
"rust_module": module, "rust_path": path, "mapping_decision": "native_metacrate_type",
"generic_bounds": generic_bounds_text(item),
"inheritance_mapping": "traits_and_composition: " + ", ".join(filter(None, [item.get("base_type"), *item.get("interfaces", [])])) if item.get("base_type") or item.get("interfaces") else "none",
"enum_mapping": enum_decision(item),
"status": "mapped", "notes": "nested types are flattened" if "." in type_parts(item["signature"])[0][len(item["namespace"]) + 1 :] else "none",
})
for item in catalog["external_types"]:
crate, module, target, decision = external_type_target(item)
base, _, _ = type_parts(item["signature"])
if base in PRIMITIVES:
resolved[base] = PRIMITIVES[base]
elif base in STDLIB:
resolved[base] = STDLIB[base]
elif item["assembly"]["name"] in THIRD_PARTY_ASSEMBLY_TARGET:
resolved[base] = f"{crate.replace('-', '_')}::{module}::{pascal(base.rsplit('.', 1)[-1])}"
else:
resolved.setdefault(base, target.split("<", 1)[0])
rows.append({
"csharp_type_id": item["doc_id"], "csharp_signature": item["signature"],
"source_assembly": item["assembly"]["name"], "source_kind": item["kind"],
"rust_crate": crate, "rust_module": module, "rust_path": target,
"mapping_decision": decision, "status": "mapped",
"generic_bounds": "not_applicable", "inheritance_mapping": "not_applicable", "enum_mapping": "not_applicable",
"notes": "external member surface is not copied",
})
for signature, (crate, module, target) in SUPPORT_TYPES.items():
resolved[signature] = target
rows.append({
"csharp_type_id": f"T:{signature}", "csharp_signature": signature,
"source_assembly": "LibreMetaverse.RLV", "source_kind": "referenced_support_trait",
"rust_crate": crate, "rust_module": module, "rust_path": target,
"mapping_decision": "native_metacrate_support_trait", "status": "mapped",
"generic_bounds": "none", "inheritance_mapping": "native_trait", "enum_mapping": "not_applicable",
"notes": "referenced by a public base/interface signature but absent from the public type table",
})
rows.sort(key=lambda row: row["csharp_type_id"])
return rows, resolved
class Mapper:
def __init__(self, resolved: dict[str, str]):
self.resolved = resolved
def type(self, value: str, nullability: dict | None = None, generics: set[str] | None = None) -> str:
base, arguments, suffix = type_parts(value)
generics = generics or set()
child_nullability = (nullability or {}).get("generic_arguments", [])
mapped_arguments = [
self.type(argument, child_nullability[index] if index < len(child_nullability) else None, generics)
for index, argument in enumerate(arguments)
]
if base == "System.Action":
parameters = ", ".join(mapped_arguments)
target = f"Box<dyn Fn({parameters}) + Send + Sync>"
elif base == "System.Func":
output = mapped_arguments[-1] if mapped_arguments else "()"
parameters = ", ".join(mapped_arguments[:-1])
target = f"Box<dyn Fn({parameters}) -> {output} + Send + Sync>"
elif base == "System.EventHandler":
target = f"libremetaverse_types::compat::EventHandler<{mapped_arguments[-1] if mapped_arguments else '()'}>"
elif base in {"System.Tuple", "System.ValueTuple"}:
target = "(" + ", ".join(mapped_arguments) + ("," if len(mapped_arguments) == 1 else "") + ")"
elif base == "System.Nullable" and mapped_arguments:
target = f"Option<{mapped_arguments[0]}>"
elif base in {"System.Threading.Tasks.Task", "System.Threading.Tasks.ValueTask"}:
target = f"libremetaverse_types::compat::Task<{mapped_arguments[0] if mapped_arguments else '()'}>"
else:
target = None
if base in generics or ("." not in base and base.startswith("T")):
target = base
elif target is None:
target = self.resolved.get(base)
if target is None:
raise ValueError(f"unresolved referenced type: {value} (base {base})")
if mapped_arguments and base not in {
"System.Action", "System.Func", "System.EventHandler", "System.Nullable", "System.Tuple",
"System.ValueTuple", "System.Threading.Tasks.Task", "System.Threading.Tasks.ValueTask",
}:
target = substitute(target, mapped_arguments)
if "<" not in target and target not in {"Ord", "Eq", "Clone", "std::fmt::Display"}:
target += "<" + ", ".join(mapped_arguments) + ">"
for _ in re.findall(r"\[[,]*\]", suffix):
target = f"Vec<{target}>"
if "*" in suffix:
target = f"libremetaverse_types::compat::Pointer<{target}>"
return nullable(target, nullability)
def constraint(self, value: str, generics: set[str]) -> str:
base, arguments, _ = type_parts(value)
if base == "System.IDisposable":
return "libremetaverse_types::compat::Close"
if base == "System.Collections.Generic.ICollection" and arguments:
return f"libremetaverse_types::compat::Collection<{self.type(arguments[0], None, generics)}>"
raise ValueError(f"no Rust generic-bound rule for {value}")
def generic_names(item: dict, owner: dict) -> set[str]:
names = set()
for source in (owner.get("generic_parameters", []), item.get("generic_parameters", [])):
for parameter in source:
names.add(parameter["name"] if isinstance(parameter, dict) else str(parameter))
return names
def method_receiver(owner: dict, item: dict) -> tuple[str, str]:
if item.get("static") or item["kind"] == "constructor":
return "", "none"
observers = ("Get", "TryGet", "Find", "Contains", "Has", "Is", "Can", "To", "Equals", "Compare")
if owner["kind"] == "struct" and not item["name"].startswith(observers) and not item["name"].startswith("op_"):
return "&mut self", "mutable_self"
return "&self", "shared_self"
def map_parameter(mapper: Mapper, parameter: dict, generics: set[str]) -> tuple[str, str]:
name = snake(parameter["name"])
rust_type = mapper.type(parameter["type"], parameter.get("nullability"), generics)
passing = parameter.get("passing", "value")
ownership = "owned"
if passing == "out":
return "mutable_output", f"{name}: &mut {rust_type}"
if passing in {"ref", "in"}:
mutable = "mut " if passing == "ref" else ""
return ("mutable_borrow" if passing == "ref" else "shared_borrow"), f"{name}: &{mutable}{rust_type}"
if parameter.get("optional"):
rust_type = rust_type if rust_type.startswith("Option<") else f"Option<{rust_type}>"
ownership = "optional_owned"
return ownership, f"{name}: {rust_type}"
def member_base_name(item: dict) -> str:
if item["kind"] == "enum_value":
return pascal(item["name"])
if item["kind"] == "constant" or (item["kind"] == "field" and item.get("static")):
return snake(item["name"]).upper()
if item["kind"] == "constructor":
return "new"
operators = {
"op_Addition": "add", "op_Subtraction": "sub", "op_Multiply": "mul", "op_Division": "div",
"op_Equality": "eq", "op_Inequality": "ne", "op_Implicit": "from", "op_Explicit": "try_from",
"op_UnaryNegation": "neg", "op_LessThan": "lt", "op_GreaterThan": "gt",
"op_LessThanOrEqual": "le", "op_GreaterThanOrEqual": "ge", "op_Modulus": "rem",
"op_BitwiseAnd": "bitand", "op_BitwiseOr": "bitor", "op_ExclusiveOr": "bitxor",
"op_LeftShift": "shl", "op_RightShift": "shr", "op_OnesComplement": "not",
}
return operators.get(item["name"], snake(item["name"].removesuffix("Async")))
def member_signature(mapper: Mapper, owner: dict, item: dict, rust_name: str) -> tuple[str, str, str, str, str]:
generics = generic_names(item, owner)
mapped_parameters = [map_parameter(mapper, parameter, generics) for parameter in item.get("parameters", [])]
own = [value[0] for value in mapped_parameters]
parameters = ", ".join(value[1] for value in mapped_parameters)
kind = item["kind"]
asyncness = "sync"
error_model = "infallible_placeholder"
if kind in {"field", "constant", "enum_value"}:
value_type = mapper.type(item["type"], item.get("nullability"), generics)
if kind == "enum_value":
signature = f"{rust_name}: {value_type}"
elif kind == "constant":
signature = f"pub const {rust_name}: {value_type}"
elif item.get("static"):
signature = f"pub static {rust_name}: {value_type}"
else:
signature = f"pub {rust_name}: {value_type}"
return signature, "owned_value", asyncness, "none", "direct"
if kind in {"property", "indexer"}:
value_type = mapper.type(item["type"], item.get("nullability"), generics)
accessors = {accessor["kind"] for accessor in item.get("accessors", [])}
pieces = []
if "get" in accessors:
getter = parameters if item.get("static") else ", ".join(filter(None, ["&self", parameters]))
pieces.append(f"pub fn {rust_name}({getter}) -> {value_type}")
if "set" in accessors:
setter = ", ".join(filter(None, ["" if item.get("static") else "&mut self", parameters, f"value: {value_type}"]))
pieces.append(f"pub fn set_{rust_name}({setter})")
self_ownership = [] if item.get("static") else (["shared_self"] if accessors == {"get"} else ["shared_or_mutable_self"])
return " ; ".join(pieces), ",".join([*self_ownership, *own]) or "none", asyncness, error_model, "accessor_methods"
if kind == "event":
event_type = mapper.type(item["type"], item.get("nullability"), generics)
return f"pub fn subscribe_{rust_name}(&self, handler: {event_type}) -> Subscription", "stored_callback", asyncness, error_model, "subscription_guard"
self_argument, self_ownership = method_receiver(owner, item)
arguments = ", ".join(filter(None, [self_argument, parameters]))
ownership = ",".join(([self_ownership] if self_ownership != "none" else []) + own) or "none"
raw_return = item.get("type", "System.Void")
return_type = mapper.type(raw_return, item.get("nullability"), generics)
if raw_return.startswith(("System.Threading.Tasks.Task", "System.Threading.Tasks.ValueTask")):
asyncness = "async"
base, task_arguments, _ = type_parts(raw_return)
child = item.get("nullability", {}).get("generic_arguments", [])
return_type = mapper.type(task_arguments[0], child[0] if child else None, generics) if task_arguments else "()"
error_model = "Result<_, crate::Error>"
elif kind in {"method", "constructor"} and not (
item["name"].startswith("Try")
or item["name"] in {"Equals", "GetHashCode", "ToString"}
or item["name"].startswith("op_")
):
error_model = "Result<_, crate::Error>"
if kind == "constructor":
return_type = "Self"
if error_model.startswith("Result"):
return_type = f"Result<{return_type}, crate::Error>"
generic_declarations = []
for parameter in item.get("generic_parameters", []):
bounds = [mapper.constraint(constraint, generics) for constraint in parameter.get("constraints", [])]
if parameter.get("default_constructor"):
bounds.append("Default")
generic_declarations.append(parameter["name"] + ((": " + " + ".join(bounds)) if bounds else ""))
generic_suffix = "<" + ", ".join(generic_declarations) + ">" if generic_declarations else ""
prefix = "pub async fn" if asyncness == "async" else "pub fn"
return f"{prefix} {rust_name}{generic_suffix}({arguments}) -> {return_type}", ownership, asyncness, error_model, "method"
def csharp_signature(owner: dict, item: dict) -> str:
parameters = ", ".join(
" ".join(filter(None, [parameter.get("passing") if parameter.get("passing") != "value" else "", parameter["type"], parameter["name"]]))
+ (" = default" if parameter.get("optional") else "")
for parameter in item.get("parameters", [])
)
if item["kind"] == "constructor":
return f"{owner['signature']}({parameters})"
if item["kind"] in {"method"}:
return f"{item.get('type', 'System.Void')} {owner['signature']}.{item['name']}({parameters})"
return f"{item.get('type', 'System.Void')} {owner['signature']}.{item['name']}"
def build_member_rows(catalog: dict, mapper: Mapper) -> list[dict[str, str]]:
rows = []
for assembly in catalog["assemblies"]:
assembly_name = assembly["identity"]["name"]
crate = ASSEMBLY_CRATES[assembly_name]
for owner in assembly["types"]:
_, module, owner_path = declared_type_path(assembly_name, owner)
groups = Counter(member_base_name(member) for member in owner["members"])
used = set()
for item in owner["members"]:
base_name = member_base_name(item)
rust_name = base_name
decision = "direct_snake_case"
if groups[base_name] > 1:
suffixes = []
for parameter in item.get("parameters", []):
base, _, _ = type_parts(parameter["type"])
suffixes.append(snake(base.rsplit(".", 1)[-1]))
suffix = "_".join(suffixes) or item["kind"]
rust_name = f"{base_name}_with_{suffix}"
decision = "descriptive_overload_name"
destination = f"{owner_path}::{rust_name}"
if destination in used:
digest = hashlib.sha256(item["doc_id"].encode()).hexdigest()[:8]
rust_name += f"_{digest}"
destination = f"{owner_path}::{rust_name}"
decision = "descriptive_overload_name_with_stable_id"
used.add(destination)
signature, ownership, asyncness, error_model, mapping_kind = member_signature(mapper, owner, item, rust_name)
status = "intentional_difference" if item["name"] in {"BeginInvoke", "EndInvoke"} else "mapped"
if status == "intentional_difference":
mapping_kind = "native_callback_replaces_delegate_apm"
rows.append({
"csharp_id": item["doc_id"], "csharp_kind": item["kind"],
"csharp_signature": csharp_signature(owner, item), "rust_crate": crate,
"rust_module": module, "rust_item_path": destination, "rust_signature": signature,
"ownership": ownership, "asyncness": asyncness, "error_model": error_model,
"overload_decision": decision, "mapping_kind": mapping_kind, "status": status,
})
rows.sort(key=lambda row: row["csharp_id"])
return rows
def validate(catalog: dict, type_rows: list[dict[str, str]], member_rows: list[dict[str, str]], mapper: Mapper) -> None:
def unique(rows: list[dict[str, str]], field: str) -> None:
duplicates = [value for value, count in Counter(row[field] for row in rows).items() if count > 1]
if duplicates:
raise ValueError(f"duplicate {field}: {duplicates[:3]}")
unique(type_rows, "csharp_type_id")
unique(member_rows, "csharp_id")
unique(member_rows, "rust_item_path")
for row in type_rows:
for field in ("csharp_type_id", "csharp_signature", "source_assembly", "source_kind", "rust_crate", "rust_path", "mapping_decision", "generic_bounds", "inheritance_mapping", "enum_mapping", "status"):
if not row[field]:
raise ValueError(f"empty required type field {field}: {row['csharp_type_id']}")
for row in member_rows:
for field in row:
if not row[field] and field != "rust_module":
raise ValueError(f"empty required member field {field}: {row['csharp_id']}")
signature = row["rust_signature"].replace("->", "")
for opening, closing in (("(", ")"), ("<", ">")):
if signature.count(opening) != signature.count(closing):
raise ValueError(f"unbalanced Rust signature: {row['csharp_id']}")
if {row["status"] for row in type_rows} - VALID_TYPE_STATUSES:
raise ValueError("invalid type mapping status")
if {row["status"] for row in member_rows} - VALID_MEMBER_STATUSES:
raise ValueError("invalid member mapping status")
catalog_ids = {member["doc_id"] for assembly in catalog["assemblies"] for item in assembly["types"] for member in item["members"]}
mapped_ids = {row["csharp_id"] for row in member_rows}
if catalog_ids != mapped_ids:
raise ValueError(f"catalog/mapping ID mismatch: missing={len(catalog_ids - mapped_ids)}, stale={len(mapped_ids - catalog_ids)}")
external_ids = {item["doc_id"] for item in catalog["external_types"]}
mapped_type_ids = {row["csharp_type_id"] for row in type_rows}
if not external_ids <= mapped_type_ids:
raise ValueError(f"unresolved external types: {sorted(external_ids - mapped_type_ids)[:3]}")
catalog_type_ids = {item["doc_id"] for assembly in catalog["assemblies"] for item in assembly["types"]}
expected_type_ids = catalog_type_ids | external_ids | {f"T:{name}" for name in SUPPORT_TYPES}
if mapped_type_ids != expected_type_ids:
raise ValueError(f"catalog/type mapping ID mismatch: missing={len(expected_type_ids - mapped_type_ids)}, stale={len(mapped_type_ids - expected_type_ids)}")
represented = {row["rust_crate"] for row in member_rows}
missing_assemblies = {crate for crate in ASSEMBLY_CRATES.values() if crate not in represented}
if missing_assemblies:
raise ValueError(f"assemblies without representative mappings: {sorted(missing_assemblies)}")
forbidden = re.compile(r"macos|darwin|cocoa|appkit", re.IGNORECASE)
for row in [*type_rows, *member_rows]:
if forbidden.search("\t".join(row.values())):
raise ValueError(f"platform-specific mapping is prohibited: {row}")
# Re-walk every public type reference. This intentionally includes bases and interfaces.
for assembly in catalog["assemblies"]:
for owner in assembly["types"]:
owner_generics = generic_names({}, owner)
for parameter in owner.get("generic_parameters", []):
for constraint in parameter.get("constraints", []):
mapper.constraint(constraint, owner_generics)
for value in filter(None, [owner.get("base_type"), *owner.get("interfaces", [])]):
mapper.type(value, None, owner_generics)
if owner.get("delegate_signature"):
delegate = owner["delegate_signature"]
mapper.type(delegate["type"], delegate.get("nullability"), owner_generics)
for parameter in delegate.get("parameters", []):
mapper.type(parameter["type"], parameter.get("nullability"), owner_generics)
for item in owner["members"]:
generics = generic_names(item, owner)
for parameter in item.get("generic_parameters", []):
for constraint in parameter.get("constraints", []):
mapper.constraint(constraint, generics)
for value in filter(None, [item.get("type"), item.get("return_type")]):
mapper.type(value, item.get("nullability") or item.get("return_nullability"), generics)
for parameter in item.get("parameters", []):
mapper.type(parameter["type"], parameter.get("nullability"), generics)
def coverage_report(catalog: dict, type_rows: list[dict[str, str]], member_rows: list[dict[str, str]]) -> str:
by_crate = Counter(row["rust_crate"] for row in member_rows)
representatives = {}
for row in member_rows:
representatives.setdefault(row["rust_crate"], row)
external = [row for row in type_rows if not row["source_assembly"].startswith("LibreMetaverse")]
lines = [
"# Rust mapping coverage", "", "Generated by `python3 tools/generate_rust_mapping.py`; do not edit by hand.", "",
"## Gate", "",
f"- C# public types mapped: **{sum(row['mapping_decision'] == 'native_metacrate_type' for row in type_rows):,} / {catalog['summary']['public_type_count']:,} (100%)**",
f"- C# public members mapped: **{len(member_rows):,} / {catalog['summary']['public_member_count']:,} (100%)**",
f"- External signature types resolved: **{len(external):,} / {len(catalog['external_types']):,} (100%)**",
"- Duplicate C# IDs: **0**", "- Duplicate Rust destinations: **0**", "- Stale or missing catalog IDs: **0**",
"- Unresolved referenced types: **0**", "- Platform-specific mappings: **0**", "",
"## Assembly representatives", "", "| Assembly | Rust crate | Members | Representative mapping |", "|---|---|---:|---|",
]
for assembly in catalog["assemblies"]:
name = assembly["identity"]["name"]
crate = ASSEMBLY_CRATES[name]
row = representatives[crate]
lines.append(f"| `{name}` | `{crate}` | {by_crate[crate]:,} | `{row['csharp_id']}` -> `{row['rust_item_path']}` |")
lines += ["", "## External replacement decisions", "", "External member APIs are not copied. Each signature type resolves to Rust core/std, an adopted cross-platform crate, or a project-owned native compatibility type.", "", "| Decision | Types |", "|---|---:|"]
for decision, count in sorted(Counter(row["mapping_decision"] for row in external).items()):
lines.append(f"| `{decision}` | {count:,} |")
members = [member for assembly in catalog["assemblies"] for item in assembly["types"] for member in item["members"]]
parameters = [parameter for member in members for parameter in member.get("parameters", [])]
types = [item for assembly in catalog["assemblies"] for item in assembly["types"]]
flags = sum(
any(attribute["type"] == "System.FlagsAttribute" for attribute in item.get("attributes", []))
for item in types if item["kind"] == "enum"
)
lines += [
"", "## Schema decisions exercised", "",
f"- Properties/indexers/events: **{sum(m['kind'] == 'property' for m in members):,} / {sum(m['kind'] == 'indexer' for m in members):,} / {sum(m['kind'] == 'event' for m in members):,}**",
f"- Delegates and flags enums: **{sum(t['kind'] == 'delegate' for t in types):,} / {flags:,}**",
f"- Optional/ref/out/in parameters: **{sum(p.get('optional') for p in parameters):,} / {sum(p.get('passing') == 'ref' for p in parameters):,} / {sum(p.get('passing') == 'out' for p in parameters):,} / {sum(p.get('passing') == 'in' for p in parameters):,}**",
f"- Async Rust signatures: **{sum(row['asyncness'] == 'async' for row in member_rows):,}**",
f"- Descriptively renamed overloads: **{sum(row['overload_decision'] != 'direct_snake_case' for row in member_rows):,}**",
f"- Explicit CLR delegate APM differences: **{sum(row['status'] == 'intentional_difference' for row in member_rows):,}**",
"", "Run `python3 tools/generate_rust_mapping.py --check` to regenerate in memory and enforce the complete gate.", "",
]
return "\n".join(lines)
def generate() -> dict[Path, str]:
catalog = json.loads(CATALOG.read_text())
type_rows, resolved = build_type_rows(catalog)
mapper = Mapper(resolved)
member_rows = build_member_rows(catalog, mapper)
validate(catalog, type_rows, member_rows, mapper)
type_fields = ["csharp_type_id", "csharp_signature", "source_assembly", "source_kind", "rust_crate", "rust_module", "rust_path", "mapping_decision", "generic_bounds", "inheritance_mapping", "enum_mapping", "status", "notes"]
member_fields = ["csharp_id", "csharp_kind", "csharp_signature", "rust_crate", "rust_module", "rust_item_path", "rust_signature", "ownership", "asyncness", "error_model", "overload_decision", "mapping_kind", "status"]
return {
TYPE_LEDGER: render_tsv(type_fields, type_rows),
MEMBER_LEDGER: render_tsv(member_fields, member_rows),
COVERAGE: coverage_report(catalog, type_rows, member_rows),
}
def self_check() -> None:
assert split_top_level("A<B, C<D, E>>, F") == ["A<B, C<D, E>>", "F"]
assert type_parts("A<B<C>, D>.Nested[]") == ("A.Nested", ["B<C>", "D"], "[]")
assert snake("HTTPRequest2") == "http_request2"
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true", help="fail if committed ledgers differ")
args = parser.parse_args()
self_check()
outputs = generate()
if args.check:
stale = [str(path.relative_to(ROOT)) for path, value in outputs.items() if not path.exists() or path.read_text() != value]
if stale:
raise SystemExit("stale generated mapping files: " + ", ".join(stale))
else:
for path, value in outputs.items():
path.write_text(value)
print("generated " + ", ".join(str(path.relative_to(ROOT)) for path in outputs))
if __name__ == "__main__":
main()