Skip to main content

powerio_dist/glm/
objects.rs

1use std::collections::BTreeMap;
2
3use powerio_core::{Error, SourceSpan};
4
5use super::{GlmProject, Statement, Token, TokenKind};
6use crate::diagnostics::codes;
7
8const MAX_OBJECTS: usize = 1 << 20;
9const MAX_PROPERTIES: usize = 1 << 20;
10
11/// Index-local object identity. It is neither a native runtime ID nor the
12/// explicit numeric key written after a class name in a GLM declaration.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
14pub struct ObjectKey(usize);
15
16impl ObjectKey {
17    #[must_use]
18    pub const fn ordinal(self) -> usize {
19        self.0
20    }
21}
22
23/// A property retains ordered source tokens or an explicit inline object link.
24#[derive(Clone, Debug)]
25#[non_exhaustive]
26pub enum GlmPropertyValue<'a> {
27    Tokens(&'a [Token]),
28    InlineObject(ObjectKey),
29}
30
31#[derive(Clone, Debug)]
32pub struct GlmProperty<'a> {
33    pub name: &'a str,
34    pub value: GlmPropertyValue<'a>,
35    pub span: &'a SourceSpan,
36}
37
38/// One explicit declaration. Unknown classes/properties remain visible and are
39/// not thereby certified as electrically supported. Properties are not cloned.
40#[derive(Debug)]
41pub struct GlmObject<'a> {
42    key: ObjectKey,
43    class_name: &'a str,
44    explicit_id: Option<u32>,
45    name: Option<String>,
46    span: &'a SourceSpan,
47    nested_parent: Option<ObjectKey>,
48    properties: Vec<GlmProperty<'a>>,
49    other_statements: Vec<&'a Statement>,
50}
51
52impl<'a> GlmObject<'a> {
53    #[must_use]
54    pub const fn key(&self) -> ObjectKey {
55        self.key
56    }
57    #[must_use]
58    pub const fn class_name(&self) -> &'a str {
59        self.class_name
60    }
61    #[must_use]
62    pub const fn explicit_id(&self) -> Option<u32> {
63        self.explicit_id
64    }
65    #[must_use]
66    pub fn name(&self) -> Option<&str> {
67        self.name.as_deref()
68    }
69    #[must_use]
70    pub const fn span(&self) -> &'a SourceSpan {
71        self.span
72    }
73    #[must_use]
74    pub fn properties(&self) -> &[GlmProperty<'a>] {
75        &self.properties
76    }
77    /// Uninterpreted body syntax, never silently discarded by the index.
78    #[must_use]
79    pub fn other_statements(&self) -> &[&'a Statement] {
80        &self.other_statements
81    }
82}
83
84/// Borrowed explicit-object index. Building it resolves parents, rejects cycles
85/// and ambiguous identities, and distinguishes nested from inline objects.
86/// Equipment references are resolved on demand; no class schema or defaults
87/// are inferred here. Dynamic objects, namespace blocks and relative/wildcard
88/// references require further interpretation and are not silently approximated.
89#[derive(Debug)]
90pub struct ObjectIndex<'a> {
91    project: &'a GlmProject,
92    objects: Vec<GlmObject<'a>>,
93    names: BTreeMap<String, ObjectKey>,
94    ids: BTreeMap<u32, ObjectKey>,
95    parents: Vec<Option<ObjectKey>>,
96    property_count: usize,
97}
98
99impl GlmProject {
100    pub fn index_objects(&self) -> Result<ObjectIndex<'_>, Error> {
101        let mut index = ObjectIndex {
102            project: self,
103            objects: Vec::new(),
104            names: BTreeMap::new(),
105            ids: BTreeMap::new(),
106            parents: Vec::new(),
107            property_count: 0,
108        };
109        for statement in self.statements() {
110            if let Statement::Block { header, body, span } = statement {
111                match header.first().map(Token::text) {
112                    Some("object") => {
113                        index.declaration(header, body, span, None)?;
114                    }
115                    Some("namespace") => {
116                        return Err(self.fail(
117                            span,
118                            "GLM namespaces require explicit qualification before object indexing",
119                        ));
120                    }
121                    _ => {}
122                }
123            }
124        }
125        index.resolve_parents()?;
126        Ok(index)
127    }
128}
129
130impl<'a> ObjectIndex<'a> {
131    pub(super) fn fail(&self, span: &SourceSpan, message: impl Into<String>) -> Error {
132        self.project.fail(span, message)
133    }
134
135    #[must_use]
136    pub fn objects(&self) -> &[GlmObject<'a>] {
137        &self.objects
138    }
139
140    #[must_use]
141    pub fn get(&self, key: ObjectKey) -> Option<&GlmObject<'a>> {
142        self.objects.get(key.0)
143    }
144
145    #[must_use]
146    pub fn parent(&self, key: ObjectKey) -> Option<ObjectKey> {
147        self.parents.get(key.0).copied().flatten()
148    }
149
150    /// Resolve a name, `class:explicit_id`, or `id:explicit_id`. `root` is the
151    /// native null reference. Anonymous objects do not acquire guessed aliases
152    /// from their position in this index.
153    pub fn resolve_reference(&self, reference: &str) -> Result<Option<ObjectKey>, Error> {
154        if reference == "root" {
155            return Ok(None);
156        }
157        let found = if let Some((class, numeric)) = reference.split_once(':') {
158            let id = numeric.parse::<u32>().map_err(|_| {
159                self.reference_error(reference, "unsupported symbolic or relative reference")
160            })?;
161            let key = self.ids.get(&id).copied().ok_or_else(|| {
162                self.reference_error(reference, "explicit object ID does not exist")
163            })?;
164            let actual = self.objects[key.0]
165                .class_name
166                .rsplit('.')
167                .next()
168                .unwrap_or("");
169            if class != "id" && class != actual {
170                return Err(
171                    self.reference_error(reference, "explicit object ID has a different class")
172                );
173            }
174            Some(key)
175        } else {
176            self.names.get(reference).copied()
177        };
178        found
179            .map(Some)
180            .ok_or_else(|| self.reference_error(reference, "object name does not exist"))
181    }
182
183    /// Resolve an object-valued property. Repeated reference assignments are
184    /// refused until their native deferred-assignment ordering is qualified.
185    pub fn resolve_property(&self, key: ObjectKey, name: &str) -> Result<Option<ObjectKey>, Error> {
186        let object = self
187            .objects
188            .get(key.0)
189            .ok_or_else(|| self.reference_error(name, "owner key does not belong to this index"))?;
190        let mut properties = object.properties.iter().filter(|p| p.name == name);
191        let Some(property) = properties.next() else {
192            return Ok(None);
193        };
194        if properties.next().is_some() {
195            return Err(self.project.fail(property.span, format!("repeated object reference property {name} requires deferred-assignment ordering")));
196        }
197        match &property.value {
198            GlmPropertyValue::InlineObject(key) => Ok(Some(*key)),
199            GlmPropertyValue::Tokens(tokens) => {
200                let reference =
201                    literal(tokens).map_err(|message| self.project.fail(property.span, message))?;
202                self.resolve_reference(&reference)
203                    .map_err(|error| self.project.fail(property.span, error.to_string()))
204            }
205        }
206    }
207
208    fn reference_error(&self, reference: &str, message: &str) -> Error {
209        Error::new(&codes::PARSE_GLM_OBJECT, format!("{message}: {reference}"))
210            .with_source(self.project.source().clone())
211    }
212
213    fn declaration(
214        &mut self,
215        header: &'a [Token],
216        body: &'a [Statement],
217        span: &'a SourceSpan,
218        parent: Option<ObjectKey>,
219    ) -> Result<Option<ObjectKey>, Error> {
220        if header.len() != 2 || header[0].text() != "object" || header[1].kind() != TokenKind::Word
221        {
222            return Err(self
223                .project
224                .fail(span, "expected object class or object class:numeric_id"));
225        }
226        let (class, first, count) =
227            declaration_ids(header[1].text()).map_err(|m| self.project.fail(span, m))?;
228        if count > MAX_OBJECTS - self.objects.len() {
229            return Err(self
230                .project
231                .fail(span, "expanded GLM object count exceeds its limit"));
232        }
233        let mut last = None;
234        for ordinal in 0..count {
235            if self.objects.len() == MAX_OBJECTS {
236                return Err(self
237                    .project
238                    .fail(span, "expanded GLM object count exceeds its limit"));
239            }
240            let id = first.map(|start| start + ordinal as u32);
241            let key = ObjectKey(self.objects.len());
242            if let Some(id) = id
243                && self.ids.insert(id, key).is_some()
244            {
245                return Err(self.project.fail(
246                    span,
247                    "duplicate GLM explicit ID; numeric keys are global across classes",
248                ));
249            }
250            self.objects.push(GlmObject {
251                key,
252                class_name: class,
253                explicit_id: id,
254                name: None,
255                span,
256                nested_parent: parent,
257                properties: Vec::new(),
258                other_statements: Vec::new(),
259            });
260            last = Some(key);
261            self.body(key, body)?;
262        }
263        Ok(last)
264    }
265
266    fn body(&mut self, owner: ObjectKey, body: &'a [Statement]) -> Result<(), Error> {
267        for statement in body {
268            match statement {
269                Statement::Simple { tokens, span }
270                    if tokens.len() < 2
271                        && tokens
272                            .first()
273                            .is_some_and(|t| matches!(t.text(), "name" | "parent" | "root")) =>
274                {
275                    return Err(self
276                        .project
277                        .fail(span, "GLM identity or parent property is missing its value"));
278                }
279                Statement::Simple { tokens, span } if tokens.len() >= 2 => {
280                    let name = tokens[0].text();
281                    if name == "name" {
282                        let value =
283                            literal(&tokens[1..]).map_err(|m| self.project.fail(span, m))?;
284                        if self.objects[owner.0].name.is_some() || self.names.contains_key(&value) {
285                            return Err(self
286                                .project
287                                .fail(span, "duplicate or reassigned GLM object name"));
288                        }
289                        self.names.insert(value.clone(), owner);
290                        self.objects[owner.0].name = Some(value);
291                    }
292                    self.property(
293                        owner,
294                        GlmProperty {
295                            name,
296                            value: GlmPropertyValue::Tokens(&tokens[1..]),
297                            span,
298                        },
299                    )?;
300                }
301                Statement::Block { header, body, span }
302                    if header.first().is_some_and(|t| t.text() == "object") =>
303                {
304                    self.declaration(header, body, span, Some(owner))?;
305                }
306                Statement::Block { header, body, span }
307                    if header.get(1).is_some_and(|t| t.text() == "object") =>
308                {
309                    let target = self
310                        .declaration(&header[1..], body, span, None)?
311                        .ok_or_else(|| self.project.fail(span, "inline object count is zero"))?;
312                    self.property(
313                        owner,
314                        GlmProperty {
315                            name: header[0].text(),
316                            value: GlmPropertyValue::InlineObject(target),
317                            span,
318                        },
319                    )?;
320                }
321                _ => self.objects[owner.0].other_statements.push(statement),
322            }
323        }
324        Ok(())
325    }
326
327    fn property(&mut self, owner: ObjectKey, property: GlmProperty<'a>) -> Result<(), Error> {
328        if self.property_count == MAX_PROPERTIES {
329            return Err(self.project.fail(
330                property.span,
331                "expanded GLM property count exceeds its limit",
332            ));
333        }
334        self.property_count += 1;
335        self.objects[owner.0].properties.push(property);
336        Ok(())
337    }
338
339    fn resolve_parents(&mut self) -> Result<(), Error> {
340        for object in &self.objects {
341            let explicit = object.properties.iter().any(|p| p.name == "parent");
342            let root = object.properties.iter().any(|p| p.name == "root");
343            if explicit && root {
344                return Err(self.project.fail(
345                    object.span,
346                    "combined root/parent assignments require deferred-assignment ordering",
347                ));
348            }
349            let parent = if explicit {
350                self.resolve_property(object.key, "parent")?
351            } else if root {
352                None
353            } else {
354                object.nested_parent
355            };
356            self.parents.push(parent);
357        }
358        let mut state = vec![0u8; self.objects.len()];
359        for start in 0..state.len() {
360            if state[start] != 0 {
361                continue;
362            }
363            let mut next = Some(ObjectKey(start));
364            while let Some(key) = next {
365                match state[key.0] {
366                    2 => break,
367                    1 => {
368                        return Err(self.project.fail(
369                            self.objects[key.0].span,
370                            "GLM parent references form a cycle",
371                        ));
372                    }
373                    _ => {
374                        state[key.0] = 1;
375                        next = self.parents[key.0];
376                    }
377                }
378            }
379            next = Some(ObjectKey(start));
380            while let Some(key) = next {
381                if state[key.0] == 2 {
382                    break;
383                }
384                state[key.0] = 2;
385                next = self.parents[key.0];
386            }
387        }
388        Ok(())
389    }
390}
391
392fn declaration_ids(text: &str) -> Result<(&str, Option<u32>, usize), &'static str> {
393    let (class, ids) = text
394        .split_once(':')
395        .map_or((text, None), |(c, ids)| (c, Some(ids)));
396    if class.is_empty()
397        || !class
398            .bytes()
399            .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.'))
400    {
401        return Err("invalid GLM class spelling");
402    }
403    let Some(ids) = ids else {
404        return Ok((class, None, 1));
405    };
406    if let Some(count) = ids.strip_prefix("..") {
407        let count = count
408            .parse::<usize>()
409            .map_err(|_| "invalid GLM object count")?;
410        return Ok((class, None, count));
411    }
412    if let Some((first, last)) = ids.split_once("..") {
413        let first = first
414            .parse::<u32>()
415            .map_err(|_| "invalid first GLM object ID")?;
416        let last = last
417            .parse::<u32>()
418            .map_err(|_| "invalid last GLM object ID")?;
419        let count = u64::from(last)
420            .checked_sub(u64::from(first))
421            .and_then(|n| usize::try_from(n + 1).ok())
422            .ok_or("invalid or oversized GLM object range")?;
423        return Ok((class, Some(first), count));
424    }
425    Ok((
426        class,
427        Some(ids.parse().map_err(|_| "invalid GLM object ID")?),
428        1,
429    ))
430}
431
432fn literal(tokens: &[Token]) -> Result<String, &'static str> {
433    let [token] = tokens else {
434        return Err("object names/references require one literal value");
435    };
436    let value = token.text();
437    let value = if token.kind() == TokenKind::Quoted {
438        value
439            .strip_prefix('"')
440            .and_then(|v| v.strip_suffix('"'))
441            .ok_or("object string literals require double quotes")?
442    } else {
443        value
444    };
445    if value.is_empty() || value.contains(['\\', '$', '\n', '\r']) {
446        return Err("object names/references require a nonempty resolved literal without escapes");
447    }
448    Ok(value.to_owned())
449}