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}
166
167#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
168pub struct ModuleOperationBinding {
169 pub host_operation: String,
170}
171
172#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
173pub struct ValueConstructorBinding {
174 pub path: Vec<String>,
175 pub type_name: String,
176 pub input_ty: TypeExpr,
177 pub output_ty: TypeExpr,
178}
179
180#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
181pub struct TriggerSourceBinding {
182 event_type: NamedDataType,
183}
184
185impl TriggerSourceBinding {
186 fn new(event_type: NamedDataType) -> Self {
187 Self { event_type }
188 }
189
190 pub fn event_type(&self) -> &NamedDataType {
191 &self.event_type
192 }
193
194 pub fn event_ty(&self) -> &TypeExpr {
195 self.event_type.ty()
196 }
197
198 pub fn event_type_name(&self) -> &str {
199 self.event_type.name()
200 }
201}
202
203#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
204pub struct LashlangHostEnvironment {
205 #[serde(default)]
206 pub resources: LashlangHostCatalog,
207 #[serde(default)]
208 pub abilities: LashlangAbilities,
209 #[serde(default)]
210 pub language_features: LashlangLanguageFeatures,
211}
212
213impl LashlangHostEnvironment {
214 pub fn new(resources: LashlangHostCatalog, abilities: LashlangAbilities) -> Self {
215 Self {
216 resources,
217 abilities,
218 language_features: LashlangLanguageFeatures::default(),
219 }
220 }
221
222 pub fn with_language_features(mut self, language_features: LashlangLanguageFeatures) -> Self {
223 self.language_features = language_features;
224 self
225 }
226
227 pub fn satisfies(&self, requirements: &HostRequirements) -> bool {
228 self.abilities.satisfies(requirements.abilities)
229 && self
230 .language_features
231 .satisfies(requirements.language_features)
232 && self.resources.satisfies(&requirements.resources)
233 }
234}
235
236#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
237#[serde(default)]
238pub struct LashlangLanguageFeatures {
239 pub label_annotations: bool,
240}
241
242impl LashlangLanguageFeatures {
243 pub fn union(self, other: Self) -> Self {
244 Self {
245 label_annotations: self.label_annotations || other.label_annotations,
246 }
247 }
248
249 pub fn satisfies(self, required: Self) -> bool {
250 !required.label_annotations || self.label_annotations
251 }
252
253 pub fn with_label_annotations(mut self) -> Self {
254 self.label_annotations = true;
255 self
256 }
257}
258
259#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(default)]
261pub struct LashlangAbilities {
262 pub processes: bool,
263 pub sleep: bool,
264 pub process_signals: bool,
265 pub triggers: bool,
266}
267
268impl LashlangAbilities {
269 pub fn union(self, other: Self) -> Self {
270 Self {
271 processes: self.processes || other.processes,
272 sleep: self.sleep || other.sleep,
273 process_signals: self.process_signals || other.process_signals,
274 triggers: self.triggers || other.triggers,
275 }
276 }
277
278 pub fn satisfies(self, required: Self) -> bool {
279 (!required.processes || self.processes)
280 && (!required.sleep || self.sleep)
281 && (!required.process_signals || self.process_signals)
282 && (!required.triggers || self.triggers)
283 }
284
285 pub fn with_processes(mut self) -> Self {
286 self.processes = true;
287 self
288 }
289
290 pub fn with_sleep(mut self) -> Self {
291 self.sleep = true;
292 self
293 }
294
295 pub fn with_process_signals(mut self) -> Self {
296 self.process_signals = true;
297 self
298 }
299
300 pub fn with_triggers(mut self) -> Self {
301 self.triggers = true;
302 self
303 }
304
305 pub fn all() -> Self {
306 Self::default()
307 .with_sleep()
308 .with_processes()
309 .with_process_signals()
310 .with_triggers()
311 }
312}
313
314fn module_path_key(path: &[impl AsRef<str>]) -> String {
315 path.iter()
316 .map(|segment| segment.as_ref())
317 .collect::<Vec<_>>()
318 .join(".")
319}
320
321#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
322pub struct LinkedModule {
323 pub module_ref: crate::ModuleRef,
324 pub host_requirements_ref: crate::HostRequirementsRef,
325 pub artifact: ModuleArtifact,
326 #[serde(skip)]
327 linked_program: Option<Program>,
328}
329
330impl LinkedModule {
331 pub fn link(
332 program: Program,
333 surface: impl Borrow<LashlangHostEnvironment>,
334 ) -> Result<Self, LinkError> {
335 let surface = surface.borrow();
336 let mut linker = Linker::new(&program, surface);
337 let program = linker.link_program()?;
338 let requirements = host_requirements_for_program_with_catalog(&program, &surface.resources);
339 let artifact =
340 ModuleArtifact::from_program_with_requirements(program.clone(), requirements).map_err(
341 |err| LinkError::ModuleHash {
342 message: err.to_string(),
343 },
344 )?;
345 Ok(Self {
346 module_ref: artifact.module_ref.clone(),
347 host_requirements_ref: artifact.host_requirements_ref.clone(),
348 artifact,
349 linked_program: Some(program),
350 })
351 }
352
353 pub fn program(&self) -> &Program {
354 self.linked_program
355 .as_ref()
356 .unwrap_or(&self.artifact.canonical_ir)
357 }
358}