Skip to main content

hara_native/vm/
bundle.rs

1//! Deterministic indexed container for the embedded Foundation bootstrap.
2
3use sha2::{Digest, Sha256};
4
5use crate::{
6    core, kernel, Runtime, EAGER_HAL_RESOURCES, EMBEDDED_CLI_RESOURCES, EMBEDDED_HAL_RESOURCES,
7};
8
9#[path = "bundle/order.rs"]
10mod order;
11use order::order_module_sources;
12
13const MAGIC: &[u8; 4] = b"HBX0";
14
15#[derive(Clone, Copy)]
16pub struct ModuleSource<'a> {
17    pub resource: &'a str,
18    pub source: &'a str,
19}
20
21/// One validated module in the shared HBX0 container format.
22///
23/// Products such as Hoplite use this descriptor to package application HBC0
24/// artifacts without maintaining a second, subtly different bundle codec.
25#[derive(Clone)]
26pub struct BytecodeBundleModule {
27    pub resource: String,
28    pub namespace_form: String,
29    pub source_digest: [u8; 32],
30    pub dependencies: Vec<String>,
31    pub eager: bool,
32    pub artifact: Vec<u8>,
33}
34
35pub fn embedded_foundation_bootstrap_sources() -> Vec<ModuleSource<'static>> {
36    if EMBEDDED_HAL_RESOURCES.is_empty() {
37        return Vec::new();
38    }
39    let ordered = std::iter::once("std.foundation")
40        .chain(EAGER_HAL_RESOURCES.iter().copied())
41        .chain(
42            EMBEDDED_HAL_RESOURCES
43                .iter()
44                .map(|(namespace, _, _)| *namespace)
45                .filter(|namespace| {
46                    standard_library_namespace(namespace)
47                        && *namespace != "std.foundation"
48                        && !EAGER_HAL_RESOURCES.contains(namespace)
49                }),
50        );
51    let sources = ordered
52        .map(|resource| {
53            let source = EMBEDDED_HAL_RESOURCES
54                .iter()
55                .find_map(|(name, _, source)| (*name == resource).then_some(*source))
56                .unwrap_or_else(|| panic!("missing embedded HAL resource: {resource}"));
57            ModuleSource { resource, source }
58        })
59        .collect::<Vec<_>>();
60    order_module_sources(&sources)
61        .expect("embedded Foundation bootstrap dependencies must be acyclic")
62        .into_iter()
63        .map(|index| sources[index])
64        .collect()
65}
66
67/// Returns the embedded CLI/test-support namespace closure in deterministic
68/// dependency order. Foundation namespaces are deliberately excluded because
69/// they are already supplied by the runtime's Foundation artifact.
70pub fn embedded_cli_sources() -> Vec<ModuleSource<'static>> {
71    let sources = EMBEDDED_CLI_RESOURCES
72        .iter()
73        .map(|(resource, _, source)| ModuleSource { resource, source })
74        .collect::<Vec<_>>();
75    order_module_sources(&sources)
76        .expect("embedded CLI bootstrap dependencies must be acyclic")
77        .into_iter()
78        .map(|index| sources[index])
79        .collect()
80}
81
82pub fn compile_bytecode_bundle(sources: &[ModuleSource<'_>]) -> Result<Vec<u8>, String> {
83    let mut runtime = Runtime::core();
84    for &(name, _, source) in EMBEDDED_HAL_RESOURCES {
85        runtime.register_resource(name, source);
86    }
87    compile_bytecode_bundle_with_runtime(&mut runtime, sources, sources)
88}
89
90/// Compiles a package against the portable core runtime. `context` is
91/// registered for resolving imports and macros, while only `sources` are
92/// emitted into the resulting HBX0 bundle. A source-owned Foundation package
93/// evaluates `std.foundation` first so its companion namespaces can resolve
94/// the root surface during their own compilation.
95pub fn compile_package_bytecode_bundle(
96    context: &[ModuleSource<'_>],
97    sources: &[ModuleSource<'_>],
98) -> Result<Vec<u8>, String> {
99    #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
100    {
101        return core::without_direct_native_execution(|| {
102            compile_package_bytecode_bundle_inner(context, sources)
103        });
104    }
105    #[cfg(not(all(feature = "direct-native", not(target_arch = "wasm32"))))]
106    compile_package_bytecode_bundle_inner(context, sources)
107}
108
109fn compile_package_bytecode_bundle_inner(
110    context: &[ModuleSource<'_>],
111    sources: &[ModuleSource<'_>],
112) -> Result<Vec<u8>, String> {
113    let mut runtime = Runtime::new();
114    let ordered = foundation_root_first(sources);
115    for source in context {
116        runtime.register_resource(source.resource, source.source);
117    }
118    #[cfg(not(target_arch = "wasm32"))]
119    if package_needs_foundation_bootstrap(context, &ordered)
120        || ordered.iter().any(|source| source.resource == "std.foundation")
121    {
122        runtime.bootstrap_source_foundation()?;
123    }
124    // Context modules are an interpreter-time compiler boundary. Keep this
125    // explicit because package builds can be invoked from a host runtime that
126    // has already selected the direct-native backend.
127    runtime.configure_execution_backend("interpreter")?;
128    // Registration makes context source discoverable to the evaluator, but it
129    // does not establish the Vars and macros that a selected module may use
130    // while its namespace form is being compiled. Load the selected modules'
131    // eager context closure, plus the language-spec roots used by the lazy
132    // grammar registry, before compiling the selected package. Selected
133    // modules remain bytecode-owned and are evaluated below as artifacts.
134    #[cfg(not(target_arch = "wasm32"))]
135    for source in context_modules_to_load(context, &ordered)? {
136        #[cfg(all(feature = "direct-native", not(target_arch = "wasm32")))]
137        let result = core::without_direct_native_execution(|| {
138            runtime.eval_native(&format!("(require (quote {}))", source.resource))
139        });
140        #[cfg(not(all(feature = "direct-native", not(target_arch = "wasm32"))))]
141        let result = runtime.eval_native(&format!("(require (quote {}))", source.resource));
142        result.map_err(|error| format!("{}: context loading: {error}", source.resource))?;
143    }
144    // Context resources were registered above and may already be loaded to
145    // establish macro state; avoid re-registering them here, which would
146    // invalidate their namespace load markers before selected compilation.
147    compile_bytecode_bundle_with_runtime(&mut runtime, &[], &ordered)
148}
149
150fn context_modules_to_load<'a>(
151    context: &[ModuleSource<'a>],
152    selected: &[ModuleSource<'a>],
153) -> Result<Vec<ModuleSource<'a>>, String> {
154    let selected_names = selected
155        .iter()
156        .map(|source| source.resource)
157        .collect::<std::collections::HashSet<_>>();
158    let positions = context
159        .iter()
160        .enumerate()
161        .map(|(index, source)| (source.resource, index))
162        .collect::<std::collections::HashMap<_, _>>();
163    let mut pending = Vec::new();
164    let mut preload = std::collections::HashSet::new();
165    for source in selected {
166        let (dependencies, script_dependencies) = module_dependencies(source.source)?;
167        pending.extend(
168            dependencies
169                .into_iter()
170                .filter(|name| {
171                    !name.starts_with("std.foundation") && !selected_names.contains(name.as_str())
172                }),
173        );
174        preload.extend(script_dependencies.iter().cloned());
175        pending.extend(script_dependencies);
176    }
177    // The Postgres grammar is loaded lazily by lang.core.registry, but its
178    // macro image is needed during bytecode compilation. Keep this host-side
179    // bootstrap explicit instead of evaluating every target grammar in the
180    // project context for every package.
181    let needs_postgres_spec = selected_names
182        .iter()
183        .any(|name| name.starts_with("postgres."));
184    let needs_xtalk_spec = selected_names
185        .iter()
186        .any(|name| name.starts_with("xt."));
187    pending.extend(
188        context
189            .iter()
190            .filter(|source| {
191                ((needs_postgres_spec && source.resource == "lang.model.v1.spec-postgres")
192                    || (needs_xtalk_spec && source.resource == "lang.model.v1.spec-xtalk"))
193                    && !selected_names.contains(source.resource)
194            })
195            .map(|source| source.resource.to_owned()),
196    );
197    let mut required = std::collections::HashSet::new();
198    while let Some(name) = pending.pop() {
199        if (!preload.contains(&name) && selected_names.contains(name.as_str()))
200            || !required.insert(name.clone())
201        {
202            continue;
203        }
204        let Some(&index) = positions.get(name.as_str()) else {
205            continue;
206        };
207        let (dependencies, script_dependencies) = module_dependencies(context[index].source)?;
208        pending.extend(
209            dependencies
210                .into_iter()
211                .filter(|dependency| {
212                    !dependency.starts_with("std.foundation")
213                        && !selected_names.contains(dependency.as_str())
214                }),
215        );
216        preload.extend(script_dependencies.iter().cloned());
217        pending.extend(script_dependencies);
218    }
219    let mut ordered = order_module_sources(context)?
220        .into_iter()
221        .map(|index| context[index])
222        .filter(|source| required.contains(source.resource))
223        .collect::<Vec<_>>();
224    // `lang.core.registry` keeps target specs as lazy aliases. Loading the
225    // selected grammar root first makes those aliases resolve to an already
226    // materialized namespace when the registry is later loaded.
227    ordered.sort_by_key(|source| {
228        let priority = (needs_postgres_spec && source.resource == "lang.model.v1.spec-postgres")
229            || (needs_xtalk_spec && source.resource == "lang.model.v1.spec-xtalk");
230        (!priority, !preload.contains(source.resource), source.resource)
231    });
232    Ok(ordered)
233}
234
235pub(super) fn module_dependencies(source: &str) -> Result<(Vec<String>, Vec<String>), String> {
236    let (namespace_form, body) = split_namespace_form(source)?;
237    let dependencies = namespace_dependencies(namespace_form)?;
238    let mut script_dependencies = Vec::new();
239    for form in kernel::parse_forms(body)? {
240        collect_script_dependencies(&form, &mut script_dependencies);
241    }
242    script_dependencies.sort();
243    script_dependencies.dedup();
244    Ok((dependencies, script_dependencies))
245}
246
247fn collect_script_dependencies(form: &kernel::Form, output: &mut Vec<String>) {
248    let form = match form {
249        kernel::Form::Metadata(_, value) => value.as_ref(),
250        value => value,
251    };
252    let kernel::Form::List(items) = form else {
253        return;
254    };
255    let is_script = matches!(
256        items.first(),
257        Some(kernel::Form::Symbol(name)) if name == "l/script" || name == "script"
258    );
259    if !is_script {
260        return;
261    }
262    for option in items.iter().skip(2) {
263        let kernel::Form::Map(entries) = option else {
264            continue;
265        };
266        for (key, value) in entries {
267            if !matches!(key, kernel::Form::Keyword(name) if name == "require") {
268                continue;
269            }
270            let kernel::Form::Vector(requirements) = value else {
271                continue;
272            };
273            for requirement in requirements {
274                let kernel::Form::Vector(spec) = requirement else {
275                    continue;
276                };
277                if let Some(kernel::Form::Symbol(name)) = spec.first() {
278                    output.push(name.clone());
279                }
280            }
281        }
282    }
283}
284
285#[cfg(not(target_arch = "wasm32"))]
286fn package_needs_foundation_bootstrap(
287    context: &[ModuleSource<'_>],
288    sources: &[ModuleSource<'_>],
289) -> bool {
290    let emits_foundation = sources
291        .iter()
292        .any(|source| source.resource == "std.foundation");
293    !emits_foundation
294        && context
295            .iter()
296            .any(|source| source.resource == "std.foundation")
297}
298
299fn foundation_root_first<'a>(sources: &[ModuleSource<'a>]) -> Vec<ModuleSource<'a>> {
300    if !sources
301        .iter()
302        .any(|source| source.resource == "std.foundation")
303    {
304        return sources.to_vec();
305    }
306    let mut ordered = Vec::with_capacity(sources.len());
307    for resource in std::iter::once("std.foundation").chain(EAGER_HAL_RESOURCES.iter().copied()) {
308        if let Some(source) = sources.iter().find(|source| source.resource == resource) {
309            ordered.push(*source);
310        }
311    }
312    let remaining = sources
313        .iter()
314        .filter(|source| {
315            !ordered
316                .iter()
317                .any(|ordered| ordered.resource == source.resource)
318        })
319        .copied()
320        .collect::<Vec<_>>();
321    ordered.extend(remaining);
322    ordered
323}
324
325fn compile_bytecode_bundle_with_runtime(
326    runtime: &mut Runtime,
327    context: &[ModuleSource<'_>],
328    sources: &[ModuleSource<'_>],
329) -> Result<Vec<u8>, String> {
330    for source in context {
331        runtime.register_resource(source.resource, source.source);
332    }
333    let mut encoded = Vec::new();
334    for index in order_module_sources(sources)? {
335        let source = &sources[index];
336        let (namespace_form, body) = split_namespace_form(source.source)?;
337        runtime
338            .eval_text(namespace_form)
339            .map_err(|error| format!("{}: namespace declaration: {error}", source.resource))?;
340        // Required modules and macro expansion are allowed to select their
341        // own namespaces. Pin compilation to the module being emitted so
342        // aliases become canonical globals owned by its declaration.
343        runtime.use_namespace(source.resource);
344        let artifact = core::with_definition_origin(kernel::VarOrigin::HalFallback, || {
345            runtime.compile_package_bytecode_artifact(body)
346        })
347        .map_err(|error| format!("{}: bytecode compilation: {error}", source.resource))?;
348        core::with_definition_origin(kernel::VarOrigin::HalFallback, || {
349            runtime.eval_bytecode_artifact(&artifact)
350        })
351        .map_err(|error| format!("{}: bytecode execution: {error}", source.resource))?;
352        let source_digest: [u8; 32] = Sha256::digest(source.source.as_bytes()).into();
353        let dependencies = namespace_dependencies(namespace_form)?;
354        let eager =
355            source.resource == "std.foundation" || EAGER_HAL_RESOURCES.contains(&source.resource);
356        encoded.push(BytecodeBundleModule {
357            resource: source.resource.to_owned(),
358            namespace_form: namespace_form.to_owned(),
359            source_digest,
360            dependencies,
361            eager,
362            artifact,
363        });
364    }
365    encode_bytecode_bundle(&encoded)
366}
367
368pub fn compile_embedded_foundation_bootstrap_bundle() -> Result<Vec<u8>, String> {
369    compile_bytecode_bundle(&embedded_foundation_bootstrap_sources())
370}
371
372/// Compiles the immutable CLI and `code.test` closure against the already
373/// bootstrapped Foundation context. The resulting bundle is installed lazily,
374/// so a test or CLI process pays only for the namespaces it actually requires.
375pub fn compile_embedded_cli_bundle() -> Result<Vec<u8>, String> {
376    let foundation = embedded_foundation_bootstrap_sources();
377    let cli = embedded_cli_sources();
378    let mut context = foundation;
379    context.extend(cli.iter().copied());
380    compile_package_bytecode_bundle(&context, &cli)
381}
382
383/// Compatibility name retained for embedding hosts built against the original
384/// standard-library bundle API. The embedded artifact is now Foundation-only.
385pub fn compile_embedded_standard_library_bundle() -> Result<Vec<u8>, String> {
386    compile_embedded_foundation_bootstrap_bundle()
387}
388
389/// Compatibility name retained for callers that previously inspected the
390/// embedded standard-library sources.
391pub fn embedded_standard_library_sources() -> Vec<ModuleSource<'static>> {
392    embedded_foundation_bootstrap_sources()
393}
394
395pub fn eval_bytecode_bundle(runtime: &mut Runtime, bytes: &[u8]) -> Result<(), String> {
396    let modules = decode(bytes)?;
397    let mut names = std::collections::HashSet::with_capacity(modules.len());
398    for module in &modules {
399        if !names.insert(module.resource.clone()) {
400            return Err(format!(
401                "duplicate bytecode bundle module: {}",
402                module.resource
403            ));
404        }
405    }
406    let namespaces_before = runtime.namespace_registry.snapshot();
407    let environment_before = runtime.execution.snapshot();
408    let macros_before = runtime.macros.borrow().clone();
409    let protocols_before = runtime.protocols.snapshot();
410    let multimethods_before = core::snapshot_multimethods();
411    let resources_before = runtime.bytecode_resources.clone();
412    let loaded_before = runtime.loaded_resources.clone();
413    let loaded = (|| {
414        for module in &modules {
415            let source = if let Some(source) = runtime.resources.get(&module.resource) {
416                Some(source.clone())
417            } else {
418                #[cfg(not(target_arch = "wasm32"))]
419                {
420                    runtime
421                        .source_catalog
422                        .as_ref()
423                        .and_then(|catalog| catalog.path(&module.resource))
424                        .as_ref()
425                        .map(|path| {
426                            std::fs::read_to_string(path).map_err(|error| {
427                                format!("cannot read bundled source {}: {error}", path.display())
428                            })
429                        })
430                        .transpose()?
431                }
432                #[cfg(target_arch = "wasm32")]
433                {
434                    None
435                }
436            };
437            let source_is_current = source
438                .as_deref()
439                .map(|source| {
440                    let digest: [u8; 32] = Sha256::digest(source.as_bytes()).into();
441                    digest == module.source_digest
442                })
443                .unwrap_or(true);
444            if !source_is_current {
445                if module.eager {
446                    return Err(format!(
447                        "stale eager bytecode bundle module: {}",
448                        module.resource
449                    ));
450                }
451                continue;
452            }
453            runtime.register_bytecode_resource(
454                module.resource.clone(),
455                module.namespace_form.clone(),
456                module.artifact.clone(),
457            );
458        }
459        for module in modules.iter().filter(|module| module.eager) {
460            core::with_definition_origin(kernel::VarOrigin::HalFallback, || {
461                runtime.load_bytecode_resource(&module.resource).map(|_| ())
462            })
463            .map_err(|error| format!("{}: {error}", module.resource))?;
464            runtime.loaded_resources.insert(module.resource.clone());
465        }
466        runtime.use_namespace("user");
467        Ok(())
468    })();
469    if let Err(error) = loaded {
470        runtime.namespace_registry.restore(namespaces_before);
471        runtime.execution.restore(environment_before);
472        *runtime.macros.borrow_mut() = macros_before;
473        runtime.protocols.restore(protocols_before);
474        core::restore_multimethods(multimethods_before);
475        runtime.bytecode_resources = resources_before;
476        runtime.loaded_resources = loaded_before;
477        return Err(error);
478    }
479    Ok(())
480}
481
482/// Transactionally load a fully eager HBX0 application bundle into an
483/// embedding host's existing namespace and protocol registries.
484///
485/// The ordinary [`eval_bytecode_bundle`] API additionally indexes lazy
486/// standard-library resources on a [`Runtime`]. Worker hosts such as Hoplite
487/// already own their registries and package every application module eagerly,
488/// so this narrower entry point preserves that ownership without falling back
489/// to source compilation.
490pub fn eval_eager_bytecode_bundle_with_registries(
491    namespaces: &kernel::NamespaceRegistry<core::Value>,
492    protocols: &core::ProtocolRegistry,
493    bytes: &[u8],
494) -> Result<(), String> {
495    let modules = decode(bytes)?;
496    if let Some(module) = modules.iter().find(|module| !module.eager) {
497        return Err(format!(
498            "embedding bundle module must be eager: {}",
499            module.resource
500        ));
501    }
502    let mut positions = std::collections::HashMap::with_capacity(modules.len());
503    for (index, module) in modules.iter().enumerate() {
504        if positions.insert(module.resource.as_str(), index).is_some() {
505            return Err(format!(
506                "duplicate bytecode bundle module: {}",
507                module.resource
508            ));
509        }
510    }
511    for (index, module) in modules.iter().enumerate() {
512        for dependency in &module.dependencies {
513            if positions
514                .get(dependency.as_str())
515                .is_some_and(|dependency_index| *dependency_index >= index)
516            {
517                return Err(format!(
518                    "{}: bundled dependency must appear first: {dependency}",
519                    module.resource
520                ));
521            }
522        }
523    }
524    let programs = modules
525        .iter()
526        .map(|module| {
527            crate::vm::decode_program(&module.artifact)
528                .map(std::rc::Rc::new)
529                .map_err(|error| format!("{}: invalid bytecode artifact: {error}", module.resource))
530        })
531        .collect::<Result<Vec<_>, _>>()?;
532    let namespaces_before = namespaces.snapshot();
533    let protocols_before = protocols.snapshot();
534    let multimethods_before = core::snapshot_multimethods();
535    let loaded = (|| {
536        for (module, program) in modules.iter().zip(programs) {
537            let forms = kernel::parse_forms(&module.namespace_form)
538                .map_err(|error| format!("{}: namespace declaration: {error}", module.resource))?;
539            if forms.len() != 1 {
540                return Err(format!(
541                    "{}: bundle namespace declaration must contain exactly one form",
542                    module.resource
543                ));
544            }
545            let mut environment = std::collections::HashMap::new();
546            core::with_namespace_registry(namespaces, || {
547                core::with_protocols(protocols, || core::eval(&forms[0], &mut environment))
548            })
549            .map_err(|error| format!("{}: namespace declaration: {error}", module.resource))?;
550            core::with_namespace_registry(namespaces, || {
551                core::with_protocols(protocols, || {
552                    crate::vm::execute_program_with_globals(program, namespaces)
553                        .map_err(|error| error.to_string())
554                })
555            })
556            .map_err(|error| format!("{}: bytecode execution: {error}", module.resource))?;
557        }
558        Ok(())
559    })();
560    if let Err(error) = loaded {
561        namespaces.restore(namespaces_before);
562        protocols.restore(protocols_before);
563        core::restore_multimethods(multimethods_before);
564        return Err(error);
565    }
566    Ok(())
567}
568
569/// Encode modules into the deterministic, checksummed HBX0 container shared by
570/// the Rust, Truffle/native-image, and embedding runtimes.
571pub fn encode_bytecode_bundle(modules: &[BytecodeBundleModule]) -> Result<Vec<u8>, String> {
572    let modules = canonical_modules(modules)?;
573    let mut payload = Vec::new();
574    put_u32(&mut payload, modules.len())?;
575    for module in &modules {
576        put_bytes(&mut payload, module.resource.as_bytes())?;
577        put_bytes(&mut payload, module.namespace_form.as_bytes())?;
578        payload.extend_from_slice(&module.source_digest);
579        put_u32(&mut payload, module.dependencies.len())?;
580        for dependency in &module.dependencies {
581            put_bytes(&mut payload, dependency.as_bytes())?;
582        }
583        payload.push(u8::from(module.eager));
584        put_bytes(&mut payload, &module.artifact)?;
585    }
586    let checksum = Sha256::digest(&payload);
587    let mut output = Vec::with_capacity(4 + checksum.len() + payload.len());
588    output.extend_from_slice(MAGIC);
589    output.extend_from_slice(&checksum);
590    output.extend_from_slice(&payload);
591    Ok(output)
592}
593
594pub fn decode_bytecode_bundle(bytes: &[u8]) -> Result<Vec<BytecodeBundleModule>, String> {
595    if bytes.len() < 36 || &bytes[..4] != MAGIC {
596        return Err("invalid HBX0 bytecode bundle header".into());
597    }
598    let payload = &bytes[36..];
599    if Sha256::digest(payload)[..] != bytes[4..36] {
600        return Err("HBX0 bytecode bundle checksum mismatch".into());
601    }
602    let mut input = payload;
603    let count = take_u32(&mut input)? as usize;
604    let mut modules = Vec::with_capacity(count);
605    for _ in 0..count {
606        let resource = take_string(&mut input)?;
607        let namespace_form = take_string(&mut input)?;
608        let source_digest = take(&mut input, 32)?.try_into().unwrap();
609        let dependency_count = take_u32(&mut input)? as usize;
610        let dependencies = (0..dependency_count)
611            .map(|_| take_string(&mut input))
612            .collect::<Result<Vec<_>, _>>()?;
613        let eager = match take(&mut input, 1)?[0] {
614            0 => false,
615            1 => true,
616            _ => return Err("HBX0 bytecode bundle contains invalid eager flag".into()),
617        };
618        let artifact = take_bytes(&mut input)?.to_vec();
619        modules.push(BytecodeBundleModule {
620            resource,
621            namespace_form,
622            source_digest,
623            dependencies,
624            eager,
625            artifact,
626        });
627    }
628    if !input.is_empty() {
629        return Err("trailing bytes in HBX0 bytecode bundle".into());
630    }
631    validate_bundle_modules(&modules)?;
632    for module in &modules {
633        crate::vm::decode_program(&module.artifact)
634            .map_err(|error| format!("{}: invalid HBC0 artifact: {error}", module.resource))?;
635    }
636    Ok(modules)
637}
638
639fn decode(bytes: &[u8]) -> Result<Vec<BytecodeBundleModule>, String> {
640    decode_bytecode_bundle(bytes)
641}
642
643fn canonical_modules(
644    modules: &[BytecodeBundleModule],
645) -> Result<Vec<BytecodeBundleModule>, String> {
646    let mut by_resource = std::collections::BTreeMap::new();
647    for module in modules {
648        if by_resource
649            .insert(module.resource.clone(), module.clone())
650            .is_some()
651        {
652            return Err(format!("duplicate HBX0 module: {}", module.resource));
653        }
654        let mut dependencies = module.dependencies.clone();
655        dependencies.sort();
656        if dependencies.windows(2).any(|pair| pair[0] == pair[1]) {
657            return Err(format!("{}: duplicate HBX0 dependency", module.resource));
658        }
659    }
660    let mut ordered = Vec::with_capacity(modules.len());
661    while !by_resource.is_empty() {
662        let available = by_resource
663            .iter()
664            .find(|(_, module)| {
665                module
666                    .dependencies
667                    .iter()
668                    .all(|dependency| !by_resource.contains_key(dependency))
669            })
670            .map(|(resource, _)| resource.clone())
671            .ok_or("HBX0 module dependencies contain a cycle")?;
672        let mut module = by_resource.remove(&available).unwrap();
673        module.dependencies.sort();
674        ordered.push(module);
675    }
676    validate_bundle_modules(&ordered)?;
677    Ok(ordered)
678}
679
680fn validate_bundle_modules(modules: &[BytecodeBundleModule]) -> Result<(), String> {
681    let mut positions = std::collections::HashMap::with_capacity(modules.len());
682    for (index, module) in modules.iter().enumerate() {
683        if module.resource.is_empty() {
684            return Err("HBX0 module resource must not be empty".into());
685        }
686        if module.namespace_form.is_empty() {
687            return Err(format!(
688                "{}: HBX0 namespace form must not be empty",
689                module.resource
690            ));
691        }
692        if positions.insert(module.resource.as_str(), index).is_some() {
693            return Err(format!("duplicate HBX0 module: {}", module.resource));
694        }
695        if module
696            .dependencies
697            .windows(2)
698            .any(|pair| pair[0] >= pair[1])
699        {
700            return Err(format!(
701                "{}: HBX0 dependencies must be unique and sorted",
702                module.resource
703            ));
704        }
705    }
706    for (index, module) in modules.iter().enumerate() {
707        for dependency in &module.dependencies {
708            if positions
709                .get(dependency.as_str())
710                .is_some_and(|position| *position >= index)
711            {
712                return Err(format!(
713                    "{}: HBX0 dependency must appear first: {dependency}",
714                    module.resource
715                ));
716            }
717        }
718    }
719    Ok(())
720}
721
722fn standard_library_namespace(namespace: &str) -> bool {
723    ["std.", "code.", "db.", "lang."]
724        .iter()
725        .any(|prefix| namespace.starts_with(prefix))
726}
727
728pub(super) fn namespace_dependencies(namespace_form: &str) -> Result<Vec<String>, String> {
729    let forms = kernel::parse_forms(namespace_form)?;
730    let Some(kernel::Form::List(items)) = forms.first() else {
731        return Err("standard-library module has invalid ns form".into());
732    };
733    let config = kernel::GeneratedNamespaceConfig::configure_with(&items[2..], |_| true)?;
734    let mut dependencies = config.required_namespaces().to_vec();
735    dependencies.extend(config.used_namespaces().iter().cloned());
736    dependencies.sort();
737    dependencies.dedup();
738    Ok(dependencies)
739}
740
741pub(super) fn split_namespace_form(source: &str) -> Result<(&str, &str), String> {
742    let start = source.find("(ns ").ok_or("HAL module is missing ns form")?;
743    let mut depth = 0usize;
744    let mut string = false;
745    let mut escape = false;
746    for (offset, ch) in source[start..].char_indices() {
747        if string {
748            if escape {
749                escape = false;
750            } else if ch == '\\' {
751                escape = true;
752            } else if ch == '"' {
753                string = false;
754            }
755            continue;
756        }
757        match ch {
758            '"' => string = true,
759            '(' => depth += 1,
760            ')' => {
761                depth = depth.checked_sub(1).ok_or("invalid ns form")?;
762                if depth == 0 {
763                    let end = start + offset + ch.len_utf8();
764                    return Ok((&source[start..end], &source[end..]));
765                }
766            }
767            _ => {}
768        }
769    }
770    Err("unterminated ns form".into())
771}
772
773fn put_u32(output: &mut Vec<u8>, value: usize) -> Result<(), String> {
774    let value = u32::try_from(value).map_err(|_| "foundation bundle exceeds u32 limits")?;
775    output.extend_from_slice(&value.to_le_bytes());
776    Ok(())
777}
778
779fn put_bytes(output: &mut Vec<u8>, value: &[u8]) -> Result<(), String> {
780    put_u32(output, value.len())?;
781    output.extend_from_slice(value);
782    Ok(())
783}
784
785fn take_u32(input: &mut &[u8]) -> Result<u32, String> {
786    let bytes = take(input, 4)?;
787    Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
788}
789
790fn take_bytes<'a>(input: &mut &'a [u8]) -> Result<&'a [u8], String> {
791    let len = take_u32(input)? as usize;
792    take(input, len)
793}
794
795fn take_string(input: &mut &[u8]) -> Result<String, String> {
796    String::from_utf8(take_bytes(input)?.to_vec())
797        .map_err(|_| "foundation bundle contains invalid UTF-8".into())
798}
799
800fn take<'a>(input: &mut &'a [u8], len: usize) -> Result<&'a [u8], String> {
801    if input.len() < len {
802        return Err("truncated HBX0 bytecode bundle".into());
803    }
804    let (value, rest) = input.split_at(len);
805    *input = rest;
806    Ok(value)
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812
813    const COMPILER_GATE_STACK_SIZE: usize = 64 * 1024 * 1024;
814
815    fn on_compiler_gate_stack(test: impl FnOnce() + Send + 'static) {
816        std::thread::Builder::new()
817            .name("foundation-bytecode-compiler-gate".into())
818            // Compiling the complete portable library exercises the recursive
819            // debug evaluator used to establish macro and declaration state.
820            // Keep that test-only headroom local instead of requiring callers
821            // to raise RUST_MIN_STACK for the entire test process.
822            .stack_size(COMPILER_GATE_STACK_SIZE)
823            .spawn(test)
824            .expect("spawn foundation compiler gate")
825            .join()
826            .expect("foundation compiler gate panicked");
827    }
828
829    #[test]
830    fn empty_host_bundle_round_trips_against_the_native_registry() {
831        on_compiler_gate_stack(|| {
832            let sources = embedded_standard_library_sources();
833            assert!(
834                sources.is_empty(),
835                "the standalone host embeds no HAL source"
836            );
837            let bytes = compile_bytecode_bundle(&sources).expect("compile empty host bundle");
838            let mut runtime = Runtime::core();
839            eval_bytecode_bundle(&mut runtime, &bytes).expect("load empty host bundle");
840            assert!(!runtime.bytecode_resources.contains_key("std.foundation"));
841            assert_eq!(
842                runtime.eval_native("(std.native.String/upper \"hara\")"),
843                Ok("\"HARA\"".into())
844            );
845        });
846    }
847
848    #[test]
849    fn bytecode_index_has_no_implicit_foundation_module() {
850        let modules = decode(&compile_bytecode_bundle(&[]).expect("compile empty host bundle"))
851            .expect("decode empty host bundle");
852        assert!(modules.is_empty());
853    }
854
855    #[test]
856    fn bundle_encoding_is_deterministic() {
857        let sources = [ModuleSource {
858            resource: "example.deterministic",
859            source: "(ns example.deterministic) (def answer 42)",
860        }];
861        let first = compile_bytecode_bundle(&sources).expect("first deterministic bundle");
862        let second = compile_bytecode_bundle(&sources).expect("second deterministic bundle");
863        assert_eq!(first, second);
864    }
865
866    #[test]
867    fn foundation_package_compiles_the_root_before_companions() {
868        let sources = [
869            ModuleSource {
870                resource: "std.foundation.bootstrap",
871                source: "(ns std.foundation.bootstrap) (def ready (str/starts-with? \"hara\" \"ha\"))",
872            },
873            ModuleSource {
874                resource: "std.foundation.string",
875                source: "(ns std.foundation.string (:config {:set-global-alias str})) (defn starts-with? [value prefix] true)",
876            },
877            ModuleSource {
878                resource: "std.foundation",
879                source: "(ns std.foundation) (def foundation-ready true)",
880            },
881        ];
882
883        let bytes = compile_package_bytecode_bundle(&sources, &sources)
884            .expect("compile source-owned Foundation package");
885        let modules = decode(&bytes).expect("decode Foundation package");
886        assert_eq!(modules[0].resource, "std.foundation");
887        assert!(modules
888            .iter()
889            .any(|module| module.resource == "std.foundation.string"));
890
891        let mut runtime = Runtime::core();
892        eval_bytecode_bundle(&mut runtime, &bytes).expect("load Foundation package");
893        runtime
894            .load_bytecode_resource("std.foundation.bootstrap")
895            .expect("load Foundation companion");
896        assert!(runtime.use_namespace("std.foundation.bootstrap"));
897        assert_eq!(runtime.eval_native("ready").unwrap(), "true");
898    }
899
900    #[test]
901    fn package_bundle_preserves_forward_globals_until_their_runtime_call_site() {
902        let sources = [ModuleSource {
903            resource: "demo.forward",
904            source: "(ns demo.forward) (defn answer [] (increment 41)) (defn increment [value] (+ value 1))",
905        }];
906        let bytes = compile_package_bytecode_bundle(&sources, &sources)
907            .expect("compile package with a forward global");
908        let mut runtime = Runtime::core();
909        eval_bytecode_bundle(&mut runtime, &bytes).expect("index forward package bundle");
910        runtime
911            .load_bytecode_resource("demo.forward")
912            .expect("load forward package module");
913        assert!(runtime.use_namespace("demo.forward"));
914        assert_eq!(runtime.eval_native("(answer)").unwrap(), "42");
915    }
916
917    #[test]
918    fn package_bundle_bootstraps_context_foundation_for_an_isolated_package() {
919        let context = [
920            ModuleSource {
921                resource: "std.foundation",
922                source: "(ns std.foundation) (defn atom [value] (Base/atom value)) (defmacro foundation-identity [value] value)",
923            },
924            ModuleSource {
925                resource: "demo.config",
926                source: "(ns demo.config) (def state (foundation-identity (atom {})))",
927            },
928        ];
929        let sources = [context[1]];
930        let bytes = compile_package_bytecode_bundle(&context, &sources)
931            .expect("compile package with intrinsic Foundation context");
932        let modules = decode(&bytes).expect("decode isolated package bundle");
933        assert_eq!(modules.len(), 1);
934        assert_eq!(modules[0].resource, "demo.config");
935    }
936
937    #[test]
938    fn package_bundle_loads_context_macros_before_selected_modules() {
939        let context = [
940            ModuleSource {
941                resource: "demo.context",
942                source: "(ns demo.context) (defmacro context-identity [value] value)",
943            },
944            ModuleSource {
945                resource: "demo.client",
946                source: "(ns demo.client (:require [demo.context :refer-macros [context-identity]])) (def answer (context-identity 42))",
947            },
948        ];
949        let bytes = compile_package_bytecode_bundle(&context, &context[1..])
950            .expect("compile selected module against context macro");
951        let modules = decode(&bytes).expect("decode context macro package");
952        assert_eq!(modules.len(), 1);
953        assert_eq!(modules[0].resource, "demo.client");
954    }
955
956    #[test]
957    fn stale_lazy_bytecode_yields_to_registered_source() {
958        let sources = [ModuleSource {
959            resource: "example.stale",
960            source: "(ns example.stale) (def answer 41)",
961        }];
962        let bytes = compile_bytecode_bundle(&sources).expect("compile stale fixture");
963        let mut runtime = Runtime::core();
964        runtime.register_resource("example.stale", "(ns example.stale) (def answer 42)");
965
966        eval_bytecode_bundle(&mut runtime, &bytes).expect("index bundle");
967
968        assert!(!runtime.bytecode_resources.contains_key("example.stale"));
969        assert_eq!(
970            runtime
971                .eval_native("(require [example.stale :as stale]) stale/answer")
972                .unwrap(),
973            "42"
974        );
975    }
976
977    #[test]
978    fn eager_failure_rolls_back_the_whole_bundle() {
979        let mut compiler = Runtime::core();
980        compiler.use_namespace("example.good");
981        let good_artifact = compiler
982            .compile_bytecode_artifact("(def marker 42)")
983            .expect("compile successful eager module");
984        compiler.use_namespace("example.bad");
985        let bad_artifact = compiler
986            .compile_bytecode_artifact("(throw \"boom\")")
987            .expect("compile failing eager module");
988        let good_digest = Sha256::digest(b"good").into();
989        let bad_digest = Sha256::digest(b"bad").into();
990        let modules = [
991            BytecodeBundleModule {
992                resource: "example.good".into(),
993                namespace_form: "(ns example.good)".into(),
994                source_digest: good_digest,
995                dependencies: vec![],
996                eager: true,
997                artifact: good_artifact,
998            },
999            BytecodeBundleModule {
1000                resource: "example.bad".into(),
1001                namespace_form: "(ns example.bad)".into(),
1002                source_digest: bad_digest,
1003                dependencies: vec![],
1004                eager: true,
1005                artifact: bad_artifact,
1006            },
1007        ];
1008        let bytes = encode_bytecode_bundle(&modules).expect("encode transactional fixture");
1009        let mut runtime = Runtime::core();
1010        let namespaces_before = runtime
1011            .namespace_registry
1012            .all()
1013            .into_iter()
1014            .map(|namespace| namespace.name().as_str().to_owned())
1015            .collect::<std::collections::HashSet<_>>();
1016
1017        let error = eval_bytecode_bundle(&mut runtime, &bytes).unwrap_err();
1018
1019        assert!(error.contains("example.bad"), "{error}");
1020        assert!(!runtime.bytecode_resources.contains_key("example.good"));
1021        assert!(!runtime.bytecode_resources.contains_key("example.bad"));
1022        assert!(!runtime.loaded_resources.contains("example.good"));
1023        assert!(!runtime.loaded_resources.contains("example.bad"));
1024        assert_eq!(
1025            runtime
1026                .namespace_registry
1027                .all()
1028                .into_iter()
1029                .map(|namespace| namespace.name().as_str().to_owned())
1030                .collect::<std::collections::HashSet<_>>(),
1031            namespaces_before
1032        );
1033        assert_eq!(runtime.namespace_registry.current().name().as_str(), "user");
1034    }
1035
1036    #[test]
1037    fn lazy_module_loads_dependency_before_consumer() {
1038        let sources = [
1039            ModuleSource {
1040                resource: "example.protocol",
1041                source: "(ns example.protocol) (defn emit-form [value] value)",
1042            },
1043            ModuleSource {
1044                resource: "example.emit",
1045                source: "(ns example.emit (:require [example.protocol :as compiler])) (defn emit [value] (compiler/emit-form value))",
1046            },
1047        ];
1048        let bytes = compile_bytecode_bundle(&sources).expect("compile lazy protocol fixture");
1049        let mut runtime = Runtime::core();
1050        eval_bytecode_bundle(&mut runtime, &bytes).expect("index lazy protocol fixture");
1051
1052        runtime
1053            .load_bytecode_resource("example.emit")
1054            .expect("load protocol consumer and dependency");
1055
1056        assert!(runtime
1057            .namespace_registry
1058            .find("example.protocol")
1059            .is_some());
1060        assert!(runtime.namespace_registry.find("example.emit").is_some());
1061    }
1062
1063    #[test]
1064    fn lazy_alias_compiles_without_an_eager_edge_and_loads_on_first_call() {
1065        let sources = [
1066            ModuleSource {
1067                resource: "example.lazy.target",
1068                source: "(ns example.lazy.target) (defn answer [] 42)",
1069            },
1070            ModuleSource {
1071                resource: "example.lazy.client",
1072                source: "(ns example.lazy.client (:require [example.lazy.target :as target :lazy true])) (defn answer [] (target/answer))",
1073            },
1074        ];
1075        let bytes = compile_bytecode_bundle(&sources).expect("compile lazy alias fixture");
1076        let modules = decode(&bytes).expect("decode lazy alias fixture");
1077        let client = modules
1078            .iter()
1079            .find(|module| module.resource == "example.lazy.client")
1080            .expect("client module");
1081        assert!(client.dependencies.is_empty());
1082
1083        let mut runtime = Runtime::core();
1084        eval_bytecode_bundle(&mut runtime, &bytes).expect("index lazy alias fixture");
1085        runtime
1086            .load_bytecode_resource("example.lazy.client")
1087            .expect("load lazy client");
1088        assert!(runtime
1089            .namespace_registry
1090            .find("example.lazy.target")
1091            .is_none());
1092        assert!(runtime.use_namespace("example.lazy.client"));
1093        assert_eq!(runtime.eval_native("(answer)").unwrap(), "42");
1094        assert!(runtime
1095            .namespace_registry
1096            .find("example.lazy.target")
1097            .is_some());
1098    }
1099
1100    #[test]
1101    fn bundle_compilation_orders_eager_dependencies_before_consumers() {
1102        let sources = [
1103            ModuleSource {
1104                resource: "example.client",
1105                source: "(ns example.client (:require [example.target :as target])) (def answer target/answer)",
1106            },
1107            ModuleSource {
1108                resource: "example.target",
1109                source: "(ns example.target) (def answer 42)",
1110            },
1111        ];
1112        let bytes = compile_bytecode_bundle(&sources).expect("compile dependency fixture");
1113        let modules = decode(&bytes).expect("decode dependency fixture");
1114        assert_eq!(modules[0].resource, "example.target");
1115        assert_eq!(modules[1].resource, "example.client");
1116    }
1117
1118    #[test]
1119    fn eager_host_module_inventory_is_empty() {
1120        let sources = embedded_standard_library_sources()
1121            .into_iter()
1122            .filter(|source| {
1123                source.resource == "std.foundation"
1124                    || EAGER_HAL_RESOURCES.contains(&source.resource)
1125            })
1126            .collect::<Vec<_>>();
1127        assert!(
1128            sources.is_empty(),
1129            "the standalone host embeds no eager HAL modules"
1130        );
1131        let bytes = compile_bytecode_bundle(&sources).expect("compile empty eager inventory");
1132        let mut runtime = Runtime::core();
1133        eval_bytecode_bundle(&mut runtime, &bytes).expect("load empty eager inventory");
1134        assert!(runtime
1135            .namespace_registry
1136            .find("std.foundation.string")
1137            .is_none());
1138    }
1139
1140    #[cfg(feature = "tracing-jit")]
1141    #[test]
1142    fn hbx_installed_functions_remain_eligible_for_jit_compilation() {
1143        let mut compiler = Runtime::core();
1144        compiler.use_namespace("example.jit");
1145        let artifact = compiler
1146            .compile_bytecode_artifact(
1147                "(defn sum-to [n] (loop [i 0 total 0] (if (< i n) (recur (+ i 1) (+ total i)) total)))",
1148            )
1149            .expect("compile hot bundle function");
1150        let bytes = encode_bytecode_bundle(&[BytecodeBundleModule {
1151            resource: "example.jit".into(),
1152            namespace_form: "(ns example.jit)".into(),
1153            source_digest: Sha256::digest(b"example.jit hot function").into(),
1154            dependencies: vec![],
1155            eager: true,
1156            artifact,
1157        }])
1158        .expect("encode eager JIT fixture");
1159        let mut runtime = Runtime::core();
1160
1161        eval_bytecode_bundle(&mut runtime, &bytes).expect("load eager JIT fixture through HBX");
1162        assert_eq!(
1163            runtime.eval_native("(example.jit/sum-to 100)").unwrap(),
1164            "4950"
1165        );
1166        let telemetry = crate::vm::machine::active_jit_telemetry();
1167        assert!(
1168            crate::vm::machine::active_compiled_trace_count() > 0,
1169            "an HBC function installed through HBX must retain its program and JIT state: {telemetry:?}"
1170        );
1171    }
1172
1173    #[test]
1174    fn embedded_bundle_has_no_foundation_bootstrap() {
1175        on_compiler_gate_stack(|| {
1176            let sources = embedded_standard_library_sources();
1177            assert!(sources.is_empty());
1178            let bytes = compile_bytecode_bundle(&sources).expect("compile empty host bootstrap");
1179            let modules = decode(&bytes).expect("decode empty host bootstrap");
1180            let actual = modules
1181                .iter()
1182                .map(|module| module.resource.as_str())
1183                .collect::<Vec<_>>();
1184            assert_eq!(
1185                actual.len(),
1186                sources.len(),
1187                "bundle inventory must be exact"
1188            );
1189            let mut inventory = actual.clone();
1190            inventory.sort_unstable();
1191            assert!(inventory.is_empty());
1192            assert!(crate::FOUNDATION_BOOTSTRAP_INVENTORY.is_empty());
1193        });
1194    }
1195
1196    #[test]
1197    fn empty_host_bundle_has_no_global_reads() {
1198        on_compiler_gate_stack(|| {
1199            let bytes = compile_bytecode_bundle(&[]).expect("compile empty host bundle");
1200            for module in decode(&bytes).expect("decode empty host bundle") {
1201                let program = crate::vm::decode_program(&module.artifact)
1202                    .unwrap_or_else(|error| panic!("decode {}: {error}", module.resource));
1203                assert_eq!(program.namespace.as_deref(), Some(module.resource.as_str()));
1204                for function in &program.functions {
1205                    for instruction in &function.code {
1206                        if let crate::vm::Instruction::GetGlobal(index) = instruction {
1207                            let name = program.constants[*index as usize].display();
1208                            assert!(
1209                                name.contains('/'),
1210                                "{} contains caller-relative global read {name}",
1211                                module.resource
1212                            );
1213                        }
1214                    }
1215                }
1216            }
1217        });
1218    }
1219}