Skip to main content

tabnas/
builtins.rs

1// Copyright (c) 2013-2026 Richard Rodger, MIT License
2
3use crate::options::InfoOptions;
4use crate::rule::Rule;
5use crate::token::{TIN_ST, TIN_TX};
6use crate::value::{ListRef, MapRef, Text, Value};
7use crate::Context;
8use indexmap::IndexMap;
9use std::cell::RefCell;
10use std::rc::Rc;
11use std::sync::Arc;
12
13pub(crate) fn is_builtin_action(name: &str) -> bool {
14    matches!(
15        name,
16        "@node$"
17            | "@capture$"
18            | "@bubble$"
19            | "@fold$"
20            | "@probeInit$"
21            | "@probeDecide$"
22            | "@object$"
23            | "@array$"
24            | "@reset$"
25            | "@key$"
26            | "@setval$"
27            | "@push$"
28            | "@value$"
29            | "@map-bo"
30            | "@list-bo"
31            | "@pairkey"
32            | "@pair-bc"
33            | "@elem-bc"
34            | "@val-bo"
35            | "@val-bc"
36    )
37}
38
39fn config_object(config: Option<&Value>) -> Option<&IndexMap<String, Value>> {
40    config.and_then(|value| match value {
41        Value::Object(map) => Some(map.as_ref()),
42        _ => None,
43    })
44}
45
46fn config_bool(config: Option<&Value>, name: &str) -> bool {
47    matches!(
48        config_object(config).and_then(|map| map.get(name)),
49        Some(Value::Bool(true))
50    )
51}
52
53fn config_string(config: Option<&Value>, name: &str) -> String {
54    match config_object(config).and_then(|map| map.get(name)) {
55        Some(Value::String(value)) => value.clone(),
56        _ => String::new(),
57    }
58}
59
60/// The config value at `name` only when it really is a string.
61///
62/// Distinct from `config_string`, which folds "absent" and "" into the
63/// same empty String: `lit` needs them apart, because an empty string is
64/// a real key. Matching the TS `typeof` guard and the Go type assertion,
65/// anything that is not a string reads as absent, so the three ports
66/// agree on every input and not merely on well-typed ones.
67fn config_str_opt(config: Option<&Value>, name: &str) -> Option<String> {
68    match config_object(config).and_then(|map| map.get(name)) {
69        Some(Value::String(value)) => Some(value.clone()),
70        _ => None,
71    }
72}
73
74fn config_usize(config: Option<&Value>, name: &str) -> usize {
75    match config_object(config).and_then(|map| map.get(name)) {
76        Some(Value::Number(value)) if value.is_finite() && *value >= 0.0 => *value as usize,
77        _ => 0,
78    }
79}
80
81fn config_index(config: Option<&Value>, name: &str) -> Option<usize> {
82    match config_object(config).and_then(|map| map.get(name)) {
83        None => Some(0),
84        Some(Value::Number(value))
85            if value.is_finite()
86                && value.fract() == 0.0
87                && *value >= 0.0
88                && *value <= usize::MAX as f64 =>
89        {
90            Some(*value as usize)
91        }
92        _ => None,
93    }
94}
95
96fn ast_node(rule: String, kind: String) -> Value {
97    let mut node = IndexMap::new();
98    if kind == "user" {
99        node.insert("rule".into(), Value::String(rule));
100    }
101    node.insert("src".into(), Value::String(String::new()));
102    node.insert("kids".into(), Value::array(Vec::new()));
103    Value::object(node)
104}
105
106/// The accumulated source text of a tree node -- the `{rule?, src, kids}`
107/// shape `ast_node` builds -- or the node unchanged when it is not one.
108///
109/// "src" is how a member whose value IS its matched text gets that text:
110/// the tree builders already accumulate it, and nothing else could read it
111/// back out. A compiler emits "src" only where it already knows the member
112/// is a scalar, so this never has to guess which it is: the fall-through
113/// exists so asking for src where no tree node was built passes the value
114/// along rather than erasing it.
115fn src_val(node: Value) -> Value {
116    if let Value::Object(map) = &node {
117        if let Some(Value::String(source)) = map.get("src") {
118            return Value::String(source.clone());
119        }
120    }
121    node
122}
123
124fn append_src(node: &mut IndexMap<String, Value>, source: &str) {
125    if let Some(Value::String(current)) = node.get_mut("src") {
126        current.push_str(source);
127    }
128}
129
130fn append_kid(node: &mut IndexMap<String, Value>, child: Value) {
131    if let Some(kids) = node.get_mut("kids").and_then(Value::as_array_mut) {
132        kids.push(child);
133    }
134}
135
136fn capture_child(node: &mut IndexMap<String, Value>, child: Value) {
137    if let Value::Object(child_map) = &child {
138        if let Some(Value::String(source)) = child_map.get("src") {
139            append_src(node, source);
140            if child_map
141                .get("rule")
142                .is_some_and(|value| !matches!(value, Value::String(rule) if rule.is_empty()))
143            {
144                append_kid(node, child);
145            } else if let Some(Value::Array(children)) = child_map.get("kids") {
146                extend_kids(node, children);
147            }
148            return;
149        }
150    }
151    append_kid(node, child);
152}
153
154/// Flatten an untagged child's kids into `node`'s.
155///
156/// A repetition compiles to untagged rules nested one per element, and
157/// each level flattens the level below it, so a rule with N elements
158/// flattens N + (N-1) + ... kids: quadratic in the element count of ONE
159/// rule. TypeScript spreads the same way, but a JavaScript push is a
160/// pointer store; here each kid cost a hash lookup, an `Arc` clone and a
161/// later drop, and 800 fields took 46 seconds against a tenth of a
162/// second (#195). A level whose own kids are still empty, which is every
163/// level of that chain, now takes the child's array by handle instead:
164/// one refcount bump, and the chain is linear again. Copy-on-write keeps
165/// the child's own view intact should anything still read it.
166fn extend_kids(node: &mut IndexMap<String, Value>, children: &Arc<Vec<Value>>) {
167    match node.get_mut("kids") {
168        Some(Value::Array(kids)) if kids.is_empty() => *kids = Arc::clone(children),
169        Some(Value::Array(kids)) => Arc::make_mut(kids).extend(children.iter().cloned()),
170        _ => {}
171    }
172}
173
174fn map_value(info: &InfoOptions, implicit: bool) -> Value {
175    if info.map {
176        Value::MapRef(Arc::new(MapRef {
177            value: IndexMap::new(),
178            implicit,
179            meta: IndexMap::new(),
180        }))
181    } else {
182        Value::object(IndexMap::new())
183    }
184}
185
186fn list_value(info: &InfoOptions, implicit: bool) -> Value {
187    if info.list {
188        Value::ListRef(Arc::new(ListRef {
189            value: Vec::new(),
190            implicit,
191            child: None,
192            meta: IndexMap::new(),
193        }))
194    } else {
195        Value::array(Vec::new())
196    }
197}
198
199fn token_value(rule: &mut Rule, context: &mut Context, index: usize, info: &InfoOptions) -> Value {
200    let Some(token) = rule.o.get(index).cloned() else {
201        return Value::Undefined;
202    };
203    let value = token.resolve_val(rule, context);
204    if info.text && matches!(token.tin, TIN_ST | TIN_TX) {
205        let string = match &value {
206            Value::String(value) => value.clone(),
207            Value::Text(value) => value.string.clone(),
208            _ => return value,
209        };
210        let quote = if token.tin == TIN_ST {
211            token
212                .src
213                .chars()
214                .next()
215                .map_or_else(String::new, |ch| ch.to_string())
216        } else {
217            String::new()
218        };
219        Value::Text(Text { quote, string })
220    } else {
221        value
222    }
223}
224
225fn map_insert(node: &mut Value, key: String, value: Value) {
226    match node {
227        Value::Object(map) => {
228            Arc::make_mut(map).insert(key, value);
229        }
230        Value::MapRef(map) => {
231            Arc::make_mut(map).value.insert(key, value);
232        }
233        _ => {}
234    }
235}
236
237fn list_push(node: &mut Value, value: Value) {
238    match node {
239        Value::Array(array) => Arc::make_mut(array).push(value),
240        Value::ListRef(list) => Arc::make_mut(list).value.push(value),
241        _ => {}
242    }
243}
244
245/// Run a built-in with metadata wrappers disabled.
246///
247/// This keeps the original public helper stable for embedders. Parser-owned
248/// execution goes through the crate-internal `run_builtin_action_with_info`,
249/// which is handed the parse's own [`InfoOptions`]; the public way to the same
250/// behaviour is [`run_builtin_action_with_context`], which reads them off the
251/// context it is given.
252pub fn run_builtin_action(name: &str, rule: &mut Rule, config: Option<&Value>) -> bool {
253    let options = std::sync::Arc::new(crate::Options::default());
254    let mut context = Context::new(
255        options.rewind.history,
256        "",
257        Value::Undefined,
258        std::sync::Arc::clone(&options),
259        crate::InstanceInfo::default(),
260    );
261    run_builtin_action_with_info(name, rule, &mut context, config, &InfoOptions::default())
262}
263
264/// Run a builtin against an explicit live parse context. Embedders that use
265/// lazy token values should prefer this over [`run_builtin_action`].
266pub fn run_builtin_action_with_context(
267    name: &str,
268    rule: &mut Rule,
269    context: &mut Context,
270    config: Option<&Value>,
271) -> bool {
272    let info = context.options.info.clone();
273    run_builtin_action_with_info(name, rule, context, config, &info)
274}
275
276pub(crate) fn run_builtin_action_with_info(
277    name: &str,
278    rule: &mut Rule,
279    context: &mut Context,
280    config: Option<&Value>,
281    info: &InfoOptions,
282) -> bool {
283    match name {
284        "@node$" => {
285            if config_bool(config, "init") {
286                rule.node = Rc::new(RefCell::new(ast_node(
287                    config_string(config, "rule"),
288                    config_string(config, "kind"),
289                )));
290            }
291            let nterms = config_usize(config, "nterms");
292            if let Some(node) = rule.node.borrow_mut().as_object_mut() {
293                for token in rule.o.iter().take(nterms) {
294                    append_src(node, &token.src);
295                }
296            }
297        }
298        "@capture$" => {
299            if rule.node.borrow().is_undefined() {
300                rule.node = Rc::new(RefCell::new(ast_node(
301                    config_string(config, "rule"),
302                    config_string(config, "kind"),
303                )));
304            }
305            if !rule.child_node.is_undefined() && !rule.child_node_is_self {
306                if let Some(node) = rule.node.borrow_mut().as_object_mut() {
307                    capture_child(node, rule.child_node.clone());
308                }
309            }
310        }
311        "@bubble$" => {
312            if rule.has_child_value() {
313                rule.node = Rc::new(RefCell::new(rule.child_value()));
314            }
315        }
316        "@fold$" => {
317            if let Some(parent_node) = &rule.parent_node {
318                let same_node = Rc::ptr_eq(parent_node, &rule.node);
319                let own = rule.node.borrow().clone();
320                if let Some(parent) = parent_node.borrow_mut().as_object_mut() {
321                    if !same_node {
322                        capture_child(parent, own);
323                    }
324                    let close_count = config_usize(config, "cN");
325                    for token in rule.c.iter().take(close_count) {
326                        append_src(parent, &token.src);
327                    }
328                }
329            }
330            rule.node = Rc::new(RefCell::new(Value::Undefined));
331        }
332        "@val-bo" => {
333            rule.node = Rc::new(RefCell::new(Value::Undefined));
334        }
335        "@val-bc" => {
336            let is_undef = rule.node.borrow().is_undefined();
337            if is_undef {
338                if rule.has_child_value() {
339                    rule.node = Rc::new(RefCell::new(rule.child_value()));
340                } else if rule.os() > 0 {
341                    rule.node = Rc::new(RefCell::new(token_value(rule, context, 0, info)));
342                }
343            }
344        }
345        "@value$" => {
346            if rule.has_child_value() {
347                rule.node = Rc::new(RefCell::new(rule.child_value()));
348            } else {
349                let value = config_index(config, "from").map_or(Value::Undefined, |index| {
350                    token_value(rule, context, index, info)
351                });
352                rule.node = Rc::new(RefCell::new(value));
353            }
354        }
355        "@object$" => {
356            rule.node = Rc::new(RefCell::new(map_value(
357                info,
358                config_bool(config, "implicit"),
359            )));
360        }
361        "@array$" => {
362            rule.node = Rc::new(RefCell::new(list_value(
363                info,
364                config_bool(config, "implicit"),
365            )));
366        }
367        "@reset$" => {
368            rule.node = Rc::new(RefCell::new(Value::Undefined));
369            rule.child_node = Value::Undefined;
370            rule.child_node_is_self = false;
371        }
372        "@key$" => {
373            let slot = {
374                let configured = config_string(config, "slot");
375                if configured.is_empty() {
376                    "key".to_string()
377                } else {
378                    configured
379                }
380            };
381            // `lit` supplies the key as a CONSTANT instead of reading it
382            // from a token, and wins outright when set. A grammar whose
383            // structure is declared rather than delimited -- `ver = maj
384            // "." min`, where `maj` names a part but no token carries the
385            // text "maj" -- has no token for @key$ to read, so without
386            // this the key side of @setval$ is unreachable for it.
387            if let Some(lit) = config_str_opt(config, "lit") {
388                rule.u_mut().insert(slot, Value::String(lit));
389            } else if let Some(value) = config_index(config, "from")
390                .and_then(|index| rule.o.get(index))
391                .map(|token| token.val.clone())
392            {
393                rule.u_mut().insert(slot, value);
394            }
395        }
396        "@setval$" => {
397            let slot = {
398                let configured = config_string(config, "slot");
399                if configured.is_empty() {
400                    "key".to_string()
401                } else {
402                    configured
403                }
404            };
405            if let Some(Value::String(key)) = rule.u.get(&slot).cloned() {
406                let mut val = rule.child_value();
407                if config_bool(config, "src") {
408                    val = src_val(val);
409                }
410                map_insert(&mut rule.node.borrow_mut(), key, val);
411            }
412        }
413        "@push$" => {
414            if rule.has_child_value() {
415                let mut val = rule.child_value();
416                if config_bool(config, "src") {
417                    val = src_val(val);
418                }
419                list_push(&mut rule.node.borrow_mut(), val);
420            }
421        }
422        "@map-bo" => {
423            rule.node = Rc::new(RefCell::new(map_value(info, false)));
424        }
425        "@list-bo" => {
426            rule.node = Rc::new(RefCell::new(list_value(info, false)));
427        }
428        // The captured key belongs only to this pair rule and is consumed by
429        // @pair-bc on that same rule. Store it in `u`, the non-propagating
430        // scratch bag, so it cannot leak into child rules as `n` or `k` would.
431        "@pairkey" => {
432            if let Some(t0) = rule.o0() {
433                let key = match &t0.val {
434                    Value::String(s) => s.clone(),
435                    _ => t0.src.to_string(),
436                };
437                rule.u_mut().insert("key".to_string(), Value::String(key));
438            }
439        }
440        "@pair-bc" => {
441            if let Some(Value::String(key)) = rule.u.get("key").cloned() {
442                let child = rule.child_value();
443                let mut node = rule.node.borrow_mut();
444                map_insert(&mut node, key, child);
445            }
446        }
447        "@elem-bc" if rule.has_child_value() => {
448            let child = rule.child_value();
449            let mut node = rule.node.borrow_mut();
450            list_push(&mut node, child);
451        }
452        _ => return false,
453    }
454    true
455}