1#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2pub struct NamedDataType {
3 name: String,
4 ty: TypeExpr,
5}
6
7impl NamedDataType {
8 pub fn new(name: impl Into<String>, ty: TypeExpr) -> Result<Self, NamedDataTypeError> {
9 let name = name.into();
10 if !is_qualified_type_name(&name) {
11 return Err(NamedDataTypeError::InvalidName { name });
12 }
13 if !matches!(ty, TypeExpr::Object(_)) {
14 return Err(NamedDataTypeError::ExpectedObject { name });
15 }
16 validate_named_data_shape(&ty)?;
17 Ok(Self { name, ty })
18 }
19
20 pub fn object(
21 name: impl Into<String>,
22 fields: Vec<TypeField>,
23 ) -> Result<Self, NamedDataTypeError> {
24 Self::new(name, TypeExpr::Object(fields))
25 }
26
27 pub fn name(&self) -> &str {
28 &self.name
29 }
30
31 pub fn ty(&self) -> &TypeExpr {
32 &self.ty
33 }
34
35 pub fn to_ref_ty(&self) -> TypeExpr {
36 TypeExpr::Ref(self.name.clone().into())
37 }
38}
39
40#[derive(Clone, Debug, PartialEq, Eq, Error)]
41pub enum NamedDataTypeError {
42 #[error("host data type name `{name}` must be qualified")]
43 InvalidName { name: String },
44 #[error("host data type `{name}` must be an object type")]
45 ExpectedObject { name: String },
46 #[error("host data type object has duplicate field `{field}`")]
47 DuplicateField { field: String },
48 #[error("host data type enum has duplicate value `{value}`")]
49 DuplicateEnumValue { value: String },
50 #[error("host data type shape cannot contain nested type ref `{name}`")]
51 NestedRef { name: String },
52 #[error("host data type shape cannot contain {ty}")]
53 UnsupportedType { ty: &'static str },
54}
55
56#[derive(Clone, Debug, PartialEq, Eq, Error)]
57pub enum LashlangHostCatalogError {
58 #[error("conflicting host data type definition `{name}`")]
59 ConflictingNamedDataType { name: String },
60 #[error(
61 "module `{alias}` already has resource type `{existing}`, cannot change it to `{incoming}`"
62 )]
63 ConflictingModuleInstance {
64 alias: String,
65 existing: String,
66 incoming: String,
67 },
68 #[error(
69 "trigger source `{source_type}` already emits `{existing}`, cannot change it to `{incoming}`"
70 )]
71 ConflictingTriggerSource {
72 source_type: String,
73 existing: String,
74 incoming: String,
75 },
76}
77
78fn is_qualified_type_name(name: &str) -> bool {
79 let mut segments = name.split('.');
80 let mut count = 0usize;
81 for segment in segments.by_ref() {
82 count += 1;
83 let mut chars = segment.chars();
84 let Some(first) = chars.next() else {
85 return false;
86 };
87 if !(first.is_ascii_alphabetic() || first == '_') {
88 return false;
89 }
90 if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
91 return false;
92 }
93 }
94 count >= 2
95}
96
97fn validate_named_data_shape(ty: &TypeExpr) -> Result<(), NamedDataTypeError> {
98 match ty {
99 TypeExpr::Any
100 | TypeExpr::Str
101 | TypeExpr::Int
102 | TypeExpr::Float
103 | TypeExpr::Bool
104 | TypeExpr::Dict
105 | TypeExpr::Null => Ok(()),
106 TypeExpr::Enum(values) => {
107 let mut seen = BTreeSet::new();
108 for value in values {
109 if !seen.insert(value.to_string()) {
110 return Err(NamedDataTypeError::DuplicateEnumValue {
111 value: value.to_string(),
112 });
113 }
114 }
115 Ok(())
116 }
117 TypeExpr::List(item) => validate_named_data_shape(item),
118 TypeExpr::Object(fields) => {
119 let mut seen = BTreeSet::new();
120 for field in fields {
121 if !seen.insert(field.name.to_string()) {
122 return Err(NamedDataTypeError::DuplicateField {
123 field: field.name.to_string(),
124 });
125 }
126 validate_named_data_shape(&field.ty)?;
127 }
128 Ok(())
129 }
130 TypeExpr::Union(items) => {
131 for item in items {
132 validate_named_data_shape(item)?;
133 }
134 Ok(())
135 }
136 TypeExpr::Ref(name) => Err(NamedDataTypeError::NestedRef {
137 name: name.to_string(),
138 }),
139 TypeExpr::Process { .. } => Err(NamedDataTypeError::UnsupportedType { ty: "process" }),
140 TypeExpr::TriggerHandle(_) => Err(NamedDataTypeError::UnsupportedType {
141 ty: "trigger handle",
142 }),
143 }
144}
145
146#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
147pub struct ResourceTypeCatalog {
148 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
149 pub operations: BTreeMap<String, ResourceOperationBinding>,
150}
151
152#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
153pub struct ModuleInstanceCatalog {
154 pub path: Vec<String>,
155 pub resource_type: String,
156 pub alias: String,
157 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
158 pub operations: BTreeMap<String, ModuleOperationBinding>,
159}
160
161#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
162pub struct ResourceOperationBinding {
163 pub input_ty: TypeExpr,
164 pub output_ty: TypeExpr,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub output_from_input: Option<OutputFromInputBinding>,
167}
168
169#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
170pub struct OutputFromInputBinding {
171 pub input_field: String,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub default_schema: Option<TypeExpr>,
174}
175
176#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
177pub struct ModuleOperationBinding {
178 pub host_operation: String,
179}
180
181#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
182pub struct ValueConstructorBinding {
183 pub path: Vec<String>,
184 pub type_name: String,
185 pub input_ty: TypeExpr,
186 pub output_ty: TypeExpr,
187}
188
189#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub struct TriggerSourceBinding {
191 event_type: NamedDataType,
192}
193
194impl TriggerSourceBinding {
195 fn new(event_type: NamedDataType) -> Self {
196 Self { event_type }
197 }
198
199 pub fn event_type(&self) -> &NamedDataType {
200 &self.event_type
201 }
202
203 pub fn event_ty(&self) -> &TypeExpr {
204 self.event_type.ty()
205 }
206
207 pub fn event_type_name(&self) -> &str {
208 self.event_type.name()
209 }
210}
211
212#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
213pub struct LashlangHostEnvironment {
214 #[serde(default)]
215 pub resources: LashlangHostCatalog,
216 #[serde(default)]
217 pub abilities: LashlangAbilities,
218 #[serde(default)]
219 pub language_features: LashlangLanguageFeatures,
220}
221
222impl LashlangHostEnvironment {
223 pub fn new(resources: LashlangHostCatalog, abilities: LashlangAbilities) -> Self {
224 Self {
225 resources,
226 abilities,
227 language_features: LashlangLanguageFeatures::default(),
228 }
229 }
230
231 pub fn with_language_features(mut self, language_features: LashlangLanguageFeatures) -> Self {
232 self.language_features = language_features;
233 self
234 }
235
236 pub fn satisfies(&self, requirements: &HostRequirements) -> bool {
237 self.abilities.satisfies(requirements.abilities)
238 && self
239 .language_features
240 .satisfies(requirements.language_features)
241 && self.resources.satisfies(&requirements.resources)
242 }
243}
244
245#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(default)]
247pub struct LashlangLanguageFeatures {
248 pub label_annotations: bool,
249}
250
251impl LashlangLanguageFeatures {
252 pub fn union(self, other: Self) -> Self {
253 Self {
254 label_annotations: self.label_annotations || other.label_annotations,
255 }
256 }
257
258 pub fn satisfies(self, required: Self) -> bool {
259 !required.label_annotations || self.label_annotations
260 }
261
262 pub fn with_label_annotations(mut self) -> Self {
263 self.label_annotations = true;
264 self
265 }
266}
267
268#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
269#[serde(default)]
270pub struct LashlangAbilities {
271 pub processes: bool,
272 pub sleep: bool,
273 pub process_signals: bool,
274 pub triggers: bool,
275}
276
277impl LashlangAbilities {
278 pub fn union(self, other: Self) -> Self {
279 Self {
280 processes: self.processes || other.processes,
281 sleep: self.sleep || other.sleep,
282 process_signals: self.process_signals || other.process_signals,
283 triggers: self.triggers || other.triggers,
284 }
285 }
286
287 pub fn satisfies(self, required: Self) -> bool {
288 (!required.processes || self.processes)
289 && (!required.sleep || self.sleep)
290 && (!required.process_signals || self.process_signals)
291 && (!required.triggers || self.triggers)
292 }
293
294 pub fn with_processes(mut self) -> Self {
295 self.processes = true;
296 self
297 }
298
299 pub fn with_sleep(mut self) -> Self {
300 self.sleep = true;
301 self
302 }
303
304 pub fn with_process_signals(mut self) -> Self {
305 self.process_signals = true;
306 self
307 }
308
309 pub fn with_triggers(mut self) -> Self {
310 self.triggers = true;
311 self
312 }
313
314 pub fn all() -> Self {
315 Self::default()
316 .with_sleep()
317 .with_processes()
318 .with_process_signals()
319 .with_triggers()
320 }
321}
322
323fn module_path_key(path: &[impl AsRef<str>]) -> String {
324 path.iter()
325 .map(|segment| segment.as_ref())
326 .collect::<Vec<_>>()
327 .join(".")
328}
329
330#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
331pub struct LinkedModule {
332 pub module_ref: crate::ModuleRef,
333 pub host_requirements_ref: crate::HostRequirementsRef,
334 pub artifact: ModuleArtifact,
335 #[serde(skip)]
336 linked_program: Option<Program>,
337}
338
339impl LinkedModule {
340 pub fn link(
341 program: Program,
342 surface: impl Borrow<LashlangHostEnvironment>,
343 ) -> Result<Self, LinkError> {
344 let surface = surface.borrow();
345 let mut linker = Linker::new(&program, surface);
346 let program = linker.link_program()?;
347 let program = materialize_default_trigger_keys(program)?;
348 let requirements = host_requirements_for_program_with_catalog(&program, &surface.resources);
349 let artifact =
350 ModuleArtifact::from_program_with_requirements(program.clone(), requirements).map_err(
351 |err| LinkError::ModuleHash {
352 message: err.to_string(),
353 },
354 )?;
355 Ok(Self {
356 module_ref: artifact.module_ref.clone(),
357 host_requirements_ref: artifact.host_requirements_ref.clone(),
358 artifact,
359 linked_program: Some(program),
360 })
361 }
362
363 pub fn program(&self) -> &Program {
364 self.linked_program
365 .as_ref()
366 .unwrap_or(&self.artifact.canonical_ir)
367 }
368}