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