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::Submit(expr) => write_optional_expr(writer, "submit", expr, normalizer),
833 Expr::Yield(expr) => write_unary_expr(writer, "yield", expr, normalizer),
834 Expr::Wake(expr) => write_unary_expr(writer, "wake", expr, normalizer),
835 Expr::Finish(expr) => write_optional_expr(writer, "finish", expr, normalizer),
836 Expr::Fail(expr) => write_unary_expr(writer, "fail", expr, normalizer),
837 Expr::BuiltinCall { name, args } => {
838 writer.atom("builtin-call");
839 writer.atom(name.as_str());
840 writer.usize(args.len());
841 for arg in args {
842 write_expr(writer, arg, normalizer);
843 }
844 }
845 Expr::Field { target, field } => {
846 writer.atom("field-access");
847 write_expr(writer, target, normalizer);
848 writer.atom(field.as_str());
849 }
850 Expr::Index { target, index } => {
851 writer.atom("index-access");
852 write_expr(writer, target, normalizer);
853 write_expr(writer, index, normalizer);
854 }
855 Expr::Unary { op, expr } => {
856 writer.atom("unary");
857 write_unary_op(writer, *op);
858 write_expr(writer, expr, normalizer);
859 }
860 Expr::Binary { left, op, right } => {
861 writer.atom("binary");
862 write_binary_op(writer, *op);
863 write_expr(writer, left, normalizer);
864 write_expr(writer, right, normalizer);
865 }
866 Expr::TypeLiteral(ty) => {
867 writer.atom("type-literal");
868 write_type(writer, ty);
869 }
870 }
871}
872
873fn write_label_metadata(writer: &mut HashWriter, label: &LabelMetadata) {
874 writer.atom("label");
875 writer.atom(label.title.as_str());
876 match &label.description {
877 Some(description) => {
878 writer.atom("description");
879 writer.atom(description.as_str());
880 }
881 None => writer.atom("no-description"),
882 }
883}
884
885fn write_unary_expr(
886 writer: &mut HashWriter,
887 tag: &'static str,
888 expr: &Expr,
889 normalizer: &NameNormalizer,
890) {
891 writer.atom(tag);
892 write_expr(writer, expr, normalizer);
893}
894
895fn write_optional_expr(
896 writer: &mut HashWriter,
897 tag: &'static str,
898 expr: &Option<Box<Expr>>,
899 normalizer: &NameNormalizer,
900) {
901 writer.atom(tag);
902 match expr {
903 Some(expr) => {
904 writer.atom("some");
905 write_expr(writer, expr, normalizer);
906 }
907 None => writer.atom("none"),
908 }
909}
910
911fn write_resource_ref(writer: &mut HashWriter, resource: &ResourceRefExpr) {
912 writer.atom("path");
913 writer.usize(resource.path.len());
914 for segment in &resource.path {
915 writer.atom(segment.as_str());
916 }
917 writer.atom("handle");
918 writer.atom(resource.resource_type.as_str());
919 writer.atom(resource.alias.as_str());
920}
921
922fn write_unary_op(writer: &mut HashWriter, op: UnaryOp) {
923 writer.atom(match op {
924 UnaryOp::Negate => "negate",
925 UnaryOp::Not => "not",
926 });
927}
928
929fn write_binary_op(writer: &mut HashWriter, op: BinaryOp) {
930 writer.atom(match op {
931 BinaryOp::Add => "add",
932 BinaryOp::Subtract => "subtract",
933 BinaryOp::Multiply => "multiply",
934 BinaryOp::Divide => "divide",
935 BinaryOp::Modulo => "modulo",
936 BinaryOp::Equal => "equal",
937 BinaryOp::NotEqual => "not-equal",
938 BinaryOp::Less => "less",
939 BinaryOp::LessEqual => "less-equal",
940 BinaryOp::Greater => "greater",
941 BinaryOp::GreaterEqual => "greater-equal",
942 BinaryOp::And => "and",
943 BinaryOp::Or => "or",
944 });
945}
946
947#[derive(Default)]
948struct NameNormalizer {
949 names: BTreeMap<String, String>,
950 abi_names: BTreeSet<String>,
951 next_local: u32,
952}
953
954impl NameNormalizer {
955 fn bind_abi(&mut self, name: &str) {
956 self.abi_names.insert(name.to_string());
957 self.names.insert(name.to_string(), format!("abi:{name}"));
958 }
959
960 fn bind_local(&mut self, name: &str) {
961 if self.abi_names.contains(name) || self.names.contains_key(name) {
962 return;
963 }
964 let token = format!("local:{}", self.next_local);
965 self.next_local += 1;
966 self.names.insert(name.to_string(), token);
967 }
968
969 fn name_token(&self, name: &str) -> String {
970 self.names
971 .get(name)
972 .cloned()
973 .unwrap_or_else(|| format!("global:{name}"))
974 }
975
976 fn collect_expr(&mut self, expr: &Expr) {
977 match expr {
983 Expr::Assign { target, expr } => {
984 self.bind_local(target.root.as_str());
985 for step in &target.steps {
986 if let AssignPathStep::Index(index) = step {
987 self.collect_expr(index);
988 }
989 }
990 self.collect_expr(expr);
991 }
992 Expr::For {
993 binding,
994 iterable,
995 body,
996 } => {
997 self.collect_expr(iterable);
998 self.bind_local(binding.as_str());
999 self.collect_expr(body);
1000 }
1001 Expr::ListComprehension { element, clauses } => {
1002 for clause in clauses {
1003 match clause {
1004 ListComprehensionClause::For { binding, iterable } => {
1005 self.collect_expr(iterable);
1006 self.bind_local(binding.as_str());
1007 }
1008 ListComprehensionClause::If { condition } => {
1009 self.collect_expr(condition);
1010 }
1011 }
1012 }
1013 self.collect_expr(element);
1014 }
1015 _ => {
1016 for child in expr.children() {
1017 self.collect_expr(child);
1018 }
1019 }
1020 }
1021 }
1022}
1023
1024#[derive(Default)]
1025struct HashWriter {
1026 bytes: Vec<u8>,
1027}
1028
1029impl HashWriter {
1030 fn new() -> Self {
1031 Self::default()
1032 }
1033
1034 fn atom(&mut self, value: &str) {
1035 self.bytes
1036 .extend_from_slice(value.len().to_string().as_bytes());
1037 self.bytes.push(b':');
1038 self.bytes.extend_from_slice(value.as_bytes());
1039 self.bytes.push(b';');
1040 }
1041
1042 fn bool(&mut self, value: bool) {
1043 self.atom(if value { "true" } else { "false" });
1044 }
1045
1046 fn usize(&mut self, value: usize) {
1047 self.atom(&value.to_string());
1048 }
1049
1050 fn u32(&mut self, value: u32) {
1051 self.atom(&value.to_string());
1052 }
1053
1054 fn u64(&mut self, value: u64) {
1055 self.atom(&value.to_string());
1056 }
1057
1058 fn finish(self) -> ContentHash {
1059 ContentHash::new(hex_digest(&Sha256::digest(self.bytes)))
1060 }
1061}
1062
1063fn hex_digest(bytes: &[u8]) -> String {
1064 const HEX: &[u8; 16] = b"0123456789abcdef";
1065 let mut out = String::with_capacity(bytes.len() * 2);
1066 for byte in bytes {
1067 out.push(HEX[(byte >> 4) as usize] as char);
1068 out.push(HEX[(byte & 0x0f) as usize] as char);
1069 }
1070 out
1071}
1072
1073#[derive(Clone, Debug)]
1074enum RequirementBinding {
1075 Value,
1076 Resource {
1077 resource_type: String,
1078 path: Option<Vec<String>>,
1079 },
1080}
1081
1082struct RequirementsCollector<'program> {
1083 program: &'program Program,
1084 resource_catalog: Option<&'program LashlangHostCatalog>,
1085 type_names: BTreeSet<String>,
1086 requirements: HostRequirements,
1087}
1088
1089impl<'program> RequirementsCollector<'program> {
1090 fn new(program: &'program Program) -> Self {
1091 let type_names = program
1092 .declarations
1093 .iter()
1094 .filter_map(|declaration| match declaration {
1095 Declaration::Type(type_decl) => Some(type_decl.name.to_string()),
1096 _ => None,
1097 })
1098 .collect();
1099 Self {
1100 program,
1101 resource_catalog: None,
1102 type_names,
1103 requirements: HostRequirements::default(),
1104 }
1105 }
1106
1107 fn with_resource_catalog(mut self, catalog: &'program LashlangHostCatalog) -> Self {
1108 self.resource_catalog = Some(catalog);
1109 self
1110 }
1111
1112 fn collect(mut self) -> HostRequirements {
1113 for declaration in &self.program.declarations {
1114 match declaration {
1115 Declaration::Type(type_decl) => self.collect_type(&type_decl.ty),
1116 Declaration::Process(process) => {
1117 self.requirements.abilities.processes = true;
1118 if process.label.is_some() {
1119 self.requirements.language_features.label_annotations = true;
1120 }
1121 let mut scope = BTreeMap::new();
1122 for param in &process.params {
1123 self.collect_type(¶m.ty);
1124 if let TypeExpr::Ref(name) = ¶m.ty
1125 && !self.type_names.contains(name.as_str())
1126 && self.is_resource_type_name(name)
1127 {
1128 self.requirements
1129 .resources
1130 .ensure_resource_type(name.to_string());
1131 scope.insert(
1132 param.name.to_string(),
1133 RequirementBinding::Resource {
1134 resource_type: name.to_string(),
1135 path: None,
1136 },
1137 );
1138 } else {
1139 scope.insert(param.name.to_string(), RequirementBinding::Value);
1140 }
1141 }
1142 if let Some(return_ty) = &process.return_ty {
1143 self.collect_type(return_ty);
1144 }
1145 scope.insert("input".to_string(), RequirementBinding::Value);
1146 scope.insert("inputs".to_string(), RequirementBinding::Value);
1147 self.collect_expr(&process.body, &mut scope);
1148 }
1149 }
1150 }
1151 let mut top_level = BTreeMap::new();
1152 self.collect_expr(&self.program.main, &mut top_level);
1153 self.requirements
1154 }
1155
1156 fn collect_type(&mut self, ty: &TypeExpr) {
1157 match ty {
1158 TypeExpr::List(item) => self.collect_type(item),
1159 TypeExpr::Object(fields) => {
1160 for field in fields {
1161 self.collect_type(&field.ty);
1162 }
1163 }
1164 TypeExpr::Union(items) => {
1165 for item in items {
1166 self.collect_type(item);
1167 }
1168 }
1169 TypeExpr::Process { input, output, .. } => {
1170 self.collect_type(input);
1171 self.collect_type(output);
1172 }
1173 TypeExpr::TriggerHandle(event) => self.collect_type(event),
1174 TypeExpr::Ref(name)
1175 if !self.type_names.contains(name.as_str())
1176 && self.is_host_data_type_name(name) =>
1177 {
1178 let data_type = self
1179 .resource_catalog
1180 .and_then(|catalog| catalog.resolve_named_data_type(name.as_str()))
1181 .expect("checked host data type presence")
1182 .clone();
1183 self.requirements
1184 .resources
1185 .add_named_data_type(data_type)
1186 .expect("host data type requirement came from host catalog");
1187 }
1188 TypeExpr::Ref(name)
1189 if !self.type_names.contains(name.as_str()) && self.is_resource_type_name(name) =>
1190 {
1191 self.requirements
1192 .resources
1193 .ensure_resource_type(name.to_string());
1194 }
1195 TypeExpr::Any
1196 | TypeExpr::Str
1197 | TypeExpr::Int
1198 | TypeExpr::Float
1199 | TypeExpr::Bool
1200 | TypeExpr::Dict
1201 | TypeExpr::Null
1202 | TypeExpr::Enum(_)
1203 | TypeExpr::Ref(_) => {}
1204 }
1205 }
1206
1207 fn is_host_data_type_name(&self, name: &str) -> bool {
1208 self.resource_catalog
1209 .map(|catalog| catalog.has_named_data_type(name))
1210 .unwrap_or(false)
1211 }
1212
1213 fn is_resource_type_name(&self, name: &str) -> bool {
1214 self.resource_catalog
1215 .map(|catalog| catalog.has_resource_type(name))
1216 .unwrap_or(true)
1217 }
1218
1219 fn collect_expr(
1220 &mut self,
1221 expr: &Expr,
1222 scope: &mut BTreeMap<String, RequirementBinding>,
1223 ) -> Option<RequirementBinding> {
1224 match expr {
1225 Expr::Block(expressions) => {
1226 let mut last = None;
1227 for expression in expressions {
1228 last = self.collect_expr(expression, scope);
1229 }
1230 last
1231 }
1232 Expr::LabelAnnotated { expr, .. } => {
1233 self.requirements.language_features.label_annotations = true;
1234 self.collect_expr(expr, scope)
1235 }
1236 Expr::Variable(name) => scope.get(name.as_str()).cloned(),
1237 Expr::Tuple(items) => {
1238 for item in items {
1239 self.collect_expr(item, scope);
1240 }
1241 Some(RequirementBinding::Value)
1242 }
1243 Expr::List(items) => {
1244 for item in items {
1245 self.collect_expr(item, scope);
1246 }
1247 Some(RequirementBinding::Value)
1248 }
1249 Expr::ListComprehension { element, clauses } => {
1250 let mut previous = Vec::new();
1251 for clause in clauses {
1252 match clause {
1253 ListComprehensionClause::For { binding, iterable } => {
1254 self.collect_expr(iterable, scope);
1255 previous.push((
1256 binding.to_string(),
1257 scope.insert(binding.to_string(), RequirementBinding::Value),
1258 ));
1259 }
1260 ListComprehensionClause::If { condition } => {
1261 self.collect_expr(condition, scope);
1262 }
1263 }
1264 }
1265 self.collect_expr(element, scope);
1266 for (binding, previous) in previous.into_iter().rev() {
1267 if let Some(previous) = previous {
1268 scope.insert(binding, previous);
1269 } else {
1270 scope.remove(binding.as_str());
1271 }
1272 }
1273 Some(RequirementBinding::Value)
1274 }
1275 Expr::Record(entries) => {
1276 for (_, value) in entries {
1277 self.collect_expr(value, scope);
1278 }
1279 Some(RequirementBinding::Value)
1280 }
1281 Expr::Assign { target, expr } => {
1282 for step in &target.steps {
1283 if let AssignPathStep::Index(index) = step {
1284 self.collect_expr(index, scope);
1285 }
1286 }
1287 let binding = self
1288 .collect_expr(expr, scope)
1289 .unwrap_or(RequirementBinding::Value);
1290 if target.steps.is_empty() {
1291 scope.insert(target.root.to_string(), binding);
1292 }
1293 Some(RequirementBinding::Value)
1294 }
1295 Expr::If {
1296 condition,
1297 then_block,
1298 else_block,
1299 } => {
1300 self.collect_expr(condition, scope);
1301 let mut then_scope = scope.clone();
1302 self.collect_expr(then_block, &mut then_scope);
1303 let mut else_scope = scope.clone();
1304 self.collect_expr(else_block, &mut else_scope);
1305 for (name, binding) in then_scope.into_iter().chain(else_scope) {
1306 scope.entry(name).or_insert(binding);
1307 }
1308 Some(RequirementBinding::Value)
1309 }
1310 Expr::For {
1311 binding,
1312 iterable,
1313 body,
1314 } => {
1315 self.collect_expr(iterable, scope);
1316 let previous = scope.insert(binding.to_string(), RequirementBinding::Value);
1317 self.collect_expr(body, scope);
1318 if let Some(previous) = previous {
1319 scope.insert(binding.to_string(), previous);
1320 } else {
1321 scope.remove(binding.as_str());
1322 }
1323 Some(RequirementBinding::Value)
1324 }
1325 Expr::While { condition, body } => {
1326 self.collect_expr(condition, scope);
1327 self.collect_expr(body, scope);
1328 Some(RequirementBinding::Value)
1329 }
1330 Expr::StartProcess(start) => {
1331 self.requirements.abilities.processes = true;
1332 for (_, value) in &start.args {
1333 self.collect_expr(value, scope);
1334 }
1335 Some(RequirementBinding::Value)
1336 }
1337 Expr::ProcessRef { .. } => {
1338 self.requirements.abilities.processes = true;
1339 Some(RequirementBinding::Value)
1340 }
1341 Expr::HostDescriptorConstructor { type_name, input } => {
1342 if let Some(catalog) = self.resource_catalog
1343 && let Some(constructor) = catalog
1344 .value_constructors()
1345 .map(|(_, constructor)| constructor)
1346 .find(|constructor| constructor.type_name == type_name.as_str())
1347 {
1348 self.requirements.resources.add_value_constructor(
1349 constructor.path.iter().map(String::as_str),
1350 constructor.input_ty.clone(),
1351 constructor.output_ty.clone(),
1352 );
1353 }
1354 if let Some(catalog) = self.resource_catalog
1355 && let Some(binding) = catalog.resolve_trigger_source(type_name.as_str())
1356 {
1357 self.requirements
1358 .resources
1359 .add_trigger_source_type(
1360 type_name.to_string(),
1361 binding.event_type().clone(),
1362 )
1363 .expect("trigger source requirement came from host catalog");
1364 }
1365 self.collect_expr(input, scope);
1366 Some(RequirementBinding::Value)
1367 }
1368 Expr::ResourceRef(resource) => {
1369 self.require_resource_ref(resource);
1370 Some(RequirementBinding::Resource {
1371 resource_type: resource.resource_type.to_string(),
1372 path: Some(resource.path.iter().map(ToString::to_string).collect()),
1373 })
1374 }
1375 Expr::ReceiverCall {
1376 receiver,
1377 operation,
1378 args,
1379 } => {
1380 let receiver = self.collect_expr(receiver, scope);
1381 if let Some(RequirementBinding::Resource {
1382 resource_type,
1383 path,
1384 }) = receiver
1385 {
1386 self.require_resource_operation(resource_type, path, operation.as_str());
1387 }
1388 for arg in args {
1389 self.collect_expr(arg, scope);
1390 }
1391 Some(RequirementBinding::Value)
1392 }
1393 Expr::SleepFor(expr) | Expr::SleepUntil(expr) => {
1394 self.requirements.abilities.sleep = true;
1395 self.collect_expr(expr, scope);
1396 Some(RequirementBinding::Value)
1397 }
1398 Expr::WaitSignal { .. } => {
1399 self.requirements.abilities.process_signals = true;
1400 Some(RequirementBinding::Value)
1401 }
1402 Expr::SignalRun { run, payload, .. } => {
1403 self.requirements.abilities.process_signals = true;
1404 self.collect_expr(run, scope);
1405 self.collect_expr(payload, scope);
1406 Some(RequirementBinding::Value)
1407 }
1408 Expr::Await(expr)
1409 | Expr::ResultUnwrap(expr)
1410 | Expr::Cancel(expr)
1411 | Expr::Print(expr)
1412 | Expr::Yield(expr)
1413 | Expr::Wake(expr)
1414 | Expr::Fail(expr)
1415 | Expr::Unary { expr, .. } => {
1416 self.collect_expr(expr, scope);
1417 Some(RequirementBinding::Value)
1418 }
1419 Expr::Submit(expr) | Expr::Finish(expr) => {
1420 if let Some(expr) = expr {
1421 self.collect_expr(expr, scope);
1422 }
1423 Some(RequirementBinding::Value)
1424 }
1425 Expr::BuiltinCall { args, .. } => {
1426 for arg in args {
1427 self.collect_expr(arg, scope);
1428 }
1429 Some(RequirementBinding::Value)
1430 }
1431 Expr::Field { target, .. } => {
1432 self.collect_expr(target, scope);
1433 Some(RequirementBinding::Value)
1434 }
1435 Expr::Index { target, index } => {
1436 self.collect_expr(target, scope);
1437 self.collect_expr(index, scope);
1438 Some(RequirementBinding::Value)
1439 }
1440 Expr::Binary { left, right, .. } => {
1441 self.collect_expr(left, scope);
1442 self.collect_expr(right, scope);
1443 Some(RequirementBinding::Value)
1444 }
1445 Expr::TypeLiteral(ty) => {
1446 self.collect_type(ty);
1447 Some(RequirementBinding::Value)
1448 }
1449 Expr::Null
1450 | Expr::Bool(_)
1451 | Expr::Number(_)
1452 | Expr::String(_)
1453 | Expr::Break
1454 | Expr::Continue => Some(RequirementBinding::Value),
1455 }
1456 }
1457
1458 fn require_resource_ref(&mut self, resource: &ResourceRefExpr) {
1459 self.requirements
1460 .resources
1461 .add_module_instance(
1462 resource.path.iter().map(|segment| segment.as_str()),
1463 resource.resource_type.to_string(),
1464 )
1465 .expect("resolved resource references cannot conflict");
1466 }
1467
1468 fn require_resource_operation(
1469 &mut self,
1470 resource_type: String,
1471 path: Option<Vec<String>>,
1472 operation: &str,
1473 ) {
1474 let (operation, input_ty, output_ty) =
1475 self.resource_operation_requirement(&resource_type, operation);
1476 if let (Some(catalog), Some(path)) = (self.resource_catalog, path.as_ref()) {
1477 let alias = path.join(".");
1478 if let Some(module_binding) =
1479 catalog.resolve_module_operation(&resource_type, &alias, &operation)
1480 {
1481 self.requirements.resources.add_module_operation(
1482 path.iter().map(String::as_str),
1483 resource_type,
1484 operation,
1485 module_binding.host_operation.clone(),
1486 input_ty,
1487 output_ty,
1488 );
1489 return;
1490 }
1491 }
1492 self.requirements
1493 .resources
1494 .add_operation(resource_type, operation, input_ty, output_ty);
1495 }
1496
1497 fn resource_operation_requirement(
1498 &self,
1499 resource_type: &str,
1500 operation: &str,
1501 ) -> (String, TypeExpr, TypeExpr) {
1502 if let Some(catalog) = self.resource_catalog
1503 && let Some(binding) = catalog.resolve_operation(resource_type, operation)
1504 {
1505 return (
1506 operation.to_string(),
1507 binding.input_ty.clone(),
1508 binding.output_ty.clone(),
1509 );
1510 }
1511 (operation.to_string(), TypeExpr::Any, TypeExpr::Any)
1512 }
1513}