From 3f3bed62d3d9ad851981bcbb6b0bcb71113c22c3 Mon Sep 17 00:00:00 2001 From: Asger F Date: Fri, 29 May 2026 17:23:02 +0200 Subject: [PATCH] yeast: type-check for missing required fields Add FieldCardinality to Schema to track required/multiple per field, populated from the ast_types.yml suffixes (bare = required single, ? = optional single, + = required multiple, * = optional multiple). dump_ast_with_type_errors now emits: <-- ERROR: missing required field 'name' for any node in the output AST whose declared schema requires a field that is absent from the actual node. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- shared/yeast/src/dump.rs | 10 ++++++ shared/yeast/src/node_types_yaml.rs | 8 +++++ shared/yeast/src/schema.rs | 47 +++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/shared/yeast/src/dump.rs b/shared/yeast/src/dump.rs index 07ee134e058..d046c192053 100644 --- a/shared/yeast/src/dump.rs +++ b/shared/yeast/src/dump.rs @@ -273,6 +273,16 @@ fn dump_node( } } + // Check for required fields that are absent + if let Some((schema, _, _)) = type_check { + for (field_id, field_name) in schema.required_fields_for_kind(node.kind_name()) { + if !node.fields.contains_key(&field_id) { + let name = field_name.unwrap_or("child"); + writeln!(out, "{prefix} <-- ERROR: missing required field '{name}'").unwrap(); + } + } + } + // Unnamed children — skip unnamed tokens (keywords, punctuation) if let Some(children) = node.fields.get(&CHILD_FIELD) { let child_type_check = type_check.map(|(schema, _, _)| { diff --git a/shared/yeast/src/node_types_yaml.rs b/shared/yeast/src/node_types_yaml.rs index eb191076be4..797f14cba72 100644 --- a/shared/yeast/src/node_types_yaml.rs +++ b/shared/yeast/src/node_types_yaml.rs @@ -314,6 +314,14 @@ fn apply_yaml_to_schema( node_types.sort_by(|a, b| a.kind.cmp(&b.kind).then(a.named.cmp(&b.named))); node_types.dedup_by(|a, b| a.kind == b.kind && a.named == b.named); schema.set_field_types(parent_kind, field_id, node_types); + schema.set_field_cardinality( + parent_kind, + field_id, + crate::schema::FieldCardinality { + multiple: spec.multiple, + required: spec.required, + }, + ); } } } diff --git a/shared/yeast/src/schema.rs b/shared/yeast/src/schema.rs index c832a57b23a..bbd425f15a2 100644 --- a/shared/yeast/src/schema.rs +++ b/shared/yeast/src/schema.rs @@ -8,6 +8,15 @@ pub struct NodeType { pub named: bool, } +/// Multiplicity/optionality of a field declaration. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FieldCardinality { + /// Whether the field may hold more than one child. + pub multiple: bool, + /// Whether at least one child must be present. + pub required: bool, +} + /// A schema defining node kinds and field names for the output AST. /// Built from a node-types.yml file, independent of any tree-sitter grammar. /// @@ -32,6 +41,7 @@ pub struct Schema { kind_names: BTreeMap, next_kind_id: KindId, field_types: BTreeMap<(String, FieldId), Vec>, + field_cardinalities: BTreeMap<(String, FieldId), FieldCardinality>, supertypes: BTreeMap>, } @@ -52,6 +62,7 @@ impl Schema { kind_names: BTreeMap::new(), next_kind_id: 1, // 0 is reserved field_types: BTreeMap::new(), + field_cardinalities: BTreeMap::new(), supertypes: BTreeMap::new(), } } @@ -196,6 +207,42 @@ impl Schema { .get(&(parent_kind.to_string(), field_id)) } + pub fn set_field_cardinality( + &mut self, + parent_kind: &str, + field_id: FieldId, + cardinality: FieldCardinality, + ) { + self.field_cardinalities + .insert((parent_kind.to_string(), field_id), cardinality); + } + + /// Returns the declared cardinality for a field, if known. + pub fn field_cardinality( + &self, + parent_kind: &str, + field_id: FieldId, + ) -> Option { + self.field_cardinalities + .get(&(parent_kind.to_string(), field_id)) + .copied() + } + + /// Returns an iterator over all `(field_id, field_name)` pairs that are + /// declared as required (`required: true`) for the given `parent_kind`. + pub fn required_fields_for_kind<'a>( + &'a self, + parent_kind: &'a str, + ) -> impl Iterator)> + 'a { + self.field_cardinalities + .iter() + .filter(move |((kind, _), card)| kind == parent_kind && card.required) + .map(move |((_, field_id), _)| { + let name = self.field_name_for_id(*field_id); + (*field_id, name) + }) + } + pub fn set_supertype_members(&mut self, supertype: &str, node_types: Vec) { self.supertypes.insert(supertype.to_string(), node_types); }