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/ /target/
__pycache__/
/tools/api-catalog/bin/ /tools/api-catalog/bin/
/tools/api-catalog/obj/ /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). implementation plan are in [RUSTREWRITE.md](RUSTREWRITE.md).
The authoritative compiled .NET surface is checked in as The authoritative compiled .NET surface is checked in as
[`api/public-api.json`](api/public-api.json); [`api/README.md`](api/README.md) [`api/public-api.json`](api/public-api.json). Every type and member has a
documents deterministic regeneration and the external types that must be reviewed Rust destination in [`api/RUST-TYPES.tsv`](api/RUST-TYPES.tsv) and
mapped to native Rust equivalents. [`api/RUST-MAPPING.tsv`](api/RUST-MAPPING.tsv); [`api/README.md`](api/README.md)
documents deterministic regeneration and validation.
```sh ```sh
cargo build --workspace cargo build --workspace
cargo test --workspace --no-run cargo test --workspace --no-run
python3 tools/generate_rust_mapping.py --check
``` ```
Running `cargo test --workspace` is intentionally red during the shim stage. 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. projects, including a SHA-256 digest.
- `api/public-api.json` is the authoritative compiled-metadata catalog for all - `api/public-api.json` is the authoritative compiled-metadata catalog for all
13 library assemblies after all six source generators run. It also records 13 library assemblies after all six source generators run. It also records
every external type referenced by a public signature as a pending Rust every external type referenced by a public signature.
mapping obligation. - `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 - `tests/upstream-tests.json` records every NUnit invocation and the SHA-256 of
its C# method body. its C# method body.
- `tests/PARITY.md` maps every NUnit invocation to its Rust test name. - `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; - 1,705 public C# type declarations produce compiling Rust type/trait shims;
- 12,195 public declaration lines are retained in the API ledger; - 12,195 public declaration lines are retained in the API ledger;
- compiled metadata records 3,066 public types and 30,789 public members with - compiled metadata records 3,066 public types and 30,789 public members with
matching independent ECMA-335 counts, plus 142 external signature types that matching independent ECMA-335 counts, with all 142 external signature types
still require explicit Rust mapping decisions; resolved to cross-platform Rust or project-owned boundary types;
- all 1,295 NUnit `[Test]`/`[TestCase]` invocations have compiling Rust test - all 1,295 NUnit `[Test]`/`[TestCase]` invocations have compiling Rust test
entries that intentionally panic with their source identity and body hash; entries that intentionally panic with their source identity and body hash;
- all nine sample/tool projects have compiling Rust binary targets; - 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 | | 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 | | 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]` 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 when servers may add values. Do not derive `Serialize`/`Deserialize` as a
substitute for the specified wire encoding: LLUDP, LLSD, MessagePack, asset 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. every LibreMetaverse API that uses them.
External entries are replacement obligations, not copied dependency APIs. 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 `RUST-TYPES.tsv` resolves every entry to Rust core/std, an adopted
crate where it preserves the contract, otherwise define a native MetaCrate cross-platform crate, or a native MetaCrate type. Third-party API objects use
type. The catalog deliberately does not enumerate members of SkiaSharp, project-owned boundary types so their eventual backend can be replaced without
SIPSorcery, MessagePack, CoreJ2K, or any other external assembly. 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: 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 duplicate documentation IDs, and writes the result only when both outputs are
byte-identical. The extractor is development-only and is absent from every byte-identical. The extractor is development-only and is absent from every
Cargo manifest and published Rust dependency graph. 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()