diff --git a/docs/architecture/bast-format.md b/docs/architecture/bast-format.md index 16928a8..303ffe1 100644 --- a/docs/architecture/bast-format.md +++ b/docs/architecture/bast-format.md @@ -274,7 +274,7 @@ These are different validators for different inputs. ### TypeRef -`TypeRef` is the central mechanism for referencing types. Four forms: +`TypeRef` is the central mechanism for referencing types. Seven forms: | Form | Example | Meaning | |------|---------|---------| @@ -282,6 +282,9 @@ These are different validators for different inputs. | `$ref` object | `{ "$ref": "#/$defs/Read" }` | Reference to a named `$defs` entry | | Array object | `{ "kind": "array", "element": "uint32", "count": 3 }` | Fixed-size array | | Record object | `{ "kind": "record", "values": "string" }` | String-keyed map | +| Inline struct | `{ "kind": "struct", "fields": [...] }` | Anonymous struct | +| Inline union | `{ "kind": "union", ... }` | Anonymous union | +| Inline enum | `{ "kind": "enum", "values": [...] }` | Anonymous enum | The `$ref` form uses standard JSON Pointer syntax **restricted to `#/$defs/`** — no external references, no fragment-only pointers, @@ -296,6 +299,14 @@ entries. Complex element types use nested `$ref`: { "kind": "array", "element": { "$ref": "#/$defs/ComplexElement" }, "count": 4 } ``` +Inline `struct`/`union`/`enum` TypeRefs are anonymous composites — a +field, array element, record value, or union variant whose type is +declared inline rather than named in `$defs`. They are structurally +identical to their named counterparts (same `StructDef`/`UnionDef`/ +`EnumDef` shape); only the reference mechanism differs. Named composites +are preferred for reuse and for `$ref`-based dispatch; inline composites +are convenient for one-off nested types. + ### Primitives The 14 primitive `kind` strings map to the unchanged `AlkTypeKind` diff --git a/src/bast.rs b/src/bast.rs index 64fbad0..fdfbfa8 100644 --- a/src/bast.rs +++ b/src/bast.rs @@ -681,12 +681,12 @@ impl<'a> BastEnum<'a> { /// A type reference — the central mechanism for typing fields, array /// elements, record values, and union variants. /// -/// Four forms, matching the +/// Seven forms, matching the /// [TypeRef](../docs/architecture/bast-format.md#typeref) `oneOf`: -/// primitive string, `$ref` object, array object, record object. -/// Composite kinds (`struct`/`union`/`enum`) only appear inline here when -/// a field/element/value is an anonymous composite; named composites go -/// through [`BastType::Ref`]. +/// primitive string, `$ref` object, array object, record object, and +/// inline `struct`/`union`/`enum`. Composite kinds (`struct`/`union`/ +/// `enum`) appear inline here when a field/element/value is an anonymous +/// composite; named composites go through [`BastType::Ref`]. #[derive(Debug, Clone)] pub enum BastType<'a> { /// A built-in primitive (`"uint32"`, `"string"`, etc.). diff --git a/src/bast_meta.rs b/src/bast_meta.rs index a4b4823..7f06023 100644 --- a/src/bast_meta.rs +++ b/src/bast_meta.rs @@ -124,6 +124,18 @@ pub static BAST_META_SCHEMA: LazyLock = LazyLock::new(|| { }, "required": ["kind", "values"], "additionalProperties": false + }, + { + "description": "Inline struct type", + "$ref": "#/$defs/StructDef" + }, + { + "description": "Inline union type", + "$ref": "#/$defs/UnionDef" + }, + { + "description": "Inline enum type", + "$ref": "#/$defs/EnumDef" } ] }, @@ -184,6 +196,26 @@ pub static BAST_META_SCHEMA: LazyLock = LazyLock::new(|| { }) }); +/// Validate a BAST document against the meta-schema. +/// +/// Returns `Ok(())` if the document is well-formed, or +/// [`crate::error::AlkTypeError::Schema`] with the first validation error +/// otherwise. +/// This is the structural gate [`crate::AlkTypeEngine::compile`] applies +/// before parsing — it catches malformed annotations (unknown `endian`/ +/// `encoding` strings, non-integer `align`/`maxLength`, missing required +/// properties) that the parser would otherwise silently tolerate. +pub fn validate_bast_doc(doc: &Value) -> Result<(), crate::error::AlkTypeError> { + let validator = jsonschema::options() + .build(&BAST_META_SCHEMA) + .map_err(|e| { + crate::error::AlkTypeError::Schema(format!("BAST meta-schema failed to build: {e}")) + })?; + validator.validate(doc).map_err(|e| { + crate::error::AlkTypeError::Schema(format!("BAST document is not well-formed: {e}")) + }) +} + #[cfg(test)] mod tests { use super::BAST_META_SCHEMA; @@ -446,4 +478,44 @@ mod tests { }); assert!(validator.validate(&doc).is_err(), "bare-name ref accepted"); } + + #[test] + fn meta_schema_accepts_inline_struct_field() { + let validator = jsonschema::options() + .build(&BAST_META_SCHEMA) + .expect("meta-schema compiles"); + let doc = serde_json::json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "inner", "kind": { + "kind": "struct", + "fields": [ { "name": "x", "kind": "uint16" } ] + } } + ] + } + } + }); + assert!(validator.validate(&doc).is_ok(), "inline struct field rejected"); + } + + #[test] + fn meta_schema_accepts_inline_struct_in_union_mapping() { + let validator = jsonschema::options() + .build(&BAST_META_SCHEMA) + .expect("meta-schema compiles"); + let doc = serde_json::json!({ + "$defs": { + "U": { + "kind": "union", + "discriminator": { "kind": "byte", "offset": 0, "type": "uint8" }, + "mapping": { + "5": { "kind": "struct", "fields": [ { "name": "x", "kind": "uint8" } ] } + } + } + } + }); + assert!(validator.validate(&doc).is_ok(), "inline struct in union mapping rejected"); + } } \ No newline at end of file diff --git a/src/engine.rs b/src/engine.rs index 6959132..40fe84b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -126,6 +126,7 @@ impl AlkTypeEngine { mode: LayoutMode, json_schema: Option<&Value>, ) -> Result { + crate::bast_meta::validate_bast_doc(bast_doc)?; let doc = BastDoc::new(bast_doc, root_name)?; let root_def = doc.root_def(); let struct_node = match root_def.kind() { @@ -913,6 +914,71 @@ mod tests { assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); } + #[test] + fn compile_rejects_unknown_endian_annotation() { + let doc = json!({ + "$defs": { + "S": { + "kind": "struct", + "endian": "middle", + "fields": [ { "name": "id", "kind": "uint32" } ] + } + } + }); + let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err(); + assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); + } + + #[test] + fn compile_rejects_unknown_encoding_annotation() { + let doc = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "blob", "kind": "bytes", "encoding": "sideways" } + ] + } + } + }); + let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err(); + assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); + } + + #[test] + fn compile_rejects_non_integer_align() { + let doc = json!({ + "$defs": { + "S": { + "kind": "struct", + "align": "wide", + "fields": [ { "name": "id", "kind": "uint32" } ] + } + } + }); + let err = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).unwrap_err(); + assert!(matches!(err, AlkTypeError::Schema(_)), "got {err:?}"); + } + + #[test] + fn compile_accepts_inline_struct_field() { + let doc = json!({ + "$defs": { + "S": { + "kind": "struct", + "fields": [ + { "name": "inner", "kind": { + "kind": "struct", + "fields": [ { "name": "x", "kind": "uint16" } ] + } } + ] + } + } + }); + let engine = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("compile"); + assert_eq!(engine.mode(), LayoutMode::Packed); + } + #[test] fn debug_formats_without_panicking() { let doc = fixed_struct_doc();