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