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