Skip to main content

lashlang/linker/
host.rs

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}