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