Skip to main content

hara_native/core/
registry.rs

1pub fn completion_symbols() -> &'static [&'static str] {
2    fiber::completion_symbols()
3}
4
5/// Closed accounting inventory for evaluator/compiler forms. These are not a
6/// native type and do not create Vars in a `std.native.Builtins` namespace.
7#[cfg_attr(not(test), allow(dead_code))]
8pub(crate) const LANGUAGE_BUILTINS: &[(&str, &[&str])] = &[
9    (
10        "evaluation",
11        &[
12            "quote",
13            "syntax-quote",
14            "do",
15            "if",
16            "let",
17            "letfn",
18            "binding",
19            "loop",
20            "recur",
21            "throw",
22            "try",
23            "fn",
24        ],
25    ),
26    (
27        "definitions",
28        &[
29            "def",
30            "declare",
31            "var",
32            "set!",
33            "defmacro",
34        ],
35    ),
36    ("namespaces", &["ns", "ns+", "require", "alias"]),
37    ("interop", &["new", "field", "."]),
38];
39
40pub(crate) fn invoke_function_sync(
41    function: Rc<Function>,
42    arguments: Vec<Value>,
43) -> Result<Value, String> {
44    fiber::invoke_function_sync(function, arguments)
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct ExtensionValue {
49    pub provider: String,
50    pub type_name: String,
51    pub handle: u64,
52}
53
54/// One field in a named value declaration. The runtime only needs the name
55/// for storage, while the source declaration keeps the schema and optional
56/// field properties beside it until the type Var is published.
57#[derive(Debug, Clone, PartialEq)]
58pub struct NamedField {
59    pub name: String,
60    pub properties: Option<Form>,
61    pub schema: Form,
62}
63
64impl NamedField {
65    pub(crate) fn from_form(form: &Form, kind: &str) -> Result<Self, String> {
66        let Form::Vector(parts) = form else {
67            return Err(format!("{kind} fields must be symbols or [name schema] vectors"));
68        };
69        let (name, properties, schema) = match parts.as_slice() {
70            [Form::Symbol(name), schema] => (name, None, schema),
71            [Form::Symbol(name), Form::Map(properties), schema] => {
72                (name, Some(Form::Map(properties.clone())), schema)
73            }
74            _ => {
75                return Err(format!(
76                    "{kind} fields must be [name schema] or [name properties schema]"
77                ))
78            }
79        };
80        if name.is_empty() || name.contains('/') {
81            return Err(format!("{kind} field names must be unqualified symbols"));
82        }
83        Ok(Self {
84            name: name.clone(),
85            properties,
86            schema: schema.clone(),
87        })
88    }
89
90    pub(crate) fn legacy(name: &str) -> Self {
91        Self {
92            name: name.to_owned(),
93            properties: None,
94            schema: Form::Keyword("any".into()),
95        }
96    }
97
98    pub(crate) fn from_value(value: &Value, kind: &str) -> Result<Self, String> {
99        match value {
100            Value::String(name) if !name.is_empty() && !name.contains('/') => {
101                Ok(Self::legacy(name))
102            }
103            Value::Vector(_) | Value::Tuple(_) => {
104                let form = value_to_form(value)?;
105                Self::from_form(&form, kind)
106            }
107            _ => Err(format!(
108                "{kind} fields must contain field names or field specification vectors"
109            )),
110        }
111    }
112
113    pub(crate) fn schema_form(&self) -> Form {
114        let mut parts = vec![Form::Keyword(self.name.clone())];
115        if let Some(properties) = &self.properties {
116            parts.push(properties.clone());
117        }
118        parts.push(self.schema.clone());
119        Form::Vector(parts)
120    }
121}
122
123/// Canonical declaration data shared by a named type, its constructors, and
124/// the schema exposed through the type Var.  The runtime keeps this beside
125/// the type object rather than registering a second schema-owned identity.
126#[derive(Debug, Clone, PartialEq)]
127pub struct NamedDeclaration {
128    pub name: String,
129    pub mutable: bool,
130    pub fields: Vec<NamedField>,
131    pub schema: Form,
132    pub positional_constructor: String,
133    pub map_constructor: String,
134}
135
136impl NamedDeclaration {
137    pub(crate) fn new(name: String, mutable: bool, fields: Vec<NamedField>, schema: Form) -> Self {
138        let local_name = name.rsplit('/').next().unwrap_or(&name).to_owned();
139        Self {
140            name,
141            mutable,
142            fields,
143            schema,
144            positional_constructor: format!("->{local_name}"),
145            map_constructor: format!("map->{local_name}"),
146        }
147    }
148}
149
150pub(crate) fn named_value_schema_form(
151    type_name: &str,
152    mutable: bool,
153    fields: &[NamedField],
154) -> Form {
155    let mut parts = vec![Form::Keyword("struct".into())];
156    if mutable {
157        parts.push(Form::Map(vec![(
158            Form::Keyword("mutable?".into()),
159            Form::Bool(true),
160        )]));
161    }
162    parts.push(Form::List(vec![
163        Form::Symbol("var".into()),
164        Form::Symbol(type_name.to_owned()),
165    ]));
166    parts.extend(fields.iter().map(NamedField::schema_form));
167    Form::Vector(parts)
168}
169
170#[derive(Debug, Clone)]
171pub struct StructType {
172    pub name: String,
173    pub fields: Vec<String>,
174    pub declaration: Option<Rc<NamedDeclaration>>,
175}
176
177impl StructType {
178    pub(crate) fn detached(name: String, fields: Vec<String>) -> Self {
179        Self {
180            name,
181            fields,
182            declaration: None,
183        }
184    }
185}
186
187#[derive(Debug, Clone)]
188pub struct MutableType {
189    pub name: String,
190    pub fields: Vec<String>,
191    pub declaration: Option<Rc<NamedDeclaration>>,
192}
193
194impl MutableType {
195    #[cfg(test)]
196    pub(crate) fn detached(name: String, fields: Vec<String>) -> Self {
197        Self {
198            name,
199            fields,
200            declaration: None,
201        }
202    }
203}
204
205#[derive(Debug, Clone)]
206pub struct StructValue {
207    pub ty: Rc<StructType>,
208    pub values: POrderedMap<Value, Value>,
209    pub metadata: Option<Rc<Metadata>>,
210}
211
212#[derive(Debug, Clone)]
213pub struct MutableValue {
214    pub ty: Rc<MutableType>,
215    pub values: Rc<RefCell<Vec<Value>>>,
216    pub metadata: Option<Rc<Metadata>>,
217}
218
219#[derive(Debug, Clone)]
220pub struct GuestProtocol {
221    pub name: String,
222    pub methods: HashMap<String, usize>,
223    pub parents: Vec<String>,
224}
225
226#[derive(Debug, Clone)]
227pub struct NativeType {
228    pub name: String,
229    pub methods: Vec<String>,
230    pub availability: NativeAvailability,
231    pub capability: Option<String>,
232    pub metadata: Option<Rc<Metadata>>,
233}
234
235#[derive(Debug, Clone)]
236pub struct RuntimeSchema {
237    pub form: Form,
238    pub ast: crate::kernel::SchemaType,
239    pub origin: Option<KernelVar<Value>>,
240}
241
242#[derive(Clone)]
243struct PackageCatalogEntry {
244    descriptor: Value,
245    name: Option<String>,
246    namespaces: Vec<String>,
247    root: Option<std::path::PathBuf>,
248    state: String,
249    pending: Option<Promise>,
250}
251
252#[derive(Clone, Default)]
253pub struct PackageCatalog {
254    entries: Rc<RefCell<HashMap<String, PackageCatalogEntry>>>,
255}
256
257impl PackageCatalog {
258    pub fn register(
259        &self,
260        coordinate: String,
261        name: Option<String>,
262        descriptor: Value,
263        namespaces: Vec<String>,
264        root: Option<std::path::PathBuf>,
265    ) {
266        self.entries.borrow_mut().insert(
267            coordinate,
268            PackageCatalogEntry {
269                descriptor,
270                name,
271                namespaces,
272                root,
273                state: "available".into(),
274                pending: None,
275            },
276        );
277    }
278
279    fn catalog_value(&self) -> Value {
280        let mut entries = self
281            .entries
282            .borrow()
283            .iter()
284            .map(|(coordinate, entry)| {
285                (
286                    Value::String(coordinate.clone()),
287                    package_descriptor_state(&entry.descriptor, &entry.state),
288                )
289            })
290            .collect::<Vec<_>>();
291        entries.sort_by(|(left, _), (right, _)| left.display().cmp(&right.display()));
292        Value::OrderedMap(Box::new(POrderedMap::from_iter(entries)))
293    }
294
295    fn find(&self, target: &str) -> Option<(String, Value)> {
296        self.entries
297            .borrow()
298            .iter()
299            .find_map(|(coordinate, entry)| {
300                (coordinate == target
301                    || entry.name.as_deref() == Some(target)
302                    || entry.namespaces.iter().any(|namespace| namespace == target))
303                .then(|| {
304                    (
305                        coordinate.clone(),
306                        package_descriptor_state(&entry.descriptor, &entry.state),
307                    )
308                })
309            })
310    }
311
312    pub fn contains_namespace(&self, namespace: &str) -> bool {
313        self.entries
314            .borrow()
315            .values()
316            .any(|entry| entry.namespaces.iter().any(|name| name == namespace))
317    }
318
319    fn coordinate_for_namespace(&self, namespace: &str) -> Option<String> {
320        self.entries
321            .borrow()
322            .iter()
323            .find_map(|(coordinate, entry)| {
324                entry
325                    .namespaces
326                    .iter()
327                    .any(|name| name == namespace)
328                    .then(|| coordinate.clone())
329            })
330    }
331
332    fn state(&self, coordinate: &str) -> Option<String> {
333        self.entries
334            .borrow()
335            .get(coordinate)
336            .map(|entry| entry.state.clone())
337    }
338
339    fn read(&self, descriptor: &Value, relative: &str) -> Result<Vec<u8>, String> {
340        let coordinate = package_descriptor_coordinate(descriptor)
341            .ok_or("std.native.Package/read descriptor requires :package/coordinate")?;
342        let expected_version = package_descriptor_version(descriptor)
343            .ok_or("std.native.Package/read descriptor requires :package/version")?;
344        let entries = self.entries.borrow();
345        let entry = entries
346            .get(&coordinate)
347            .ok_or_else(|| format!("package/not-installed: {coordinate}"))?;
348        let actual_version = package_descriptor_version(&entry.descriptor)
349            .ok_or_else(|| format!("package/invalid-descriptor: {coordinate}"))?;
350        if expected_version != actual_version {
351            return Err(format!(
352                "package/version-mismatch: {coordinate} expected {expected_version}, installed {actual_version}"
353            ));
354        }
355        let root = entry
356            .root
357            .as_ref()
358            .ok_or_else(|| format!("package/content-unavailable: {coordinate}"))?;
359        let path = std::path::Path::new(relative);
360        if path.is_absolute()
361            || path.as_os_str().is_empty()
362            || path.components().any(|component| {
363                matches!(component, std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_))
364            })
365        {
366            return Err("std.native.Package/read path must be a non-empty safe relative path".into());
367        }
368        std::fs::read(root.join(path)).map_err(|error| {
369            format!(
370                "package/content-read-failed: {coordinate} {relative}: {error}"
371            )
372        })
373    }
374
375    pub(crate) fn set_state(&self, coordinate: &str, state: &str) {
376        if let Some(entry) = self.entries.borrow_mut().get_mut(coordinate) {
377            entry.state = state.into();
378        }
379    }
380
381    fn pending(&self, coordinate: &str) -> Option<Promise> {
382        self.entries
383            .borrow()
384            .get(coordinate)
385            .and_then(|entry| entry.pending.clone())
386    }
387
388    fn set_pending(&self, coordinate: &str, pending: Option<Promise>) {
389        if let Some(entry) = self.entries.borrow_mut().get_mut(coordinate) {
390            entry.pending = pending;
391        }
392    }
393}
394
395fn package_descriptor_state(descriptor: &Value, state: &str) -> Value {
396    let Value::OrderedMap(values) = descriptor else {
397        return descriptor.clone();
398    };
399    Value::OrderedMap(Box::new(POrderedMap::from_iter(
400        values
401            .iter()
402            .map(|(key, value)| (key.clone(), value.clone()))
403            .chain(std::iter::once((
404                Value::Keyword("package/state".into()),
405                Value::Keyword(state.into()),
406            ))),
407    )))
408}
409
410fn package_descriptor_coordinate(descriptor: &Value) -> Option<String> {
411    let Value::OrderedMap(values) = descriptor else {
412        return None;
413    };
414    match values.get(&Value::Keyword("package/coordinate".into())) {
415        Some(Value::String(coordinate)) => Some(coordinate.clone()),
416        Some(Value::Symbol(coordinate)) => Some(coordinate.as_str().to_owned()),
417        _ => None,
418    }
419}
420
421fn package_descriptor_version(descriptor: &Value) -> Option<String> {
422    let Value::OrderedMap(values) = descriptor else {
423        return None;
424    };
425    match values.get(&Value::Keyword("package/version".into())) {
426        Some(Value::String(version)) => Some(version.clone()),
427        _ => None,
428    }
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq)]
432pub enum NativeAvailability {
433    Portable,
434    CapabilityGated,
435    InventoryOnly,
436}
437
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub struct NativeOperationDeclaration {
440    pub name: &'static str,
441    pub arity: u16,
442}
443
444pub type NativeProvider = fn(&str, &str) -> Result<Value, String>;
445
446#[derive(Debug, Clone, Copy)]
447pub struct NativeDeclaration {
448    pub namespace: &'static str,
449    pub name: &'static str,
450    pub methods: &'static [&'static str],
451    pub whole_wasm_methods: &'static [NativeOperationDeclaration],
452    pub provider: NativeProvider,
453    pub availability: NativeAvailability,
454    pub capability: Option<&'static str>,
455}
456
457impl NativeDeclaration {
458    pub fn qualified_name(self) -> String {
459        format!("{}.{}", self.namespace, self.name)
460    }
461
462    pub fn method(self, name: &str) -> bool {
463        self.methods.iter().any(|method| *method == name)
464    }
465
466    pub fn whole_wasm_method(self, name: &str) -> Option<NativeOperationDeclaration> {
467        self.whole_wasm_methods
468            .iter()
469            .copied()
470            .find(|method| method.name == name)
471    }
472}
473
474pub const NATIVE_DECLARATIONS: &[NativeDeclaration] = DECLARATIONS_DECLARATIONS;
475
476pub fn native_declarations() -> &'static [NativeDeclaration] {
477    NATIVE_DECLARATIONS
478}
479
480pub(crate) fn native_descriptor_value(declaration: NativeDeclaration) -> Value {
481    Value::NativeType(Rc::new(NativeType {
482        name: declaration.qualified_name(),
483        methods: declaration
484            .methods
485            .iter()
486            .map(|method| (*method).to_owned())
487            .collect(),
488        availability: declaration.availability,
489        capability: declaration.capability.map(str::to_owned),
490        metadata: None,
491    }))
492}
493
494pub fn native_type_values() -> Vec<(String, Value)> {
495    NATIVE_DECLARATIONS
496        .iter()
497        .map(|declaration| {
498            (declaration.qualified_name(), native_descriptor_value(*declaration))
499        })
500        .collect()
501}
502
503/// Returns the closed native declaration surface in a stable, comparison-friendly form.
504///
505/// This is intentionally a derived inspection view. The annotations remain the source of
506/// truth; the manifest only makes the Rust and Java declaration surfaces comparable in tests
507/// and diagnostics.
508pub fn native_manifest() -> Vec<String> {
509    let mut manifest = NATIVE_DECLARATIONS
510        .iter()
511        .filter(|declaration| declaration.namespace == "std.native")
512        .map(|declaration| {
513            let mut methods = declaration
514                .methods
515                .iter()
516                .map(|method| format!("{}/{method}", declaration.qualified_name()))
517                .collect::<Vec<_>>();
518            methods.sort();
519            format!(
520                "native|{}|{}|{}|annotation|{}",
521                declaration.qualified_name(),
522                native_availability_name(declaration.availability),
523                declaration.capability.unwrap_or_default(),
524                methods.join(",")
525            )
526        })
527        .collect::<Vec<_>>();
528    manifest.sort();
529    manifest
530}
531
532/// Returns the closed annotated protocol surface in a stable, comparison-friendly form.
533///
534/// Protocol method arities use the declaration arity (`-1` for variadic methods), and method
535/// entries carry their canonical runtime origin. Inherited methods are not copied into a child;
536/// the parent list is part of the manifest instead.
537pub fn protocol_manifest() -> Vec<String> {
538    let mut manifest = protocol_declarations()
539        .iter()
540        .map(|declaration| {
541            let mut parents = declaration
542                .parents
543                .iter()
544                .map(|parent| (*parent).to_owned())
545                .collect::<Vec<_>>();
546            parents.sort();
547            let mut methods = declaration
548                .methods
549                .iter()
550                .map(|method| {
551                    format!(
552                        "{}/{}:{}",
553                        declaration.runtime_name(),
554                        method.name,
555                        protocol_arity_name(method.arity)
556                    )
557                })
558                .collect::<Vec<_>>();
559            methods.sort();
560            format!(
561                "protocol|{}|{}|{}|{}|annotation|{}|{}",
562                declaration.runtime_name(),
563                declaration.name,
564                protocol_availability_name(declaration.availability),
565                declaration.capability.unwrap_or_default(),
566                parents.join(","),
567                methods.join(",")
568            )
569        })
570        .collect::<Vec<_>>();
571    manifest.sort();
572    manifest
573}
574
575fn native_availability_name(availability: NativeAvailability) -> &'static str {
576    match availability {
577        NativeAvailability::Portable => "portable",
578        NativeAvailability::CapabilityGated => "capability-gated",
579        NativeAvailability::InventoryOnly => "inventory-only",
580    }
581}
582
583fn protocol_availability_name(
584    availability: crate::lang::protocol::ProtocolAvailability,
585) -> &'static str {
586    match availability {
587        crate::lang::protocol::ProtocolAvailability::Portable => "portable",
588        crate::lang::protocol::ProtocolAvailability::CapabilityGated => "capability-gated",
589        crate::lang::protocol::ProtocolAvailability::InventoryOnly => "inventory-only",
590    }
591}
592
593fn protocol_arity_name(arity: crate::lang::protocol::ProtocolArity) -> String {
594    match arity {
595        crate::lang::protocol::ProtocolArity::Fixed(value) => value.to_string(),
596        crate::lang::protocol::ProtocolArity::Variadic { .. } => "-1".into(),
597    }
598}
599
600pub(crate) fn protocol_declarations() -> &'static [crate::lang::protocol::ProtocolDeclaration] {
601    crate::lang::protocol::protocol_declarations()
602}
603
604pub fn builtin_protocol_namespace(protocol: &str) -> String {
605    let simple = protocol.strip_prefix("std.foundation/").unwrap_or(protocol);
606    crate::lang::protocol::find_protocol(simple)
607        .map(|declaration| declaration.runtime_name())
608        .unwrap_or_else(|| {
609            if simple.starts_with("std.protocol.") {
610                simple.to_owned()
611            } else {
612                format!("std.protocol.{}.{}", simple.to_ascii_lowercase(), simple)
613            }
614        })
615}
616
617pub(crate) fn builtin_protocol_name(protocol: &str) -> String {
618    let simple = protocol.strip_prefix("std.foundation/").unwrap_or(protocol);
619    crate::lang::protocol::find_protocol(simple)
620        .map(|declaration| declaration.runtime_name())
621        .unwrap_or_else(|| protocol.to_owned())
622}
623
624pub(crate) fn canonical_protocol_name(protocol: &str) -> String {
625    builtin_protocol_name(protocol)
626}
627
628pub(crate) fn canonical_intrinsic_protocol_symbol(symbol: &str) -> Option<String> {
629    let (protocol, method) = symbol.rsplit_once('/')?;
630    let canonical = canonical_protocol_name(protocol);
631    (canonical != protocol).then(|| format!("{canonical}/{method}"))
632}
633
634/// Resolves the short spelling of an annotated native type to its registered
635/// namespace. Protocol names deliberately do not go through this function:
636/// their aliases are installed from the protocol declaration registry and
637/// ordinary namespace resolution must handle them like every other alias.
638pub(crate) fn canonical_native_symbol(symbol: &str) -> Option<String> {
639    // `file/*` is the long-standing lowercase spelling used by the portable
640    // Hara library.  Keep it as a compatibility alias for the annotated
641    // `std.native.File` surface so source compilation and tree evaluation
642    // agree on the same callable identity.
643    if let Some(method) = symbol.strip_prefix("file/") {
644        return Some(format!("std.native.File/{method}"));
645    }
646    if let Some(method) = symbol.strip_prefix("os/") {
647        return Some(format!("std.native.OS/{method}"));
648    }
649    if NATIVE_DECLARATIONS
650        .iter()
651        .any(|declaration| declaration.namespace == "std.native" && declaration.name == symbol)
652    {
653        return Some(format!("std.native.{symbol}"));
654    }
655    let (native_type, method) = symbol.rsplit_once('/')?;
656    NATIVE_DECLARATIONS
657        .iter()
658        .any(|declaration| declaration.namespace == "std.native" && declaration.name == native_type)
659        .then(|| format!("std.native.{native_type}/{method}"))
660}
661
662pub(crate) fn canonical_intrinsic_symbol(symbol: &str) -> Option<String> {
663    canonical_intrinsic_protocol_symbol(symbol).or_else(|| canonical_native_symbol(symbol))
664}
665
666/// Returns the canonical identity of a callable owned by the native or
667/// protocol registries. Ordinary Foundation functions deliberately do not
668/// appear here: they must resolve through their namespace Vars after
669/// `std.foundation` has been loaded.
670pub(crate) fn canonical_intrinsic_callable_symbol(symbol: &str) -> Option<String> {
671    let canonical = canonical_intrinsic_symbol(symbol).unwrap_or_else(|| symbol.to_owned());
672    if let Some((native_type, method)) = canonical.rsplit_once('/') {
673        if NATIVE_DECLARATIONS.iter().any(|declaration| {
674            declaration.qualified_name() == native_type && declaration.method(method)
675        }) {
676            return Some(canonical);
677        }
678    }
679    let (namespace, method) = canonical.split_once('/')?;
680    protocol_declarations()
681        .iter()
682        .find(|declaration| declaration.runtime_name() == namespace)
683        .filter(|declaration| declaration.methods.iter().any(|candidate| candidate.name == method))
684        .map(|_| canonical)
685}
686
687/// Resolves a canonical native/protocol callable for bytecode instructions.
688/// The registry is the only source of these values; no unqualified fallback
689/// catalog is consulted.
690pub(crate) fn bytecode_callable_value(name: &str) -> Result<Value, String> {
691    let canonical = canonical_intrinsic_callable_symbol(name)
692        .ok_or_else(|| format!("unknown canonical builtin: {name}"))?;
693    let registry = namespace_registry()?;
694    registry
695        .resolve(&crate::lang::data::Symbol::parse(&canonical))
696        .map(|var| var.deref_value())
697        .ok_or_else(|| format!("unbound canonical builtin: {canonical}"))
698}
699
700pub fn foundation_protocol_values() -> Vec<(String, Value)> {
701    protocol_declarations()
702        .iter()
703        .filter(|declaration| declaration.availability.is_guest_visible())
704        .map(|declaration| {
705            (
706                declaration.name.to_owned(),
707                Value::Protocol(Rc::new(guest_protocol(*declaration))),
708            )
709        })
710        .collect()
711}
712
713pub fn builtin_protocol_method_values() -> Vec<(String, String, Value)> {
714    protocol_declarations()
715        .iter()
716        .filter(|declaration| declaration.availability.is_guest_visible())
717        .flat_map(|declaration| {
718            declaration.methods.iter().map(move |method| {
719                let protocol_name = declaration.runtime_name();
720                let namespace = protocol_name.clone();
721                let method_name = method.name.to_owned();
722                let display_name = format!("{namespace}/{}", method.name);
723                let arity_display_name = display_name.clone();
724                let (minimum_arity, maximum_arity) = method.arity.range();
725                let value = if protocol_name == "std.protocol.ideref.IDeref" && method.name == "deref" {
726                    native_protocol_fiber_function(
727                        &display_name,
728                        &protocol_name,
729                        &method_name,
730                        minimum_arity,
731                        maximum_arity.is_none(),
732                        {
733                            let protocol_name = protocol_name.clone();
734                            let method_name = method_name.clone();
735                            move |arguments| protocol_call(&protocol_name, &method_name, &arguments)
736                        },
737                        protocol_deref_fiber,
738                    )
739                } else if protocol_name == "std.protocol.icoroutine.ICoroutine"
740                    && method.name == "resume"
741                {
742                    native_protocol_fiber_function(
743                        &display_name,
744                        &protocol_name,
745                        &method_name,
746                        minimum_arity,
747                        maximum_arity.is_none(),
748                        {
749                            let protocol_name = protocol_name.clone();
750                            let method_name = method_name.clone();
751                            move |arguments| protocol_call(&protocol_name, &method_name, &arguments)
752                        },
753                        protocol_coroutine_resume_fiber,
754                    )
755                } else {
756                    native_variadic_function(&display_name, move |arguments| {
757                        if arguments.len() < minimum_arity
758                            || maximum_arity.is_some_and(|maximum| arguments.len() > maximum)
759                        {
760                            let expected = match maximum_arity {
761                                Some(maximum) if maximum == minimum_arity => {
762                                    minimum_arity.to_string()
763                                }
764                                Some(maximum) => format!("{minimum_arity} to {maximum}"),
765                                None => format!("at least {minimum_arity}"),
766                            };
767                            return Err(format!(
768                                "protocol/arity: {arity_display_name} expects {expected} arguments, received {}",
769                                arguments.len()
770                            ));
771                        }
772                        protocol_call(&protocol_name, &method_name, &arguments)
773                    })
774                };
775                (
776                    namespace,
777                    method.name.to_owned(),
778                    value,
779                )
780            })
781        })
782        .collect()
783}
784
785fn guest_protocol(declaration: crate::lang::protocol::ProtocolDeclaration) -> GuestProtocol {
786    GuestProtocol {
787        name: declaration.runtime_name(),
788        methods: declaration
789            .methods
790            .iter()
791            .map(|method| (method.name.to_owned(), method.arity.guest_arity()))
792            .collect(),
793        parents: declaration
794            .parents
795            .iter()
796            .map(|parent| {
797                crate::lang::protocol::find_protocol(parent)
798                    .map(|declaration| declaration.runtime_name())
799                    .unwrap_or_else(|| (*parent).to_owned())
800            })
801            .collect(),
802    }
803}
804
805#[cfg(test)]
806mod native_work_protocol_tests {
807    use super::*;
808
809    fn methods(name: &str) -> Vec<(&'static str, usize)> {
810        protocol_declarations()
811            .iter()
812            .find(|declaration| declaration.name == name)
813            .map(|declaration| {
814                declaration
815                    .methods
816                    .iter()
817                    .map(|method| (method.name, method.arity.guest_arity()))
818                    .collect()
819            })
820            .expect("protocol must exist")
821    }
822
823    fn protocol(name: &str) -> Rc<GuestProtocol> {
824        foundation_protocol_values()
825            .into_iter()
826            .find(|(candidate, _)| candidate == name)
827            .and_then(|(_, value)| match value {
828                Value::Protocol(protocol) => Some(protocol),
829                _ => None,
830            })
831            .expect("protocol value must exist")
832    }
833
834    #[test]
835    fn protocol_aliases_resolve_to_annotation_owned_namespaces() {
836        let namespaces = crate::core::minimal_namespace_registry();
837        let assoc = namespaces
838            .resolve(&crate::lang::data::Symbol::parse("IAssoc/assoc"))
839            .expect("annotated protocol alias");
840        assert_eq!(
841            assoc.symbol().as_str(),
842            "std.protocol.iassoc.IAssoc/assoc"
843        );
844        assert_eq!(
845            crate::lang::protocol::find_protocol("IAssoc")
846                .expect("annotated protocol")
847                .runtime_name(),
848            "std.protocol.iassoc.IAssoc"
849        );
850        assert_eq!(
851            canonical_native_symbol("Base/vec"),
852            Some("std.native.Base/vec".into())
853        );
854        assert_eq!(canonical_native_symbol("std.native/Base"), None);
855        assert_eq!(
856            canonical_native_symbol("Coroutine"),
857            Some("std.native.Coroutine".into())
858        );
859        assert_eq!(
860            canonical_native_symbol("file/join"),
861            Some("std.native.File/join".into())
862        );
863        assert_eq!(
864            canonical_native_symbol("os/cwd"),
865            Some("std.native.OS/cwd".into())
866        );
867    }
868
869    #[test]
870    fn native_registry_rejects_unknown_annotated_methods() {
871        let error = crate::core::native_type_function_value("String", "missing").unwrap_err();
872        assert_eq!(
873            error,
874            "unknown annotated native method: std.native.String/missing"
875        );
876    }
877
878    #[test]
879    fn native_work_protocol_methods_are_stable() {
880        assert_eq!(methods("IWork"), vec![("work-spec", 1)]);
881        assert_eq!(methods("IWorkExecutor"), vec![("work-execute", 2)]);
882        assert_eq!(
883            methods("IWorkStore"),
884            vec![("work-query", 2), ("work-transact", 2)]
885        );
886        assert_eq!(methods("IWorkRef"), vec![("work-id", 1)]);
887        assert_eq!(
888            methods("IWorkHost"),
889            vec![("work-submit", 4), ("work-resolve", 2)]
890        );
891        assert_eq!(
892            methods("IWorkRun"),
893            vec![
894                ("work-status", 1),
895                ("work-result", 1),
896                ("work-events", 2),
897                ("work-cancel", 2),
898            ]
899        );
900    }
901
902    #[test]
903    fn native_work_protocol_parents_match_the_lifecycle_contract() {
904        assert!(protocol("IWorkExecutor").parents.is_empty());
905        assert!(protocol("IWorkStore").parents.is_empty());
906        assert_eq!(
907            protocol("IWorkHost").parents,
908            vec![crate::lang::protocol::find_protocol("IComponent")
909                .expect("annotated protocol")
910                .runtime_name()]
911        );
912        assert_eq!(
913            protocol("IWorkRun").parents,
914            vec![
915                crate::lang::protocol::find_protocol("IWorkRef")
916                    .expect("annotated protocol")
917                    .runtime_name(),
918                crate::lang::protocol::find_protocol("IClosed")
919                    .expect("annotated protocol")
920                    .runtime_name(),
921            ]
922        );
923    }
924}