Skip to main content

relon_eval_api/
schema_lower.rs

1//! `relon_analyzer::SchemaDef` -> [`crate::schema_canonical::Schema`]
2//! conversion.
3//!
4//! The wasm AOT backend needs a deterministic, ABI-shaped view of a
5//! schema so it can compute the same `relon.abi` hash both at codegen
6//! time (host side) and at validation time (wasm-blob loader). The
7//! analyzer's [`SchemaDef`] is the static skeleton the rest of the
8//! pipeline reasons about; this module strips it down to the field
9//! `(name, type)` pairs the binary layout cares about.
10//!
11//! Phase 2.b scope: only `Int` / `Float` / `Bool` field types
12//! are supported by [`lower_schema_def`]; the function is used to
13//! lower the synthesised `MainParams` schema. Pointer-indirect leaves
14//! (`String`, `List<Int>`) and nested branded schemas are constructed
15//! directly inside the IR lowering pass (it has access to the
16//! analyzer's schema table for cross-schema resolution).
17
18use crate::schema_canonical::{Field, Schema, TypeRepr};
19use relon_analyzer::schema::SchemaDef;
20use relon_parser::TypeNode;
21use thiserror::Error;
22
23/// Reasons schema lowering can fail.
24#[derive(Debug, Error, Clone, PartialEq, Eq)]
25pub enum SchemaLowerError {
26    /// A field used a type Phase 2.b's layout pass does not yet
27    /// model. The string in `ty` is the type head as written in
28    /// source (e.g. `"String"`, `"List<Int>"`), so the user sees
29    /// the exact annotation that triggered the gap.
30    #[error("field `{field}` has unsupported type `{ty}` (Phase 2.b layout supports Int / Float / Bool only)")]
31    UnsupportedFieldType {
32        /// Field name that triggered the error.
33        field: String,
34        /// Human-readable rendering of the offending type.
35        ty: String,
36    },
37    /// A field declared no static type. The analyzer surfaces this as
38    /// `SchemaFieldUntyped` in its own diagnostics; we propagate the
39    /// shape so codegen can refuse to emit a layout with unknown
40    /// slot widths.
41    #[error("field `{field}` has no declared type")]
42    UntypedField {
43        /// Field name that triggered the error.
44        field: String,
45    },
46}
47
48/// Lower a [`SchemaDef`] to its canonical [`Schema`] form.
49///
50/// The output preserves declaration order (canonical schemas hash
51/// order-sensitively) and uses the supplied `name` as the canonical
52/// schema name when the analyzer-side `SchemaDef::name` is `None`
53/// (anonymous `#schema` annotations on data).
54pub fn lower_schema_def(def: &SchemaDef, fallback_name: &str) -> Result<Schema, SchemaLowerError> {
55    let name = def
56        .name
57        .clone()
58        .unwrap_or_else(|| fallback_name.to_string());
59    if let Some(elements) = &def.tuple_elements {
60        let mut tys = Vec::with_capacity(elements.len());
61        for (idx, ty_node) in elements.iter().enumerate() {
62            tys.push(lower_type_node(&idx.to_string(), ty_node)?);
63        }
64        let mut schema = Schema::tuple(name, tys);
65        schema.generics = def.generics.clone();
66        return Ok(schema);
67    }
68    let mut fields = Vec::with_capacity(def.fields.len());
69    for f in &def.fields {
70        let ty_node = f
71            .type_hint
72            .as_ref()
73            .ok_or_else(|| SchemaLowerError::UntypedField {
74                field: f.name.clone(),
75            })?;
76        let ty = lower_type_node(&f.name, ty_node)?;
77        fields.push(Field {
78            name: f.name.clone(),
79            ty,
80            // Phase 2.b ignores compile-time defaults — the layout
81            // pass only needs the slot shape. Defaults re-enter the
82            // canonical form when the codegen pipeline starts
83            // populating them in a later phase.
84            default: None,
85        });
86    }
87    Ok(Schema {
88        name,
89        generics: def.generics.clone(),
90        fields,
91        is_tuple: false,
92    })
93}
94
95/// Lower a single [`TypeNode`] to a [`TypeRepr`]. Rejects every
96/// composite / variable-size type — see [`SchemaLowerError`].
97pub fn lower_type_node(field_name: &str, ty: &TypeNode) -> Result<TypeRepr, SchemaLowerError> {
98    let unsupported = || SchemaLowerError::UnsupportedFieldType {
99        field: field_name.to_string(),
100        ty: format_type_head(ty),
101    };
102    if ty.path.len() != 1 || !ty.generics.is_empty() || ty.variant_fields.is_some() {
103        return Err(unsupported());
104    }
105    match ty.path[0].as_str() {
106        "Int" => Ok(TypeRepr::Int),
107        "Float" => Ok(TypeRepr::Float),
108        "Bool" => Ok(TypeRepr::Bool),
109        _ => Err(unsupported()),
110    }
111}
112
113/// Format a `TypeNode` head + generics for the error message. Local
114/// to this module so we don't drag the analyzer's full type
115/// formatter through the dependency graph.
116fn format_type_head(t: &TypeNode) -> String {
117    if t.path.is_empty() {
118        return "<empty>".to_string();
119    }
120    let mut s = t.path.join(".");
121    if !t.generics.is_empty() {
122        s.push('<');
123        for (i, g) in t.generics.iter().enumerate() {
124            if i > 0 {
125                s.push_str(", ");
126            }
127            s.push_str(&format_type_head(g));
128        }
129        s.push('>');
130    }
131    s
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use relon_analyzer::schema::SchemaFieldDef;
138    use relon_parser::{Expr, Node, NodeId, TokenRange, TypeNode};
139    use std::sync::Arc;
140
141    fn dummy_range() -> TokenRange {
142        TokenRange::default()
143    }
144
145    fn dummy_node() -> Arc<Node> {
146        Arc::new(Node {
147            id: NodeId::SYNTHETIC,
148            expr: Arc::new(Expr::Int(0)),
149            decorators: vec![],
150            directives: vec![],
151            type_hint: None,
152            range: dummy_range(),
153            doc_comment: None,
154        })
155    }
156
157    fn type_node(name: &str) -> TypeNode {
158        TypeNode {
159            path: vec![name.to_string()],
160            generics: vec![],
161            is_optional: false,
162            range: dummy_range(),
163            variant_fields: None,
164            doc_comment: None,
165        }
166    }
167
168    fn field(name: &str, ty: TypeNode) -> SchemaFieldDef {
169        SchemaFieldDef {
170            name: name.to_string(),
171            type_hint: Some(ty),
172            value_range: dummy_range(),
173            is_wildcard: true,
174            value_node: dummy_node(),
175            meta_decorators: vec![],
176            doc_comment: None,
177        }
178    }
179
180    fn schema_def(name: &str, fields: Vec<SchemaFieldDef>) -> SchemaDef {
181        SchemaDef {
182            name: Some(name.to_string()),
183            generics: vec![],
184            fields,
185            tuple_elements: None,
186            bases: vec![],
187            range: dummy_range(),
188            variants: vec![],
189            methods: vec![],
190            schema_no_auto_derives: vec![],
191            doc_comment: None,
192        }
193    }
194
195    #[test]
196    fn lowers_int_float_bool() {
197        let def = schema_def(
198            "Mix",
199            vec![
200                field("a", type_node("Int")),
201                field("b", type_node("Float")),
202                field("c", type_node("Bool")),
203            ],
204        );
205        let s = lower_schema_def(&def, "fallback").expect("lower");
206        assert_eq!(s.name, "Mix");
207        assert_eq!(s.fields.len(), 3);
208        assert_eq!(s.fields[0].ty, TypeRepr::Int);
209        assert_eq!(s.fields[1].ty, TypeRepr::Float);
210        assert_eq!(s.fields[2].ty, TypeRepr::Bool);
211    }
212
213    #[test]
214    fn rejects_unknown_field_type() {
215        let def = schema_def("S", vec![field("custom", type_node("Bytes"))]);
216        let err = lower_schema_def(&def, "fallback").expect_err("must reject");
217        assert!(matches!(
218            err,
219            SchemaLowerError::UnsupportedFieldType { ref field, ref ty }
220            if field == "custom" && ty == "Bytes"
221        ));
222    }
223
224    #[test]
225    fn rejects_string_field() {
226        let def = schema_def("S", vec![field("name", type_node("String"))]);
227        let err = lower_schema_def(&def, "fallback").expect_err("must reject");
228        assert!(matches!(
229            err,
230            SchemaLowerError::UnsupportedFieldType { ref field, ref ty }
231            if field == "name" && ty == "String"
232        ));
233    }
234
235    #[test]
236    fn rejects_untyped_field() {
237        let mut def = schema_def("S", vec![field("x", type_node("Int"))]);
238        def.fields[0].type_hint = None;
239        let err = lower_schema_def(&def, "fallback").expect_err("must reject");
240        assert!(matches!(
241            err,
242            SchemaLowerError::UntypedField { ref field } if field == "x"
243        ));
244    }
245
246    #[test]
247    fn anonymous_schema_uses_fallback_name() {
248        let mut def = schema_def("ignored", vec![field("v", type_node("Int"))]);
249        def.name = None;
250        let s = lower_schema_def(&def, "MainParams").expect("lower");
251        assert_eq!(s.name, "MainParams");
252    }
253}