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}
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}