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