1use std::borrow::Borrow;
2use std::collections::{BTreeMap, BTreeSet};
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::artifact::{
8 ModuleArtifact, SurfaceRequirements, surface_requirements_for_program_with_catalog,
9};
10use crate::ast::{
11 AssignPathStep, AstString, Declaration, Expr, ProcessDecl, ProcessParam, Program,
12 ResourceRefExpr, TypeExpr, TypeField, format_type_expr,
13};
14use crate::lexer::Span;
15
16#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
17pub struct ResourceCatalog {
18 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
19 module_instances: BTreeMap<String, ModuleInstanceCatalog>,
20 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
21 resource_types: BTreeMap<String, ResourceTypeCatalog>,
22 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
23 named_data_types: BTreeMap<String, NamedDataType>,
24 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
25 value_constructors: BTreeMap<String, ValueConstructorBinding>,
26 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
27 trigger_sources: BTreeMap<String, TriggerSourceBinding>,
28}
29
30impl ResourceCatalog {
31 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn tool_default(operations: impl IntoIterator<Item = impl Into<String>>) -> Self {
36 let mut catalog = Self::new();
37 for operation in operations {
38 let operation = operation.into();
39 catalog.add_module_operation(
40 ["tools"],
41 "Tools",
42 operation.clone(),
43 operation,
44 TypeExpr::Any,
45 TypeExpr::Any,
46 );
47 }
48 catalog
49 }
50
51 pub fn add_module_instance(
52 &mut self,
53 module_path: impl IntoIterator<Item = impl Into<String>>,
54 resource_type: impl Into<String>,
55 ) -> Result<(), ResourceCatalogError> {
56 let path = module_path.into_iter().map(Into::into).collect::<Vec<_>>();
57 assert!(!path.is_empty(), "module path must not be empty");
58 let resource_type = resource_type.into();
59 let key = module_path_key(&path);
60 if let Some(existing) = self.module_instances.get(&key) {
61 if existing.resource_type != resource_type {
62 return Err(ResourceCatalogError::ConflictingModuleInstance {
63 alias: key,
64 existing: existing.resource_type.clone(),
65 incoming: resource_type,
66 });
67 }
68 self.ensure_resource_type(resource_type);
69 return Ok(());
70 }
71 self.module_instances.insert(
72 key.clone(),
73 ModuleInstanceCatalog {
74 path,
75 resource_type: resource_type.clone(),
76 alias: key,
77 operations: BTreeMap::new(),
78 },
79 );
80 self.ensure_resource_type(resource_type);
81 Ok(())
82 }
83
84 pub fn ensure_resource_type(&mut self, resource_type: impl Into<String>) {
85 self.resource_types.entry(resource_type.into()).or_default();
86 }
87
88 pub fn add_operation(
89 &mut self,
90 resource_type: impl Into<String>,
91 operation: impl Into<String>,
92 input_ty: TypeExpr,
93 output_ty: TypeExpr,
94 ) {
95 self.resource_types
96 .entry(resource_type.into())
97 .or_default()
98 .operations
99 .insert(
100 operation.into(),
101 ResourceOperationBinding {
102 input_ty,
103 output_ty,
104 },
105 );
106 }
107
108 pub fn add_module_operation(
109 &mut self,
110 module_path: impl IntoIterator<Item = impl Into<String>>,
111 resource_type: impl Into<String>,
112 operation: impl Into<String>,
113 host_operation: impl Into<String>,
114 input_ty: TypeExpr,
115 output_ty: TypeExpr,
116 ) {
117 let path = module_path.into_iter().map(Into::into).collect::<Vec<_>>();
118 assert!(!path.is_empty(), "module path must not be empty");
119 let resource_type = resource_type.into();
120 let operation = operation.into();
121 self.add_module_instance(path.iter().map(String::as_str), resource_type.clone())
122 .expect("module operation resource type cannot conflict with existing module alias");
123 self.add_operation(resource_type, operation.clone(), input_ty, output_ty);
124 let key = module_path_key(&path);
125 self.module_instances
126 .get_mut(&key)
127 .expect("module instance was just inserted")
128 .operations
129 .insert(
130 operation,
131 ModuleOperationBinding {
132 host_operation: host_operation.into(),
133 },
134 );
135 }
136
137 pub fn add_value_constructor(
138 &mut self,
139 path: impl IntoIterator<Item = impl Into<String>>,
140 input_ty: TypeExpr,
141 output_ty: TypeExpr,
142 ) {
143 let path = path.into_iter().map(Into::into).collect::<Vec<_>>();
144 assert!(!path.is_empty(), "constructor path must not be empty");
145 let key = module_path_key(&path);
146 self.value_constructors.insert(
147 key.clone(),
148 ValueConstructorBinding {
149 path,
150 type_name: format_type_expr(&output_ty),
151 input_ty,
152 output_ty,
153 },
154 );
155 }
156
157 pub fn add_named_data_type(
158 &mut self,
159 data_type: NamedDataType,
160 ) -> Result<(), ResourceCatalogError> {
161 self.merge_named_data_type(data_type)
162 }
163
164 pub fn add_trigger_source_constructor(
165 &mut self,
166 path: impl IntoIterator<Item = impl Into<String>>,
167 input_ty: TypeExpr,
168 event_ty: NamedDataType,
169 ) -> Result<(), ResourceCatalogError> {
170 let path = path.into_iter().map(Into::into).collect::<Vec<_>>();
171 assert!(!path.is_empty(), "constructor path must not be empty");
172 let source_type = module_path_key(&path);
173 self.check_named_data_type(&event_ty)?;
174 if let Some(existing) = self.trigger_sources.get(source_type.as_str())
175 && existing.event_type() != &event_ty
176 {
177 return Err(ResourceCatalogError::ConflictingTriggerSource {
178 source_type,
179 existing: existing.event_type().name().to_string(),
180 incoming: event_ty.name().to_string(),
181 });
182 }
183 self.add_value_constructor(path, input_ty, TypeExpr::Ref(source_type.clone().into()));
184 self.add_trigger_source_type(source_type, event_ty)?;
185 Ok(())
186 }
187
188 pub(crate) fn add_trigger_source_type(
189 &mut self,
190 source_ty: impl Into<String>,
191 event_ty: NamedDataType,
192 ) -> Result<(), ResourceCatalogError> {
193 self.merge_named_data_type(event_ty.clone())?;
194 self.trigger_sources
195 .insert(source_ty.into(), TriggerSourceBinding::new(event_ty));
196 Ok(())
197 }
198
199 pub fn extend(&mut self, other: Self) {
200 self.try_extend(other)
201 .expect("conflicting resource catalog entries");
202 }
203
204 pub fn try_extend(&mut self, other: Self) -> Result<(), ResourceCatalogError> {
205 for (resource_type, incoming) in other.resource_types {
206 let entry = self.resource_types.entry(resource_type).or_default();
207 entry.operations.extend(incoming.operations);
208 }
209 for (alias, incoming) in other.module_instances {
210 match self.module_instances.get_mut(&alias) {
211 Some(existing)
212 if existing.path == incoming.path
213 && existing.resource_type == incoming.resource_type
214 && existing.alias == incoming.alias =>
215 {
216 existing.operations.extend(incoming.operations);
217 }
218 Some(existing) => {
219 return Err(ResourceCatalogError::ConflictingModuleInstance {
220 alias,
221 existing: existing.resource_type.clone(),
222 incoming: incoming.resource_type,
223 });
224 }
225 None => {
226 self.module_instances.insert(alias, incoming);
227 }
228 }
229 }
230 for data_type in other.named_data_types.into_values() {
231 self.merge_named_data_type(data_type)?;
232 }
233 self.value_constructors.extend(other.value_constructors);
234 self.trigger_sources.extend(other.trigger_sources);
235 Ok(())
236 }
237
238 pub fn union(mut self, other: Self) -> Self {
239 self.extend(other);
240 self
241 }
242
243 pub fn satisfies(&self, required: &Self) -> bool {
244 for (path, required_module) in &required.module_instances {
245 let Some(module) = self.module_instances.get(path) else {
246 return false;
247 };
248 if module.path != required_module.path
249 || module.resource_type != required_module.resource_type
250 || module.alias != required_module.alias
251 {
252 return false;
253 }
254 for (operation, required_binding) in &required_module.operations {
255 if module.operations.get(operation) != Some(required_binding) {
256 return false;
257 }
258 }
259 }
260 for (resource_type, required_catalog) in &required.resource_types {
261 let Some(catalog) = self.resource_types.get(resource_type) else {
262 return false;
263 };
264 for (operation, required_binding) in &required_catalog.operations {
265 if catalog.operations.get(operation) != Some(required_binding) {
266 return false;
267 }
268 }
269 }
270 for (path, required_constructor) in &required.value_constructors {
271 if self.value_constructors.get(path) != Some(required_constructor) {
272 return false;
273 }
274 }
275 for (name, required_data_type) in &required.named_data_types {
276 if self.named_data_types.get(name) != Some(required_data_type) {
277 return false;
278 }
279 }
280 for (source_type, required_binding) in &required.trigger_sources {
281 if self.trigger_sources.get(source_type) != Some(required_binding) {
282 return false;
283 }
284 }
285 true
286 }
287
288 pub fn has_resource_type(&self, resource_type: &str) -> bool {
289 self.resource_types.contains_key(resource_type)
290 }
291
292 pub fn has_named_data_type(&self, name: &str) -> bool {
293 self.named_data_types.contains_key(name)
294 }
295
296 pub fn is_known_opaque_value_type(&self, name: &str) -> bool {
297 self.trigger_sources.contains_key(name)
298 || self.value_constructors.values().any(|constructor| {
299 matches!(&constructor.output_ty, TypeExpr::Ref(type_name) if type_name == name)
300 })
301 }
302
303 pub fn decode_host_value_as<T: serde::de::DeserializeOwned>(
304 &self,
305 source_type: &str,
306 value: serde_json::Value,
307 ) -> Result<T, crate::HostValueError> {
308 if !self.is_known_opaque_value_type(source_type) {
309 return Err(crate::HostValueError::UnknownSourceType {
310 source_type: source_type.to_string(),
311 });
312 }
313 serde_json::from_value(value).map_err(|err| crate::HostValueError::MalformedPayload {
314 source_type: source_type.to_string(),
315 message: err.to_string(),
316 })
317 }
318
319 pub fn module_instances(&self) -> impl Iterator<Item = (&str, &ModuleInstanceCatalog)> {
320 self.module_instances
321 .iter()
322 .map(|(path, module)| (path.as_str(), module))
323 }
324
325 pub fn resource_types(&self) -> impl Iterator<Item = (&str, &ResourceTypeCatalog)> {
326 self.resource_types
327 .iter()
328 .map(|(resource_type, catalog)| (resource_type.as_str(), catalog))
329 }
330
331 pub fn named_data_types(&self) -> impl Iterator<Item = (&str, &NamedDataType)> {
332 self.named_data_types
333 .iter()
334 .map(|(name, data_type)| (name.as_str(), data_type))
335 }
336
337 pub fn value_constructors(&self) -> impl Iterator<Item = (&str, &ValueConstructorBinding)> {
338 self.value_constructors
339 .iter()
340 .map(|(path, constructor)| (path.as_str(), constructor))
341 }
342
343 pub fn trigger_sources(&self) -> impl Iterator<Item = (&str, &TriggerSourceBinding)> {
344 self.trigger_sources
345 .iter()
346 .map(|(source_type, binding)| (source_type.as_str(), binding))
347 }
348
349 pub fn resolve_named_data_type(&self, name: &str) -> Option<&NamedDataType> {
350 self.named_data_types.get(name)
351 }
352
353 pub fn resolve_trigger_source(&self, source_ty: &str) -> Option<&TriggerSourceBinding> {
354 self.trigger_sources.get(source_ty)
355 }
356
357 pub fn resolve_module_path(&self, path: &[impl AsRef<str>]) -> Option<ResourceRefExpr> {
358 let key = module_path_key(path);
359 let module = self.module_instances.get(&key)?;
360 Some(ResourceRefExpr::resolved(
361 module
362 .path
363 .iter()
364 .map(|segment| segment.as_str().into())
365 .collect(),
366 module.resource_type.clone(),
367 module.alias.clone(),
368 ))
369 }
370
371 pub fn resolve_alias(&self, resource: &ResourceRefExpr) -> Option<&ResourceTypeCatalog> {
372 if !resource.resource_type.is_empty() {
373 return self.resource_types.get(resource.resource_type.as_str());
374 }
375 let resolved = self.resolve_module_path(&resource.path)?;
376 self.resource_types.get(resolved.resource_type.as_str())
377 }
378
379 pub fn resolve_operation(
380 &self,
381 resource_type: &str,
382 operation: &str,
383 ) -> Option<&ResourceOperationBinding> {
384 self.resource_types
385 .get(resource_type)?
386 .operations
387 .get(operation)
388 }
389
390 pub fn has_operations(&self) -> bool {
391 self.resource_types
392 .values()
393 .any(|resource_type| !resource_type.operations.is_empty())
394 }
395
396 pub fn resolve_module_operation(
397 &self,
398 resource_type: &str,
399 alias: &str,
400 operation: &str,
401 ) -> Option<&ModuleOperationBinding> {
402 let module = self.module_instances.get(alias)?;
403 (module.resource_type == resource_type).then_some(())?;
404 module.operations.get(operation)
405 }
406
407 pub fn resolve_value_constructor(
408 &self,
409 path: &[impl AsRef<str>],
410 ) -> Option<&ValueConstructorBinding> {
411 self.value_constructors.get(&module_path_key(path))
412 }
413
414 pub fn trigger_source_event(&self, source_ty: &TypeExpr) -> Option<TypeExpr> {
415 let TypeExpr::Ref(name) = source_ty else {
416 return None;
417 };
418 self.trigger_sources
419 .get(name.as_str())
420 .map(|binding| binding.event_type().to_ref_ty())
421 }
422
423 pub fn operation_suggestions_for_host(&self, host_operation: &str) -> Vec<String> {
424 let mut suggestions = Vec::new();
425 for module in self.module_instances.values() {
426 for (operation, binding) in &module.operations {
427 if binding.host_operation == host_operation {
428 suggestions.push(format!("{}.{}", module.alias, operation));
429 }
430 }
431 }
432 suggestions.sort();
433 suggestions.dedup();
434 suggestions
435 }
436
437 pub fn operation_suggestions_for_prefix(
438 &self,
439 prefix: &[impl AsRef<str>],
440 operation: &str,
441 ) -> Vec<String> {
442 let prefix = module_path_key(prefix);
443 let mut suggestions = Vec::new();
444 for module in self.module_instances.values() {
445 if module.alias == prefix || !module.alias.starts_with(&format!("{prefix}.")) {
446 continue;
447 }
448 if self
449 .resolve_operation(&module.resource_type, operation)
450 .is_some()
451 {
452 suggestions.push(format!("{}.{}", module.alias, operation));
453 }
454 }
455 suggestions.sort();
456 suggestions.dedup();
457 suggestions
458 }
459
460 fn check_named_data_type(&self, data_type: &NamedDataType) -> Result<(), ResourceCatalogError> {
461 if let Some(existing) = self.named_data_types.get(data_type.name())
462 && existing != data_type
463 {
464 return Err(ResourceCatalogError::ConflictingNamedDataType {
465 name: data_type.name().to_string(),
466 });
467 }
468 Ok(())
469 }
470
471 fn merge_named_data_type(
472 &mut self,
473 data_type: NamedDataType,
474 ) -> Result<(), ResourceCatalogError> {
475 self.check_named_data_type(&data_type)?;
476 self.named_data_types
477 .entry(data_type.name().to_string())
478 .or_insert(data_type);
479 Ok(())
480 }
481}
482
483#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
484pub struct NamedDataType {
485 name: String,
486 ty: TypeExpr,
487}
488
489impl NamedDataType {
490 pub fn new(name: impl Into<String>, ty: TypeExpr) -> Result<Self, NamedDataTypeError> {
491 let name = name.into();
492 if !is_qualified_type_name(&name) {
493 return Err(NamedDataTypeError::InvalidName { name });
494 }
495 if !matches!(ty, TypeExpr::Object(_)) {
496 return Err(NamedDataTypeError::ExpectedObject { name });
497 }
498 validate_named_data_shape(&ty)?;
499 Ok(Self { name, ty })
500 }
501
502 pub fn object(
503 name: impl Into<String>,
504 fields: Vec<TypeField>,
505 ) -> Result<Self, NamedDataTypeError> {
506 Self::new(name, TypeExpr::Object(fields))
507 }
508
509 pub fn name(&self) -> &str {
510 &self.name
511 }
512
513 pub fn ty(&self) -> &TypeExpr {
514 &self.ty
515 }
516
517 pub fn to_ref_ty(&self) -> TypeExpr {
518 TypeExpr::Ref(self.name.clone().into())
519 }
520}
521
522#[derive(Clone, Debug, PartialEq, Eq, Error)]
523pub enum NamedDataTypeError {
524 #[error("host data type name `{name}` must be qualified")]
525 InvalidName { name: String },
526 #[error("host data type `{name}` must be an object type")]
527 ExpectedObject { name: String },
528 #[error("host data type object has duplicate field `{field}`")]
529 DuplicateField { field: String },
530 #[error("host data type enum has duplicate value `{value}`")]
531 DuplicateEnumValue { value: String },
532 #[error("host data type shape cannot contain nested type ref `{name}`")]
533 NestedRef { name: String },
534 #[error("host data type shape cannot contain {ty}")]
535 UnsupportedType { ty: &'static str },
536}
537
538#[derive(Clone, Debug, PartialEq, Eq, Error)]
539pub enum ResourceCatalogError {
540 #[error("conflicting host data type definition `{name}`")]
541 ConflictingNamedDataType { name: String },
542 #[error(
543 "module `{alias}` already has resource type `{existing}`, cannot change it to `{incoming}`"
544 )]
545 ConflictingModuleInstance {
546 alias: String,
547 existing: String,
548 incoming: String,
549 },
550 #[error(
551 "trigger source `{source_type}` already emits `{existing}`, cannot change it to `{incoming}`"
552 )]
553 ConflictingTriggerSource {
554 source_type: String,
555 existing: String,
556 incoming: String,
557 },
558}
559
560fn is_qualified_type_name(name: &str) -> bool {
561 let mut segments = name.split('.');
562 let mut count = 0usize;
563 for segment in segments.by_ref() {
564 count += 1;
565 let mut chars = segment.chars();
566 let Some(first) = chars.next() else {
567 return false;
568 };
569 if !(first.is_ascii_alphabetic() || first == '_') {
570 return false;
571 }
572 if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
573 return false;
574 }
575 }
576 count >= 2
577}
578
579fn validate_named_data_shape(ty: &TypeExpr) -> Result<(), NamedDataTypeError> {
580 match ty {
581 TypeExpr::Any
582 | TypeExpr::Str
583 | TypeExpr::Int
584 | TypeExpr::Float
585 | TypeExpr::Bool
586 | TypeExpr::Dict
587 | TypeExpr::Null => Ok(()),
588 TypeExpr::Enum(values) => {
589 let mut seen = BTreeSet::new();
590 for value in values {
591 if !seen.insert(value.to_string()) {
592 return Err(NamedDataTypeError::DuplicateEnumValue {
593 value: value.to_string(),
594 });
595 }
596 }
597 Ok(())
598 }
599 TypeExpr::List(item) => validate_named_data_shape(item),
600 TypeExpr::Object(fields) => {
601 let mut seen = BTreeSet::new();
602 for field in fields {
603 if !seen.insert(field.name.to_string()) {
604 return Err(NamedDataTypeError::DuplicateField {
605 field: field.name.to_string(),
606 });
607 }
608 validate_named_data_shape(&field.ty)?;
609 }
610 Ok(())
611 }
612 TypeExpr::Union(items) => {
613 for item in items {
614 validate_named_data_shape(item)?;
615 }
616 Ok(())
617 }
618 TypeExpr::Ref(name) => Err(NamedDataTypeError::NestedRef {
619 name: name.to_string(),
620 }),
621 TypeExpr::Process { .. } => Err(NamedDataTypeError::UnsupportedType { ty: "process" }),
622 TypeExpr::TriggerHandle(_) => Err(NamedDataTypeError::UnsupportedType {
623 ty: "trigger handle",
624 }),
625 }
626}
627
628#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
629pub struct ResourceTypeCatalog {
630 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
631 pub operations: BTreeMap<String, ResourceOperationBinding>,
632}
633
634#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
635pub struct ModuleInstanceCatalog {
636 pub path: Vec<String>,
637 pub resource_type: String,
638 pub alias: String,
639 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
640 pub operations: BTreeMap<String, ModuleOperationBinding>,
641}
642
643#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
644pub struct ResourceOperationBinding {
645 pub input_ty: TypeExpr,
646 pub output_ty: TypeExpr,
647}
648
649#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
650pub struct ModuleOperationBinding {
651 pub host_operation: String,
652}
653
654#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
655pub struct ValueConstructorBinding {
656 pub path: Vec<String>,
657 pub type_name: String,
658 pub input_ty: TypeExpr,
659 pub output_ty: TypeExpr,
660}
661
662#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
663pub struct TriggerSourceBinding {
664 event_type: NamedDataType,
665}
666
667impl TriggerSourceBinding {
668 fn new(event_type: NamedDataType) -> Self {
669 Self { event_type }
670 }
671
672 pub fn event_type(&self) -> &NamedDataType {
673 &self.event_type
674 }
675
676 pub fn event_ty(&self) -> &TypeExpr {
677 self.event_type.ty()
678 }
679
680 pub fn event_type_name(&self) -> &str {
681 self.event_type.name()
682 }
683}
684
685#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
686pub struct LashlangSurface {
687 #[serde(default)]
688 pub resources: ResourceCatalog,
689 #[serde(default)]
690 pub abilities: LashlangAbilities,
691 #[serde(default)]
692 pub language_features: LashlangLanguageFeatures,
693}
694
695impl LashlangSurface {
696 pub fn new(resources: ResourceCatalog, abilities: LashlangAbilities) -> Self {
697 Self {
698 resources,
699 abilities,
700 language_features: LashlangLanguageFeatures::default(),
701 }
702 }
703
704 pub fn with_language_features(mut self, language_features: LashlangLanguageFeatures) -> Self {
705 self.language_features = language_features;
706 self
707 }
708
709 pub fn satisfies(&self, requirements: &SurfaceRequirements) -> bool {
710 self.abilities.satisfies(requirements.abilities)
711 && self
712 .language_features
713 .satisfies(requirements.language_features)
714 && self.resources.satisfies(&requirements.resources)
715 }
716}
717
718#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
719#[serde(default)]
720pub struct LashlangLanguageFeatures {
721 pub label_annotations: bool,
722}
723
724impl LashlangLanguageFeatures {
725 pub fn union(self, other: Self) -> Self {
726 Self {
727 label_annotations: self.label_annotations || other.label_annotations,
728 }
729 }
730
731 pub fn satisfies(self, required: Self) -> bool {
732 !required.label_annotations || self.label_annotations
733 }
734
735 pub fn with_label_annotations(mut self) -> Self {
736 self.label_annotations = true;
737 self
738 }
739}
740
741#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
742#[serde(default)]
743pub struct LashlangAbilities {
744 pub processes: bool,
745 pub sleep: bool,
746 pub process_signals: bool,
747 pub triggers: bool,
748}
749
750impl LashlangAbilities {
751 pub fn union(self, other: Self) -> Self {
752 Self {
753 processes: self.processes || other.processes,
754 sleep: self.sleep || other.sleep,
755 process_signals: self.process_signals || other.process_signals,
756 triggers: self.triggers || other.triggers,
757 }
758 }
759
760 pub fn satisfies(self, required: Self) -> bool {
761 (!required.processes || self.processes)
762 && (!required.sleep || self.sleep)
763 && (!required.process_signals || self.process_signals)
764 && (!required.triggers || self.triggers)
765 }
766
767 pub fn with_processes(mut self) -> Self {
768 self.processes = true;
769 self
770 }
771
772 pub fn with_sleep(mut self) -> Self {
773 self.sleep = true;
774 self
775 }
776
777 pub fn with_process_signals(mut self) -> Self {
778 self.process_signals = true;
779 self
780 }
781
782 pub fn with_triggers(mut self) -> Self {
783 self.triggers = true;
784 self
785 }
786
787 pub fn all() -> Self {
788 Self::default()
789 .with_sleep()
790 .with_processes()
791 .with_process_signals()
792 .with_triggers()
793 }
794}
795
796fn module_path_key(path: &[impl AsRef<str>]) -> String {
797 path.iter()
798 .map(|segment| segment.as_ref())
799 .collect::<Vec<_>>()
800 .join(".")
801}
802
803#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
804pub struct LinkedModule {
805 pub module_ref: crate::ModuleRef,
806 pub required_surface_ref: crate::RequiredSurfaceRef,
807 pub artifact: ModuleArtifact,
808 #[serde(skip)]
809 linked_program: Option<Program>,
810}
811
812impl LinkedModule {
813 pub fn link(
814 program: Program,
815 surface: impl Borrow<LashlangSurface>,
816 ) -> Result<Self, LinkError> {
817 let surface = surface.borrow();
818 let mut linker = Linker::new(&program, surface);
819 let program = linker.link_program()?;
820 let requirements =
821 surface_requirements_for_program_with_catalog(&program, &surface.resources);
822 let artifact =
823 ModuleArtifact::from_program_with_requirements(program.clone(), requirements).map_err(
824 |err| LinkError::ModuleHash {
825 message: err.to_string(),
826 },
827 )?;
828 Ok(Self {
829 module_ref: artifact.module_ref.clone(),
830 required_surface_ref: artifact.required_surface_ref.clone(),
831 artifact,
832 linked_program: Some(program),
833 })
834 }
835
836 pub fn program(&self) -> &Program {
837 self.linked_program
838 .as_ref()
839 .unwrap_or(&self.artifact.canonical_ir)
840 }
841}
842
843#[derive(Clone, Debug, Error, PartialEq, Eq)]
844pub enum LinkError {
845 #[error("duplicate declaration `{name}`")]
846 DuplicateDeclaration { name: String, span: Option<Span> },
847 #[error("duplicate process parameter `{name}`")]
848 DuplicateProcessParam { name: String, span: Option<Span> },
849 #[error("unknown process `{name}`")]
850 UnknownProcess { name: String, span: Option<Span> },
851 #[error("process `{process}` is missing argument `{arg}`")]
852 MissingProcessArgument {
853 process: String,
854 arg: String,
855 span: Option<Span>,
856 },
857 #[error("process `{process}` does not accept argument `{arg}`")]
858 UnexpectedProcessArgument {
859 process: String,
860 arg: String,
861 span: Option<Span>,
862 },
863 #[error("duplicate process argument `{arg}`")]
864 DuplicateProcessArgument { arg: String, span: Option<Span> },
865 #[error("unknown name `{name}`")]
866 UnknownName { name: String, span: Option<Span> },
867 #[error("unknown builtin `{name}`")]
868 UnknownBuiltin { name: String, span: Option<Span> },
869 #[error("unknown module `{path}`")]
870 UnknownResource { path: String, span: Option<Span> },
871 #[error("unknown type `{name}`")]
872 UnknownType { name: String, span: Option<Span> },
873 #[error("constructor `{path}` expects {expected}, got {actual}")]
874 IncompatibleConstructorInput {
875 path: String,
876 expected: String,
877 actual: String,
878 span: Option<Span>,
879 },
880 #[error("operation `{operation}` expects {expected}, got {actual}")]
881 IncompatibleOperationInput {
882 operation: String,
883 expected: String,
884 actual: String,
885 span: Option<Span>,
886 },
887 #[error("process `{process}` return type is incompatible: expected {expected}, got {actual}")]
888 IncompatibleProcessReturn {
889 process: String,
890 expected: String,
891 actual: String,
892 span: Option<Span>,
893 },
894 #[error("trigger registration requires {{ source, target, inputs, name? }}")]
895 InvalidTriggerRegistration { span: Option<Span> },
896 #[error("trigger registration `inputs` must be a literal record")]
897 InvalidTriggerInputs { span: Option<Span> },
898 #[error("trigger registration input `{input}` is duplicated")]
899 DuplicateTriggerInput { input: String, span: Option<Span> },
900 #[error("trigger target `{process}` input `{input}` is not mapped")]
901 MissingTriggerInput {
902 process: String,
903 input: String,
904 span: Option<Span>,
905 },
906 #[error("trigger target `{process}` has no input `{input}`")]
907 UnknownTriggerInput {
908 process: String,
909 input: String,
910 span: Option<Span>,
911 },
912 #[error("trigger registration `inputs` must map at least one param to `trigger.event`")]
913 MissingTriggerEventInput { span: Option<Span> },
914 #[error("`trigger.event` is only valid as a direct value inside `triggers.register` inputs")]
915 TriggerEventOutsideInputs { span: Option<Span> },
916 #[error(
917 "`trigger.event` represents the whole event; projections such as `trigger.event.field` are not supported"
918 )]
919 TriggerEventProjection { span: Option<Span> },
920 #[error("trigger listing requires {{ target }}")]
921 InvalidTriggerList { span: Option<Span> },
922 #[error("trigger cancellation requires {{ handle }}")]
923 InvalidTriggerCancel { span: Option<Span> },
924 #[error("trigger source type `{source_ty}` is not registered as a TriggerSource")]
925 UnknownTriggerSourceType {
926 source_ty: String,
927 span: Option<Span>,
928 },
929 #[error("trigger target must be a process value, got {actual}")]
930 InvalidTriggerTarget { actual: String, span: Option<Span> },
931 #[error("trigger source emits {event}, but target input `{input_name}` expects {input}")]
932 TriggerEventMismatch {
933 event: String,
934 input_name: String,
935 input: String,
936 span: Option<Span>,
937 },
938 #[error("receiver for operation `{operation}` is not a module authority")]
939 UnresolvedReceiver {
940 operation: String,
941 span: Option<Span>,
942 },
943 #[error("resource type `{resource_type}` does not expose operation `{operation}`")]
944 UnknownResourceOperation {
945 resource_type: String,
946 operation: String,
947 span: Option<Span>,
948 },
949 #[error("module `{module_path}` does not expose operation `{operation}`; available identity-qualified paths: {}", suggestions.join(", "))]
950 AmbiguousModuleOperation {
951 module_path: String,
952 operation: String,
953 suggestions: Vec<String>,
954 span: Option<Span>,
955 },
956 #[error("tools must be called through module paths, e.g. `{suggestion}`")]
957 BareToolCall {
958 name: String,
959 suggestion: String,
960 span: Option<Span>,
961 },
962 #[error(
963 "process `{process}` argument `{arg}` has incompatible authority type: expected {expected}, got {actual}"
964 )]
965 IncompatibleProcessArgument {
966 process: String,
967 arg: String,
968 expected: String,
969 actual: String,
970 span: Option<Span>,
971 },
972 #[error("lashlang feature `{feature}` is disabled by this host")]
973 FeatureDisabled {
974 feature: &'static str,
975 span: Option<Span>,
976 },
977 #[error("`{keyword}` can only be used inside a process body")]
978 ProcessLifecycleOutsideProcess {
979 keyword: &'static str,
980 span: Option<Span>,
981 },
982 #[error("cannot access `{access}` on opaque host value `{type_name}`")]
983 OpaqueHostValueAccess {
984 type_name: String,
985 access: String,
986 span: Option<Span>,
987 },
988 #[error("failed to hash linked module: {message}")]
989 ModuleHash { message: String },
990}
991
992impl LinkError {
993 pub fn span(&self) -> Option<Span> {
994 match self {
995 Self::DuplicateDeclaration { span, .. }
996 | Self::DuplicateProcessParam { span, .. }
997 | Self::UnknownProcess { span, .. }
998 | Self::MissingProcessArgument { span, .. }
999 | Self::UnexpectedProcessArgument { span, .. }
1000 | Self::DuplicateProcessArgument { span, .. }
1001 | Self::UnknownName { span, .. }
1002 | Self::UnknownBuiltin { span, .. }
1003 | Self::UnknownResource { span, .. }
1004 | Self::UnknownType { span, .. }
1005 | Self::IncompatibleConstructorInput { span, .. }
1006 | Self::IncompatibleOperationInput { span, .. }
1007 | Self::IncompatibleProcessReturn { span, .. }
1008 | Self::InvalidTriggerRegistration { span }
1009 | Self::InvalidTriggerInputs { span }
1010 | Self::DuplicateTriggerInput { span, .. }
1011 | Self::MissingTriggerInput { span, .. }
1012 | Self::UnknownTriggerInput { span, .. }
1013 | Self::MissingTriggerEventInput { span }
1014 | Self::TriggerEventOutsideInputs { span }
1015 | Self::TriggerEventProjection { span }
1016 | Self::InvalidTriggerList { span }
1017 | Self::InvalidTriggerCancel { span }
1018 | Self::UnknownTriggerSourceType { span, .. }
1019 | Self::InvalidTriggerTarget { span, .. }
1020 | Self::TriggerEventMismatch { span, .. }
1021 | Self::UnresolvedReceiver { span, .. }
1022 | Self::UnknownResourceOperation { span, .. }
1023 | Self::AmbiguousModuleOperation { span, .. }
1024 | Self::BareToolCall { span, .. }
1025 | Self::IncompatibleProcessArgument { span, .. }
1026 | Self::FeatureDisabled { span, .. }
1027 | Self::ProcessLifecycleOutsideProcess { span, .. }
1028 | Self::OpaqueHostValueAccess { span, .. } => *span,
1029 Self::ModuleHash { .. } => None,
1030 }
1031 }
1032}
1033
1034#[derive(Clone, Debug, PartialEq, Eq)]
1035enum Binding {
1036 Value(TypeExpr),
1037 Resource { resource_type: String },
1038}
1039
1040struct Linker<'module> {
1041 program: &'module Program,
1042 surface: &'module LashlangSurface,
1043 process_names: BTreeSet<String>,
1044 process_types: BTreeMap<String, TypeExpr>,
1045 type_names: BTreeSet<String>,
1046 type_defs: BTreeMap<String, TypeExpr>,
1047}
1048
1049impl<'module> Linker<'module> {
1050 fn new(program: &'module Program, surface: &'module LashlangSurface) -> Self {
1051 Self {
1052 program,
1053 surface,
1054 process_names: BTreeSet::new(),
1055 process_types: BTreeMap::new(),
1056 type_names: BTreeSet::new(),
1057 type_defs: BTreeMap::new(),
1058 }
1059 }
1060
1061 fn link_program(&mut self) -> Result<Program, LinkError> {
1062 self.collect_declarations()?;
1067 let declarations = self
1068 .program
1069 .declarations
1070 .iter()
1071 .enumerate()
1072 .map(|(index, declaration)| {
1073 let span = self.program.declaration_spans.get(index).copied();
1074 self.lower_declaration(declaration, span)
1075 })
1076 .collect::<Result<Vec<_>, _>>()?;
1077 let mut scope = Scope::new(true, false, None);
1078 let main = self.lower_expr(&self.program.main, &mut scope)?.0;
1079 Ok(Program {
1080 declarations,
1081 main,
1082 declaration_spans: self.program.declaration_spans.clone(),
1083 expression_spans: self.program.expression_spans.clone(),
1084 })
1085 }
1086
1087 fn collect_declarations(&mut self) -> Result<(), LinkError> {
1088 self.ensure_label_annotations_enabled_for_program()?;
1089 let mut names = BTreeSet::new();
1090 for (index, declaration) in self.program.declarations.iter().enumerate() {
1091 let span = self.program.declaration_spans.get(index).copied();
1092 let (namespace, name) = match declaration {
1093 Declaration::Type(decl) => {
1094 let name = decl.name.as_str();
1095 if !names.insert(("type", name.to_string())) {
1096 return Err(LinkError::DuplicateDeclaration {
1097 name: name.to_string(),
1098 span,
1099 });
1100 }
1101 self.type_names.insert(decl.name.to_string());
1102 self.type_defs
1103 .insert(decl.name.to_string(), decl.ty.clone());
1104 continue;
1105 }
1106 Declaration::Process(decl) => {
1107 self.ensure_feature(self.surface.abilities.processes, "processes", span)?;
1108 ("process", decl.name.as_str())
1109 }
1110 };
1111 if !names.insert((namespace, name.to_string())) {
1112 return Err(LinkError::DuplicateDeclaration {
1113 name: name.to_string(),
1114 span,
1115 });
1116 }
1117 if let Declaration::Process(decl) = declaration {
1118 self.process_names.insert(decl.name.to_string());
1119 }
1120 }
1121 for declaration in &self.program.declarations {
1122 match declaration {
1123 Declaration::Type(type_decl) => self.validate_type_refs(&type_decl.ty, None)?,
1124 Declaration::Process(process) => {
1125 for param in &process.params {
1126 self.validate_type_refs(¶m.ty, None)?;
1127 }
1128 if let Some(return_ty) = &process.return_ty {
1129 self.validate_type_refs(return_ty, None)?;
1130 }
1131 }
1132 }
1133 }
1134 for declaration in &self.program.declarations {
1135 if let Declaration::Process(process) = declaration {
1136 self.process_types.insert(
1137 process.name.to_string(),
1138 process_type_for_decl(process, TypeExpr::Any),
1139 );
1140 }
1141 }
1142 for (index, declaration) in self.program.declarations.iter().enumerate() {
1143 let Declaration::Process(process) = declaration else {
1144 continue;
1145 };
1146 let span = self.program.declaration_spans.get(index).copied();
1147 let output = self.infer_process_output(process, span)?;
1148 if let Some(expected) = &process.return_ty
1149 && !self.is_type_assignable(&output, expected)
1150 {
1151 return Err(LinkError::IncompatibleProcessReturn {
1152 process: process.name.to_string(),
1153 expected: format_type_expr(&self.resolve_type_aliases(expected)),
1154 actual: format_type_expr(&self.resolve_type_aliases(&output)),
1155 span,
1156 });
1157 }
1158 self.process_types.insert(
1159 process.name.to_string(),
1160 process_type_for_decl(process, output),
1161 );
1162 }
1163 Ok(())
1164 }
1165
1166 fn ensure_label_annotations_enabled_for_program(&self) -> Result<(), LinkError> {
1167 if self.surface.language_features.label_annotations {
1168 return Ok(());
1169 }
1170 for (index, declaration) in self.program.declarations.iter().enumerate() {
1171 let span = self.program.declaration_spans.get(index).copied();
1172 if let Declaration::Process(process) = declaration {
1173 if process.label.is_some() || expr_has_label_annotation(&process.body) {
1174 return Err(LinkError::FeatureDisabled {
1175 feature: "label annotations",
1176 span,
1177 });
1178 }
1179 }
1180 }
1181 if expr_has_label_annotation(&self.program.main) {
1182 return Err(LinkError::FeatureDisabled {
1183 feature: "label annotations",
1184 span: self.program.expression_spans.first().copied(),
1185 });
1186 }
1187 Ok(())
1188 }
1189
1190 fn binding_for_type(&self, ty: &TypeExpr) -> Binding {
1191 match self.resource_type_for_type(ty) {
1192 Some(resource_type) => Binding::Resource { resource_type },
1193 _ => Binding::Value(ty.clone()),
1194 }
1195 }
1196
1197 fn resource_type_for_type(&self, ty: &TypeExpr) -> Option<String> {
1198 match self.resolve_type_aliases(ty) {
1199 TypeExpr::Ref(name) if self.surface.resources.has_resource_type(name.as_str()) => {
1200 Some(name.to_string())
1201 }
1202 _ => None,
1203 }
1204 }
1205
1206 fn resolve_type_aliases(&self, ty: &TypeExpr) -> TypeExpr {
1207 self.resolve_type_aliases_inner(ty, &mut BTreeSet::new())
1208 }
1209
1210 fn resolve_type_aliases_inner(&self, ty: &TypeExpr, seen: &mut BTreeSet<String>) -> TypeExpr {
1211 match ty {
1212 TypeExpr::Ref(name) => {
1213 if !seen.insert(name.to_string()) {
1214 return ty.clone();
1215 }
1216 let resolved = if let Some(ty) = self.type_defs.get(name.as_str()) {
1217 self.resolve_type_aliases_inner(ty, seen)
1218 } else if let Some(data_type) = self
1219 .surface
1220 .resources
1221 .resolve_named_data_type(name.as_str())
1222 {
1223 data_type.ty().clone()
1224 } else {
1225 ty.clone()
1226 };
1227 seen.remove(name.as_str());
1228 resolved
1229 }
1230 TypeExpr::List(item) => {
1231 TypeExpr::List(Box::new(self.resolve_type_aliases_inner(item, seen)))
1232 }
1233 TypeExpr::Object(fields) => TypeExpr::Object(
1234 fields
1235 .iter()
1236 .map(|field| TypeField {
1237 name: field.name.clone(),
1238 ty: self.resolve_type_aliases_inner(&field.ty, seen),
1239 optional: field.optional,
1240 })
1241 .collect(),
1242 ),
1243 TypeExpr::Union(items) => TypeExpr::Union(
1244 items
1245 .iter()
1246 .map(|item| self.resolve_type_aliases_inner(item, seen))
1247 .collect(),
1248 ),
1249 TypeExpr::Process {
1250 input,
1251 output,
1252 input_count,
1253 } => TypeExpr::Process {
1254 input: Box::new(self.resolve_type_aliases_inner(input, seen)),
1255 output: Box::new(self.resolve_type_aliases_inner(output, seen)),
1256 input_count: *input_count,
1257 },
1258 TypeExpr::TriggerHandle(event) => {
1259 TypeExpr::TriggerHandle(Box::new(self.resolve_type_aliases_inner(event, seen)))
1260 }
1261 TypeExpr::Any
1262 | TypeExpr::Str
1263 | TypeExpr::Int
1264 | TypeExpr::Float
1265 | TypeExpr::Bool
1266 | TypeExpr::Dict
1267 | TypeExpr::Null
1268 | TypeExpr::Enum(_) => ty.clone(),
1269 }
1270 }
1271
1272 fn is_type_assignable(&self, source: &TypeExpr, target: &TypeExpr) -> bool {
1273 let source = self.resolve_type_aliases(source);
1274 let target = self.resolve_type_aliases(target);
1275 crate::trigger::is_resolved_type_assignable(&source, &target)
1276 }
1277
1278 fn validate_type_refs(&self, ty: &TypeExpr, span: Option<Span>) -> Result<(), LinkError> {
1279 match ty {
1280 TypeExpr::Ref(name) => {
1281 if self.type_defs.contains_key(name.as_str())
1282 || self.surface.resources.has_resource_type(name.as_str())
1283 || self.surface.resources.has_named_data_type(name.as_str())
1284 || self
1285 .surface
1286 .resources
1287 .is_known_opaque_value_type(name.as_str())
1288 {
1289 Ok(())
1290 } else {
1291 Err(LinkError::UnknownType {
1292 name: name.to_string(),
1293 span,
1294 })
1295 }
1296 }
1297 TypeExpr::List(item) => self.validate_type_refs(item, span),
1298 TypeExpr::Object(fields) => {
1299 for field in fields {
1300 self.validate_type_refs(&field.ty, span)?;
1301 }
1302 Ok(())
1303 }
1304 TypeExpr::Union(items) => {
1305 for item in items {
1306 self.validate_type_refs(item, span)?;
1307 }
1308 Ok(())
1309 }
1310 TypeExpr::Process { input, output, .. } => {
1311 self.validate_type_refs(input, span)?;
1312 self.validate_type_refs(output, span)
1313 }
1314 TypeExpr::TriggerHandle(event) => self.validate_type_refs(event, span),
1315 TypeExpr::Any
1316 | TypeExpr::Str
1317 | TypeExpr::Int
1318 | TypeExpr::Float
1319 | TypeExpr::Bool
1320 | TypeExpr::Dict
1321 | TypeExpr::Null
1322 | TypeExpr::Enum(_) => Ok(()),
1323 }
1324 }
1325
1326 fn field_type(
1327 &self,
1328 target: &TypeExpr,
1329 field: &str,
1330 span: Option<Span>,
1331 ) -> Result<TypeExpr, LinkError> {
1332 let target = self.resolve_type_aliases(target);
1333 field_type(&target, field, span, |name| {
1334 self.surface.resources.is_known_opaque_value_type(name)
1335 })
1336 }
1337
1338 fn index_type(&self, target: &TypeExpr, span: Option<Span>) -> Result<TypeExpr, LinkError> {
1339 let target = self.resolve_type_aliases(target);
1340 index_type(&target, span, |name| {
1341 self.surface.resources.is_known_opaque_value_type(name)
1342 })
1343 }
1344
1345 fn ensure_feature(
1346 &self,
1347 enabled: bool,
1348 feature: &'static str,
1349 span: Option<Span>,
1350 ) -> Result<(), LinkError> {
1351 if enabled {
1352 Ok(())
1353 } else {
1354 Err(LinkError::FeatureDisabled { feature, span })
1355 }
1356 }
1357
1358 fn validate_resource_ref(
1359 &self,
1360 resource: &ResourceRefExpr,
1361 span: Option<Span>,
1362 ) -> Result<ResourceRefExpr, LinkError> {
1363 if !resource.resource_type.is_empty() {
1364 return self
1365 .surface
1366 .resources
1367 .resolve_alias(resource)
1368 .map(|_| resource.clone())
1369 .ok_or_else(|| LinkError::UnknownResource {
1370 path: resource.path_string(),
1371 span,
1372 });
1373 }
1374 self.surface
1375 .resources
1376 .resolve_module_path(&resource.path)
1377 .ok_or_else(|| LinkError::UnknownResource {
1378 path: resource.path_string(),
1379 span,
1380 })
1381 }
1382
1383 fn lower_declaration(
1384 &self,
1385 declaration: &Declaration,
1386 span: Option<Span>,
1387 ) -> Result<Declaration, LinkError> {
1388 Ok(match declaration {
1389 Declaration::Type(type_decl) => Declaration::Type(type_decl.clone()),
1390 Declaration::Process(process) => {
1391 self.ensure_feature(self.surface.abilities.processes, "processes", span)?;
1392 if process.label.is_some() {
1393 self.ensure_feature(
1394 self.surface.language_features.label_annotations,
1395 "label annotations",
1396 span,
1397 )?;
1398 }
1399 let mut scope = Scope::new(false, true, span);
1400 let mut seen = BTreeSet::new();
1401 for param in &process.params {
1402 if !seen.insert(param.name.to_string()) {
1403 return Err(LinkError::DuplicateProcessParam {
1404 name: param.name.to_string(),
1405 span,
1406 });
1407 }
1408 scope.bind(param.name.as_str(), self.binding_for_type(¶m.ty));
1409 }
1410 scope.bind("input", Binding::Value(process_input_type(process)));
1411 scope.bind("inputs", Binding::Value(process_input_record_type(process)));
1412 let body = self.lower_expr(&process.body, &mut scope)?.0;
1413 Declaration::Process(ProcessDecl {
1414 name: process.name.clone(),
1415 params: process.params.clone(),
1416 return_ty: process.return_ty.clone(),
1417 label: process.label.clone(),
1418 body,
1419 })
1420 }
1421 })
1422 }
1423
1424 fn lower_expr(
1425 &self,
1426 expr: &Expr,
1427 scope: &mut Scope,
1428 ) -> Result<(Expr, Option<Binding>), LinkError> {
1429 self.reject_trigger_event_special_form(expr, scope.span)?;
1430 if matches!(expr, Expr::Variable(_) | Expr::Field { .. })
1431 && let Some(resource) = self.resolve_module_expr(expr, scope)
1432 {
1433 return Ok((
1434 Expr::ResourceRef(resource.clone()),
1435 Some(Binding::Resource {
1436 resource_type: resource.resource_type.to_string(),
1437 }),
1438 ));
1439 }
1440 Ok(match expr {
1441 Expr::Block(expressions) => {
1442 let mut lowered = Vec::with_capacity(expressions.len());
1443 let mut last = None;
1444 for expression in expressions {
1445 let (expr, binding) = self.lower_expr(expression, scope)?;
1446 lowered.push(expr);
1447 last = binding;
1448 }
1449 (Expr::Block(lowered), last)
1450 }
1451 Expr::LabelAnnotated { label, expr } => {
1452 self.ensure_feature(
1453 self.surface.language_features.label_annotations,
1454 "label annotations",
1455 scope.span,
1456 )?;
1457 let (expr, binding) = self.lower_expr(expr, scope)?;
1458 (
1459 Expr::LabelAnnotated {
1460 label: label.clone(),
1461 expr: Box::new(expr),
1462 },
1463 binding,
1464 )
1465 }
1466 Expr::Variable(name) => {
1467 if let Some(binding) = scope.get(name) {
1468 (Expr::Variable(name.clone()), Some(binding))
1469 } else if let Some(process_ty) = self.process_types.get(name.as_str()) {
1470 (
1471 Expr::ProcessRef {
1472 process: name.clone(),
1473 },
1474 Some(Binding::Value(process_ty.clone())),
1475 )
1476 } else if scope.allow_unknown_globals {
1477 (
1480 Expr::Variable(name.clone()),
1481 Some(Binding::Value(TypeExpr::Any)),
1482 )
1483 } else {
1484 return Err(LinkError::UnknownName {
1485 name: name.to_string(),
1486 span: scope.span,
1487 });
1488 }
1489 }
1490 Expr::Null
1491 | Expr::Bool(_)
1492 | Expr::Number(_)
1493 | Expr::String(_)
1494 | Expr::Break
1495 | Expr::Continue
1496 | Expr::TypeLiteral(_) => (expr.clone(), Some(Binding::Value(literal_type(expr)))),
1497 Expr::List(items) => {
1498 let mut lowered = Vec::with_capacity(items.len());
1499 let mut item_types = Vec::with_capacity(items.len());
1500 for item in items {
1501 let (item, binding) = self.lower_expr(item, scope)?;
1502 lowered.push(item);
1503 item_types.push(binding_type(binding.as_ref()));
1504 }
1505 (
1506 Expr::List(lowered),
1507 Some(Binding::Value(TypeExpr::List(Box::new(union_type(
1508 item_types,
1509 ))))),
1510 )
1511 }
1512 Expr::Record(entries) => {
1513 let mut lowered = Vec::with_capacity(entries.len());
1514 let mut fields = Vec::with_capacity(entries.len());
1515 for (name, value) in entries {
1516 let (value, binding) = self.lower_expr(value, scope)?;
1517 fields.push(TypeField {
1518 name: name.clone(),
1519 ty: binding_type(binding.as_ref()),
1520 optional: false,
1521 });
1522 lowered.push((name.clone(), value));
1523 }
1524 (
1525 Expr::Record(lowered),
1526 Some(Binding::Value(TypeExpr::Object(fields))),
1527 )
1528 }
1529 Expr::Assign { target, expr } => {
1530 for step in &target.steps {
1531 if let AssignPathStep::Index(index) = step {
1532 self.lower_expr(index, scope)?;
1533 }
1534 }
1535 let (lowered, binding) = self.lower_expr(expr, scope)?;
1536 if target.steps.is_empty() {
1537 scope.bind(
1538 target.root.as_str(),
1539 binding.clone().unwrap_or(any_binding()),
1540 );
1541 } else if scope.get(&target.root).is_none() && !scope.allow_unknown_globals {
1542 return Err(LinkError::UnknownName {
1543 name: target.root.to_string(),
1544 span: scope.span,
1545 });
1546 }
1547 (
1548 Expr::Assign {
1549 target: target.clone(),
1550 expr: Box::new(lowered),
1551 },
1552 binding,
1553 )
1554 }
1555 Expr::If {
1556 condition,
1557 then_block,
1558 else_block,
1559 } => {
1560 let condition = self.lower_expr(condition, scope)?.0;
1561 let mut then_scope = scope.clone();
1562 let (then_block, then_binding) = self.lower_expr(then_block, &mut then_scope)?;
1563 let mut else_scope = scope.clone();
1564 let (else_block, else_binding) = self.lower_expr(else_block, &mut else_scope)?;
1565 scope.merge_from(then_scope);
1566 scope.merge_from(else_scope);
1567 (
1568 Expr::If {
1569 condition: Box::new(condition),
1570 then_block: Box::new(then_block),
1571 else_block: Box::new(else_block),
1572 },
1573 Some(Binding::Value(union_type(vec![
1574 binding_type(then_binding.as_ref()),
1575 binding_type(else_binding.as_ref()),
1576 ]))),
1577 )
1578 }
1579 Expr::For {
1580 binding,
1581 iterable,
1582 body,
1583 } => {
1584 let iterable = self.lower_expr(iterable, scope)?.0;
1585 let previous = scope.bind(binding.as_str(), Binding::Value(TypeExpr::Any));
1586 let body = self.lower_expr(body, scope)?.0;
1587 scope.restore(binding.as_str(), previous);
1588 (
1589 Expr::For {
1590 binding: binding.clone(),
1591 iterable: Box::new(iterable),
1592 body: Box::new(body),
1593 },
1594 Some(Binding::Value(TypeExpr::Null)),
1595 )
1596 }
1597 Expr::While { condition, body } => {
1598 let condition = self.lower_expr(condition, scope)?.0;
1599 let body = self.lower_expr(body, scope)?.0;
1600 (
1601 Expr::While {
1602 condition: Box::new(condition),
1603 body: Box::new(body),
1604 },
1605 Some(Binding::Value(TypeExpr::Null)),
1606 )
1607 }
1608 Expr::StartProcess(start) => {
1609 self.ensure_feature(self.surface.abilities.processes, "processes", scope.span)?;
1610 let Some(process) = self.program.process(start.process.as_str()) else {
1611 return Err(LinkError::UnknownProcess {
1612 name: start.process.to_string(),
1613 span: scope.span,
1614 });
1615 };
1616 let mut seen = BTreeSet::new();
1617 let mut lowered_args = Vec::with_capacity(start.args.len());
1618 for (arg, value) in &start.args {
1619 if !seen.insert(arg.to_string()) {
1620 return Err(LinkError::DuplicateProcessArgument {
1621 arg: arg.to_string(),
1622 span: scope.span,
1623 });
1624 }
1625 let Some(param) = process.params.iter().find(|param| param.name == *arg) else {
1626 return Err(LinkError::UnexpectedProcessArgument {
1627 process: process.name.to_string(),
1628 arg: arg.to_string(),
1629 span: scope.span,
1630 });
1631 };
1632 let (lowered, binding) = self.lower_expr(value, scope)?;
1633 self.validate_process_arg_binding(
1634 process.name.as_str(),
1635 arg.as_str(),
1636 ¶m.ty,
1637 binding.as_ref(),
1638 scope.span,
1639 )?;
1640 lowered_args.push((arg.clone(), lowered));
1641 }
1642 for param in &process.params {
1643 if !seen.contains(param.name.as_str()) {
1644 return Err(LinkError::MissingProcessArgument {
1645 process: process.name.to_string(),
1646 arg: param.name.to_string(),
1647 span: scope.span,
1648 });
1649 }
1650 }
1651 (
1652 Expr::StartProcess(crate::ast::ProcessStartExpr {
1653 process: start.process.clone(),
1654 args: lowered_args,
1655 }),
1656 Some(Binding::Value(TypeExpr::Any)),
1657 )
1658 }
1659 Expr::ProcessRef { process } => {
1660 let Some(process_ty) = self.process_types.get(process.as_str()) else {
1661 return Err(LinkError::UnknownProcess {
1662 name: process.to_string(),
1663 span: scope.span,
1664 });
1665 };
1666 (
1667 Expr::ProcessRef {
1668 process: process.clone(),
1669 },
1670 Some(Binding::Value(process_ty.clone())),
1671 )
1672 }
1673 Expr::HostValueConstructor { type_name, input } => (
1674 Expr::HostValueConstructor {
1675 type_name: type_name.clone(),
1676 input: Box::new(self.lower_expr(input, scope)?.0),
1677 },
1678 Some(Binding::Value(TypeExpr::Ref(type_name.clone()))),
1679 ),
1680 Expr::ResourceRef(resource) => {
1681 let resource = self.validate_resource_ref(resource, scope.span)?;
1682 (
1683 Expr::ResourceRef(resource.clone()),
1684 Some(Binding::Resource {
1685 resource_type: resource.resource_type.to_string(),
1686 }),
1687 )
1688 }
1689 Expr::ReceiverCall {
1690 receiver,
1691 operation,
1692 args,
1693 } => {
1694 if let Some(mut path) = module_path_for_expr(receiver) {
1695 path.push(operation.clone());
1696 if let Some(constructor) =
1697 self.surface.resources.resolve_value_constructor(&path)
1698 {
1699 if args.len() != 1 {
1700 return Err(LinkError::IncompatibleConstructorInput {
1701 path: module_path_key(&path),
1702 expected: format_type_expr(&constructor.input_ty),
1703 actual: format!("{} arguments", args.len()),
1704 span: scope.span,
1705 });
1706 }
1707 let (input, input_binding) = self.lower_expr(&args[0], scope)?;
1708 let actual_ty = binding_type(input_binding.as_ref());
1709 if !self.is_type_assignable(&actual_ty, &constructor.input_ty) {
1710 return Err(LinkError::IncompatibleConstructorInput {
1711 path: module_path_key(&path),
1712 expected: format_type_expr(
1713 &self.resolve_type_aliases(&constructor.input_ty),
1714 ),
1715 actual: format_type_expr(&self.resolve_type_aliases(&actual_ty)),
1716 span: scope.span,
1717 });
1718 }
1719 return Ok((
1720 Expr::HostValueConstructor {
1721 type_name: constructor.type_name.clone().into(),
1722 input: Box::new(input),
1723 },
1724 Some(Binding::Value(constructor.output_ty.clone())),
1725 ));
1726 }
1727 }
1728 let resolved_receiver = self.resolve_module_expr(receiver, scope);
1729 let (lowered_receiver, resource_type, receiver_alias) =
1730 if let Some(resource) = resolved_receiver.as_ref() {
1731 (
1732 Expr::ResourceRef(resource.clone()),
1733 Some(resource.resource_type.to_string()),
1734 Some(resource.alias.to_string()),
1735 )
1736 } else {
1737 let (lowered_receiver, binding) = self.lower_expr(receiver, scope)?;
1738 let resource_type = match binding {
1739 Some(Binding::Resource { resource_type }) => Some(resource_type),
1740 _ => None,
1741 };
1742 (lowered_receiver, resource_type, None)
1743 };
1744 let Some(resource_type) = resource_type else {
1745 if let Some(path) = module_path_for_expr(receiver) {
1746 let suggestions = self
1747 .surface
1748 .resources
1749 .operation_suggestions_for_prefix(&path, operation.as_str());
1750 if !suggestions.is_empty() {
1751 return Err(LinkError::AmbiguousModuleOperation {
1752 module_path: module_path_key(&path),
1753 operation: operation.to_string(),
1754 suggestions,
1755 span: scope.span,
1756 });
1757 }
1758 }
1759 return Err(LinkError::UnresolvedReceiver {
1760 operation: operation.to_string(),
1761 span: scope.span,
1762 });
1763 };
1764 if let Some(alias) = receiver_alias.as_deref()
1765 && self
1766 .surface
1767 .resources
1768 .resolve_module_operation(&resource_type, alias, operation.as_str())
1769 .is_none()
1770 {
1771 return Err(LinkError::UnknownResourceOperation {
1772 resource_type: resource_type.clone(),
1773 operation: operation.to_string(),
1774 span: scope.span,
1775 });
1776 }
1777 let Some(operation_binding) = self
1778 .surface
1779 .resources
1780 .resolve_operation(&resource_type, operation)
1781 .cloned()
1782 else {
1783 return Err(LinkError::UnknownResourceOperation {
1784 resource_type: resource_type.clone(),
1785 operation: operation.to_string(),
1786 span: scope.span,
1787 });
1788 };
1789 let trigger_operation = if crate::is_trigger_resource_type(&resource_type) {
1790 crate::TriggerHostOperation::from_receiver_method(operation.as_str())
1791 } else {
1792 None
1793 };
1794 if let Some(trigger_operation) = trigger_operation {
1795 self.ensure_feature(self.surface.abilities.triggers, "triggers", scope.span)?;
1796 let (lowered_args, output_ty) =
1797 self.lower_trigger_operation_args(trigger_operation, args, scope)?;
1798 return Ok((
1799 Expr::ReceiverCall {
1800 receiver: Box::new(lowered_receiver),
1801 operation: operation.clone(),
1802 args: lowered_args,
1803 },
1804 Some(Binding::Value(output_ty)),
1805 ));
1806 }
1807 let mut lowered_args = Vec::with_capacity(args.len());
1808 let mut arg_types = Vec::with_capacity(args.len());
1809 for arg in args {
1810 let (arg, binding) = self.lower_expr(arg, scope)?;
1811 lowered_args.push(arg);
1812 arg_types.push(binding_type(binding.as_ref()));
1813 }
1814 let actual_input = call_input_type(arg_types);
1815 if !self.is_type_assignable(&actual_input, &operation_binding.input_ty) {
1816 return Err(LinkError::IncompatibleOperationInput {
1817 operation: operation.to_string(),
1818 expected: format_type_expr(
1819 &self.resolve_type_aliases(&operation_binding.input_ty),
1820 ),
1821 actual: format_type_expr(&self.resolve_type_aliases(&actual_input)),
1822 span: scope.span,
1823 });
1824 }
1825 (
1826 Expr::ReceiverCall {
1827 receiver: Box::new(lowered_receiver),
1828 operation: operation.clone(),
1829 args: lowered_args,
1830 },
1831 Some(Binding::Value(operation_binding.output_ty.clone())),
1832 )
1833 }
1834 Expr::Await(inner) => {
1835 let (inner, binding) = self.lower_expr(inner, scope)?;
1836 (Expr::Await(Box::new(inner)), binding)
1837 }
1838 Expr::SleepFor(inner) => {
1839 self.ensure_feature(self.surface.abilities.sleep, "sleep", scope.span)?;
1840 (
1841 Expr::SleepFor(Box::new(self.lower_expr(inner, scope)?.0)),
1842 Some(Binding::Value(TypeExpr::Null)),
1843 )
1844 }
1845 Expr::SleepUntil(inner) => {
1846 self.ensure_feature(self.surface.abilities.sleep, "sleep", scope.span)?;
1847 (
1848 Expr::SleepUntil(Box::new(self.lower_expr(inner, scope)?.0)),
1849 Some(Binding::Value(TypeExpr::Null)),
1850 )
1851 }
1852 Expr::WaitSignal => {
1853 self.ensure_feature(
1854 self.surface.abilities.process_signals,
1855 "process signals",
1856 scope.span,
1857 )?;
1858 if !scope.process_body {
1859 return Err(LinkError::ProcessLifecycleOutsideProcess {
1860 keyword: "wait signal",
1861 span: scope.span,
1862 });
1863 }
1864 (Expr::WaitSignal, Some(Binding::Value(TypeExpr::Any)))
1865 }
1866 Expr::SignalRun { run, payload } => {
1867 self.ensure_feature(
1868 self.surface.abilities.process_signals,
1869 "process signals",
1870 scope.span,
1871 )?;
1872 (
1876 Expr::SignalRun {
1877 run: Box::new(self.lower_expr(run, scope)?.0),
1878 payload: Box::new(self.lower_expr(payload, scope)?.0),
1879 },
1880 Some(Binding::Value(TypeExpr::Null)),
1881 )
1882 }
1883 Expr::ResultUnwrap(inner) => {
1884 let (inner, binding) = self.lower_expr(inner, scope)?;
1885 (Expr::ResultUnwrap(Box::new(inner)), binding)
1886 }
1887 Expr::Cancel(inner) => (
1888 Expr::Cancel(Box::new(self.lower_expr(inner, scope)?.0)),
1889 Some(Binding::Value(TypeExpr::Any)),
1890 ),
1891 Expr::Print(inner) => (
1892 Expr::Print(Box::new(self.lower_expr(inner, scope)?.0)),
1893 Some(Binding::Value(TypeExpr::Null)),
1894 ),
1895 Expr::Submit(inner) => (
1896 Expr::Submit(
1897 inner
1898 .as_deref()
1899 .map(|inner| {
1900 self.lower_expr(inner, scope)
1901 .map(|(expr, _)| Box::new(expr))
1902 })
1903 .transpose()?,
1904 ),
1905 Some(Binding::Value(TypeExpr::Null)),
1906 ),
1907 Expr::Yield(inner) => (
1908 Expr::Yield(Box::new(self.lower_expr(inner, scope)?.0)),
1909 Some(Binding::Value(TypeExpr::Null)),
1910 ),
1911 Expr::Wake(inner) => (
1912 Expr::Wake(Box::new(self.lower_expr(inner, scope)?.0)),
1913 Some(Binding::Value(TypeExpr::Null)),
1914 ),
1915 Expr::Finish(inner) => {
1916 let mut finish_ty = TypeExpr::Null;
1917 let inner = inner
1918 .as_deref()
1919 .map(|inner| {
1920 let (expr, binding) = self.lower_expr(inner, scope)?;
1921 finish_ty = binding_type(binding.as_ref());
1922 Ok(Box::new(expr))
1923 })
1924 .transpose()?;
1925 (Expr::Finish(inner), Some(Binding::Value(finish_ty)))
1926 }
1927 Expr::Fail(inner) => (
1928 Expr::Fail(Box::new(self.lower_expr(inner, scope)?.0)),
1929 Some(Binding::Value(TypeExpr::Null)),
1930 ),
1931 Expr::BuiltinCall { name, args } => {
1932 if !crate::builtins::is_builtin(name.as_str()) {
1933 if let Some(suggestion) = self
1934 .surface
1935 .resources
1936 .operation_suggestions_for_host(name.as_str())
1937 .into_iter()
1938 .next()
1939 {
1940 return Err(LinkError::BareToolCall {
1941 name: name.to_string(),
1942 suggestion,
1943 span: scope.span,
1944 });
1945 }
1946 return Err(LinkError::UnknownBuiltin {
1947 name: name.to_string(),
1948 span: scope.span,
1949 });
1950 }
1951 (
1952 Expr::BuiltinCall {
1953 name: name.clone(),
1954 args: args
1955 .iter()
1956 .map(|arg| self.lower_expr(arg, scope).map(|(expr, _)| expr))
1957 .collect::<Result<Vec<_>, _>>()?,
1958 },
1959 Some(Binding::Value(builtin_return_type(name.as_str()))),
1960 )
1961 }
1962 Expr::Field { target, field } => {
1963 let (target, binding) = self.lower_expr(target, scope)?;
1964 let ty =
1965 self.field_type(&binding_type(binding.as_ref()), field.as_str(), scope.span)?;
1966 (
1967 Expr::Field {
1968 target: Box::new(target),
1969 field: field.clone(),
1970 },
1971 Some(Binding::Value(ty)),
1972 )
1973 }
1974 Expr::Index { target, index } => {
1975 let (target, target_binding) = self.lower_expr(target, scope)?;
1976 let index = self.lower_expr(index, scope)?.0;
1977 (
1978 Expr::Index {
1979 target: Box::new(target),
1980 index: Box::new(index),
1981 },
1982 Some(Binding::Value(self.index_type(
1983 &binding_type(target_binding.as_ref()),
1984 scope.span,
1985 )?)),
1986 )
1987 }
1988 Expr::Unary { op, expr } => (
1989 Expr::Unary {
1990 op: *op,
1991 expr: Box::new(self.lower_expr(expr, scope)?.0),
1992 },
1993 Some(Binding::Value(match op {
1994 crate::ast::UnaryOp::Not => TypeExpr::Bool,
1995 crate::ast::UnaryOp::Negate => TypeExpr::Float,
1996 })),
1997 ),
1998 Expr::Binary { left, op, right } => (
1999 Expr::Binary {
2000 left: Box::new(self.lower_expr(left, scope)?.0),
2001 op: *op,
2002 right: Box::new(self.lower_expr(right, scope)?.0),
2003 },
2004 Some(Binding::Value(binary_return_type(*op))),
2005 ),
2006 })
2007 }
2008
2009 fn resolve_module_expr(&self, expr: &Expr, scope: &Scope) -> Option<ResourceRefExpr> {
2010 let path = module_path_for_expr(expr)?;
2011 if path
2012 .first()
2013 .and_then(|root| scope.get_str(root.as_str()))
2014 .is_some()
2015 {
2016 return None;
2017 }
2018 self.surface.resources.resolve_module_path(&path)
2019 }
2020
2021 fn reject_trigger_event_special_form(
2022 &self,
2023 expr: &Expr,
2024 span: Option<Span>,
2025 ) -> Result<(), LinkError> {
2026 if is_trigger_event_projection_expr(expr) {
2027 return Err(LinkError::TriggerEventProjection { span });
2028 }
2029 if is_trigger_event_expr(expr) {
2030 return Err(LinkError::TriggerEventOutsideInputs { span });
2031 }
2032 Ok(())
2033 }
2034
2035 fn validate_process_arg_binding(
2036 &self,
2037 process: &str,
2038 arg: &str,
2039 expected_ty: &TypeExpr,
2040 actual: Option<&Binding>,
2041 span: Option<Span>,
2042 ) -> Result<(), LinkError> {
2043 let Some(expected_resource) = self.resource_type_for_type(expected_ty) else {
2044 return Ok(());
2045 };
2046 match actual {
2047 Some(Binding::Resource { resource_type }) if *resource_type == expected_resource => {
2048 Ok(())
2049 }
2050 Some(Binding::Resource { resource_type }) => {
2051 Err(LinkError::IncompatibleProcessArgument {
2052 process: process.to_string(),
2053 arg: arg.to_string(),
2054 expected: expected_resource,
2055 actual: resource_type.clone(),
2056 span,
2057 })
2058 }
2059 _ => Err(LinkError::IncompatibleProcessArgument {
2060 process: process.to_string(),
2061 arg: arg.to_string(),
2062 expected: expected_resource,
2063 actual: "value".to_string(),
2064 span,
2065 }),
2066 }
2067 }
2068
2069 fn validate_trigger_operation_args(
2070 &self,
2071 operation: crate::TriggerHostOperation,
2072 args: &[Expr],
2073 scope: &Scope,
2074 ) -> Result<TypeExpr, LinkError> {
2075 match operation {
2076 crate::TriggerHostOperation::Register => {
2077 let call = crate::register_call_args(args)
2078 .map_err(|_| LinkError::InvalidTriggerRegistration { span: scope.span })?;
2079 let source_ty = self.infer_expr_type(call.source, &mut scope.clone())?;
2080 let event_ty = self
2081 .surface
2082 .resources
2083 .trigger_source_event(&source_ty)
2084 .ok_or_else(|| LinkError::UnknownTriggerSourceType {
2085 source_ty: format_type_expr(&source_ty),
2086 span: scope.span,
2087 })?;
2088 let target_ty = self.infer_expr_type(call.target, &mut scope.clone())?;
2089 let params = self.trigger_target_params(call.target, &target_ty, scope.span)?;
2090 let mut validation_scope = scope.clone();
2091 self.lower_trigger_input_record(
2092 trigger_target_process_label(call.target).as_str(),
2093 ¶ms,
2094 &event_ty,
2095 call.inputs,
2096 &mut validation_scope,
2097 )?;
2098 Ok(TypeExpr::TriggerHandle(Box::new(event_ty)))
2099 }
2100 crate::TriggerHostOperation::List => {
2101 let call = crate::list_call_args(args)
2102 .map_err(|_| LinkError::InvalidTriggerList { span: scope.span })?;
2103 for (name, expr) in call.entries {
2104 match name.as_str() {
2105 "target" => {
2106 let target_ty = self.infer_expr_type(expr, &mut scope.clone())?;
2107 if !matches!(target_ty, TypeExpr::Process { .. }) {
2108 return Err(LinkError::InvalidTriggerTarget {
2109 actual: format_type_expr(&target_ty),
2110 span: scope.span,
2111 });
2112 }
2113 }
2114 "name" | "source_type" => {
2115 let filter_ty = self.infer_expr_type(expr, &mut scope.clone())?;
2116 if !self.is_type_assignable(&filter_ty, &TypeExpr::Str) {
2117 return Err(LinkError::IncompatibleOperationInput {
2118 operation: operation.receiver_method().to_string(),
2119 expected: format_type_expr(&TypeExpr::Str),
2120 actual: format_type_expr(&filter_ty),
2121 span: scope.span,
2122 });
2123 }
2124 }
2125 "enabled" => {
2126 let filter_ty = self.infer_expr_type(expr, &mut scope.clone())?;
2127 if !self.is_type_assignable(&filter_ty, &TypeExpr::Bool) {
2128 return Err(LinkError::IncompatibleOperationInput {
2129 operation: operation.receiver_method().to_string(),
2130 expected: format_type_expr(&TypeExpr::Bool),
2131 actual: format_type_expr(&filter_ty),
2132 span: scope.span,
2133 });
2134 }
2135 }
2136 _ => unreachable!("list_call_args rejects unknown trigger filters"),
2137 }
2138 }
2139 Ok(operation.output_ty())
2140 }
2141 crate::TriggerHostOperation::Cancel => {
2142 crate::cancel_call_args(args)
2143 .map_err(|_| LinkError::InvalidTriggerCancel { span: scope.span })?;
2144 Ok(operation.output_ty())
2145 }
2146 }
2147 }
2148
2149 fn lower_trigger_operation_args(
2150 &self,
2151 operation: crate::TriggerHostOperation,
2152 args: &[Expr],
2153 scope: &mut Scope,
2154 ) -> Result<(Vec<Expr>, TypeExpr), LinkError> {
2155 match operation {
2156 crate::TriggerHostOperation::Register => {
2157 self.lower_trigger_registration_args(args, scope)
2158 }
2159 crate::TriggerHostOperation::List => {
2160 let call = crate::list_call_args(args)
2161 .map_err(|_| LinkError::InvalidTriggerList { span: scope.span })?;
2162 let mut entries = Vec::with_capacity(call.entries.len());
2163 for (name, expr) in call.entries {
2164 match name.as_str() {
2165 "target" => {
2166 let target_ty = self.infer_expr_type(expr, &mut scope.clone())?;
2167 if !matches!(target_ty, TypeExpr::Process { .. }) {
2168 return Err(LinkError::InvalidTriggerTarget {
2169 actual: format_type_expr(&target_ty),
2170 span: scope.span,
2171 });
2172 }
2173 }
2174 "name" | "source_type" => {
2175 let filter_ty = self.infer_expr_type(expr, &mut scope.clone())?;
2176 if !self.is_type_assignable(&filter_ty, &TypeExpr::Str) {
2177 return Err(LinkError::IncompatibleOperationInput {
2178 operation: operation.receiver_method().to_string(),
2179 expected: format_type_expr(&TypeExpr::Str),
2180 actual: format_type_expr(&filter_ty),
2181 span: scope.span,
2182 });
2183 }
2184 }
2185 "enabled" => {
2186 let filter_ty = self.infer_expr_type(expr, &mut scope.clone())?;
2187 if !self.is_type_assignable(&filter_ty, &TypeExpr::Bool) {
2188 return Err(LinkError::IncompatibleOperationInput {
2189 operation: operation.receiver_method().to_string(),
2190 expected: format_type_expr(&TypeExpr::Bool),
2191 actual: format_type_expr(&filter_ty),
2192 span: scope.span,
2193 });
2194 }
2195 }
2196 _ => unreachable!("list_call_args rejects unknown trigger filters"),
2197 }
2198 entries.push((name.clone(), self.lower_expr(expr, scope)?.0));
2199 }
2200 Ok((vec![Expr::Record(entries)], operation.output_ty()))
2201 }
2202 crate::TriggerHostOperation::Cancel => {
2203 let call = crate::cancel_call_args(args)
2204 .map_err(|_| LinkError::InvalidTriggerCancel { span: scope.span })?;
2205 Ok((
2206 vec![Expr::Record(vec![(
2207 "handle".into(),
2208 self.lower_expr(call.handle, scope)?.0,
2209 )])],
2210 operation.output_ty(),
2211 ))
2212 }
2213 }
2214 }
2215
2216 fn lower_trigger_registration_args(
2217 &self,
2218 args: &[Expr],
2219 scope: &mut Scope,
2220 ) -> Result<(Vec<Expr>, TypeExpr), LinkError> {
2221 let call = crate::register_call_args(args)
2222 .map_err(|_| LinkError::InvalidTriggerRegistration { span: scope.span })?;
2223 let source_ty = self.infer_expr_type(call.source, &mut scope.clone())?;
2224 let event_ty = self
2225 .surface
2226 .resources
2227 .trigger_source_event(&source_ty)
2228 .ok_or_else(|| LinkError::UnknownTriggerSourceType {
2229 source_ty: format_type_expr(&source_ty),
2230 span: scope.span,
2231 })?;
2232 let target_ty = self.infer_expr_type(call.target, &mut scope.clone())?;
2233 let params = self.trigger_target_params(call.target, &target_ty, scope.span)?;
2234 let process = trigger_target_process_label(call.target);
2235
2236 let source = self.lower_expr(call.source, scope)?.0;
2237 let target = self.lower_expr(call.target, scope)?.0;
2238 let inputs = self.lower_trigger_input_record(
2239 process.as_str(),
2240 ¶ms,
2241 &event_ty,
2242 call.inputs,
2243 scope,
2244 )?;
2245 let mut entries = vec![
2246 ("source".into(), source),
2247 ("target".into(), target),
2248 ("inputs".into(), inputs),
2249 ];
2250 if let Some(name) = call.name {
2251 entries.push(("name".into(), self.lower_expr(name, scope)?.0));
2252 }
2253 Ok((
2254 vec![Expr::Record(entries)],
2255 TypeExpr::TriggerHandle(Box::new(event_ty)),
2256 ))
2257 }
2258
2259 fn lower_trigger_input_record(
2260 &self,
2261 process: &str,
2262 params: &[ProcessParam],
2263 event_ty: &TypeExpr,
2264 inputs: &Expr,
2265 scope: &mut Scope,
2266 ) -> Result<Expr, LinkError> {
2267 let Expr::Record(entries) = inputs else {
2268 return Err(LinkError::InvalidTriggerInputs { span: scope.span });
2269 };
2270 let mut seen = BTreeSet::new();
2271 let mut saw_event = false;
2272 let mut lowered = Vec::with_capacity(entries.len());
2273 for (name, value) in entries {
2274 if !seen.insert(name.to_string()) {
2275 return Err(LinkError::DuplicateTriggerInput {
2276 input: name.to_string(),
2277 span: scope.span,
2278 });
2279 }
2280 let Some(param) = params.iter().find(|param| param.name == *name) else {
2281 return Err(LinkError::UnknownTriggerInput {
2282 process: process.to_string(),
2283 input: name.to_string(),
2284 span: scope.span,
2285 });
2286 };
2287 if is_trigger_event_projection_expr(value) {
2288 return Err(LinkError::TriggerEventProjection { span: scope.span });
2289 }
2290 if is_trigger_event_expr(value) {
2291 saw_event = true;
2292 if !self.is_type_assignable(event_ty, ¶m.ty) {
2293 return Err(LinkError::TriggerEventMismatch {
2294 event: format_type_expr(&self.resolve_type_aliases(event_ty)),
2295 input_name: name.to_string(),
2296 input: format_type_expr(&self.resolve_type_aliases(¶m.ty)),
2297 span: scope.span,
2298 });
2299 }
2300 lowered.push((name.clone(), crate::trigger_event_placeholder_expr()));
2301 continue;
2302 }
2303 let (lowered_value, binding) = self.lower_expr(value, scope)?;
2304 self.validate_process_arg_binding(
2305 process,
2306 name.as_str(),
2307 ¶m.ty,
2308 binding.as_ref(),
2309 scope.span,
2310 )?;
2311 lowered.push((name.clone(), lowered_value));
2312 }
2313 for param in params {
2314 if !seen.contains(param.name.as_str()) {
2315 return Err(LinkError::MissingTriggerInput {
2316 process: process.to_string(),
2317 input: param.name.to_string(),
2318 span: scope.span,
2319 });
2320 }
2321 }
2322 if !saw_event {
2323 return Err(LinkError::MissingTriggerEventInput { span: scope.span });
2324 }
2325 Ok(Expr::Record(lowered))
2326 }
2327
2328 fn trigger_target_params(
2329 &self,
2330 target: &Expr,
2331 target_ty: &TypeExpr,
2332 span: Option<Span>,
2333 ) -> Result<Vec<ProcessParam>, LinkError> {
2334 if let Some(process_name) = trigger_target_process_name(target)
2335 && let Some(process) = self.program.process(process_name.as_str())
2336 {
2337 return Ok(process.params.clone());
2338 }
2339 let TypeExpr::Process {
2340 input, input_count, ..
2341 } = target_ty
2342 else {
2343 return Err(LinkError::InvalidTriggerTarget {
2344 actual: format_type_expr(target_ty),
2345 span,
2346 });
2347 };
2348 match (input_count, input.as_ref()) {
2349 (0, _) => Ok(Vec::new()),
2350 (count, TypeExpr::Object(fields)) if *count > 1 => Ok(fields
2351 .iter()
2352 .map(|field| ProcessParam {
2353 name: field.name.clone(),
2354 ty: field.ty.clone(),
2355 })
2356 .collect()),
2357 _ => Err(LinkError::InvalidTriggerTarget {
2358 actual: format_type_expr(target_ty),
2359 span,
2360 }),
2361 }
2362 }
2363
2364 fn infer_process_output(
2365 &self,
2366 process: &ProcessDecl,
2367 span: Option<Span>,
2368 ) -> Result<TypeExpr, LinkError> {
2369 let mut scope = Scope::new(false, true, span);
2370 for param in &process.params {
2371 scope.bind(param.name.as_str(), self.binding_for_type(¶m.ty));
2372 }
2373 scope.bind("input", Binding::Value(process_input_type(process)));
2374 scope.bind("inputs", Binding::Value(process_input_record_type(process)));
2375 let completion = self.infer_completion(&process.body, &mut scope)?;
2376 let mut outputs = completion.finishes;
2377 if completion.can_fallthrough {
2378 outputs.push(TypeExpr::Null);
2379 }
2380 Ok(union_type(outputs))
2381 }
2382
2383 fn infer_completion(&self, expr: &Expr, scope: &mut Scope) -> Result<Completion, LinkError> {
2384 match expr {
2385 Expr::LabelAnnotated { expr, .. } => self.infer_completion(expr, scope),
2386 Expr::Finish(Some(value)) => Ok(Completion {
2387 finishes: vec![self.infer_expr_type(value, scope)?],
2388 can_fallthrough: false,
2389 }),
2390 Expr::Finish(None) => Ok(Completion {
2391 finishes: vec![TypeExpr::Null],
2392 can_fallthrough: false,
2393 }),
2394 Expr::Fail(_) => Ok(Completion {
2395 finishes: Vec::new(),
2396 can_fallthrough: false,
2397 }),
2398 Expr::Block(expressions) => {
2399 let mut finishes = Vec::new();
2400 let mut can_fallthrough = true;
2401 for expression in expressions {
2402 if !can_fallthrough {
2403 break;
2404 }
2405 let completion = self.infer_completion(expression, scope)?;
2406 finishes.extend(completion.finishes);
2407 can_fallthrough = completion.can_fallthrough;
2408 }
2409 Ok(Completion {
2410 finishes,
2411 can_fallthrough,
2412 })
2413 }
2414 Expr::If {
2415 condition,
2416 then_block,
2417 else_block,
2418 } => {
2419 self.infer_expr_type(condition, scope)?;
2420 let mut then_scope = scope.clone();
2421 let then_completion = self.infer_completion(then_block, &mut then_scope)?;
2422 let mut else_scope = scope.clone();
2423 let else_completion = self.infer_completion(else_block, &mut else_scope)?;
2424 scope.merge_from(then_scope);
2425 scope.merge_from(else_scope);
2426 let mut finishes = then_completion.finishes;
2427 finishes.extend(else_completion.finishes);
2428 Ok(Completion {
2429 finishes,
2430 can_fallthrough: then_completion.can_fallthrough
2431 || else_completion.can_fallthrough,
2432 })
2433 }
2434 Expr::For {
2435 binding,
2436 iterable,
2437 body,
2438 } => {
2439 self.infer_expr_type(iterable, scope)?;
2440 let previous = scope.bind(binding.as_str(), Binding::Value(TypeExpr::Any));
2441 let mut completion = self.infer_completion(body, scope)?;
2442 scope.restore(binding.as_str(), previous);
2443 completion.can_fallthrough = true;
2444 Ok(completion)
2445 }
2446 Expr::While { condition, body } => {
2447 self.infer_expr_type(condition, scope)?;
2448 let mut completion = self.infer_completion(body, scope)?;
2449 completion.can_fallthrough = true;
2450 Ok(completion)
2451 }
2452 Expr::Assign { target, expr } if target.steps.is_empty() => {
2453 let ty = self.infer_expr_type(expr, scope)?;
2454 scope.bind(target.root.as_str(), self.binding_for_type(&ty));
2455 Ok(Completion::fallthrough())
2456 }
2457 other => {
2458 self.infer_expr_type(other, scope)?;
2459 Ok(Completion::fallthrough())
2460 }
2461 }
2462 }
2463
2464 fn infer_expr_type(&self, expr: &Expr, scope: &mut Scope) -> Result<TypeExpr, LinkError> {
2465 self.reject_trigger_event_special_form(expr, scope.span)?;
2466 if matches!(expr, Expr::Variable(_) | Expr::Field { .. })
2467 && let Some(resource) = self.resolve_module_expr(expr, scope)
2468 {
2469 return Ok(TypeExpr::Ref(resource.resource_type));
2470 }
2471 Ok(match expr {
2472 Expr::LabelAnnotated { expr, .. } => self.infer_expr_type(expr, scope)?,
2473 Expr::Block(expressions) => {
2474 let mut last = TypeExpr::Null;
2475 for expression in expressions {
2476 last = self.infer_expr_type(expression, scope)?;
2477 }
2478 last
2479 }
2480 Expr::Null
2481 | Expr::Bool(_)
2482 | Expr::Number(_)
2483 | Expr::String(_)
2484 | Expr::Break
2485 | Expr::Continue
2486 | Expr::TypeLiteral(_) => literal_type(expr),
2487 Expr::Variable(name) => {
2488 if let Some(binding) = scope.get(name) {
2489 binding_type(Some(&binding))
2490 } else if let Some(process_ty) = self.process_types.get(name.as_str()) {
2491 process_ty.clone()
2492 } else if scope.allow_unknown_globals {
2493 TypeExpr::Any
2494 } else {
2495 return Err(LinkError::UnknownName {
2496 name: name.to_string(),
2497 span: scope.span,
2498 });
2499 }
2500 }
2501 Expr::ProcessRef { process } => self
2502 .process_types
2503 .get(process.as_str())
2504 .cloned()
2505 .ok_or_else(|| LinkError::UnknownProcess {
2506 name: process.to_string(),
2507 span: scope.span,
2508 })?,
2509 Expr::HostValueConstructor { type_name, .. } => TypeExpr::Ref(type_name.clone()),
2510 Expr::List(items) => TypeExpr::List(Box::new(union_type(
2511 items
2512 .iter()
2513 .map(|item| self.infer_expr_type(item, scope))
2514 .collect::<Result<Vec<_>, _>>()?,
2515 ))),
2516 Expr::Record(entries) => TypeExpr::Object(
2517 entries
2518 .iter()
2519 .map(|(name, value)| {
2520 Ok(TypeField {
2521 name: name.clone(),
2522 ty: self.infer_expr_type(value, scope)?,
2523 optional: false,
2524 })
2525 })
2526 .collect::<Result<Vec<_>, LinkError>>()?,
2527 ),
2528 Expr::Assign { target, expr } => {
2529 let ty = self.infer_expr_type(expr, scope)?;
2530 if target.steps.is_empty() {
2531 scope.bind(target.root.as_str(), self.binding_for_type(&ty));
2532 }
2533 ty
2534 }
2535 Expr::If {
2536 condition,
2537 then_block,
2538 else_block,
2539 } => {
2540 self.infer_expr_type(condition, scope)?;
2541 let mut then_scope = scope.clone();
2542 let then_ty = self.infer_expr_type(then_block, &mut then_scope)?;
2543 let mut else_scope = scope.clone();
2544 let else_ty = self.infer_expr_type(else_block, &mut else_scope)?;
2545 scope.merge_from(then_scope);
2546 scope.merge_from(else_scope);
2547 union_type(vec![then_ty, else_ty])
2548 }
2549 Expr::For {
2550 binding,
2551 iterable,
2552 body,
2553 } => {
2554 self.infer_expr_type(iterable, scope)?;
2555 let previous = scope.bind(binding.as_str(), Binding::Value(TypeExpr::Any));
2556 self.infer_expr_type(body, scope)?;
2557 scope.restore(binding.as_str(), previous);
2558 TypeExpr::Null
2559 }
2560 Expr::While { condition, body } => {
2561 self.infer_expr_type(condition, scope)?;
2562 self.infer_expr_type(body, scope)?;
2563 TypeExpr::Null
2564 }
2565 Expr::StartProcess(_) => TypeExpr::Any,
2566 Expr::ResourceRef(resource) => TypeExpr::Ref(resource.resource_type.clone()),
2567 Expr::ReceiverCall {
2568 receiver,
2569 operation,
2570 args,
2571 } => {
2572 if let Some(mut path) = module_path_for_expr(receiver) {
2573 path.push(operation.clone());
2574 if let Some(constructor) =
2575 self.surface.resources.resolve_value_constructor(&path)
2576 {
2577 return Ok(constructor.output_ty.clone());
2578 }
2579 }
2580 let resolved_receiver = self.resolve_module_expr(receiver, scope);
2581 let (resource_type, receiver_alias) =
2582 if let Some(resource) = resolved_receiver.as_ref() {
2583 (
2584 resource.resource_type.to_string(),
2585 Some(resource.alias.to_string()),
2586 )
2587 } else {
2588 let receiver_ty = self.infer_expr_type(receiver, scope)?;
2589 (
2590 self.resource_type_for_type(&receiver_ty).ok_or_else(|| {
2591 LinkError::UnresolvedReceiver {
2592 operation: operation.to_string(),
2593 span: scope.span,
2594 }
2595 })?,
2596 None,
2597 )
2598 };
2599 if let Some(alias) = receiver_alias.as_deref()
2600 && self
2601 .surface
2602 .resources
2603 .resolve_module_operation(&resource_type, alias, operation.as_str())
2604 .is_none()
2605 {
2606 return Err(LinkError::UnknownResourceOperation {
2607 resource_type: resource_type.clone(),
2608 operation: operation.to_string(),
2609 span: scope.span,
2610 });
2611 }
2612 let binding = self
2613 .surface
2614 .resources
2615 .resolve_operation(&resource_type, operation)
2616 .ok_or_else(|| LinkError::UnknownResourceOperation {
2617 resource_type: resource_type.clone(),
2618 operation: operation.to_string(),
2619 span: scope.span,
2620 })?;
2621 if crate::is_trigger_resource_type(&resource_type)
2622 && let Some(trigger_operation) =
2623 crate::TriggerHostOperation::from_receiver_method(operation.as_str())
2624 {
2625 self.validate_trigger_operation_args(trigger_operation, args, scope)?
2626 } else {
2627 binding.output_ty.clone()
2628 }
2629 }
2630 Expr::Await(inner) | Expr::ResultUnwrap(inner) => self.infer_expr_type(inner, scope)?,
2631 Expr::SleepFor(_) | Expr::SleepUntil(_) => TypeExpr::Null,
2632 Expr::WaitSignal => TypeExpr::Any,
2633 Expr::SignalRun { .. }
2634 | Expr::Cancel(_)
2635 | Expr::Print(_)
2636 | Expr::Submit(_)
2637 | Expr::Yield(_)
2638 | Expr::Wake(_)
2639 | Expr::Fail(_) => TypeExpr::Null,
2640 Expr::Finish(Some(inner)) => self.infer_expr_type(inner, scope)?,
2641 Expr::Finish(None) => TypeExpr::Null,
2642 Expr::BuiltinCall { name, .. } => builtin_return_type(name.as_str()),
2643 Expr::Field { target, field } => {
2644 self.field_type(&self.infer_expr_type(target, scope)?, field, scope.span)?
2645 }
2646 Expr::Index { target, .. } => {
2647 self.index_type(&self.infer_expr_type(target, scope)?, scope.span)?
2648 }
2649 Expr::Unary { op, .. } => match op {
2650 crate::ast::UnaryOp::Not => TypeExpr::Bool,
2651 crate::ast::UnaryOp::Negate => TypeExpr::Float,
2652 },
2653 Expr::Binary { op, .. } => binary_return_type(*op),
2654 })
2655 }
2656}
2657
2658#[derive(Clone)]
2659struct Scope {
2660 bindings: BTreeMap<String, Binding>,
2661 allow_unknown_globals: bool,
2662 process_body: bool,
2663 span: Option<Span>,
2664}
2665
2666impl Scope {
2667 fn new(allow_unknown_globals: bool, process_body: bool, span: Option<Span>) -> Self {
2668 Self {
2669 bindings: BTreeMap::new(),
2670 allow_unknown_globals,
2671 process_body,
2672 span,
2673 }
2674 }
2675
2676 fn bind(&mut self, name: &str, binding: Binding) -> Option<Binding> {
2677 self.bindings.insert(name.to_string(), binding)
2678 }
2679
2680 fn restore(&mut self, name: &str, previous: Option<Binding>) {
2681 match previous {
2682 Some(binding) => {
2683 self.bindings.insert(name.to_string(), binding);
2684 }
2685 None => {
2686 self.bindings.remove(name);
2687 }
2688 }
2689 }
2690
2691 fn get(&self, name: &AstString) -> Option<Binding> {
2692 self.bindings.get(name.as_str()).cloned()
2693 }
2694
2695 fn get_str(&self, name: &str) -> Option<Binding> {
2696 self.bindings.get(name).cloned()
2697 }
2698
2699 fn merge_from(&mut self, other: Scope) {
2700 for (name, binding) in other.bindings {
2701 self.bindings.entry(name).or_insert(binding);
2702 }
2703 }
2704}
2705
2706struct Completion {
2707 finishes: Vec<TypeExpr>,
2708 can_fallthrough: bool,
2709}
2710
2711impl Completion {
2712 fn fallthrough() -> Self {
2713 Self {
2714 finishes: Vec::new(),
2715 can_fallthrough: true,
2716 }
2717 }
2718}
2719
2720fn any_binding() -> Binding {
2721 Binding::Value(TypeExpr::Any)
2722}
2723
2724fn binding_type(binding: Option<&Binding>) -> TypeExpr {
2725 match binding {
2726 Some(Binding::Value(ty)) => ty.clone(),
2727 Some(Binding::Resource { resource_type }) => TypeExpr::Ref(resource_type.as_str().into()),
2728 None => TypeExpr::Any,
2729 }
2730}
2731
2732fn literal_type(expr: &Expr) -> TypeExpr {
2733 match expr {
2734 Expr::Null => TypeExpr::Null,
2735 Expr::Bool(_) => TypeExpr::Bool,
2736 Expr::Number(_) => TypeExpr::Float,
2737 Expr::String(_) => TypeExpr::Str,
2738 Expr::TypeLiteral(_) => TypeExpr::Any,
2739 Expr::Break | Expr::Continue => TypeExpr::Null,
2740 Expr::LabelAnnotated { expr, .. } => literal_type(expr),
2741 _ => TypeExpr::Any,
2742 }
2743}
2744
2745fn union_type(items: Vec<TypeExpr>) -> TypeExpr {
2746 let mut flattened = Vec::new();
2747 for item in items {
2748 match item {
2749 TypeExpr::Union(items) => flattened.extend(items),
2750 other => flattened.push(other),
2751 }
2752 }
2753 let mut unique = Vec::new();
2754 for item in flattened {
2755 if !unique.contains(&item) {
2756 unique.push(item);
2757 }
2758 }
2759 match unique.as_slice() {
2760 [] => TypeExpr::Null,
2761 [one] => one.clone(),
2762 _ => TypeExpr::Union(unique),
2763 }
2764}
2765
2766fn call_input_type(arg_types: Vec<TypeExpr>) -> TypeExpr {
2767 match arg_types.as_slice() {
2768 [] => TypeExpr::Null,
2769 [one] => one.clone(),
2770 _ => TypeExpr::List(Box::new(union_type(arg_types))),
2771 }
2772}
2773
2774fn field_type(
2775 target: &TypeExpr,
2776 field: &str,
2777 span: Option<Span>,
2778 is_opaque: impl Fn(&str) -> bool + Copy,
2779) -> Result<TypeExpr, LinkError> {
2780 match target {
2781 TypeExpr::Any | TypeExpr::Dict => Ok(TypeExpr::Any),
2782 TypeExpr::Ref(name) if is_opaque(name.as_str()) => Err(LinkError::OpaqueHostValueAccess {
2783 type_name: name.to_string(),
2784 access: format!(".{field}"),
2785 span,
2786 }),
2787 TypeExpr::Ref(_) => Ok(TypeExpr::Any),
2788 TypeExpr::Object(fields) => Ok(fields
2789 .iter()
2790 .find(|candidate| candidate.name.as_str() == field)
2791 .map(|field| field.ty.clone())
2792 .unwrap_or(TypeExpr::Any)),
2793 TypeExpr::Union(items) => {
2794 let fields = items
2795 .iter()
2796 .map(|item| field_type(item, field, span, is_opaque))
2797 .collect::<Result<Vec<_>, _>>()?;
2798 Ok(union_type(fields))
2799 }
2800 _ => Ok(TypeExpr::Any),
2801 }
2802}
2803
2804fn index_type(
2805 target: &TypeExpr,
2806 span: Option<Span>,
2807 is_opaque: impl Fn(&str) -> bool + Copy,
2808) -> Result<TypeExpr, LinkError> {
2809 match target {
2810 TypeExpr::List(item) => Ok(*item.clone()),
2811 TypeExpr::Ref(name) if is_opaque(name.as_str()) => Err(LinkError::OpaqueHostValueAccess {
2812 type_name: name.to_string(),
2813 access: "[]".to_string(),
2814 span,
2815 }),
2816 TypeExpr::Ref(_) => Ok(TypeExpr::Any),
2817 TypeExpr::Union(items) => {
2818 let items = items
2819 .iter()
2820 .map(|item| index_type(item, span, is_opaque))
2821 .collect::<Result<Vec<_>, _>>()?;
2822 Ok(union_type(items))
2823 }
2824 _ => Ok(TypeExpr::Any),
2825 }
2826}
2827
2828fn builtin_return_type(name: &str) -> TypeExpr {
2829 match name {
2830 "len" | "find" | "to_int" | "ceil_div" | "floor_div" => TypeExpr::Int,
2831 "empty" | "contains" | "starts_with" | "ends_with" => TypeExpr::Bool,
2832 "to_float" => TypeExpr::Float,
2833 "to_string" | "trim" | "join" => TypeExpr::Str,
2834 "keys" | "values" | "split" | "grep_text" | "range" | "push" => {
2835 TypeExpr::List(Box::new(TypeExpr::Any))
2836 }
2837 "json_parse" | "validate" | "format" => TypeExpr::Any,
2838 _ => TypeExpr::Any,
2839 }
2840}
2841
2842fn binary_return_type(op: crate::ast::BinaryOp) -> TypeExpr {
2843 match op {
2844 crate::ast::BinaryOp::Equal
2845 | crate::ast::BinaryOp::NotEqual
2846 | crate::ast::BinaryOp::Less
2847 | crate::ast::BinaryOp::LessEqual
2848 | crate::ast::BinaryOp::Greater
2849 | crate::ast::BinaryOp::GreaterEqual
2850 | crate::ast::BinaryOp::And
2851 | crate::ast::BinaryOp::Or => TypeExpr::Bool,
2852 crate::ast::BinaryOp::Add
2853 | crate::ast::BinaryOp::Subtract
2854 | crate::ast::BinaryOp::Multiply
2855 | crate::ast::BinaryOp::Divide
2856 | crate::ast::BinaryOp::Modulo => TypeExpr::Float,
2857 }
2858}
2859
2860fn process_input_type(process: &ProcessDecl) -> TypeExpr {
2861 match process.params.as_slice() {
2862 [] => TypeExpr::Null,
2863 [param] => param.ty.clone(),
2864 _ => process_input_record_type(process),
2865 }
2866}
2867
2868fn process_input_record_type(process: &ProcessDecl) -> TypeExpr {
2869 TypeExpr::Object(
2870 process
2871 .params
2872 .iter()
2873 .map(|param| TypeField {
2874 name: param.name.clone(),
2875 ty: param.ty.clone(),
2876 optional: false,
2877 })
2878 .collect(),
2879 )
2880}
2881
2882fn process_type_for_decl(process: &ProcessDecl, output: TypeExpr) -> TypeExpr {
2883 TypeExpr::Process {
2884 input: Box::new(process_input_type(process)),
2885 output: Box::new(output),
2886 input_count: process.params.len(),
2887 }
2888}
2889
2890fn module_path_for_expr(expr: &Expr) -> Option<Vec<AstString>> {
2891 match expr {
2892 Expr::LabelAnnotated { expr, .. } => module_path_for_expr(expr),
2893 Expr::Variable(name) => Some(vec![name.clone()]),
2894 Expr::Field { target, field } => {
2895 let mut path = module_path_for_expr(target)?;
2896 path.push(field.clone());
2897 Some(path)
2898 }
2899 Expr::ResourceRef(resource) => Some(resource.path.clone()),
2900 _ => None,
2901 }
2902}
2903
2904fn is_trigger_event_expr(expr: &Expr) -> bool {
2905 matches!(
2906 module_path_for_expr(expr).as_deref(),
2907 Some([trigger, event]) if trigger.as_str() == "trigger" && event.as_str() == "event"
2908 )
2909}
2910
2911fn is_trigger_event_projection_expr(expr: &Expr) -> bool {
2912 module_path_for_expr(expr).is_some_and(|path| {
2913 path.len() > 2 && path[0].as_str() == "trigger" && path[1].as_str() == "event"
2914 })
2915}
2916
2917fn trigger_target_process_name(expr: &Expr) -> Option<String> {
2918 match expr {
2919 Expr::LabelAnnotated { expr, .. } => trigger_target_process_name(expr),
2920 Expr::Variable(name) | Expr::ProcessRef { process: name } => Some(name.to_string()),
2921 _ => None,
2922 }
2923}
2924
2925fn trigger_target_process_label(expr: &Expr) -> String {
2926 trigger_target_process_name(expr).unwrap_or_else(|| "target".to_string())
2927}
2928
2929fn expr_has_label_annotation(expr: &Expr) -> bool {
2930 match expr {
2931 Expr::LabelAnnotated { .. } => true,
2932 other => other.children().any(expr_has_label_annotation),
2933 }
2934}
2935
2936#[cfg(test)]
2937mod tests {
2938 use super::*;
2939
2940 fn resources() -> ResourceCatalog {
2941 let mut catalog = ResourceCatalog::new();
2942 catalog.add_module_operation(
2943 ["tools"],
2944 "Tools",
2945 "read_file",
2946 "read_file",
2947 TypeExpr::Object(vec![TypeField {
2948 name: "path".into(),
2949 ty: TypeExpr::Str,
2950 optional: false,
2951 }]),
2952 TypeExpr::Str,
2953 );
2954 catalog.add_module_operation(
2955 ["tools"],
2956 "Tools",
2957 "echo",
2958 "echo",
2959 TypeExpr::Any,
2960 TypeExpr::Any,
2961 );
2962 crate::add_trigger_resource_operations(&mut catalog);
2963 catalog
2964 .add_trigger_source_constructor(
2965 ["timer", "Schedule"],
2966 TypeExpr::Object(vec![
2967 TypeField {
2968 name: "expr".into(),
2969 ty: TypeExpr::Str,
2970 optional: false,
2971 },
2972 TypeField {
2973 name: "tz".into(),
2974 ty: TypeExpr::Str,
2975 optional: true,
2976 },
2977 ]),
2978 NamedDataType::object(
2979 "timer.Tick",
2980 vec![TypeField {
2981 name: "fired_at".into(),
2982 ty: TypeExpr::Str,
2983 optional: false,
2984 }],
2985 )
2986 .expect("valid timer tick type"),
2987 )
2988 .expect("valid timer trigger source");
2989 catalog
2990 }
2991
2992 fn full_surface() -> LashlangSurface {
2993 LashlangSurface::new(resources(), LashlangAbilities::all())
2994 }
2995
2996 fn full_label_surface() -> LashlangSurface {
2997 full_surface()
2998 .with_language_features(LashlangLanguageFeatures::default().with_label_annotations())
2999 }
3000
3001 fn timer_tick_type_with_field(field: &'static str) -> NamedDataType {
3002 NamedDataType::object(
3003 "timer.Tick",
3004 vec![TypeField {
3005 name: field.into(),
3006 ty: TypeExpr::Str,
3007 optional: false,
3008 }],
3009 )
3010 .expect("valid timer tick type")
3011 }
3012
3013 fn resources_with_timer_event(event_type: NamedDataType) -> ResourceCatalog {
3014 let mut catalog = ResourceCatalog::new();
3015 crate::add_trigger_resource_operations(&mut catalog);
3016 catalog
3017 .add_trigger_source_constructor(
3018 ["timer", "Schedule"],
3019 TypeExpr::Object(vec![TypeField {
3020 name: "expr".into(),
3021 ty: TypeExpr::Str,
3022 optional: false,
3023 }]),
3024 event_type,
3025 )
3026 .expect("valid timer trigger source");
3027 catalog
3028 }
3029
3030 #[test]
3031 fn named_host_data_type_validation_rejects_invalid_shapes() {
3032 let duplicate_field = NamedDataType::object(
3033 "timer.Tick",
3034 vec![
3035 TypeField {
3036 name: "fired_at".into(),
3037 ty: TypeExpr::Str,
3038 optional: false,
3039 },
3040 TypeField {
3041 name: "fired_at".into(),
3042 ty: TypeExpr::Str,
3043 optional: false,
3044 },
3045 ],
3046 )
3047 .expect_err("duplicate fields should be rejected");
3048 assert!(matches!(
3049 duplicate_field,
3050 NamedDataTypeError::DuplicateField { .. }
3051 ));
3052
3053 let nested_ref = NamedDataType::object(
3054 "timer.Tick",
3055 vec![TypeField {
3056 name: "nested".into(),
3057 ty: TypeExpr::Ref("Other.Type".into()),
3058 optional: false,
3059 }],
3060 )
3061 .expect_err("nested refs should be rejected");
3062 assert!(matches!(nested_ref, NamedDataTypeError::NestedRef { .. }));
3063
3064 let duplicate_enum = NamedDataType::object(
3065 "timer.Tick",
3066 vec![TypeField {
3067 name: "kind".into(),
3068 ty: TypeExpr::Enum(vec!["Red".into(), "Red".into()]),
3069 optional: false,
3070 }],
3071 )
3072 .expect_err("duplicate enum values should be rejected");
3073 assert!(matches!(
3074 duplicate_enum,
3075 NamedDataTypeError::DuplicateEnumValue { .. }
3076 ));
3077
3078 let simple_name = NamedDataType::object("Tick", vec![])
3079 .expect_err("host data type names must be qualified");
3080 assert!(matches!(
3081 simple_name,
3082 NamedDataTypeError::InvalidName { .. }
3083 ));
3084 }
3085
3086 #[test]
3087 fn resource_catalog_rejects_conflicting_named_host_data_type_definitions() {
3088 let mut catalog = ResourceCatalog::new();
3089 catalog
3090 .add_named_data_type(timer_tick_type_with_field("fired_at"))
3091 .expect("first definition");
3092 let err = catalog
3093 .add_named_data_type(timer_tick_type_with_field("delivered_at"))
3094 .expect_err("same host type name with different shape should be rejected");
3095
3096 assert!(matches!(
3097 err,
3098 ResourceCatalogError::ConflictingNamedDataType { .. }
3099 ));
3100 }
3101
3102 #[test]
3103 fn linked_module_accepts_named_processes_resource_params_and_activations() {
3104 let program = crate::parse(
3105 r#"
3106 type ChangeEvent = { path: str }
3107 process scan(tool: Tools, event: ChangeEvent) {
3108 text = await tool.read_file({ path: "changed.txt" })?
3109 finish text
3110 }
3111 process watcher(run: any) {
3112 sleep for "0ms"
3113 signal = wait signal
3114 signal run run with signal
3115 finish signal
3116 }
3117 process from_tick(tick: timer.Tick) {
3118 finish tick.fired_at
3119 }
3120 source = timer.Schedule({ expr: "0 8 * * *", tz: "UTC" })
3121 handle = await triggers.register({
3122 source: source,
3123 target: from_tick,
3124 inputs: { tick: trigger.event },
3125 name: "changed"
3126 })?
3127 submit handle
3128 "#,
3129 )
3130 .expect("parse module");
3131
3132 let linked = LinkedModule::link(program, full_surface()).expect("link module");
3133
3134 assert!(
3135 linked
3136 .module_ref
3137 .as_str()
3138 .starts_with("lashlang:v1:sha256:")
3139 );
3140 }
3141
3142 #[test]
3143 fn linked_module_allows_trigger_registration_name_to_match_target_process() {
3144 let program = crate::parse(
3145 r#"
3146 process changed(tick: timer.Tick) {
3147 finish true
3148 }
3149 source = timer.Schedule({ expr: "0 8 * * *" })
3150 await triggers.register({
3151 source: source,
3152 target: changed,
3153 inputs: { tick: trigger.event },
3154 name: "changed"
3155 })?
3156 "#,
3157 )
3158 .expect("parse module");
3159
3160 LinkedModule::link(program, full_surface())
3161 .expect("trigger registration names and process names occupy different namespaces");
3162 }
3163
3164 #[test]
3165 fn linked_module_resolves_host_named_data_refs_for_fields_and_structural_assignability() {
3166 let direct_ref = crate::parse(
3167 r#"
3168 process from_tick(tick: timer.Tick) {
3169 finish tick.fired_at
3170 }
3171 submit true
3172 "#,
3173 )
3174 .expect("parse direct host data ref");
3175 LinkedModule::link(direct_ref, full_surface()).expect("host data ref fields should link");
3176
3177 let structural_input = crate::parse(
3178 r#"
3179 process from_tick(tick: { fired_at: str }) {
3180 finish tick.fired_at
3181 }
3182 source = timer.Schedule({ expr: "0 8 * * *" })
3183 await triggers.register({
3184 source: source,
3185 target: from_tick,
3186 inputs: { tick: trigger.event }
3187 })?
3188 "#,
3189 )
3190 .expect("parse structural target input");
3191 LinkedModule::link(structural_input, full_surface())
3192 .expect("host data shape should be structurally assignable");
3193 }
3194
3195 #[test]
3196 fn linked_module_rejects_unknown_host_data_refs_and_opaque_source_field_access() {
3197 let unknown = crate::parse(
3198 r#"
3199 process from_tick(tick: foo.Tick) {
3200 finish true
3201 }
3202 submit true
3203 "#,
3204 )
3205 .expect("parse unknown host type");
3206 assert!(matches!(
3207 LinkedModule::link(unknown, full_surface()),
3208 Err(LinkError::UnknownType { name, .. }) if name == "foo.Tick"
3209 ));
3210
3211 let opaque = crate::parse(
3212 r#"
3213 source = timer.Schedule({ expr: "0 8 * * *" })
3214 submit source.expr
3215 "#,
3216 )
3217 .expect("parse opaque source access");
3218 assert!(matches!(
3219 LinkedModule::link(opaque, full_surface()),
3220 Err(LinkError::OpaqueHostValueAccess { type_name, .. }) if type_name == "timer.Schedule"
3221 ));
3222 }
3223
3224 #[test]
3225 fn required_surface_ref_tracks_host_named_data_type_shape_changes() {
3226 let program = crate::parse(
3227 r#"
3228 process from_tick(tick: any) {
3229 finish true
3230 }
3231 source = timer.Schedule({ expr: "0 8 * * *" })
3232 await triggers.register({
3233 source: source,
3234 target: from_tick,
3235 inputs: { tick: trigger.event }
3236 })?
3237 "#,
3238 )
3239 .expect("parse trigger registration");
3240 let first = LinkedModule::link(
3241 program.clone(),
3242 LashlangSurface::new(
3243 resources_with_timer_event(timer_tick_type_with_field("fired_at")),
3244 LashlangAbilities::all(),
3245 ),
3246 )
3247 .expect("link first host event shape");
3248 let second = LinkedModule::link(
3249 program,
3250 LashlangSurface::new(
3251 resources_with_timer_event(timer_tick_type_with_field("delivered_at")),
3252 LashlangAbilities::all(),
3253 ),
3254 )
3255 .expect("link changed host event shape");
3256
3257 assert_ne!(first.required_surface_ref, second.required_surface_ref);
3258 }
3259
3260 #[test]
3261 fn linked_module_accepts_top_level_sleep() {
3262 let program = crate::parse("sleep for 1").expect("parse sleep");
3263
3264 LinkedModule::link(program, full_surface()).expect("top-level sleep should link");
3265 }
3266
3267 #[test]
3268 fn linked_module_rejects_process_lifecycle_outside_process_body() {
3269 let program = crate::parse("payload = wait signal").expect("parse wait signal");
3270
3271 let err = LinkedModule::link(program, full_surface())
3272 .expect_err("top-level process lifecycle should be rejected");
3273
3274 assert!(
3275 matches!(
3276 err,
3277 LinkError::ProcessLifecycleOutsideProcess {
3278 keyword: "wait signal",
3279 ..
3280 }
3281 ),
3282 "{err}"
3283 );
3284 }
3285
3286 #[test]
3287 fn linked_module_accepts_top_level_signal_run() {
3288 let program =
3291 crate::parse("signal run \"handle\" with \"ping\"").expect("parse signal run");
3292
3293 LinkedModule::link(program, full_surface()).expect("top-level signal run should link");
3294 }
3295
3296 #[test]
3297 fn linked_module_rejects_bad_process_args_and_unresolved_operations() {
3298 let missing_arg = crate::parse(
3299 r#"
3300 process scan(tool: Tools, path: str) { finish path }
3301 start scan(tool: tools)
3302 "#,
3303 )
3304 .expect("parse missing arg");
3305 assert!(matches!(
3306 LinkedModule::link(missing_arg, full_surface()),
3307 Err(LinkError::MissingProcessArgument { arg, .. }) if arg == "path"
3308 ));
3309
3310 let bad_operation = crate::parse(
3311 r#"
3312 process scan(tool: Tools) {
3313 finish await tool.missing({})?
3314 }
3315 "#,
3316 )
3317 .expect("parse bad operation");
3318 assert!(matches!(
3319 LinkedModule::link(bad_operation, full_surface()),
3320 Err(LinkError::UnknownResourceOperation { operation, .. }) if operation == "missing"
3321 ));
3322 }
3323
3324 #[test]
3325 fn linked_module_rejects_disabled_abilities() {
3326 let process =
3327 crate::parse("process worker() { finish null }").expect("parse disabled process");
3328 assert!(matches!(
3329 LinkedModule::link(
3330 process,
3331 LashlangSurface::new(resources(), LashlangAbilities::default())
3332 ),
3333 Err(LinkError::FeatureDisabled {
3334 feature: "processes",
3335 ..
3336 })
3337 ));
3338
3339 let start = crate::parse("start worker()").expect("parse disabled start");
3340 assert!(matches!(
3341 LinkedModule::link(
3342 start,
3343 LashlangSurface::new(resources(), LashlangAbilities::default())
3344 ),
3345 Err(LinkError::FeatureDisabled {
3346 feature: "processes",
3347 ..
3348 })
3349 ));
3350
3351 let sleep = crate::parse("sleep for \"1s\"").expect("parse disabled sleep");
3352 assert!(matches!(
3353 LinkedModule::link(
3354 sleep,
3355 LashlangSurface::new(resources(), LashlangAbilities::default())
3356 ),
3357 Err(LinkError::FeatureDisabled {
3358 feature: "sleep",
3359 ..
3360 })
3361 ));
3362
3363 let signal = crate::parse("process worker() { payload = wait signal }")
3364 .expect("parse disabled process signal");
3365 assert!(matches!(
3366 LinkedModule::link(
3367 signal,
3368 LashlangSurface::new(resources(), LashlangAbilities::default().with_processes())
3369 ),
3370 Err(LinkError::FeatureDisabled {
3371 feature: "process signals",
3372 ..
3373 })
3374 ));
3375
3376 let trigger = crate::parse(
3377 r#"
3378 process worker(tick: timer.Tick) { finish true }
3379 source = timer.Schedule({ expr: "0 8 * * *" })
3380 await triggers.register({
3381 source: source,
3382 target: worker,
3383 inputs: { tick: trigger.event }
3384 })?
3385 "#,
3386 )
3387 .expect("parse disabled trigger");
3388 assert!(matches!(
3389 LinkedModule::link(
3390 trigger,
3391 LashlangSurface::new(resources(), LashlangAbilities::default().with_processes())
3392 ),
3393 Err(LinkError::FeatureDisabled {
3394 feature: "triggers",
3395 ..
3396 })
3397 ));
3398 }
3399
3400 #[test]
3401 fn linked_module_validates_value_constructors_and_trigger_registry_ops() {
3402 let program = crate::parse(
3403 r#"
3404 process scan(tick: timer.Tick) -> bool {
3405 finish true
3406 }
3407 source = timer.Schedule({ expr: "0 8 * * *", tz: "UTC" })
3408 handle = await triggers.register({
3409 source: source,
3410 target: scan,
3411 inputs: { tick: trigger.event },
3412 name: "scan"
3413 })?
3414 registrations = await triggers.list({ target: scan })?
3415 cancelled = await triggers.cancel({ handle: handle })?
3416 submit { handle: handle, registrations: registrations, cancelled: cancelled }
3417 "#,
3418 )
3419 .expect("parse trigger registry program");
3420 assert!(LinkedModule::link(program, full_surface()).is_ok());
3421 }
3422
3423 #[test]
3424 fn linked_module_accepts_explicit_trigger_input_mappings() {
3425 let repeated_event = crate::parse(
3426 r#"
3427 process scan(a: timer.Tick, b: { fired_at: str }) {
3428 finish { a: a.fired_at, b: b.fired_at }
3429 }
3430 source = timer.Schedule({ expr: "0 8 * * *" })
3431 await triggers.register({
3432 source: source,
3433 target: scan,
3434 inputs: { a: trigger.event, b: trigger.event }
3435 })?
3436 "#,
3437 )
3438 .expect("parse repeated event mapping");
3439 LinkedModule::link(repeated_event, full_surface())
3440 .expect("event payload should map to multiple assignable params");
3441
3442 let fixed_authority = crate::parse(
3443 r#"
3444 process scan(tick: timer.Tick, tool: Tools) {
3445 text = await tool.read_file({ path: tick.fired_at })?
3446 finish text
3447 }
3448 source = timer.Schedule({ expr: "0 8 * * *" })
3449 await triggers.register({
3450 source: source,
3451 target: scan,
3452 inputs: { tick: trigger.event, tool: tools }
3453 })?
3454 "#,
3455 )
3456 .expect("parse fixed authority mapping");
3457 LinkedModule::link(fixed_authority, full_surface())
3458 .expect("fixed resource inputs should satisfy process authority params");
3459 }
3460
3461 #[test]
3462 fn linked_module_captures_concrete_process_body_resources_statically() {
3463 let program = crate::parse(
3464 r#"
3465 process scan(tick: timer.Tick) {
3466 text = await tools.read_file({ path: tick.fired_at })?
3467 finish text
3468 }
3469 source = timer.Schedule({ expr: "0 8 * * *" })
3470 await triggers.register({
3471 source: source,
3472 target: scan,
3473 inputs: { tick: trigger.event }
3474 })?
3475 "#,
3476 )
3477 .expect("parse captured authority process");
3478 let linked = LinkedModule::link(program, full_surface())
3479 .expect("process body should capture concrete host resources");
3480 let process = linked
3481 .artifact
3482 .canonical_ir
3483 .process("scan")
3484 .expect("scan process");
3485 fn contains_resource_ref(expr: &Expr, path: &str) -> bool {
3486 matches!(expr, Expr::ResourceRef(resource) if resource.path_string() == path)
3487 || expr
3488 .children()
3489 .any(|child| contains_resource_ref(child, path))
3490 }
3491 assert!(
3492 contains_resource_ref(&process.body, "tools"),
3493 "linked process body should contain a persisted tools resource ref"
3494 );
3495
3496 let shadowed = crate::parse(
3497 r#"
3498 tool = tools
3499 process scan(tick: timer.Tick) {
3500 text = await tool.read_file({ path: tick.fired_at })?
3501 finish text
3502 }
3503 source = timer.Schedule({ expr: "0 8 * * *" })
3504 await triggers.register({
3505 source: source,
3506 target: scan,
3507 inputs: { tick: trigger.event }
3508 })?
3509 "#,
3510 )
3511 .expect("parse foreground variable capture");
3512 assert!(matches!(
3513 LinkedModule::link(shadowed, full_surface()),
3514 Err(LinkError::UnknownName { name, .. }) if name == "tool"
3515 ));
3516 }
3517
3518 #[test]
3519 fn linked_module_accepts_button_trigger_source_constructor() {
3520 let mut resources = resources();
3521 resources
3522 .add_trigger_source_constructor(
3523 ["ui", "button", "pressed"],
3524 TypeExpr::Object(vec![]),
3525 NamedDataType::object(
3526 "ui.button.Pressed",
3527 vec![
3528 TypeField {
3529 name: "button".into(),
3530 ty: TypeExpr::Union(vec![
3531 TypeExpr::Enum(vec!["Red".into()]),
3532 TypeExpr::Enum(vec!["Blue".into()]),
3533 ]),
3534 optional: false,
3535 },
3536 TypeField {
3537 name: "message".into(),
3538 ty: TypeExpr::Str,
3539 optional: false,
3540 },
3541 TypeField {
3542 name: "pressed_at".into(),
3543 ty: TypeExpr::Str,
3544 optional: false,
3545 },
3546 ],
3547 )
3548 .expect("valid button event type"),
3549 )
3550 .expect("valid button trigger source");
3551 let program = crate::parse(
3552 r#"
3553 process on_button(event: ui.button.Pressed) {
3554 wake { kind: "button_pressed", button: event.button, message: event.message }
3555 finish true
3556 }
3557
3558 handle = await triggers.register({
3559 source: ui.button.pressed({}),
3560 target: on_button,
3561 inputs: { event: trigger.event },
3562 name: "button watcher"
3563 })?
3564 submit handle
3565 "#,
3566 )
3567 .expect("parse button trigger source");
3568
3569 LinkedModule::link(
3570 program,
3571 LashlangSurface::new(resources, LashlangAbilities::all()),
3572 )
3573 .expect("button trigger source should link");
3574 }
3575
3576 #[test]
3577 fn linked_module_rejects_bad_trigger_registry_bindings() {
3578 let missing = crate::parse(
3579 r#"
3580 process scan(tick: timer.Tick) { finish true }
3581 source = timer.Schedule({ expr: "0 8 * * *" })
3582 await triggers.register({ target: scan })?
3583 "#,
3584 )
3585 .expect("parse missing source");
3586 assert!(matches!(
3587 LinkedModule::link(missing, full_surface()),
3588 Err(LinkError::InvalidTriggerRegistration { .. })
3589 ));
3590
3591 let missing_inputs = crate::parse(
3592 r#"
3593 process scan(tick: timer.Tick) { finish true }
3594 source = timer.Schedule({ expr: "0 8 * * *" })
3595 await triggers.register({ source: source, target: scan })?
3596 "#,
3597 )
3598 .expect("parse missing inputs");
3599 assert!(matches!(
3600 LinkedModule::link(missing_inputs, full_surface()),
3601 Err(LinkError::InvalidTriggerRegistration { .. })
3602 ));
3603
3604 let wrong_source = crate::parse(
3605 r#"
3606 process scan(tick: timer.Tick) { finish true }
3607 await triggers.register({
3608 source: { expr: "0 8 * * *" },
3609 target: scan,
3610 inputs: { tick: trigger.event }
3611 })?
3612 "#,
3613 )
3614 .expect("parse wrong source");
3615 assert!(matches!(
3616 LinkedModule::link(wrong_source, full_surface()),
3617 Err(LinkError::UnknownTriggerSourceType { .. })
3618 ));
3619
3620 let payload_mismatch = crate::parse(
3621 r#"
3622 process scan(tick: str) { finish tick }
3623 source = timer.Schedule({ expr: "0 8 * * *" })
3624 await triggers.register({
3625 source: source,
3626 target: scan,
3627 inputs: { tick: trigger.event }
3628 })?
3629 "#,
3630 )
3631 .expect("parse payload mismatch");
3632 assert!(matches!(
3633 LinkedModule::link(payload_mismatch, full_surface()),
3634 Err(LinkError::TriggerEventMismatch { .. })
3635 ));
3636
3637 let unknown_input = crate::parse(
3638 r#"
3639 process scan(tick: timer.Tick) { finish true }
3640 source = timer.Schedule({ expr: "0 8 * * *" })
3641 await triggers.register({
3642 source: source,
3643 target: scan,
3644 inputs: { tick: trigger.event, extra: "nope" }
3645 })?
3646 "#,
3647 )
3648 .expect("parse unknown input");
3649 assert!(matches!(
3650 LinkedModule::link(unknown_input, full_surface()),
3651 Err(LinkError::UnknownTriggerInput { input, .. }) if input == "extra"
3652 ));
3653
3654 let duplicate_input = crate::parse(
3655 r#"
3656 process scan(tick: timer.Tick) { finish true }
3657 source = timer.Schedule({ expr: "0 8 * * *" })
3658 await triggers.register({
3659 source: source,
3660 target: scan,
3661 inputs: { tick: trigger.event, tick: trigger.event }
3662 })?
3663 "#,
3664 )
3665 .expect("parse duplicate input");
3666 assert!(matches!(
3667 LinkedModule::link(duplicate_input, full_surface()),
3668 Err(LinkError::DuplicateTriggerInput { input, .. }) if input == "tick"
3669 ));
3670
3671 let no_event_input = crate::parse(
3672 r#"
3673 process scan(tick: timer.Tick, label: str) { finish label }
3674 source = timer.Schedule({ expr: "0 8 * * *" })
3675 await triggers.register({
3676 source: source,
3677 target: scan,
3678 inputs: { tick: { fired_at: "static" }, label: "static" }
3679 })?
3680 "#,
3681 )
3682 .expect("parse no event input");
3683 assert!(matches!(
3684 LinkedModule::link(no_event_input, full_surface()),
3685 Err(LinkError::MissingTriggerEventInput { .. })
3686 ));
3687
3688 let event_projection = crate::parse(
3689 r#"
3690 process scan(fired_at: str) { finish fired_at }
3691 source = timer.Schedule({ expr: "0 8 * * *" })
3692 await triggers.register({
3693 source: source,
3694 target: scan,
3695 inputs: { fired_at: trigger.event.fired_at }
3696 })?
3697 "#,
3698 )
3699 .expect("parse event projection");
3700 assert!(matches!(
3701 LinkedModule::link(event_projection, full_surface()),
3702 Err(LinkError::TriggerEventProjection { .. })
3703 ));
3704
3705 let event_outside_inputs = crate::parse(
3706 r#"
3707 process scan(tick: timer.Tick) { finish true }
3708 submit trigger.event
3709 "#,
3710 )
3711 .expect("parse event outside inputs");
3712 assert!(matches!(
3713 LinkedModule::link(event_outside_inputs, full_surface()),
3714 Err(LinkError::TriggerEventOutsideInputs { .. })
3715 ));
3716
3717 let multi_input = crate::parse(
3718 r#"
3719 process scan(tick: timer.Tick, extra: str) { finish extra }
3720 source = timer.Schedule({ expr: "0 8 * * *" })
3721 await triggers.register({
3722 source: source,
3723 target: scan,
3724 inputs: { tick: trigger.event }
3725 })?
3726 "#,
3727 )
3728 .expect("parse multi-input target");
3729 assert!(matches!(
3730 LinkedModule::link(multi_input, full_surface()),
3731 Err(LinkError::MissingTriggerInput { input, .. }) if input == "extra"
3732 ));
3733
3734 let target_is_not_process = crate::parse(
3735 r#"
3736 process scan(tick: timer.Tick) { finish true }
3737 source = timer.Schedule({ expr: "0 8 * * *" })
3738 await triggers.register({
3739 source: source,
3740 target: source,
3741 inputs: { tick: trigger.event }
3742 })?
3743 "#,
3744 )
3745 .expect("parse non-process target");
3746 assert!(matches!(
3747 LinkedModule::link(target_is_not_process, full_surface()),
3748 Err(LinkError::InvalidTriggerTarget { .. })
3749 ));
3750
3751 let list_without_filters = crate::parse(
3752 r#"
3753 process scan(tick: timer.Tick) { finish true }
3754 await triggers.list({})?
3755 "#,
3756 )
3757 .expect("parse trigger list without filters");
3758 assert!(LinkedModule::link(list_without_filters, full_surface()).is_ok());
3759
3760 let list_with_filters = crate::parse(
3761 r#"
3762 process scan(tick: timer.Tick) { finish true }
3763 await triggers.list({
3764 target: scan,
3765 name: "daily",
3766 source_type: "timer.Schedule",
3767 enabled: true
3768 })?
3769 "#,
3770 )
3771 .expect("parse trigger list filters");
3772 assert!(LinkedModule::link(list_with_filters, full_surface()).is_ok());
3773
3774 let list_target_is_not_process = crate::parse(
3775 r#"
3776 process scan(tick: timer.Tick) { finish true }
3777 source = timer.Schedule({ expr: "0 8 * * *" })
3778 await triggers.list({ target: source })?
3779 "#,
3780 )
3781 .expect("parse trigger list non-process target");
3782 assert!(matches!(
3783 LinkedModule::link(list_target_is_not_process, full_surface()),
3784 Err(LinkError::InvalidTriggerTarget { .. })
3785 | Err(LinkError::IncompatibleOperationInput { .. })
3786 ));
3787
3788 let constructor_mismatch = crate::parse(
3789 r#"
3790 source = timer.Schedule({ expr: 1 })
3791 submit source
3792 "#,
3793 )
3794 .expect("parse constructor mismatch");
3795 assert!(matches!(
3796 LinkedModule::link(constructor_mismatch, full_surface()),
3797 Err(LinkError::IncompatibleConstructorInput { .. })
3798 ));
3799
3800 let operation_mismatch = crate::parse(
3801 r#"
3802 await tools.read_file({ path: 1 })?
3803 "#,
3804 )
3805 .expect("parse operation mismatch");
3806 assert!(matches!(
3807 LinkedModule::link(operation_mismatch, full_surface()),
3808 Err(LinkError::IncompatibleOperationInput { .. })
3809 ));
3810 }
3811
3812 #[test]
3813 fn linked_module_infers_process_output_and_validates_return_annotations() {
3814 let inferred = crate::parse(
3815 r#"
3816 process done(tick: timer.Tick) -> bool {
3817 finish true
3818 }
3819 source = timer.Schedule({ expr: "0 8 * * *" })
3820 await triggers.register({
3821 source: source,
3822 target: done,
3823 inputs: { tick: trigger.event }
3824 })?
3825 "#,
3826 )
3827 .expect("parse inferred output");
3828 assert!(LinkedModule::link(inferred, full_surface()).is_ok());
3829
3830 let union_mismatch = crate::parse(
3831 r#"
3832 process done(tick: timer.Tick) -> bool {
3833 if true {
3834 finish true
3835 }
3836 finish "done"
3837 }
3838 "#,
3839 )
3840 .expect("parse union mismatch");
3841 assert!(matches!(
3842 LinkedModule::link(union_mismatch, full_surface()),
3843 Err(LinkError::IncompatibleProcessReturn { .. })
3844 ));
3845 }
3846
3847 #[test]
3848 fn linked_module_hash_ignores_unused_host_abilities() {
3849 let program = crate::parse("submit 1").expect("parse");
3850 let minimal = LinkedModule::link(
3851 program.clone(),
3852 LashlangSurface::new(resources(), LashlangAbilities::default()),
3853 )
3854 .expect("link minimal");
3855 let processes = LinkedModule::link(
3856 program,
3857 LashlangSurface::new(resources(), LashlangAbilities::default().with_processes()),
3858 )
3859 .expect("link process ability");
3860
3861 assert_eq!(minimal.module_ref, processes.module_ref);
3862 assert_eq!(minimal.required_surface_ref, processes.required_surface_ref);
3863 }
3864
3865 #[test]
3866 fn label_annotations_require_enabled_language_feature() {
3867 let program = crate::parse(
3868 r#"
3869 @label(title: "Scan files")
3870 process scan(tool: Tools) {
3871 @label(title: "Read file")
3872 text = await tool.read_file({ path: "." })?
3873 finish text
3874 }
3875 "#,
3876 )
3877 .expect("parse annotated process");
3878
3879 let err = LinkedModule::link(program.clone(), full_surface())
3880 .expect_err("default surface should reject label annotations");
3881 assert!(matches!(
3882 err,
3883 LinkError::FeatureDisabled {
3884 feature: "label annotations",
3885 ..
3886 }
3887 ));
3888
3889 let linked =
3890 LinkedModule::link(program, full_label_surface()).expect("enabled surface should link");
3891 assert!(
3892 linked
3893 .artifact
3894 .required_surface
3895 .language_features
3896 .label_annotations
3897 );
3898 let process = linked.program().process("scan").expect("linked process");
3899 assert_eq!(
3900 process.label.as_ref().map(|label| label.title.as_str()),
3901 Some("Scan files")
3902 );
3903 }
3904
3905 #[test]
3906 fn label_annotation_text_inside_strings_does_not_require_feature() {
3907 let linked = LinkedModule::link(
3908 crate::parse(r####"submit r'''@label(title: "Plain text")'''"####)
3909 .expect("parse string"),
3910 full_surface(),
3911 )
3912 .expect("disabled label annotations should not reject string text");
3913
3914 assert!(
3915 !linked
3916 .artifact
3917 .required_surface
3918 .language_features
3919 .label_annotations
3920 );
3921 }
3922
3923 #[test]
3924 fn label_metadata_round_trips_and_changes_artifact_identity() {
3925 let first = LinkedModule::link(
3926 crate::parse(
3927 r#"
3928 @label(title: "Scan files")
3929 process scan(tool: Tools) {
3930 @label(title: "Read file", description: "Load source text")
3931 text = await tool.read_file({ path: "." })?
3932 @label(title: "Finish")
3933 finish text
3934 }
3935 "#,
3936 )
3937 .expect("parse first"),
3938 full_label_surface(),
3939 )
3940 .expect("link first");
3941 let changed = LinkedModule::link(
3942 crate::parse(
3943 r#"
3944 @label(title: "Scan files")
3945 process scan(tool: Tools) {
3946 @label(title: "Read source", description: "Load source text")
3947 text = await tool.read_file({ path: "." })?
3948 @label(title: "Finish")
3949 finish text
3950 }
3951 "#,
3952 )
3953 .expect("parse changed"),
3954 full_label_surface(),
3955 )
3956 .expect("link changed");
3957
3958 let bytes = first
3959 .artifact
3960 .to_store_bytes()
3961 .expect("encode annotated artifact");
3962 let decoded = ModuleArtifact::from_store_bytes(&bytes).expect("decode annotated artifact");
3963 assert_eq!(decoded, first.artifact);
3964 assert_ne!(first.module_ref, changed.module_ref);
3965 assert_ne!(
3966 first.artifact.process_ref("scan"),
3967 changed.artifact.process_ref("scan")
3968 );
3969 }
3970
3971 #[test]
3972 fn module_ref_ignores_spans_and_formatting() {
3973 let compact = LinkedModule::link(
3974 crate::parse("process scan(root: str) { finish root }").expect("parse compact"),
3975 full_surface(),
3976 )
3977 .expect("link compact");
3978 let formatted = LinkedModule::link(
3979 crate::parse(
3980 r#"
3981 process scan(root: str) {
3982 finish root
3983 }
3984 "#,
3985 )
3986 .expect("parse formatted"),
3987 full_surface(),
3988 )
3989 .expect("link formatted");
3990
3991 assert_eq!(compact.module_ref, formatted.module_ref);
3992 }
3993
3994 #[test]
3995 fn process_ref_tracks_abi_and_body_but_not_local_binder_names() {
3996 let original = LinkedModule::link(
3997 crate::parse("process scan(root: str) { value = root\nfinish value }")
3998 .expect("parse original"),
3999 full_surface(),
4000 )
4001 .expect("link original");
4002 let renamed_local = LinkedModule::link(
4003 crate::parse("process scan(root: str) { renamed = root\nfinish renamed }")
4004 .expect("parse renamed local"),
4005 full_surface(),
4006 )
4007 .expect("link renamed local");
4008 let renamed_param = LinkedModule::link(
4009 crate::parse("process scan(path: str) { value = path\nfinish value }")
4010 .expect("parse renamed param"),
4011 full_surface(),
4012 )
4013 .expect("link renamed param");
4014 let changed_body = LinkedModule::link(
4015 crate::parse("process scan(root: str) { value = root\nfinish { value: value } }")
4016 .expect("parse changed body"),
4017 full_surface(),
4018 )
4019 .expect("link changed body");
4020
4021 assert_eq!(
4022 original.artifact.process_ref("scan"),
4023 renamed_local.artifact.process_ref("scan")
4024 );
4025 assert_ne!(
4026 original.artifact.process_ref("scan"),
4027 renamed_param.artifact.process_ref("scan")
4028 );
4029 assert_ne!(
4030 original.artifact.process_ref("scan"),
4031 changed_body.artifact.process_ref("scan")
4032 );
4033 }
4034
4035 #[test]
4036 fn required_surface_ref_tracks_resource_requirements_not_unrelated_tools() {
4037 let mut with_extra = resources();
4038 with_extra.add_module_operation(
4039 ["tools"],
4040 "Tools",
4041 "unrelated",
4042 "unrelated",
4043 TypeExpr::Any,
4044 TypeExpr::Any,
4045 );
4046 let program = crate::parse(
4047 "process scan(tool: Tools) { finish (await tool.read_file({ path: \".\" }))? }",
4048 )
4049 .expect("parse process");
4050
4051 let base = LinkedModule::link(program.clone(), full_surface()).expect("link base");
4052 let extra = LinkedModule::link(
4053 program.clone(),
4054 LashlangSurface::new(with_extra, LashlangAbilities::all()),
4055 )
4056 .expect("link extra");
4057 let changed_requirement = LinkedModule::link(
4058 crate::parse(
4059 "process scan(tool: Tools) { finish (await tool.echo({ value: \".\" }))? }",
4060 )
4061 .expect("parse changed resource"),
4062 full_surface(),
4063 )
4064 .expect("link changed requirement");
4065
4066 assert_eq!(base.module_ref, extra.module_ref);
4067 assert_eq!(base.required_surface_ref, extra.required_surface_ref);
4068 assert_ne!(
4069 base.required_surface_ref,
4070 changed_requirement.required_surface_ref
4071 );
4072 }
4073
4074 #[test]
4075 fn module_aliases_sharing_resource_type_route_to_distinct_host_operations() {
4076 let mut catalog = ResourceCatalog::new();
4077 catalog.add_module_operation(
4078 ["inbox", "work"],
4079 "Inbox",
4080 "send",
4081 "inbox__work__send",
4082 TypeExpr::Any,
4083 TypeExpr::Any,
4084 );
4085 catalog.add_module_operation(
4086 ["inbox", "personal"],
4087 "Inbox",
4088 "send",
4089 "inbox__personal__send",
4090 TypeExpr::Any,
4091 TypeExpr::Any,
4092 );
4093
4094 assert_eq!(
4095 catalog
4096 .resolve_module_operation("Inbox", "inbox.work", "send")
4097 .map(|binding| binding.host_operation.as_str()),
4098 Some("inbox__work__send")
4099 );
4100 assert_eq!(
4101 catalog
4102 .resolve_module_operation("Inbox", "inbox.personal", "send")
4103 .map(|binding| binding.host_operation.as_str()),
4104 Some("inbox__personal__send")
4105 );
4106 }
4107
4108 #[test]
4109 fn reusing_module_alias_for_different_resource_type_fails() {
4110 let mut catalog = ResourceCatalog::new();
4111 catalog
4112 .add_module_instance(["tools"], "Tools")
4113 .expect("initial module instance");
4114
4115 assert!(matches!(
4116 catalog.add_module_instance(["tools"], "Inbox"),
4117 Err(ResourceCatalogError::ConflictingModuleInstance {
4118 alias,
4119 existing,
4120 incoming,
4121 }) if alias == "tools" && existing == "Tools" && incoming == "Inbox"
4122 ));
4123 }
4124
4125 #[test]
4132 fn declaration_errors_surface_before_main_errors() {
4133 let program = crate::parse(
4136 r#"
4137 process scan() { finish missing_in_body }
4138 submit missing_in_main
4139 "#,
4140 )
4141 .expect("parse");
4142 let err = LinkedModule::link(program, full_surface())
4143 .expect_err("both bodies reference unknowns");
4144 assert!(
4145 matches!(&err, LinkError::UnknownName { name, .. } if name == "missing_in_body"),
4146 "{err:?}"
4147 );
4148 }
4149
4150 #[test]
4151 fn unknown_name_in_process_body_carries_declaration_span() {
4152 let program = crate::parse("process scan() { finish missing }").expect("parse");
4153 let err = LinkedModule::link(program, full_surface()).expect_err("unknown name");
4154 let LinkError::UnknownName { name, span } = &err else {
4155 panic!("expected UnknownName, got {err:?}");
4156 };
4157 assert_eq!(name, "missing");
4158 assert!(span.is_some(), "declaration-body error should carry a span");
4159 }
4160
4161 #[test]
4162 fn linker_reproduces_full_error_set() {
4163 type ErrorCase = (&'static str, fn(&LinkError) -> bool);
4168 let cases: &[ErrorCase] = &[
4169 (
4170 "process scan() { finish missing }",
4171 |err| matches!(err, LinkError::UnknownName { name, .. } if name == "missing"),
4172 ),
4173 (
4174 "process scan() { missing[0] = 1 }",
4175 |err| matches!(err, LinkError::UnknownName { name, .. } if name == "missing"),
4176 ),
4177 (
4178 "submit not_a_builtin(1)",
4179 |err| matches!(err, LinkError::UnknownBuiltin { name, .. } if name == "not_a_builtin"),
4180 ),
4181 (
4182 "x = 1\nsubmit x.read_file({})",
4183 |err| matches!(err, LinkError::UnresolvedReceiver { operation, .. } if operation == "read_file"),
4184 ),
4185 (
4186 "process scan() { finish 1 }\nstart scan(extra: 1)",
4187 |err| matches!(err, LinkError::UnexpectedProcessArgument { arg, .. } if arg == "extra"),
4188 ),
4189 (
4190 "process scan(needed: str) { finish needed }\nstart scan()",
4191 |err| matches!(err, LinkError::MissingProcessArgument { arg, .. } if arg == "needed"),
4192 ),
4193 (
4194 "start ghost()",
4195 |err| matches!(err, LinkError::UnknownProcess { name, .. } if name == "ghost"),
4196 ),
4197 ];
4198
4199 for (source, predicate) in cases {
4200 let program =
4201 crate::parse(source).unwrap_or_else(|err| panic!("parse {source:?}: {err}"));
4202 let err = LinkedModule::link(program, full_surface())
4203 .err()
4204 .unwrap_or_else(|| panic!("{source:?} should fail to link"));
4205 assert!(predicate(&err), "unexpected error for {source:?}: {err:?}");
4206 }
4207 }
4208
4209 #[test]
4210 fn unknown_resource_operation_still_rejected_after_receiver_resolves() {
4211 let program = crate::parse(
4212 r#"
4213 process scan(tool: Tools) { finish await tool.does_not_exist({})? }
4214 "#,
4215 )
4216 .expect("parse");
4217 let err = LinkedModule::link(program, full_surface()).expect_err("operation missing");
4218 assert!(
4219 matches!(&err, LinkError::UnknownResourceOperation { operation, .. } if operation == "does_not_exist"),
4220 "{err:?}"
4221 );
4222 }
4223
4224 #[tokio::test]
4225 async fn module_artifact_store_bytes_reject_corruption() {
4226 use crate::LashlangArtifactStore;
4227
4228 let linked = LinkedModule::link(
4229 crate::parse("process scan() { finish 1 }").expect("parse module"),
4230 full_surface(),
4231 )
4232 .expect("link module");
4233 let store = crate::InMemoryLashlangArtifactStore::new();
4234
4235 store
4236 .put_module_artifact(&linked.artifact)
4237 .await
4238 .expect("put artifact");
4239 assert_eq!(
4240 store
4241 .get_module_artifact(&linked.module_ref)
4242 .await
4243 .expect("get artifact")
4244 .expect("artifact exists")
4245 .module_ref,
4246 linked.module_ref
4247 );
4248
4249 assert!(ModuleArtifact::from_store_bytes(b"not json").is_err());
4250 }
4251}