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