Skip to main content

lashlang/
artifact.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::{Arc, Mutex, OnceLock};
3
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use thiserror::Error;
7
8use crate::ast::{
9    AssignPathStep, BinaryOp, Declaration, Expr, LabelMetadata, ProcessDecl, Program,
10    ResourceRefExpr, TypeExpr, UnaryOp,
11};
12use crate::linker::{LashlangAbilities, LashlangHostCatalog, LashlangLanguageFeatures};
13
14pub const LASHLANG_SEMANTIC_HASH_VERSION: &str = "lashlang-semantic-v2";
15pub const LASHLANG_COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION");
16pub const LASHLANG_VM_ABI_VERSION: &str = "lashlang-vm-abi-v1";
17
18/// Durability tier of an execution path's wired store or effect host.
19///
20/// Durability is a property established by what the host wired, not a mode
21/// flag: each runtime trait reports the tier of the concrete implementation
22/// behind it, and the runtime validates that wiring is internally consistent.
23/// `Inline` covers in-memory / build-time wiring; `Durable` covers a
24/// crash-recoverable store or effect host (e.g. Sqlite-backed persistence or a
25/// Restate-backed effect host).
26#[derive(
27    Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
28)]
29#[serde(rename_all = "snake_case")]
30pub enum DurabilityTier {
31    #[default]
32    Inline,
33    Durable,
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
37#[serde(transparent)]
38pub struct ContentHash(String);
39
40impl ContentHash {
41    pub fn new(hex: impl Into<String>) -> Self {
42        Self(hex.into())
43    }
44
45    pub fn as_str(&self) -> &str {
46        &self.0
47    }
48}
49
50impl std::fmt::Display for ContentHash {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.write_str(&self.0)
53    }
54}
55
56#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
57#[serde(transparent)]
58pub struct ModuleRef(String);
59
60impl ModuleRef {
61    pub fn new(hash: &ContentHash) -> Self {
62        Self(format!("lashlang:v1:sha256:{hash}"))
63    }
64
65    pub fn as_str(&self) -> &str {
66        &self.0
67    }
68
69    pub fn hash_hex(&self) -> Option<&str> {
70        self.0.strip_prefix("lashlang:v1:sha256:")
71    }
72}
73
74impl std::fmt::Display for ModuleRef {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.write_str(&self.0)
77    }
78}
79
80#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
81pub struct ProcessRef {
82    pub component: ContentHash,
83    pub pos: u32,
84}
85
86impl ProcessRef {
87    pub fn new(component: ContentHash, pos: u32) -> Self {
88        Self { component, pos }
89    }
90}
91
92#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
93#[serde(transparent)]
94pub struct HostRequirementsRef(String);
95
96impl HostRequirementsRef {
97    pub fn new(hash: &ContentHash) -> Self {
98        Self(format!("lashlang-host-requirements:v1:sha256:{hash}"))
99    }
100
101    pub fn as_str(&self) -> &str {
102        &self.0
103    }
104}
105
106impl std::fmt::Display for HostRequirementsRef {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        f.write_str(&self.0)
109    }
110}
111
112#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
113pub struct HostRequirements {
114    #[serde(default)]
115    pub resources: LashlangHostCatalog,
116    #[serde(default)]
117    pub abilities: LashlangAbilities,
118    #[serde(default)]
119    pub language_features: LashlangLanguageFeatures,
120}
121
122#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
123pub struct ModuleExports {
124    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
125    pub processes: BTreeMap<String, ProcessRef>,
126}
127
128#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
129pub struct ModuleArtifact {
130    pub module_ref: ModuleRef,
131    pub host_requirements_ref: HostRequirementsRef,
132    pub host_requirements: HostRequirements,
133    pub exports: ModuleExports,
134    pub canonical_ir: Program,
135    #[serde(default, skip_serializing_if = "Vec::is_empty")]
136    pub dependencies: Vec<ModuleRef>,
137}
138
139impl ModuleArtifact {
140    pub fn from_program(program: Program) -> Result<Self, ModuleArtifactError> {
141        let canonical_ir = canonical_program_ir(program);
142        let requirements = host_requirements_for_program(&canonical_ir);
143        Self::from_canonical_ir_and_requirements(canonical_ir, requirements)
144    }
145
146    pub(crate) fn from_program_with_requirements(
147        program: Program,
148        requirements: HostRequirements,
149    ) -> Result<Self, ModuleArtifactError> {
150        let canonical_ir = canonical_program_ir(program);
151        Self::from_canonical_ir_and_requirements(canonical_ir, requirements)
152    }
153
154    fn from_canonical_ir_and_requirements(
155        canonical_ir: Program,
156        requirements: HostRequirements,
157    ) -> Result<Self, ModuleArtifactError> {
158        let host_requirements_ref = host_requirements_ref(&requirements);
159        let exports = module_exports(&canonical_ir);
160        let module_ref = module_ref(&canonical_ir, &host_requirements_ref, &exports);
161        Ok(Self {
162            module_ref,
163            host_requirements_ref,
164            host_requirements: requirements,
165            exports,
166            canonical_ir,
167            dependencies: Vec::new(),
168        })
169    }
170
171    pub fn process_ref(&self, process_name: &str) -> Option<&ProcessRef> {
172        self.exports.processes.get(process_name)
173    }
174
175    pub fn process_name_for_ref(&self, process_ref: &ProcessRef) -> Option<&str> {
176        self.exports
177            .processes
178            .iter()
179            .find_map(|(name, candidate)| (candidate == process_ref).then_some(name.as_str()))
180    }
181
182    pub fn verify(&self) -> Result<(), ModuleArtifactError> {
183        let rebuilt = Self::from_program_with_requirements(
184            self.canonical_ir.clone(),
185            self.host_requirements.clone(),
186        )?;
187        if rebuilt.module_ref != self.module_ref {
188            return Err(ModuleArtifactError::HashMismatch {
189                field: "module_ref",
190                expected: rebuilt.module_ref.to_string(),
191                actual: self.module_ref.to_string(),
192            });
193        }
194        if rebuilt.host_requirements_ref != self.host_requirements_ref {
195            return Err(ModuleArtifactError::HashMismatch {
196                field: "host_requirements_ref",
197                expected: rebuilt.host_requirements_ref.to_string(),
198                actual: self.host_requirements_ref.to_string(),
199            });
200        }
201        if rebuilt.exports != self.exports {
202            return Err(ModuleArtifactError::HashMismatch {
203                field: "exports",
204                expected: "canonical exports".to_string(),
205                actual: "artifact exports".to_string(),
206            });
207        }
208        Ok(())
209    }
210
211    pub fn to_store_bytes(&self) -> Result<Vec<u8>, ModuleArtifactError> {
212        self.verify()?;
213        serde_json::to_vec(self).map_err(|err| ModuleArtifactError::Codec(err.to_string()))
214    }
215
216    pub fn from_store_bytes(bytes: &[u8]) -> Result<Self, ModuleArtifactError> {
217        let artifact: Self = serde_json::from_slice(bytes)
218            .map_err(|err| ModuleArtifactError::Codec(err.to_string()))?;
219        artifact.verify()?;
220        Ok(artifact)
221    }
222}
223
224#[derive(Clone, Debug, Error, PartialEq, Eq)]
225pub enum ModuleArtifactError {
226    #[error("failed to encode module artifact: {0}")]
227    Codec(String),
228    #[error("module artifact {field} mismatch: expected {expected}, got {actual}")]
229    HashMismatch {
230        field: &'static str,
231        expected: String,
232        actual: String,
233    },
234}
235
236#[derive(Debug, Error)]
237pub enum ArtifactStoreError {
238    #[error("failed to encode lashlang artifact: {0}")]
239    Encode(String),
240    #[error("failed to decode lashlang artifact: {0}")]
241    Decode(String),
242    #[error("artifact store backend error: {0}")]
243    Backend(String),
244}
245
246impl From<ModuleArtifactError> for ArtifactStoreError {
247    fn from(value: ModuleArtifactError) -> Self {
248        match value {
249            ModuleArtifactError::Codec(message) => Self::Decode(message),
250            ModuleArtifactError::HashMismatch { .. } => Self::Decode(value.to_string()),
251        }
252    }
253}
254
255#[async_trait::async_trait]
256pub trait LashlangArtifactStore: Send + Sync {
257    /// Durability tier this artifact store provides; defaults to [`DurabilityTier::Inline`].
258    fn durability_tier(&self) -> DurabilityTier {
259        DurabilityTier::Inline
260    }
261
262    async fn put_module_artifact(
263        &self,
264        artifact: &ModuleArtifact,
265    ) -> Result<(), ArtifactStoreError>;
266
267    async fn get_module_artifact(
268        &self,
269        module_ref: &ModuleRef,
270    ) -> Result<Option<Arc<ModuleArtifact>>, ArtifactStoreError>;
271
272    async fn put_artifact_bytes(
273        &self,
274        artifact_ref: &str,
275        descriptor: &str,
276        bytes: &[u8],
277    ) -> Result<(), ArtifactStoreError>;
278
279    async fn get_artifact_bytes(
280        &self,
281        artifact_ref: &str,
282    ) -> Result<Option<Vec<u8>>, ArtifactStoreError>;
283}
284
285#[derive(Clone, Default)]
286pub struct InMemoryLashlangArtifactStore {
287    modules: Arc<Mutex<BTreeMap<ModuleRef, Arc<ModuleArtifact>>>>,
288    artifacts: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
289}
290
291impl InMemoryLashlangArtifactStore {
292    pub fn new() -> Self {
293        Self::default()
294    }
295}
296
297pub fn global_in_memory_lashlang_artifact_store() -> Arc<InMemoryLashlangArtifactStore> {
298    static STORE: OnceLock<Arc<InMemoryLashlangArtifactStore>> = OnceLock::new();
299    STORE
300        .get_or_init(|| Arc::new(InMemoryLashlangArtifactStore::new()))
301        .clone()
302}
303
304#[async_trait::async_trait]
305impl LashlangArtifactStore for InMemoryLashlangArtifactStore {
306    async fn put_module_artifact(
307        &self,
308        artifact: &ModuleArtifact,
309    ) -> Result<(), ArtifactStoreError> {
310        let mut modules = self
311            .modules
312            .lock()
313            .map_err(|_| ArtifactStoreError::Backend("artifact store lock poisoned".to_string()))?;
314        modules.insert(artifact.module_ref.clone(), Arc::new(artifact.clone()));
315        Ok(())
316    }
317
318    async fn get_module_artifact(
319        &self,
320        module_ref: &ModuleRef,
321    ) -> Result<Option<Arc<ModuleArtifact>>, ArtifactStoreError> {
322        let modules = self
323            .modules
324            .lock()
325            .map_err(|_| ArtifactStoreError::Backend("artifact store lock poisoned".to_string()))?;
326        Ok(modules.get(module_ref).cloned())
327    }
328
329    async fn put_artifact_bytes(
330        &self,
331        artifact_ref: &str,
332        _descriptor: &str,
333        bytes: &[u8],
334    ) -> Result<(), ArtifactStoreError> {
335        self.artifacts
336            .lock()
337            .map_err(|_| ArtifactStoreError::Backend("artifact store lock poisoned".to_string()))?
338            .insert(artifact_ref.to_string(), bytes.to_vec());
339        Ok(())
340    }
341
342    async fn get_artifact_bytes(
343        &self,
344        artifact_ref: &str,
345    ) -> Result<Option<Vec<u8>>, ArtifactStoreError> {
346        Ok(self
347            .artifacts
348            .lock()
349            .map_err(|_| ArtifactStoreError::Backend("artifact store lock poisoned".to_string()))?
350            .get(artifact_ref)
351            .cloned())
352    }
353}
354
355#[derive(Clone)]
356pub(crate) struct CompiledModuleContext {
357    pub(crate) module_ref: ModuleRef,
358    pub(crate) host_requirements_ref: HostRequirementsRef,
359    pub(crate) process_refs: BTreeMap<String, ProcessRef>,
360}
361
362impl From<&ModuleArtifact> for CompiledModuleContext {
363    fn from(value: &ModuleArtifact) -> Self {
364        Self {
365            module_ref: value.module_ref.clone(),
366            host_requirements_ref: value.host_requirements_ref.clone(),
367            process_refs: value.exports.processes.clone(),
368        }
369    }
370}
371
372pub fn canonical_program_ir(mut program: Program) -> Program {
373    program.declaration_spans.clear();
374    program.expression_spans.clear();
375    program
376}
377
378pub fn host_requirements_for_program(program: &Program) -> HostRequirements {
379    RequirementsCollector::new(program).collect()
380}
381
382pub(crate) fn host_requirements_for_program_with_catalog(
383    program: &Program,
384    catalog: &LashlangHostCatalog,
385) -> HostRequirements {
386    RequirementsCollector::new(program)
387        .with_resource_catalog(catalog)
388        .collect()
389}
390
391fn module_exports(program: &Program) -> ModuleExports {
392    let mut exports = ModuleExports::default();
393    let mut process_pos = 0u32;
394    for declaration in &program.declarations {
395        if let Declaration::Process(process) = declaration {
396            exports.processes.insert(
397                process.name.to_string(),
398                ProcessRef::new(process_component_hash(process), process_pos),
399            );
400            process_pos += 1;
401        }
402    }
403    exports
404}
405
406fn module_ref(
407    program: &Program,
408    host_requirements_ref: &HostRequirementsRef,
409    exports: &ModuleExports,
410) -> ModuleRef {
411    let mut writer = HashWriter::new();
412    writer.atom(LASHLANG_SEMANTIC_HASH_VERSION);
413    writer.atom("module");
414    writer.atom(host_requirements_ref.as_str());
415    write_exports(&mut writer, exports);
416    write_program(&mut writer, program);
417    ModuleRef::new(&writer.finish())
418}
419
420fn host_requirements_ref(requirements: &HostRequirements) -> HostRequirementsRef {
421    let mut writer = HashWriter::new();
422    writer.atom(LASHLANG_SEMANTIC_HASH_VERSION);
423    writer.atom("host-requirements");
424    write_host_requirements(&mut writer, requirements);
425    HostRequirementsRef::new(&writer.finish())
426}
427
428fn process_component_hash(process: &ProcessDecl) -> ContentHash {
429    let mut writer = HashWriter::new();
430    writer.atom(LASHLANG_SEMANTIC_HASH_VERSION);
431    writer.atom("process");
432    write_process(&mut writer, process);
433    writer.finish()
434}
435
436fn write_exports(writer: &mut HashWriter, exports: &ModuleExports) {
437    writer.atom("exports");
438    writer.usize(exports.processes.len());
439    for (name, process_ref) in &exports.processes {
440        writer.atom("process-export");
441        writer.atom(name);
442        writer.atom(process_ref.component.as_str());
443        writer.u32(process_ref.pos);
444    }
445}
446
447fn write_host_requirements(writer: &mut HashWriter, requirements: &HostRequirements) {
448    writer.atom("abilities");
449    writer.bool(requirements.abilities.processes);
450    writer.bool(requirements.abilities.sleep);
451    writer.bool(requirements.abilities.process_signals);
452    writer.bool(requirements.abilities.triggers);
453    if requirements.language_features.label_annotations {
454        writer.atom("language-features");
455        writer.atom("label-annotations");
456    }
457    writer.atom("resources");
458    writer.atom("modules");
459    writer.usize(requirements.resources.module_instances().count());
460    for (module_path, module) in requirements.resources.module_instances() {
461        writer.atom(module_path);
462        writer.atom(&module.resource_type);
463        writer.atom(&module.alias);
464        writer.atom("operations");
465        writer.usize(module.operations.len());
466        for (operation, binding) in &module.operations {
467            writer.atom(operation);
468            writer.atom(&binding.host_operation);
469        }
470    }
471    writer.usize(requirements.resources.resource_types().count());
472    for (resource_type, catalog) in requirements.resources.resource_types() {
473        writer.atom(resource_type);
474        writer.atom("operations");
475        writer.usize(catalog.operations.len());
476        for (operation, binding) in &catalog.operations {
477            writer.atom(operation);
478            write_type(writer, &binding.input_ty);
479            write_type(writer, &binding.output_ty);
480        }
481    }
482    writer.atom("named-data-types");
483    writer.usize(requirements.resources.named_data_types().count());
484    for (name, data_type) in requirements.resources.named_data_types() {
485        writer.atom(name);
486        write_type(writer, data_type.ty());
487    }
488    writer.atom("constructors");
489    writer.usize(requirements.resources.value_constructors().count());
490    for (path, constructor) in requirements.resources.value_constructors() {
491        writer.atom(path);
492        writer.atom(&constructor.type_name);
493        write_type(writer, &constructor.input_ty);
494        write_type(writer, &constructor.output_ty);
495    }
496    writer.atom("trigger-sources");
497    writer.usize(requirements.resources.trigger_sources().count());
498    for (source_ty, binding) in requirements.resources.trigger_sources() {
499        writer.atom(source_ty);
500        writer.atom(binding.event_type_name());
501    }
502}
503
504fn write_program(writer: &mut HashWriter, program: &Program) {
505    writer.atom("program");
506    writer.usize(program.declarations.len());
507    for declaration in &program.declarations {
508        write_declaration(writer, declaration);
509    }
510    let mut normalizer = NameNormalizer::default();
511    normalizer.collect_expr(&program.main);
512    write_expr(writer, &program.main, &normalizer);
513}
514
515fn write_declaration(writer: &mut HashWriter, declaration: &Declaration) {
516    match declaration {
517        Declaration::Type(type_decl) => {
518            writer.atom("type-decl");
519            writer.atom(type_decl.name.as_str());
520            write_type(writer, &type_decl.ty);
521        }
522        Declaration::Process(process) => write_process(writer, process),
523    }
524}
525
526fn write_process(writer: &mut HashWriter, process: &ProcessDecl) {
527    writer.atom("process-decl");
528    writer.atom(process.name.as_str());
529    writer.usize(process.params.len());
530    for param in &process.params {
531        writer.atom(param.name.as_str());
532        write_type(writer, &param.ty);
533    }
534    writer.atom("signals");
535    writer.usize(process.signals.len());
536    for signal in &process.signals {
537        writer.atom(signal.name.as_str());
538        write_type(writer, &signal.ty);
539    }
540    match &process.return_ty {
541        Some(ty) => {
542            writer.atom("return");
543            write_type(writer, ty);
544        }
545        None => writer.atom("no-return"),
546    }
547    if let Some(label) = &process.label {
548        write_label_metadata(writer, label);
549    }
550    let mut normalizer = NameNormalizer::default();
551    for param in &process.params {
552        normalizer.bind_abi(param.name.as_str());
553    }
554    normalizer.bind_abi("input");
555    normalizer.bind_abi("inputs");
556    normalizer.collect_expr(&process.body);
557    write_expr(writer, &process.body, &normalizer);
558}
559
560fn write_type(writer: &mut HashWriter, ty: &TypeExpr) {
561    match ty {
562        TypeExpr::Any => writer.atom("type:any"),
563        TypeExpr::Str => writer.atom("type:str"),
564        TypeExpr::Int => writer.atom("type:int"),
565        TypeExpr::Float => writer.atom("type:float"),
566        TypeExpr::Bool => writer.atom("type:bool"),
567        TypeExpr::Dict => writer.atom("type:dict"),
568        TypeExpr::Null => writer.atom("type:null"),
569        TypeExpr::Enum(values) => {
570            writer.atom("type:enum");
571            writer.usize(values.len());
572            for value in values {
573                writer.atom(value.as_str());
574            }
575        }
576        TypeExpr::List(item) => {
577            writer.atom("type:list");
578            write_type(writer, item);
579        }
580        TypeExpr::Object(fields) => {
581            writer.atom("type:object");
582            writer.usize(fields.len());
583            for field in fields {
584                writer.atom(field.name.as_str());
585                writer.bool(field.optional);
586                write_type(writer, &field.ty);
587            }
588        }
589        TypeExpr::Ref(name) => {
590            writer.atom("type:ref");
591            writer.atom(name.as_str());
592        }
593        TypeExpr::Process {
594            input,
595            output,
596            input_count,
597        } => {
598            writer.atom("type:process");
599            writer.usize(*input_count);
600            write_type(writer, input);
601            write_type(writer, output);
602        }
603        TypeExpr::TriggerHandle(event) => {
604            writer.atom("type:trigger-handle");
605            write_type(writer, event);
606        }
607        TypeExpr::Union(items) => {
608            writer.atom("type:union");
609            writer.usize(items.len());
610            for item in items {
611                write_type(writer, item);
612            }
613        }
614    }
615}
616
617fn write_expr(writer: &mut HashWriter, expr: &Expr, normalizer: &NameNormalizer) {
618    match expr {
619        Expr::Block(expressions) => {
620            writer.atom("block");
621            writer.usize(expressions.len());
622            for expression in expressions {
623                write_expr(writer, expression, normalizer);
624            }
625        }
626        Expr::LabelAnnotated { label, expr } => {
627            writer.atom("label-annotated");
628            write_label_metadata(writer, label);
629            write_expr(writer, expr, normalizer);
630        }
631        Expr::Null => writer.atom("null"),
632        Expr::Bool(value) => {
633            writer.atom("bool");
634            writer.bool(*value);
635        }
636        Expr::Number(value) => {
637            writer.atom("number");
638            writer.u64(if *value == 0.0 { 0 } else { value.to_bits() });
639        }
640        Expr::String(value) => {
641            writer.atom("string");
642            writer.atom(value.as_str());
643        }
644        Expr::Variable(name) => {
645            writer.atom("variable");
646            writer.atom(&normalizer.name_token(name.as_str()));
647        }
648        Expr::List(items) => {
649            writer.atom("list");
650            writer.usize(items.len());
651            for item in items {
652                write_expr(writer, item, normalizer);
653            }
654        }
655        Expr::Record(entries) => {
656            writer.atom("record");
657            writer.usize(entries.len());
658            for (key, value) in entries {
659                writer.atom(key.as_str());
660                write_expr(writer, value, normalizer);
661            }
662        }
663        Expr::Assign { target, expr } => {
664            writer.atom("assign");
665            writer.atom(&normalizer.name_token(target.root.as_str()));
666            writer.usize(target.steps.len());
667            for step in &target.steps {
668                match step {
669                    AssignPathStep::Field(field) => {
670                        writer.atom("field");
671                        writer.atom(field.as_str());
672                    }
673                    AssignPathStep::Index(index) => {
674                        writer.atom("index");
675                        write_expr(writer, index, normalizer);
676                    }
677                }
678            }
679            write_expr(writer, expr, normalizer);
680        }
681        Expr::If {
682            condition,
683            then_block,
684            else_block,
685        } => {
686            writer.atom("if");
687            write_expr(writer, condition, normalizer);
688            write_expr(writer, then_block, normalizer);
689            write_expr(writer, else_block, normalizer);
690        }
691        Expr::For {
692            binding,
693            iterable,
694            body,
695        } => {
696            writer.atom("for");
697            writer.atom(&normalizer.name_token(binding.as_str()));
698            write_expr(writer, iterable, normalizer);
699            write_expr(writer, body, normalizer);
700        }
701        Expr::While { condition, body } => {
702            writer.atom("while");
703            write_expr(writer, condition, normalizer);
704            write_expr(writer, body, normalizer);
705        }
706        Expr::Break => writer.atom("break"),
707        Expr::Continue => writer.atom("continue"),
708        Expr::StartProcess(start) => {
709            writer.atom("start-process");
710            writer.atom(start.process.as_str());
711            writer.usize(start.args.len());
712            for (key, value) in &start.args {
713                writer.atom(key.as_str());
714                write_expr(writer, value, normalizer);
715            }
716        }
717        Expr::ProcessRef { process } => {
718            writer.atom("process-ref");
719            writer.atom(process.as_str());
720        }
721        Expr::HostDescriptorConstructor { type_name, input } => {
722            writer.atom("host-value-constructor");
723            writer.atom(type_name.as_str());
724            write_expr(writer, input, normalizer);
725        }
726        Expr::ResourceRef(resource) => {
727            writer.atom("resource-ref");
728            write_resource_ref(writer, resource);
729        }
730        Expr::ReceiverCall {
731            receiver,
732            operation,
733            args,
734        } => {
735            writer.atom("receiver-call");
736            write_expr(writer, receiver, normalizer);
737            writer.atom(operation.as_str());
738            writer.usize(args.len());
739            for arg in args {
740                write_expr(writer, arg, normalizer);
741            }
742        }
743        Expr::Await(expr) => write_unary_expr(writer, "await", expr, normalizer),
744        Expr::SleepFor(expr) => write_unary_expr(writer, "sleep-for", expr, normalizer),
745        Expr::SleepUntil(expr) => write_unary_expr(writer, "sleep-until", expr, normalizer),
746        Expr::WaitSignal { name } => {
747            writer.atom("wait-signal");
748            writer.atom(name.as_str());
749        }
750        Expr::SignalRun { run, name, payload } => {
751            writer.atom("signal-run");
752            writer.atom(name.as_str());
753            write_expr(writer, run, normalizer);
754            write_expr(writer, payload, normalizer);
755        }
756        Expr::ResultUnwrap(expr) => write_unary_expr(writer, "unwrap", expr, normalizer),
757        Expr::Cancel(expr) => write_unary_expr(writer, "cancel", expr, normalizer),
758        Expr::Print(expr) => write_unary_expr(writer, "print", expr, normalizer),
759        Expr::Submit(expr) => write_optional_expr(writer, "submit", expr, normalizer),
760        Expr::Yield(expr) => write_unary_expr(writer, "yield", expr, normalizer),
761        Expr::Wake(expr) => write_unary_expr(writer, "wake", expr, normalizer),
762        Expr::Finish(expr) => write_optional_expr(writer, "finish", expr, normalizer),
763        Expr::Fail(expr) => write_unary_expr(writer, "fail", expr, normalizer),
764        Expr::BuiltinCall { name, args } => {
765            writer.atom("builtin-call");
766            writer.atom(name.as_str());
767            writer.usize(args.len());
768            for arg in args {
769                write_expr(writer, arg, normalizer);
770            }
771        }
772        Expr::Field { target, field } => {
773            writer.atom("field-access");
774            write_expr(writer, target, normalizer);
775            writer.atom(field.as_str());
776        }
777        Expr::Index { target, index } => {
778            writer.atom("index-access");
779            write_expr(writer, target, normalizer);
780            write_expr(writer, index, normalizer);
781        }
782        Expr::Unary { op, expr } => {
783            writer.atom("unary");
784            write_unary_op(writer, *op);
785            write_expr(writer, expr, normalizer);
786        }
787        Expr::Binary { left, op, right } => {
788            writer.atom("binary");
789            write_binary_op(writer, *op);
790            write_expr(writer, left, normalizer);
791            write_expr(writer, right, normalizer);
792        }
793        Expr::TypeLiteral(ty) => {
794            writer.atom("type-literal");
795            write_type(writer, ty);
796        }
797    }
798}
799
800fn write_label_metadata(writer: &mut HashWriter, label: &LabelMetadata) {
801    writer.atom("label");
802    writer.atom(label.title.as_str());
803    match &label.description {
804        Some(description) => {
805            writer.atom("description");
806            writer.atom(description.as_str());
807        }
808        None => writer.atom("no-description"),
809    }
810}
811
812fn write_unary_expr(
813    writer: &mut HashWriter,
814    tag: &'static str,
815    expr: &Expr,
816    normalizer: &NameNormalizer,
817) {
818    writer.atom(tag);
819    write_expr(writer, expr, normalizer);
820}
821
822fn write_optional_expr(
823    writer: &mut HashWriter,
824    tag: &'static str,
825    expr: &Option<Box<Expr>>,
826    normalizer: &NameNormalizer,
827) {
828    writer.atom(tag);
829    match expr {
830        Some(expr) => {
831            writer.atom("some");
832            write_expr(writer, expr, normalizer);
833        }
834        None => writer.atom("none"),
835    }
836}
837
838fn write_resource_ref(writer: &mut HashWriter, resource: &ResourceRefExpr) {
839    writer.atom("path");
840    writer.usize(resource.path.len());
841    for segment in &resource.path {
842        writer.atom(segment.as_str());
843    }
844    writer.atom("handle");
845    writer.atom(resource.resource_type.as_str());
846    writer.atom(resource.alias.as_str());
847}
848
849fn write_unary_op(writer: &mut HashWriter, op: UnaryOp) {
850    writer.atom(match op {
851        UnaryOp::Negate => "negate",
852        UnaryOp::Not => "not",
853    });
854}
855
856fn write_binary_op(writer: &mut HashWriter, op: BinaryOp) {
857    writer.atom(match op {
858        BinaryOp::Add => "add",
859        BinaryOp::Subtract => "subtract",
860        BinaryOp::Multiply => "multiply",
861        BinaryOp::Divide => "divide",
862        BinaryOp::Modulo => "modulo",
863        BinaryOp::Equal => "equal",
864        BinaryOp::NotEqual => "not-equal",
865        BinaryOp::Less => "less",
866        BinaryOp::LessEqual => "less-equal",
867        BinaryOp::Greater => "greater",
868        BinaryOp::GreaterEqual => "greater-equal",
869        BinaryOp::And => "and",
870        BinaryOp::Or => "or",
871    });
872}
873
874#[derive(Default)]
875struct NameNormalizer {
876    names: BTreeMap<String, String>,
877    abi_names: BTreeSet<String>,
878    next_local: u32,
879}
880
881impl NameNormalizer {
882    fn bind_abi(&mut self, name: &str) {
883        self.abi_names.insert(name.to_string());
884        self.names.insert(name.to_string(), format!("abi:{name}"));
885    }
886
887    fn bind_local(&mut self, name: &str) {
888        if self.abi_names.contains(name) || self.names.contains_key(name) {
889            return;
890        }
891        let token = format!("local:{}", self.next_local);
892        self.next_local += 1;
893        self.names.insert(name.to_string(), token);
894    }
895
896    fn name_token(&self, name: &str) -> String {
897        self.names
898            .get(name)
899            .cloned()
900            .unwrap_or_else(|| format!("global:{name}"))
901    }
902
903    fn collect_expr(&mut self, expr: &Expr) {
904        // Local binders are the only nodes that carry naming semantics; every
905        // other node just feeds its sub-expressions back through `collect_expr`,
906        // so the generic arm folds over `Expr::children()`. `Assign` and `For`
907        // stay explicit because they must register their binder name in the
908        // same order the original full walk did.
909        match expr {
910            Expr::Assign { target, expr } => {
911                self.bind_local(target.root.as_str());
912                for step in &target.steps {
913                    if let AssignPathStep::Index(index) = step {
914                        self.collect_expr(index);
915                    }
916                }
917                self.collect_expr(expr);
918            }
919            Expr::For {
920                binding,
921                iterable,
922                body,
923            } => {
924                self.collect_expr(iterable);
925                self.bind_local(binding.as_str());
926                self.collect_expr(body);
927            }
928            _ => {
929                for child in expr.children() {
930                    self.collect_expr(child);
931                }
932            }
933        }
934    }
935}
936
937#[derive(Default)]
938struct HashWriter {
939    bytes: Vec<u8>,
940}
941
942impl HashWriter {
943    fn new() -> Self {
944        Self::default()
945    }
946
947    fn atom(&mut self, value: &str) {
948        self.bytes
949            .extend_from_slice(value.len().to_string().as_bytes());
950        self.bytes.push(b':');
951        self.bytes.extend_from_slice(value.as_bytes());
952        self.bytes.push(b';');
953    }
954
955    fn bool(&mut self, value: bool) {
956        self.atom(if value { "true" } else { "false" });
957    }
958
959    fn usize(&mut self, value: usize) {
960        self.atom(&value.to_string());
961    }
962
963    fn u32(&mut self, value: u32) {
964        self.atom(&value.to_string());
965    }
966
967    fn u64(&mut self, value: u64) {
968        self.atom(&value.to_string());
969    }
970
971    fn finish(self) -> ContentHash {
972        ContentHash::new(hex_digest(&Sha256::digest(self.bytes)))
973    }
974}
975
976fn hex_digest(bytes: &[u8]) -> String {
977    const HEX: &[u8; 16] = b"0123456789abcdef";
978    let mut out = String::with_capacity(bytes.len() * 2);
979    for byte in bytes {
980        out.push(HEX[(byte >> 4) as usize] as char);
981        out.push(HEX[(byte & 0x0f) as usize] as char);
982    }
983    out
984}
985
986#[derive(Clone, Debug)]
987enum RequirementBinding {
988    Value,
989    Resource {
990        resource_type: String,
991        path: Option<Vec<String>>,
992    },
993}
994
995struct RequirementsCollector<'program> {
996    program: &'program Program,
997    resource_catalog: Option<&'program LashlangHostCatalog>,
998    type_names: BTreeSet<String>,
999    requirements: HostRequirements,
1000}
1001
1002impl<'program> RequirementsCollector<'program> {
1003    fn new(program: &'program Program) -> Self {
1004        let type_names = program
1005            .declarations
1006            .iter()
1007            .filter_map(|declaration| match declaration {
1008                Declaration::Type(type_decl) => Some(type_decl.name.to_string()),
1009                _ => None,
1010            })
1011            .collect();
1012        Self {
1013            program,
1014            resource_catalog: None,
1015            type_names,
1016            requirements: HostRequirements::default(),
1017        }
1018    }
1019
1020    fn with_resource_catalog(mut self, catalog: &'program LashlangHostCatalog) -> Self {
1021        self.resource_catalog = Some(catalog);
1022        self
1023    }
1024
1025    fn collect(mut self) -> HostRequirements {
1026        for declaration in &self.program.declarations {
1027            match declaration {
1028                Declaration::Type(type_decl) => self.collect_type(&type_decl.ty),
1029                Declaration::Process(process) => {
1030                    self.requirements.abilities.processes = true;
1031                    if process.label.is_some() {
1032                        self.requirements.language_features.label_annotations = true;
1033                    }
1034                    let mut scope = BTreeMap::new();
1035                    for param in &process.params {
1036                        self.collect_type(&param.ty);
1037                        if let TypeExpr::Ref(name) = &param.ty
1038                            && !self.type_names.contains(name.as_str())
1039                            && self.is_resource_type_name(name)
1040                        {
1041                            self.requirements
1042                                .resources
1043                                .ensure_resource_type(name.to_string());
1044                            scope.insert(
1045                                param.name.to_string(),
1046                                RequirementBinding::Resource {
1047                                    resource_type: name.to_string(),
1048                                    path: None,
1049                                },
1050                            );
1051                        } else {
1052                            scope.insert(param.name.to_string(), RequirementBinding::Value);
1053                        }
1054                    }
1055                    if let Some(return_ty) = &process.return_ty {
1056                        self.collect_type(return_ty);
1057                    }
1058                    scope.insert("input".to_string(), RequirementBinding::Value);
1059                    scope.insert("inputs".to_string(), RequirementBinding::Value);
1060                    self.collect_expr(&process.body, &mut scope);
1061                }
1062            }
1063        }
1064        let mut top_level = BTreeMap::new();
1065        self.collect_expr(&self.program.main, &mut top_level);
1066        self.requirements
1067    }
1068
1069    fn collect_type(&mut self, ty: &TypeExpr) {
1070        match ty {
1071            TypeExpr::List(item) => self.collect_type(item),
1072            TypeExpr::Object(fields) => {
1073                for field in fields {
1074                    self.collect_type(&field.ty);
1075                }
1076            }
1077            TypeExpr::Union(items) => {
1078                for item in items {
1079                    self.collect_type(item);
1080                }
1081            }
1082            TypeExpr::Process { input, output, .. } => {
1083                self.collect_type(input);
1084                self.collect_type(output);
1085            }
1086            TypeExpr::TriggerHandle(event) => self.collect_type(event),
1087            TypeExpr::Ref(name)
1088                if !self.type_names.contains(name.as_str())
1089                    && self.is_host_data_type_name(name) =>
1090            {
1091                let data_type = self
1092                    .resource_catalog
1093                    .and_then(|catalog| catalog.resolve_named_data_type(name.as_str()))
1094                    .expect("checked host data type presence")
1095                    .clone();
1096                self.requirements
1097                    .resources
1098                    .add_named_data_type(data_type)
1099                    .expect("host data type requirement came from host catalog");
1100            }
1101            TypeExpr::Ref(name)
1102                if !self.type_names.contains(name.as_str()) && self.is_resource_type_name(name) =>
1103            {
1104                self.requirements
1105                    .resources
1106                    .ensure_resource_type(name.to_string());
1107            }
1108            TypeExpr::Any
1109            | TypeExpr::Str
1110            | TypeExpr::Int
1111            | TypeExpr::Float
1112            | TypeExpr::Bool
1113            | TypeExpr::Dict
1114            | TypeExpr::Null
1115            | TypeExpr::Enum(_)
1116            | TypeExpr::Ref(_) => {}
1117        }
1118    }
1119
1120    fn is_host_data_type_name(&self, name: &str) -> bool {
1121        self.resource_catalog
1122            .map(|catalog| catalog.has_named_data_type(name))
1123            .unwrap_or(false)
1124    }
1125
1126    fn is_resource_type_name(&self, name: &str) -> bool {
1127        self.resource_catalog
1128            .map(|catalog| catalog.has_resource_type(name))
1129            .unwrap_or(true)
1130    }
1131
1132    fn collect_expr(
1133        &mut self,
1134        expr: &Expr,
1135        scope: &mut BTreeMap<String, RequirementBinding>,
1136    ) -> Option<RequirementBinding> {
1137        match expr {
1138            Expr::Block(expressions) => {
1139                let mut last = None;
1140                for expression in expressions {
1141                    last = self.collect_expr(expression, scope);
1142                }
1143                last
1144            }
1145            Expr::LabelAnnotated { expr, .. } => {
1146                self.requirements.language_features.label_annotations = true;
1147                self.collect_expr(expr, scope)
1148            }
1149            Expr::Variable(name) => scope.get(name.as_str()).cloned(),
1150            Expr::List(items) => {
1151                for item in items {
1152                    self.collect_expr(item, scope);
1153                }
1154                Some(RequirementBinding::Value)
1155            }
1156            Expr::Record(entries) => {
1157                for (_, value) in entries {
1158                    self.collect_expr(value, scope);
1159                }
1160                Some(RequirementBinding::Value)
1161            }
1162            Expr::Assign { target, expr } => {
1163                for step in &target.steps {
1164                    if let AssignPathStep::Index(index) = step {
1165                        self.collect_expr(index, scope);
1166                    }
1167                }
1168                let binding = self
1169                    .collect_expr(expr, scope)
1170                    .unwrap_or(RequirementBinding::Value);
1171                if target.steps.is_empty() {
1172                    scope.insert(target.root.to_string(), binding);
1173                }
1174                Some(RequirementBinding::Value)
1175            }
1176            Expr::If {
1177                condition,
1178                then_block,
1179                else_block,
1180            } => {
1181                self.collect_expr(condition, scope);
1182                let mut then_scope = scope.clone();
1183                self.collect_expr(then_block, &mut then_scope);
1184                let mut else_scope = scope.clone();
1185                self.collect_expr(else_block, &mut else_scope);
1186                for (name, binding) in then_scope.into_iter().chain(else_scope) {
1187                    scope.entry(name).or_insert(binding);
1188                }
1189                Some(RequirementBinding::Value)
1190            }
1191            Expr::For {
1192                binding,
1193                iterable,
1194                body,
1195            } => {
1196                self.collect_expr(iterable, scope);
1197                let previous = scope.insert(binding.to_string(), RequirementBinding::Value);
1198                self.collect_expr(body, scope);
1199                if let Some(previous) = previous {
1200                    scope.insert(binding.to_string(), previous);
1201                } else {
1202                    scope.remove(binding.as_str());
1203                }
1204                Some(RequirementBinding::Value)
1205            }
1206            Expr::While { condition, body } => {
1207                self.collect_expr(condition, scope);
1208                self.collect_expr(body, scope);
1209                Some(RequirementBinding::Value)
1210            }
1211            Expr::StartProcess(start) => {
1212                self.requirements.abilities.processes = true;
1213                for (_, value) in &start.args {
1214                    self.collect_expr(value, scope);
1215                }
1216                Some(RequirementBinding::Value)
1217            }
1218            Expr::ProcessRef { .. } => {
1219                self.requirements.abilities.processes = true;
1220                Some(RequirementBinding::Value)
1221            }
1222            Expr::HostDescriptorConstructor { type_name, input } => {
1223                if let Some(catalog) = self.resource_catalog
1224                    && let Some(constructor) = catalog
1225                        .value_constructors()
1226                        .map(|(_, constructor)| constructor)
1227                        .find(|constructor| constructor.type_name == type_name.as_str())
1228                {
1229                    self.requirements.resources.add_value_constructor(
1230                        constructor.path.iter().map(String::as_str),
1231                        constructor.input_ty.clone(),
1232                        constructor.output_ty.clone(),
1233                    );
1234                }
1235                if let Some(catalog) = self.resource_catalog
1236                    && let Some(binding) = catalog.resolve_trigger_source(type_name.as_str())
1237                {
1238                    self.requirements
1239                        .resources
1240                        .add_trigger_source_type(
1241                            type_name.to_string(),
1242                            binding.event_type().clone(),
1243                        )
1244                        .expect("trigger source requirement came from host catalog");
1245                }
1246                self.collect_expr(input, scope);
1247                Some(RequirementBinding::Value)
1248            }
1249            Expr::ResourceRef(resource) => {
1250                self.require_resource_ref(resource);
1251                Some(RequirementBinding::Resource {
1252                    resource_type: resource.resource_type.to_string(),
1253                    path: Some(resource.path.iter().map(ToString::to_string).collect()),
1254                })
1255            }
1256            Expr::ReceiverCall {
1257                receiver,
1258                operation,
1259                args,
1260            } => {
1261                let receiver = self.collect_expr(receiver, scope);
1262                if let Some(RequirementBinding::Resource {
1263                    resource_type,
1264                    path,
1265                }) = receiver
1266                {
1267                    self.require_resource_operation(resource_type, path, operation.as_str());
1268                }
1269                for arg in args {
1270                    self.collect_expr(arg, scope);
1271                }
1272                Some(RequirementBinding::Value)
1273            }
1274            Expr::SleepFor(expr) | Expr::SleepUntil(expr) => {
1275                self.requirements.abilities.sleep = true;
1276                self.collect_expr(expr, scope);
1277                Some(RequirementBinding::Value)
1278            }
1279            Expr::WaitSignal { .. } => {
1280                self.requirements.abilities.process_signals = true;
1281                Some(RequirementBinding::Value)
1282            }
1283            Expr::SignalRun { run, payload, .. } => {
1284                self.requirements.abilities.process_signals = true;
1285                self.collect_expr(run, scope);
1286                self.collect_expr(payload, scope);
1287                Some(RequirementBinding::Value)
1288            }
1289            Expr::Await(expr)
1290            | Expr::ResultUnwrap(expr)
1291            | Expr::Cancel(expr)
1292            | Expr::Print(expr)
1293            | Expr::Yield(expr)
1294            | Expr::Wake(expr)
1295            | Expr::Fail(expr)
1296            | Expr::Unary { expr, .. } => {
1297                self.collect_expr(expr, scope);
1298                Some(RequirementBinding::Value)
1299            }
1300            Expr::Submit(expr) | Expr::Finish(expr) => {
1301                if let Some(expr) = expr {
1302                    self.collect_expr(expr, scope);
1303                }
1304                Some(RequirementBinding::Value)
1305            }
1306            Expr::BuiltinCall { args, .. } => {
1307                for arg in args {
1308                    self.collect_expr(arg, scope);
1309                }
1310                Some(RequirementBinding::Value)
1311            }
1312            Expr::Field { target, .. } => {
1313                self.collect_expr(target, scope);
1314                Some(RequirementBinding::Value)
1315            }
1316            Expr::Index { target, index } => {
1317                self.collect_expr(target, scope);
1318                self.collect_expr(index, scope);
1319                Some(RequirementBinding::Value)
1320            }
1321            Expr::Binary { left, right, .. } => {
1322                self.collect_expr(left, scope);
1323                self.collect_expr(right, scope);
1324                Some(RequirementBinding::Value)
1325            }
1326            Expr::TypeLiteral(ty) => {
1327                self.collect_type(ty);
1328                Some(RequirementBinding::Value)
1329            }
1330            Expr::Null
1331            | Expr::Bool(_)
1332            | Expr::Number(_)
1333            | Expr::String(_)
1334            | Expr::Break
1335            | Expr::Continue => Some(RequirementBinding::Value),
1336        }
1337    }
1338
1339    fn require_resource_ref(&mut self, resource: &ResourceRefExpr) {
1340        self.requirements
1341            .resources
1342            .add_module_instance(
1343                resource.path.iter().map(|segment| segment.as_str()),
1344                resource.resource_type.to_string(),
1345            )
1346            .expect("resolved resource references cannot conflict");
1347    }
1348
1349    fn require_resource_operation(
1350        &mut self,
1351        resource_type: String,
1352        path: Option<Vec<String>>,
1353        operation: &str,
1354    ) {
1355        let (operation, input_ty, output_ty) =
1356            self.resource_operation_requirement(&resource_type, operation);
1357        if let (Some(catalog), Some(path)) = (self.resource_catalog, path.as_ref()) {
1358            let alias = path.join(".");
1359            if let Some(module_binding) =
1360                catalog.resolve_module_operation(&resource_type, &alias, &operation)
1361            {
1362                self.requirements.resources.add_module_operation(
1363                    path.iter().map(String::as_str),
1364                    resource_type,
1365                    operation,
1366                    module_binding.host_operation.clone(),
1367                    input_ty,
1368                    output_ty,
1369                );
1370                return;
1371            }
1372        }
1373        self.requirements
1374            .resources
1375            .add_operation(resource_type, operation, input_ty, output_ty);
1376    }
1377
1378    fn resource_operation_requirement(
1379        &self,
1380        resource_type: &str,
1381        operation: &str,
1382    ) -> (String, TypeExpr, TypeExpr) {
1383        if let Some(catalog) = self.resource_catalog
1384            && let Some(binding) = catalog.resolve_operation(resource_type, operation)
1385        {
1386            return (
1387                operation.to_string(),
1388                binding.input_ty.clone(),
1389                binding.output_ty.clone(),
1390            );
1391        }
1392        (operation.to_string(), TypeExpr::Any, TypeExpr::Any)
1393    }
1394}