@@ -45,6 +45,70 @@ struct CargoMetadataTarget {
4545struct NativeBindingsManifest {
4646 #[ serde( default ) ]
4747 bindings : BTreeMap < String , String > ,
48+ /// Compact whole-boundary binding: one `[adapter.<Namespace>]` section binds
49+ /// many `Namespace.method` native functions to `<crate>::<method>` without a
50+ /// per-function line. Expands into `bindings` at load time, so every
51+ /// downstream consumer (lowering, VM shim, conformance checks) sees the same
52+ /// flat map — there is no separate adapter code path.
53+ #[ serde( default ) ]
54+ adapter : BTreeMap < String , AdapterBinding > ,
55+ }
56+
57+ #[ derive( Debug , Deserialize ) ]
58+ struct AdapterBinding {
59+ /// The Rust wrapper crate that owns every method in this boundary.
60+ #[ serde( rename = "crate" ) ]
61+ crate_name : String ,
62+ /// Method names declared under the namespace (the part after the dot). Each
63+ /// `m` binds `Namespace.m` to `<crate>::m`.
64+ #[ serde( default ) ]
65+ functions : Vec < String > ,
66+ /// Optional per-method Rust name overrides for `m` whose Rust function name
67+ /// differs from the RSScript method name (`Namespace.m -> <crate>::<rename[m]>`).
68+ #[ serde( default ) ]
69+ rename : BTreeMap < String , String > ,
70+ }
71+
72+ /// Flatten a binding manifest into the canonical `symbol -> rust target` map,
73+ /// expanding any compact `[adapter.*]` sections. Errors on malformed adapters and
74+ /// on a symbol produced by more than one source (explicit binding or adapter), so
75+ /// the binding surface stays unambiguous for review.
76+ fn flatten_native_bindings (
77+ manifest : NativeBindingsManifest ,
78+ ) -> Result < BTreeMap < String , String > , String > {
79+ let mut flat = manifest. bindings ;
80+ for ( namespace, adapter) in manifest. adapter {
81+ let crate_name = adapter. crate_name . trim ( ) ;
82+ if crate_name. is_empty ( ) {
83+ return Err ( format ! (
84+ "adapter `{namespace}` must set a non-empty `crate`."
85+ ) ) ;
86+ }
87+ if adapter. functions . is_empty ( ) {
88+ return Err ( format ! (
89+ "adapter `{namespace}` lists no `functions`; add the method names it binds or remove the section."
90+ ) ) ;
91+ }
92+ for method in & adapter. functions {
93+ let method = method. trim ( ) ;
94+ if method. is_empty ( ) {
95+ return Err ( format ! ( "adapter `{namespace}` has an empty function name." ) ) ;
96+ }
97+ let symbol = format ! ( "{namespace}.{method}" ) ;
98+ let rust_method = adapter
99+ . rename
100+ . get ( method)
101+ . map ( String :: as_str)
102+ . unwrap_or ( method) ;
103+ let target = format ! ( "{crate_name}::{rust_method}" ) ;
104+ if flat. insert ( symbol. clone ( ) , target) . is_some ( ) {
105+ return Err ( format ! (
106+ "native binding `{symbol}` is defined twice (by `[adapter.{namespace}]` and an explicit `[bindings]` entry, or duplicated)."
107+ ) ) ;
108+ }
109+ }
110+ }
111+ Ok ( flat)
48112}
49113
50114pub ( super ) fn package_native_rust_dependencies (
@@ -190,7 +254,7 @@ pub(super) fn package_native_bindings(
190254 . map_err ( |error| format ! ( "failed to read {}: {error}" , path. display( ) ) ) ?;
191255 let manifest: NativeBindingsManifest = toml:: from_str ( & source)
192256 . map_err ( |error| format ! ( "failed to parse {}: {error}" , path. display( ) ) ) ?;
193- Ok ( manifest. bindings )
257+ flatten_native_bindings ( manifest) . map_err ( |error| format ! ( "{}: {error}" , path . display ( ) ) )
194258}
195259
196260pub ( super ) fn native_binding_interface_sources (
@@ -1035,3 +1099,87 @@ pub(super) fn manifest_native_unsafe_boundary(manifest: &Manifest) -> bool {
10351099 . and_then ( |native| native. effective_unsafe_policy ( ) )
10361100 . is_some_and ( |policy| policy != "forbid" )
10371101}
1102+
1103+ #[ cfg( test) ]
1104+ mod adapter_binding_tests {
1105+ use super :: * ;
1106+
1107+ fn flatten ( toml_src : & str ) -> Result < BTreeMap < String , String > , String > {
1108+ let manifest: NativeBindingsManifest = toml:: from_str ( toml_src) . expect ( "manifest parses" ) ;
1109+ flatten_native_bindings ( manifest)
1110+ }
1111+
1112+ #[ test]
1113+ fn adapter_section_expands_to_per_method_bindings ( ) {
1114+ let flat = flatten (
1115+ "[adapter.Rayon]\n crate = \" rss_rayon_native\" \n functions = [\" sum_int\" , \" sort_int\" ]\n " ,
1116+ )
1117+ . expect ( "adapter expands" ) ;
1118+ assert_eq ! (
1119+ flat. get( "Rayon.sum_int" ) . map( String :: as_str) ,
1120+ Some ( "rss_rayon_native::sum_int" )
1121+ ) ;
1122+ assert_eq ! (
1123+ flat. get( "Rayon.sort_int" ) . map( String :: as_str) ,
1124+ Some ( "rss_rayon_native::sort_int" )
1125+ ) ;
1126+ assert_eq ! ( flat. len( ) , 2 ) ;
1127+ }
1128+
1129+ #[ test]
1130+ fn adapter_form_matches_the_explicit_form ( ) {
1131+ let compact = flatten (
1132+ "[adapter.Rayon]\n crate = \" rss_rayon_native\" \n functions = [\" sum_int\" , \" sort_int\" ]\n " ,
1133+ )
1134+ . unwrap ( ) ;
1135+ let explicit = flatten (
1136+ "[bindings]\n \" Rayon.sum_int\" = \" rss_rayon_native::sum_int\" \n \" Rayon.sort_int\" = \" rss_rayon_native::sort_int\" \n " ,
1137+ )
1138+ . unwrap ( ) ;
1139+ assert_eq ! ( compact, explicit) ;
1140+ }
1141+
1142+ #[ test]
1143+ fn rename_overrides_the_rust_method_name ( ) {
1144+ let flat = flatten (
1145+ "[adapter.File]\n crate = \" file_native\" \n functions = [\" open\" ]\n \n [adapter.File.rename]\n open = \" open_file\" \n " ,
1146+ )
1147+ . unwrap ( ) ;
1148+ assert_eq ! (
1149+ flat. get( "File.open" ) . map( String :: as_str) ,
1150+ Some ( "file_native::open_file" )
1151+ ) ;
1152+ }
1153+
1154+ #[ test]
1155+ fn explicit_bindings_and_adapters_compose ( ) {
1156+ let flat = flatten (
1157+ "[bindings]\n \" Extra.ping\" = \" extra::ping\" \n \n [adapter.File]\n crate = \" file_native\" \n functions = [\" open\" ]\n " ,
1158+ )
1159+ . unwrap ( ) ;
1160+ assert_eq ! ( flat. len( ) , 2 ) ;
1161+ assert ! ( flat. contains_key( "Extra.ping" ) ) ;
1162+ assert ! ( flat. contains_key( "File.open" ) ) ;
1163+ }
1164+
1165+ #[ test]
1166+ fn duplicate_symbol_across_adapter_and_bindings_errors ( ) {
1167+ let error = flatten (
1168+ "[bindings]\n \" File.open\" = \" file_native::open\" \n \n [adapter.File]\n crate = \" file_native\" \n functions = [\" open\" ]\n " ,
1169+ )
1170+ . expect_err ( "duplicate symbol should error" ) ;
1171+ assert ! ( error. contains( "File.open" ) , "error: {error}" ) ;
1172+ }
1173+
1174+ #[ test]
1175+ fn empty_crate_and_empty_functions_error ( ) {
1176+ assert ! (
1177+ flatten( "[adapter.File]\n crate = \" \" \n functions = [\" open\" ]\n " ) . is_err( ) ,
1178+ "empty crate must error"
1179+ ) ;
1180+ assert ! (
1181+ flatten( "[adapter.File]\n crate = \" file_native\" \n functions = []\n " ) . is_err( ) ,
1182+ "empty functions must error"
1183+ ) ;
1184+ }
1185+ }
0 commit comments