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#[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 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, ¶m.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 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(¶m.ty);
989 if let TypeExpr::Ref(name) = ¶m.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}