Skip to main content

lashlang/
artifact.rs

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