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    let mut runtime = Runtime::new();
100    let ordered = foundation_root_first(sources);
101    for source in context {
102        runtime.register_resource(source.resource, source.source);
103    }
104    #[cfg(not(target_arch = "wasm32"))]
105    if package_needs_foundation_bootstrap(context, &ordered) {
106        runtime.bootstrap_source_foundation()?;
107    }
108    compile_bytecode_bundle_with_runtime(&mut runtime, context, &ordered)
109}
110
111#[cfg(not(target_arch = "wasm32"))]
112fn package_needs_foundation_bootstrap(
113    context: &[ModuleSource<'_>],
114    sources: &[ModuleSource<'_>],
115) -> bool {
116    let emits_foundation = sources
117        .iter()
118        .any(|source| source.resource == "std.foundation");
119    !emits_foundation
120        && context
121            .iter()
122            .any(|source| source.resource == "std.foundation")
123}
124
125fn foundation_root_first<'a>(sources: &[ModuleSource<'a>]) -> Vec<ModuleSource<'a>> {
126    if !sources
127        .iter()
128        .any(|source| source.resource == "std.foundation")
129    {
130        return sources.to_vec();
131    }
132    let mut ordered = Vec::with_capacity(sources.len());
133    for resource in std::iter::once("std.foundation").chain(EAGER_HAL_RESOURCES.iter().copied()) {
134        if let Some(source) = sources.iter().find(|source| source.resource == resource) {
135            ordered.push(*source);
136        }
137    }
138    let remaining = sources
139        .iter()
140        .filter(|source| {
141            !ordered
142                .iter()
143                .any(|ordered| ordered.resource == source.resource)
144        })
145        .copied()
146        .collect::<Vec<_>>();
147    ordered.extend(remaining);
148    ordered
149}
150
151fn compile_bytecode_bundle_with_runtime(
152    runtime: &mut Runtime,
153    context: &[ModuleSource<'_>],
154    sources: &[ModuleSource<'_>],
155) -> Result<Vec<u8>, String> {
156    for source in context {
157        runtime.register_resource(source.resource, source.source);
158    }
159    let mut encoded = Vec::new();
160    for index in order_module_sources(sources)? {
161        let source = &sources[index];
162        let (namespace_form, body) = split_namespace_form(source.source)?;
163        runtime
164            .eval_text(namespace_form)
165            .map_err(|error| format!("{}: namespace declaration: {error}", source.resource))?;
166        // Required modules and macro expansion are allowed to select their
167        // own namespaces. Pin compilation to the module being emitted so
168        // aliases become canonical globals owned by its declaration.
169        runtime.use_namespace(source.resource);
170        let artifact = core::with_definition_origin(kernel::VarOrigin::HalFallback, || {
171            runtime.compile_package_bytecode_artifact(body)
172        })
173        .map_err(|error| format!("{}: bytecode compilation: {error}", source.resource))?;
174        core::with_definition_origin(kernel::VarOrigin::HalFallback, || {
175            runtime.eval_bytecode_artifact(&artifact)
176        })
177        .map_err(|error| format!("{}: bytecode execution: {error}", source.resource))?;
178        let source_digest: [u8; 32] = Sha256::digest(source.source.as_bytes()).into();
179        let dependencies = namespace_dependencies(namespace_form)?;
180        let eager =
181            source.resource == "std.foundation" || EAGER_HAL_RESOURCES.contains(&source.resource);
182        encoded.push(BytecodeBundleModule {
183            resource: source.resource.to_owned(),
184            namespace_form: namespace_form.to_owned(),
185            source_digest,
186            dependencies,
187            eager,
188            artifact,
189        });
190    }
191    encode_bytecode_bundle(&encoded)
192}
193
194pub fn compile_embedded_foundation_bootstrap_bundle() -> Result<Vec<u8>, String> {
195    compile_bytecode_bundle(&embedded_foundation_bootstrap_sources())
196}
197
198/// Compiles the immutable CLI and `code.test` closure against the already
199/// bootstrapped Foundation context. The resulting bundle is installed lazily,
200/// so a test or CLI process pays only for the namespaces it actually requires.
201pub fn compile_embedded_cli_bundle() -> Result<Vec<u8>, String> {
202    let foundation = embedded_foundation_bootstrap_sources();
203    let cli = embedded_cli_sources();
204    let mut context = foundation;
205    context.extend(cli.iter().copied());
206    compile_package_bytecode_bundle(&context, &cli)
207}
208
209/// Compatibility name retained for embedding hosts built against the original
210/// standard-library bundle API. The embedded artifact is now Foundation-only.
211pub fn compile_embedded_standard_library_bundle() -> Result<Vec<u8>, String> {
212    compile_embedded_foundation_bootstrap_bundle()
213}
214
215/// Compatibility name retained for callers that previously inspected the
216/// embedded standard-library sources.
217pub fn embedded_standard_library_sources() -> Vec<ModuleSource<'static>> {
218    embedded_foundation_bootstrap_sources()
219}
220
221pub fn eval_bytecode_bundle(runtime: &mut Runtime, bytes: &[u8]) -> Result<(), String> {
222    let modules = decode(bytes)?;
223    let mut names = std::collections::HashSet::with_capacity(modules.len());
224    for module in &modules {
225        if !names.insert(module.resource.clone()) {
226            return Err(format!(
227                "duplicate bytecode bundle module: {}",
228                module.resource
229            ));
230        }
231    }
232    let namespaces_before = runtime.namespace_registry.snapshot();
233    let environment_before = runtime.execution.snapshot();
234    let macros_before = runtime.macros.borrow().clone();
235    let protocols_before = runtime.protocols.snapshot();
236    let multimethods_before = core::snapshot_multimethods();
237    let resources_before = runtime.bytecode_resources.clone();
238    let loaded_before = runtime.loaded_resources.clone();
239    let loaded = (|| {
240        for module in &modules {
241            let source = if let Some(source) = runtime.resources.get(&module.resource) {
242                Some(source.clone())
243            } else {
244                #[cfg(not(target_arch = "wasm32"))]
245                {
246                    runtime
247                        .source_catalog
248                        .as_ref()
249                        .and_then(|catalog| catalog.path(&module.resource))
250                        .as_ref()
251                        .map(|path| {
252                            std::fs::read_to_string(path).map_err(|error| {
253                                format!("cannot read bundled source {}: {error}", path.display())
254                            })
255                        })
256                        .transpose()?
257                }
258                #[cfg(target_arch = "wasm32")]
259                {
260                    None
261                }
262            };
263            let source_is_current = source
264                .as_deref()
265                .map(|source| {
266                    let digest: [u8; 32] = Sha256::digest(source.as_bytes()).into();
267                    digest == module.source_digest
268                })
269                .unwrap_or(true);
270            if !source_is_current {
271                if module.eager {
272                    return Err(format!(
273                        "stale eager bytecode bundle module: {}",
274                        module.resource
275                    ));
276                }
277                continue;
278            }
279            runtime.register_bytecode_resource(
280                module.resource.clone(),
281                module.namespace_form.clone(),
282                module.artifact.clone(),
283            );
284        }
285        for module in modules.iter().filter(|module| module.eager) {
286            core::with_definition_origin(kernel::VarOrigin::HalFallback, || {
287                runtime.load_bytecode_resource(&module.resource).map(|_| ())
288            })
289            .map_err(|error| format!("{}: {error}", module.resource))?;
290            runtime.loaded_resources.insert(module.resource.clone());
291        }
292        runtime.use_namespace("user");
293        Ok(())
294    })();
295    if let Err(error) = loaded {
296        runtime.namespace_registry.restore(namespaces_before);
297        runtime.execution.restore(environment_before);
298        *runtime.macros.borrow_mut() = macros_before;
299        runtime.protocols.restore(protocols_before);
300        core::restore_multimethods(multimethods_before);
301        runtime.bytecode_resources = resources_before;
302        runtime.loaded_resources = loaded_before;
303        return Err(error);
304    }
305    Ok(())
306}
307
308/// Transactionally load a fully eager HBX0 application bundle into an
309/// embedding host's existing namespace and protocol registries.
310///
311/// The ordinary [`eval_bytecode_bundle`] API additionally indexes lazy
312/// standard-library resources on a [`Runtime`]. Worker hosts such as Hoplite
313/// already own their registries and package every application module eagerly,
314/// so this narrower entry point preserves that ownership without falling back
315/// to source compilation.
316pub fn eval_eager_bytecode_bundle_with_registries(
317    namespaces: &kernel::NamespaceRegistry<core::Value>,
318    protocols: &core::ProtocolRegistry,
319    bytes: &[u8],
320) -> Result<(), String> {
321    let modules = decode(bytes)?;
322    if let Some(module) = modules.iter().find(|module| !module.eager) {
323        return Err(format!(
324            "embedding bundle module must be eager: {}",
325            module.resource
326        ));
327    }
328    let mut positions = std::collections::HashMap::with_capacity(modules.len());
329    for (index, module) in modules.iter().enumerate() {
330        if positions.insert(module.resource.as_str(), index).is_some() {
331            return Err(format!(
332                "duplicate bytecode bundle module: {}",
333                module.resource
334            ));
335        }
336    }
337    for (index, module) in modules.iter().enumerate() {
338        for dependency in &module.dependencies {
339            if positions
340                .get(dependency.as_str())
341                .is_some_and(|dependency_index| *dependency_index >= index)
342            {
343                return Err(format!(
344                    "{}: bundled dependency must appear first: {dependency}",
345                    module.resource
346                ));
347            }
348        }
349    }
350    let programs = modules
351        .iter()
352        .map(|module| {
353            crate::vm::decode_program(&module.artifact)
354                .map(std::rc::Rc::new)
355                .map_err(|error| format!("{}: invalid bytecode artifact: {error}", module.resource))
356        })
357        .collect::<Result<Vec<_>, _>>()?;
358    let namespaces_before = namespaces.snapshot();
359    let protocols_before = protocols.snapshot();
360    let multimethods_before = core::snapshot_multimethods();
361    let loaded = (|| {
362        for (module, program) in modules.iter().zip(programs) {
363            let forms = kernel::parse_forms(&module.namespace_form)
364                .map_err(|error| format!("{}: namespace declaration: {error}", module.resource))?;
365            if forms.len() != 1 {
366                return Err(format!(
367                    "{}: bundle namespace declaration must contain exactly one form",
368                    module.resource
369                ));
370            }
371            let mut environment = std::collections::HashMap::new();
372            core::with_namespace_registry(namespaces, || {
373                core::with_protocols(protocols, || core::eval(&forms[0], &mut environment))
374            })
375            .map_err(|error| format!("{}: namespace declaration: {error}", module.resource))?;
376            core::with_namespace_registry(namespaces, || {
377                core::with_protocols(protocols, || {
378                    crate::vm::execute_program_with_globals(program, namespaces)
379                        .map_err(|error| error.to_string())
380                })
381            })
382            .map_err(|error| format!("{}: bytecode execution: {error}", module.resource))?;
383        }
384        Ok(())
385    })();
386    if let Err(error) = loaded {
387        namespaces.restore(namespaces_before);
388        protocols.restore(protocols_before);
389        core::restore_multimethods(multimethods_before);
390        return Err(error);
391    }
392    Ok(())
393}
394
395/// Encode modules into the deterministic, checksummed HBX0 container shared by
396/// the Rust, Truffle/native-image, and embedding runtimes.
397pub fn encode_bytecode_bundle(modules: &[BytecodeBundleModule]) -> Result<Vec<u8>, String> {
398    let modules = canonical_modules(modules)?;
399    let mut payload = Vec::new();
400    put_u32(&mut payload, modules.len())?;
401    for module in &modules {
402        put_bytes(&mut payload, module.resource.as_bytes())?;
403        put_bytes(&mut payload, module.namespace_form.as_bytes())?;
404        payload.extend_from_slice(&module.source_digest);
405        put_u32(&mut payload, module.dependencies.len())?;
406        for dependency in &module.dependencies {
407            put_bytes(&mut payload, dependency.as_bytes())?;
408        }
409        payload.push(u8::from(module.eager));
410        put_bytes(&mut payload, &module.artifact)?;
411    }
412    let checksum = Sha256::digest(&payload);
413    let mut output = Vec::with_capacity(4 + checksum.len() + payload.len());
414    output.extend_from_slice(MAGIC);
415    output.extend_from_slice(&checksum);
416    output.extend_from_slice(&payload);
417    Ok(output)
418}
419
420pub fn decode_bytecode_bundle(bytes: &[u8]) -> Result<Vec<BytecodeBundleModule>, String> {
421    if bytes.len() < 36 || &bytes[..4] != MAGIC {
422        return Err("invalid HBX0 bytecode bundle header".into());
423    }
424    let payload = &bytes[36..];
425    if Sha256::digest(payload)[..] != bytes[4..36] {
426        return Err("HBX0 bytecode bundle checksum mismatch".into());
427    }
428    let mut input = payload;
429    let count = take_u32(&mut input)? as usize;
430    let mut modules = Vec::with_capacity(count);
431    for _ in 0..count {
432        let resource = take_string(&mut input)?;
433        let namespace_form = take_string(&mut input)?;
434        let source_digest = take(&mut input, 32)?.try_into().unwrap();
435        let dependency_count = take_u32(&mut input)? as usize;
436        let dependencies = (0..dependency_count)
437            .map(|_| take_string(&mut input))
438            .collect::<Result<Vec<_>, _>>()?;
439        let eager = match take(&mut input, 1)?[0] {
440            0 => false,
441            1 => true,
442            _ => return Err("HBX0 bytecode bundle contains invalid eager flag".into()),
443        };
444        let artifact = take_bytes(&mut input)?.to_vec();
445        modules.push(BytecodeBundleModule {
446            resource,
447            namespace_form,
448            source_digest,
449            dependencies,
450            eager,
451            artifact,
452        });
453    }
454    if !input.is_empty() {
455        return Err("trailing bytes in HBX0 bytecode bundle".into());
456    }
457    validate_bundle_modules(&modules)?;
458    for module in &modules {
459        crate::vm::decode_program(&module.artifact)
460            .map_err(|error| format!("{}: invalid HBC0 artifact: {error}", module.resource))?;
461    }
462    Ok(modules)
463}
464
465fn decode(bytes: &[u8]) -> Result<Vec<BytecodeBundleModule>, String> {
466    decode_bytecode_bundle(bytes)
467}
468
469fn canonical_modules(
470    modules: &[BytecodeBundleModule],
471) -> Result<Vec<BytecodeBundleModule>, String> {
472    let mut by_resource = std::collections::BTreeMap::new();
473    for module in modules {
474        if by_resource
475            .insert(module.resource.clone(), module.clone())
476            .is_some()
477        {
478            return Err(format!("duplicate HBX0 module: {}", module.resource));
479        }
480        let mut dependencies = module.dependencies.clone();
481        dependencies.sort();
482        if dependencies.windows(2).any(|pair| pair[0] == pair[1]) {
483            return Err(format!("{}: duplicate HBX0 dependency", module.resource));
484        }
485    }
486    let mut ordered = Vec::with_capacity(modules.len());
487    while !by_resource.is_empty() {
488        let available = by_resource
489            .iter()
490            .find(|(_, module)| {
491                module
492                    .dependencies
493                    .iter()
494                    .all(|dependency| !by_resource.contains_key(dependency))
495            })
496            .map(|(resource, _)| resource.clone())
497            .ok_or("HBX0 module dependencies contain a cycle")?;
498        let mut module = by_resource.remove(&available).unwrap();
499        module.dependencies.sort();
500        ordered.push(module);
501    }
502    validate_bundle_modules(&ordered)?;
503    Ok(ordered)
504}
505
506fn validate_bundle_modules(modules: &[BytecodeBundleModule]) -> Result<(), String> {
507    let mut positions = std::collections::HashMap::with_capacity(modules.len());
508    for (index, module) in modules.iter().enumerate() {
509        if module.resource.is_empty() {
510            return Err("HBX0 module resource must not be empty".into());
511        }
512        if module.namespace_form.is_empty() {
513            return Err(format!(
514                "{}: HBX0 namespace form must not be empty",
515                module.resource
516            ));
517        }
518        if positions.insert(module.resource.as_str(), index).is_some() {
519            return Err(format!("duplicate HBX0 module: {}", module.resource));
520        }
521        if module
522            .dependencies
523            .windows(2)
524            .any(|pair| pair[0] >= pair[1])
525        {
526            return Err(format!(
527                "{}: HBX0 dependencies must be unique and sorted",
528                module.resource
529            ));
530        }
531    }
532    for (index, module) in modules.iter().enumerate() {
533        for dependency in &module.dependencies {
534            if positions
535                .get(dependency.as_str())
536                .is_some_and(|position| *position >= index)
537            {
538                return Err(format!(
539                    "{}: HBX0 dependency must appear first: {dependency}",
540                    module.resource
541                ));
542            }
543        }
544    }
545    Ok(())
546}
547
548fn standard_library_namespace(namespace: &str) -> bool {
549    ["std.", "code.", "db.", "lang."]
550        .iter()
551        .any(|prefix| namespace.starts_with(prefix))
552}
553
554pub(super) fn namespace_dependencies(namespace_form: &str) -> Result<Vec<String>, String> {
555    let forms = kernel::parse_forms(namespace_form)?;
556    let Some(kernel::Form::List(items)) = forms.first() else {
557        return Err("standard-library module has invalid ns form".into());
558    };
559    let config = kernel::GeneratedNamespaceConfig::configure_with(&items[2..], |_| true)?;
560    let mut dependencies = config.required_namespaces().to_vec();
561    dependencies.extend(config.used_namespaces().iter().cloned());
562    dependencies.sort();
563    dependencies.dedup();
564    Ok(dependencies)
565}
566
567pub(super) fn split_namespace_form(source: &str) -> Result<(&str, &str), String> {
568    let start = source.find("(ns ").ok_or("HAL module is missing ns form")?;
569    let mut depth = 0usize;
570    let mut string = false;
571    let mut escape = false;
572    for (offset, ch) in source[start..].char_indices() {
573        if string {
574            if escape {
575                escape = false;
576            } else if ch == '\\' {
577                escape = true;
578            } else if ch == '"' {
579                string = false;
580            }
581            continue;
582        }
583        match ch {
584            '"' => string = true,
585            '(' => depth += 1,
586            ')' => {
587                depth = depth.checked_sub(1).ok_or("invalid ns form")?;
588                if depth == 0 {
589                    let end = start + offset + ch.len_utf8();
590                    return Ok((&source[start..end], &source[end..]));
591                }
592            }
593            _ => {}
594        }
595    }
596    Err("unterminated ns form".into())
597}
598
599fn put_u32(output: &mut Vec<u8>, value: usize) -> Result<(), String> {
600    let value = u32::try_from(value).map_err(|_| "foundation bundle exceeds u32 limits")?;
601    output.extend_from_slice(&value.to_le_bytes());
602    Ok(())
603}
604
605fn put_bytes(output: &mut Vec<u8>, value: &[u8]) -> Result<(), String> {
606    put_u32(output, value.len())?;
607    output.extend_from_slice(value);
608    Ok(())
609}
610
611fn take_u32(input: &mut &[u8]) -> Result<u32, String> {
612    let bytes = take(input, 4)?;
613    Ok(u32::from_le_bytes(bytes.try_into().unwrap()))
614}
615
616fn take_bytes<'a>(input: &mut &'a [u8]) -> Result<&'a [u8], String> {
617    let len = take_u32(input)? as usize;
618    take(input, len)
619}
620
621fn take_string(input: &mut &[u8]) -> Result<String, String> {
622    String::from_utf8(take_bytes(input)?.to_vec())
623        .map_err(|_| "foundation bundle contains invalid UTF-8".into())
624}
625
626fn take<'a>(input: &mut &'a [u8], len: usize) -> Result<&'a [u8], String> {
627    if input.len() < len {
628        return Err("truncated HBX0 bytecode bundle".into());
629    }
630    let (value, rest) = input.split_at(len);
631    *input = rest;
632    Ok(value)
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638
639    const COMPILER_GATE_STACK_SIZE: usize = 64 * 1024 * 1024;
640
641    fn on_compiler_gate_stack(test: impl FnOnce() + Send + 'static) {
642        std::thread::Builder::new()
643            .name("foundation-bytecode-compiler-gate".into())
644            // Compiling the complete portable library exercises the recursive
645            // debug evaluator used to establish macro and declaration state.
646            // Keep that test-only headroom local instead of requiring callers
647            // to raise RUST_MIN_STACK for the entire test process.
648            .stack_size(COMPILER_GATE_STACK_SIZE)
649            .spawn(test)
650            .expect("spawn foundation compiler gate")
651            .join()
652            .expect("foundation compiler gate panicked");
653    }
654
655    #[test]
656    fn empty_host_bundle_round_trips_against_the_native_registry() {
657        on_compiler_gate_stack(|| {
658            let sources = embedded_standard_library_sources();
659            assert!(
660                sources.is_empty(),
661                "the standalone host embeds no HAL source"
662            );
663            let bytes = compile_bytecode_bundle(&sources).expect("compile empty host bundle");
664            let mut runtime = Runtime::core();
665            eval_bytecode_bundle(&mut runtime, &bytes).expect("load empty host bundle");
666            assert!(!runtime.bytecode_resources.contains_key("std.foundation"));
667            assert_eq!(
668                runtime.eval_native("(std.native.String/upper \"hara\")"),
669                Ok("\"HARA\"".into())
670            );
671        });
672    }
673
674    #[test]
675    fn bytecode_index_has_no_implicit_foundation_module() {
676        let modules = decode(&compile_bytecode_bundle(&[]).expect("compile empty host bundle"))
677            .expect("decode empty host bundle");
678        assert!(modules.is_empty());
679    }
680
681    #[test]
682    fn bundle_encoding_is_deterministic() {
683        let sources = [ModuleSource {
684            resource: "example.deterministic",
685            source: "(ns example.deterministic) (def answer 42)",
686        }];
687        let first = compile_bytecode_bundle(&sources).expect("first deterministic bundle");
688        let second = compile_bytecode_bundle(&sources).expect("second deterministic bundle");
689        assert_eq!(first, second);
690    }
691
692    #[test]
693    fn foundation_package_compiles_the_root_before_companions() {
694        let sources = [
695            ModuleSource {
696                resource: "std.foundation.bootstrap",
697                source: "(ns std.foundation.bootstrap) (def ready (str/starts-with? \"hara\" \"ha\"))",
698            },
699            ModuleSource {
700                resource: "std.foundation.string",
701                source: "(ns std.foundation.string (:config {:set-global-alias str})) (defn starts-with? [value prefix] true)",
702            },
703            ModuleSource {
704                resource: "std.foundation",
705                source: "(ns std.foundation) (def foundation-ready true)",
706            },
707        ];
708
709        let bytes = compile_package_bytecode_bundle(&sources, &sources)
710            .expect("compile source-owned Foundation package");
711        let modules = decode(&bytes).expect("decode Foundation package");
712        assert_eq!(modules[0].resource, "std.foundation");
713        assert!(modules
714            .iter()
715            .any(|module| module.resource == "std.foundation.string"));
716
717        let mut runtime = Runtime::core();
718        eval_bytecode_bundle(&mut runtime, &bytes).expect("load Foundation package");
719        runtime
720            .load_bytecode_resource("std.foundation.bootstrap")
721            .expect("load Foundation companion");
722        assert!(runtime.use_namespace("std.foundation.bootstrap"));
723        assert_eq!(runtime.eval_native("ready").unwrap(), "true");
724    }
725
726    #[test]
727    fn package_bundle_preserves_forward_globals_until_their_runtime_call_site() {
728        let sources = [ModuleSource {
729            resource: "demo.forward",
730            source: "(ns demo.forward) (defn answer [] (increment 41)) (defn increment [value] (+ value 1))",
731        }];
732        let bytes = compile_package_bytecode_bundle(&sources, &sources)
733            .expect("compile package with a forward global");
734        let mut runtime = Runtime::core();
735        eval_bytecode_bundle(&mut runtime, &bytes).expect("index forward package bundle");
736        runtime
737            .load_bytecode_resource("demo.forward")
738            .expect("load forward package module");
739        assert!(runtime.use_namespace("demo.forward"));
740        assert_eq!(runtime.eval_native("(answer)").unwrap(), "42");
741    }
742
743    #[test]
744    fn package_bundle_bootstraps_context_foundation_for_an_isolated_package() {
745        let context = [
746            ModuleSource {
747                resource: "std.foundation",
748                source: "(ns std.foundation) (defn atom [value] (Base/atom value)) (defmacro foundation-identity [value] value)",
749            },
750            ModuleSource {
751                resource: "demo.config",
752                source: "(ns demo.config) (def state (foundation-identity (atom {})))",
753            },
754        ];
755        let sources = [context[1]];
756        let bytes = compile_package_bytecode_bundle(&context, &sources)
757            .expect("compile package with intrinsic Foundation context");
758        let modules = decode(&bytes).expect("decode isolated package bundle");
759        assert_eq!(modules.len(), 1);
760        assert_eq!(modules[0].resource, "demo.config");
761    }
762
763    #[test]
764    fn stale_lazy_bytecode_yields_to_registered_source() {
765        let sources = [ModuleSource {
766            resource: "example.stale",
767            source: "(ns example.stale) (def answer 41)",
768        }];
769        let bytes = compile_bytecode_bundle(&sources).expect("compile stale fixture");
770        let mut runtime = Runtime::core();
771        runtime.register_resource("example.stale", "(ns example.stale) (def answer 42)");
772
773        eval_bytecode_bundle(&mut runtime, &bytes).expect("index bundle");
774
775        assert!(!runtime.bytecode_resources.contains_key("example.stale"));
776        assert_eq!(
777            runtime
778                .eval_native("(require [example.stale :as stale]) stale/answer")
779                .unwrap(),
780            "42"
781        );
782    }
783
784    #[test]
785    fn eager_failure_rolls_back_the_whole_bundle() {
786        let mut compiler = Runtime::core();
787        compiler.use_namespace("example.good");
788        let good_artifact = compiler
789            .compile_bytecode_artifact("(def marker 42)")
790            .expect("compile successful eager module");
791        compiler.use_namespace("example.bad");
792        let bad_artifact = compiler
793            .compile_bytecode_artifact("(throw \"boom\")")
794            .expect("compile failing eager module");
795        let good_digest = Sha256::digest(b"good").into();
796        let bad_digest = Sha256::digest(b"bad").into();
797        let modules = [
798            BytecodeBundleModule {
799                resource: "example.good".into(),
800                namespace_form: "(ns example.good)".into(),
801                source_digest: good_digest,
802                dependencies: vec![],
803                eager: true,
804                artifact: good_artifact,
805            },
806            BytecodeBundleModule {
807                resource: "example.bad".into(),
808                namespace_form: "(ns example.bad)".into(),
809                source_digest: bad_digest,
810                dependencies: vec![],
811                eager: true,
812                artifact: bad_artifact,
813            },
814        ];
815        let bytes = encode_bytecode_bundle(&modules).expect("encode transactional fixture");
816        let mut runtime = Runtime::core();
817        let namespaces_before = runtime
818            .namespace_registry
819            .all()
820            .into_iter()
821            .map(|namespace| namespace.name().as_str().to_owned())
822            .collect::<std::collections::HashSet<_>>();
823
824        let error = eval_bytecode_bundle(&mut runtime, &bytes).unwrap_err();
825
826        assert!(error.contains("example.bad"), "{error}");
827        assert!(!runtime.bytecode_resources.contains_key("example.good"));
828        assert!(!runtime.bytecode_resources.contains_key("example.bad"));
829        assert!(!runtime.loaded_resources.contains("example.good"));
830        assert!(!runtime.loaded_resources.contains("example.bad"));
831        assert_eq!(
832            runtime
833                .namespace_registry
834                .all()
835                .into_iter()
836                .map(|namespace| namespace.name().as_str().to_owned())
837                .collect::<std::collections::HashSet<_>>(),
838            namespaces_before
839        );
840        assert_eq!(runtime.namespace_registry.current().name().as_str(), "user");
841    }
842
843    #[test]
844    fn lazy_module_loads_dependency_before_consumer() {
845        let sources = [
846            ModuleSource {
847                resource: "example.protocol",
848                source: "(ns example.protocol) (defn emit-form [value] value)",
849            },
850            ModuleSource {
851                resource: "example.emit",
852                source: "(ns example.emit (:require [example.protocol :as compiler])) (defn emit [value] (compiler/emit-form value))",
853            },
854        ];
855        let bytes = compile_bytecode_bundle(&sources).expect("compile lazy protocol fixture");
856        let mut runtime = Runtime::core();
857        eval_bytecode_bundle(&mut runtime, &bytes).expect("index lazy protocol fixture");
858
859        runtime
860            .load_bytecode_resource("example.emit")
861            .expect("load protocol consumer and dependency");
862
863        assert!(runtime
864            .namespace_registry
865            .find("example.protocol")
866            .is_some());
867        assert!(runtime.namespace_registry.find("example.emit").is_some());
868    }
869
870    #[test]
871    fn lazy_alias_compiles_without_an_eager_edge_and_loads_on_first_call() {
872        let sources = [
873            ModuleSource {
874                resource: "example.lazy.target",
875                source: "(ns example.lazy.target) (defn answer [] 42)",
876            },
877            ModuleSource {
878                resource: "example.lazy.client",
879                source: "(ns example.lazy.client (:require [example.lazy.target :as target :lazy true])) (defn answer [] (target/answer))",
880            },
881        ];
882        let bytes = compile_bytecode_bundle(&sources).expect("compile lazy alias fixture");
883        let modules = decode(&bytes).expect("decode lazy alias fixture");
884        let client = modules
885            .iter()
886            .find(|module| module.resource == "example.lazy.client")
887            .expect("client module");
888        assert!(client.dependencies.is_empty());
889
890        let mut runtime = Runtime::core();
891        eval_bytecode_bundle(&mut runtime, &bytes).expect("index lazy alias fixture");
892        runtime
893            .load_bytecode_resource("example.lazy.client")
894            .expect("load lazy client");
895        assert!(runtime
896            .namespace_registry
897            .find("example.lazy.target")
898            .is_none());
899        assert!(runtime.use_namespace("example.lazy.client"));
900        assert_eq!(runtime.eval_native("(answer)").unwrap(), "42");
901        assert!(runtime
902            .namespace_registry
903            .find("example.lazy.target")
904            .is_some());
905    }
906
907    #[test]
908    fn bundle_compilation_orders_eager_dependencies_before_consumers() {
909        let sources = [
910            ModuleSource {
911                resource: "example.client",
912                source: "(ns example.client (:require [example.target :as target])) (def answer target/answer)",
913            },
914            ModuleSource {
915                resource: "example.target",
916                source: "(ns example.target) (def answer 42)",
917            },
918        ];
919        let bytes = compile_bytecode_bundle(&sources).expect("compile dependency fixture");
920        let modules = decode(&bytes).expect("decode dependency fixture");
921        assert_eq!(modules[0].resource, "example.target");
922        assert_eq!(modules[1].resource, "example.client");
923    }
924
925    #[test]
926    fn eager_host_module_inventory_is_empty() {
927        let sources = embedded_standard_library_sources()
928            .into_iter()
929            .filter(|source| {
930                source.resource == "std.foundation"
931                    || EAGER_HAL_RESOURCES.contains(&source.resource)
932            })
933            .collect::<Vec<_>>();
934        assert!(
935            sources.is_empty(),
936            "the standalone host embeds no eager HAL modules"
937        );
938        let bytes = compile_bytecode_bundle(&sources).expect("compile empty eager inventory");
939        let mut runtime = Runtime::core();
940        eval_bytecode_bundle(&mut runtime, &bytes).expect("load empty eager inventory");
941        assert!(runtime
942            .namespace_registry
943            .find("std.foundation.string")
944            .is_none());
945    }
946
947    #[cfg(feature = "tracing-jit")]
948    #[test]
949    fn hbx_installed_functions_remain_eligible_for_jit_compilation() {
950        let mut compiler = Runtime::core();
951        compiler.use_namespace("example.jit");
952        let artifact = compiler
953            .compile_bytecode_artifact(
954                "(defn sum-to [n] (loop [i 0 total 0] (if (< i n) (recur (+ i 1) (+ total i)) total)))",
955            )
956            .expect("compile hot bundle function");
957        let bytes = encode_bytecode_bundle(&[BytecodeBundleModule {
958            resource: "example.jit".into(),
959            namespace_form: "(ns example.jit)".into(),
960            source_digest: Sha256::digest(b"example.jit hot function").into(),
961            dependencies: vec![],
962            eager: true,
963            artifact,
964        }])
965        .expect("encode eager JIT fixture");
966        let mut runtime = Runtime::core();
967
968        eval_bytecode_bundle(&mut runtime, &bytes).expect("load eager JIT fixture through HBX");
969        assert_eq!(
970            runtime.eval_native("(example.jit/sum-to 100)").unwrap(),
971            "4950"
972        );
973        let telemetry = crate::vm::machine::active_jit_telemetry();
974        assert!(
975            crate::vm::machine::active_compiled_trace_count() > 0,
976            "an HBC function installed through HBX must retain its program and JIT state: {telemetry:?}"
977        );
978    }
979
980    #[test]
981    fn embedded_bundle_has_no_foundation_bootstrap() {
982        on_compiler_gate_stack(|| {
983            let sources = embedded_standard_library_sources();
984            assert!(sources.is_empty());
985            let bytes = compile_bytecode_bundle(&sources).expect("compile empty host bootstrap");
986            let modules = decode(&bytes).expect("decode empty host bootstrap");
987            let actual = modules
988                .iter()
989                .map(|module| module.resource.as_str())
990                .collect::<Vec<_>>();
991            assert_eq!(
992                actual.len(),
993                sources.len(),
994                "bundle inventory must be exact"
995            );
996            let mut inventory = actual.clone();
997            inventory.sort_unstable();
998            assert!(inventory.is_empty());
999            assert!(crate::FOUNDATION_BOOTSTRAP_INVENTORY.is_empty());
1000        });
1001    }
1002
1003    #[test]
1004    fn empty_host_bundle_has_no_global_reads() {
1005        on_compiler_gate_stack(|| {
1006            let bytes = compile_bytecode_bundle(&[]).expect("compile empty host bundle");
1007            for module in decode(&bytes).expect("decode empty host bundle") {
1008                let program = crate::vm::decode_program(&module.artifact)
1009                    .unwrap_or_else(|error| panic!("decode {}: {error}", module.resource));
1010                assert_eq!(program.namespace.as_deref(), Some(module.resource.as_str()));
1011                for function in &program.functions {
1012                    for instruction in &function.code {
1013                        if let crate::vm::Instruction::GetGlobal(index) = instruction {
1014                            let name = program.constants[*index as usize].display();
1015                            assert!(
1016                                name.contains('/'),
1017                                "{} contains caller-relative global read {name}",
1018                                module.resource
1019                            );
1020                        }
1021                    }
1022                }
1023            }
1024        });
1025    }
1026}