11use proc_macro2:: { Ident , Literal } ;
2+ use yeast_schema:: schema:: Schema ;
23
34/// A complete rewrite rule such as `(input) where guard => (output)`.
45pub ( crate ) struct Rule {
@@ -117,6 +118,8 @@ pub(crate) enum Content {
117118 Static ( Literal ) ,
118119 /// Computed leaf content such as `#{name}` in `(identifier #{name})`.
119120 Computed ( syn:: Block ) ,
121+ /// Value inherited from the node matched by the enclosing rule.
122+ Inherited ,
120123}
121124
122125pub ( crate ) enum Field {
@@ -137,3 +140,112 @@ pub(crate) enum Field {
137140 block : syn:: Block ,
138141 } ,
139142}
143+
144+ impl Rule {
145+ pub ( crate ) fn resolve_bare_leaf_content ( & mut self , schema : & Schema ) -> syn:: Result < ( ) > {
146+ let Replacement :: Templates ( templates) = & mut self . replacement else {
147+ return Ok ( ( ) ) ;
148+ } ;
149+ for template in templates {
150+ template. resolve_bare_leaf_content ( schema, true ) ?;
151+ }
152+ Ok ( ( ) )
153+ }
154+ }
155+
156+ impl Template {
157+ fn resolve_bare_leaf_content (
158+ & mut self ,
159+ schema : & Schema ,
160+ is_rule_result : bool ,
161+ ) -> syn:: Result < ( ) > {
162+ let Template :: Node ( node) = self else {
163+ return Ok ( ( ) ) ;
164+ } ;
165+
166+ for field in & mut node. fields {
167+ if let Field :: Named { value, .. } = field {
168+ value. resolve_bare_leaf_content ( schema, false ) ?;
169+ }
170+ }
171+
172+ if node. content . is_none ( )
173+ && node. fields . is_empty ( )
174+ && schema. is_leaf_kind ( & node. kind . to_string ( ) )
175+ {
176+ if !is_rule_result {
177+ return Err ( syn:: Error :: new_spanned (
178+ & node. kind ,
179+ format ! (
180+ "bare leaf `{}` must be a top-level rule result or have explicit content" ,
181+ node. kind
182+ ) ,
183+ ) ) ;
184+ }
185+ node. content = Some ( Content :: Inherited ) ;
186+ }
187+
188+ Ok ( ( ) )
189+ }
190+ }
191+
192+ #[ cfg( test) ]
193+ mod tests {
194+ use super :: * ;
195+ use quote:: quote;
196+
197+ fn output_schema ( ) -> Schema {
198+ yeast_schema:: node_types_yaml:: schema_from_yaml (
199+ r#"
200+ named:
201+ assignment:
202+ left: identifier
203+ identifier:
204+ "# ,
205+ )
206+ . unwrap ( )
207+ }
208+
209+ #[ test]
210+ fn marks_top_level_bare_leaves_as_inherited ( ) {
211+ let mut rule = crate :: rule_parse:: parse_rule ( quote ! ( ( source) => ( identifier) ) ) . unwrap ( ) ;
212+ rule. resolve_bare_leaf_content ( & output_schema ( ) ) . unwrap ( ) ;
213+
214+ let Replacement :: Templates ( templates) = rule. replacement else {
215+ panic ! ( "expected template replacement" ) ;
216+ } ;
217+ let [ Template :: Node ( node) ] = templates. as_slice ( ) else {
218+ panic ! ( "expected one output node" ) ;
219+ } ;
220+ assert ! ( matches!( node. content, Some ( Content :: Inherited ) ) ) ;
221+ }
222+
223+ #[ test]
224+ fn rejects_nested_bare_leaves ( ) {
225+ let mut rule =
226+ crate :: rule_parse:: parse_rule ( quote ! ( ( source) => ( assignment left: ( identifier) ) ) )
227+ . unwrap ( ) ;
228+ let error = rule
229+ . resolve_bare_leaf_content ( & output_schema ( ) )
230+ . unwrap_err ( ) ;
231+
232+ assert_eq ! (
233+ error. to_string( ) ,
234+ "bare leaf `identifier` must be a top-level rule result or have explicit content"
235+ ) ;
236+ }
237+
238+ #[ test]
239+ fn leaves_fieldless_structural_nodes_unchanged ( ) {
240+ let mut rule = crate :: rule_parse:: parse_rule ( quote ! ( ( source) => ( assignment) ) ) . unwrap ( ) ;
241+ rule. resolve_bare_leaf_content ( & output_schema ( ) ) . unwrap ( ) ;
242+
243+ let Replacement :: Templates ( templates) = rule. replacement else {
244+ panic ! ( "expected template replacement" ) ;
245+ } ;
246+ let [ Template :: Node ( node) ] = templates. as_slice ( ) else {
247+ panic ! ( "expected one output node" ) ;
248+ } ;
249+ assert ! ( node. content. is_none( ) ) ;
250+ }
251+ }
0 commit comments