Skip to main content

cljrs_runtime/interp/
syntax_quote.rs

1//! Syntax-quote (backtick) expansion.
2//!
3//! Syntax-quote is evaluated directly to a `Value`, rather than being
4//! expanded to intermediate AST and then evaluated.
5
6use crate::builtins::SPECIAL_FORMS;
7use crate::builtins::form::{expand_reader_conds_cow, form_to_value, select_reader_cond};
8use crate::env::env::Env;
9use crate::env::error::{EvalError, EvalResult};
10use cljrs_gc::GcPtr;
11use cljrs_reader::Form;
12use cljrs_reader::form::FormKind;
13use cljrs_value::value::SetValue;
14use cljrs_value::{Keyword, PersistentList, PersistentVector, Symbol, Value};
15use std::sync::Arc;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18static GENSYM_COUNTER: AtomicU64 = AtomicU64::new(0);
19
20/// Expand a syntax-quoted form to a Value.
21pub fn syntax_quote(form: &Form, env: &mut Env) -> EvalResult {
22    let mut gensyms = std::collections::HashMap::new();
23    sq_form(form, env, &mut gensyms)
24}
25
26fn sq_form(
27    form: &Form,
28    env: &mut Env,
29    gensyms: &mut std::collections::HashMap<String, Arc<str>>,
30) -> EvalResult {
31    match &form.kind {
32        // ~expr — evaluate normally.
33        FormKind::Unquote(inner) => crate::interp::eval::eval(inner, env),
34
35        // ~@expr at top level is an error.
36        FormKind::UnquoteSplice(_) => Err(EvalError::Runtime(
37            "splice-unquote outside list/vector context".into(),
38        )),
39
40        // Symbols: auto-qualify with current namespace; auto-gensym `foo#`.
41        FormKind::Symbol(s) => {
42            let qualified = qualify_symbol(s, env, gensyms);
43            Ok(Value::symbol(Symbol::parse(&qualified)))
44        }
45
46        // Atoms: return as quoted values.
47        FormKind::Nil => Ok(Value::Nil),
48        FormKind::Bool(b) => Ok(Value::Bool(*b)),
49        FormKind::Int(n) => Ok(Value::Long(*n)),
50        FormKind::Float(f) => Ok(Value::Double(*f)),
51        FormKind::Str(s) => Ok(Value::string(s.clone())),
52        FormKind::Char(c) => Ok(Value::Char(*c)),
53        FormKind::Keyword(s) => Ok(Value::keyword(Keyword::parse(s))),
54        FormKind::AutoKeyword(s) => {
55            let full = env
56                .globals
57                .resolve_auto_keyword(&env.current_ns, s)
58                .map_err(EvalError::Runtime)?;
59            Ok(Value::keyword(Keyword::parse(&full)))
60        }
61        FormKind::AutoSymbol(s) => {
62            let full = env
63                .globals
64                .resolve_auto_keyword(&env.current_ns, s)
65                .map_err(EvalError::Runtime)?;
66            Ok(Value::symbol(Symbol::parse(&full)))
67        }
68
69        // Lists: process each element, splicing ~@ items.
70        FormKind::List(forms) => {
71            let parts = sq_seq(forms, env, gensyms)?;
72            // Concatenate segments.
73            let mut out: Vec<Value> = Vec::new();
74            for part in parts {
75                match part {
76                    Segment::One(v) => out.push(v),
77                    Segment::Many(vs) => out.extend(vs),
78                }
79            }
80            Ok(Value::List(GcPtr::new(PersistentList::from_iter(out))))
81        }
82
83        // Vectors: process each element, splicing ~@ items.
84        FormKind::Vector(forms) => {
85            let parts = sq_seq(forms, env, gensyms)?;
86            let mut out: Vec<Value> = Vec::new();
87            for part in parts {
88                match part {
89                    Segment::One(v) => out.push(v),
90                    Segment::Many(vs) => out.extend(vs),
91                }
92            }
93            Ok(Value::Vector(GcPtr::new(PersistentVector::from_iter(out))))
94        }
95
96        // Maps: treat flat k/v pairs like a sequence.
97        FormKind::Map(forms) => {
98            let parts = sq_seq(forms, env, gensyms)?;
99            let mut out: Vec<Value> = Vec::new();
100            for part in parts {
101                match part {
102                    Segment::One(v) => out.push(v),
103                    Segment::Many(vs) => out.extend(vs),
104                }
105            }
106            if !out.len().is_multiple_of(2) {
107                return Err(EvalError::Runtime(
108                    "syntax-quote map requires even number of forms".into(),
109                ));
110            }
111            let mut m = cljrs_value::MapValue::empty();
112            for pair in out.chunks(2) {
113                m = m.assoc(pair[0].clone(), pair[1].clone());
114            }
115            Ok(Value::Map(m))
116        }
117
118        // Sets.
119        FormKind::Set(forms) => {
120            let parts = sq_seq(forms, env, gensyms)?;
121            let mut out: Vec<Value> = Vec::new();
122            for part in parts {
123                match part {
124                    Segment::One(v) => out.push(v),
125                    Segment::Many(vs) => out.extend(vs),
126                }
127            }
128            let set = out
129                .into_iter()
130                .fold(cljrs_value::PersistentHashSet::empty(), |s, v| s.conj(v));
131            Ok(Value::Set(SetValue::Hash(GcPtr::new(set))))
132        }
133
134        // `'inner` inside syntax-quote: recursively process `inner` so that
135        // unquotes like `'~x` work — they evaluate x and wrap the result in (quote ...).
136        FormKind::Quote(inner) => {
137            let processed = sq_form(inner, env, gensyms)?;
138            Ok(Value::List(GcPtr::new(PersistentList::from_iter([
139                Value::symbol(Symbol::simple("quote")),
140                processed,
141            ]))))
142        }
143
144        // `#?(...)` in a non-sibling position: syntax-quote the selected branch.
145        FormKind::ReaderCond {
146            splicing: false,
147            clauses,
148        } => match select_reader_cond(clauses) {
149            Some(selected) => sq_form(selected, env, gensyms),
150            None => Ok(Value::Nil),
151        },
152
153        // `#?@(...)` here has no sibling sequence to splice into.
154        FormKind::ReaderCond { splicing: true, .. } => Err(EvalError::Runtime(
155            "splicing reader conditional not in a sequence context".into(),
156        )),
157
158        // #(...) anonymous function: expand to (fn* [...] ...) then syntax-quote.
159        FormKind::AnonFn(body) => {
160            let expanded = crate::builtins::form::expand_anon_fn(body, form.span.clone());
161            sq_form(&expanded, env, gensyms)
162        }
163
164        // Everything else: wrap as literal data.
165        _other => form_to_value(form),
166    }
167}
168
169enum Segment {
170    One(Value),
171    Many(Vec<Value>),
172}
173
174/// Process a sibling sequence, resolving `#?`/`#?@` for `:rust` first so a
175/// splice contributes its branch elements to the enclosing collection.
176fn sq_seq(
177    forms: &[Form],
178    env: &mut Env,
179    gensyms: &mut std::collections::HashMap<String, Arc<str>>,
180) -> EvalResult<Vec<Segment>> {
181    let forms = expand_reader_conds_cow(forms);
182    let mut out = Vec::with_capacity(forms.len());
183    for f in forms.iter() {
184        match &f.kind {
185            FormKind::UnquoteSplice(inner) => {
186                // ~@expr: evaluate and spread.
187                let v = crate::interp::eval::eval(inner, env)?;
188                let items = crate::interp::destructure::value_to_seq_vec(&v);
189                out.push(Segment::Many(items));
190            }
191            _ => {
192                let v = sq_form(f, env, gensyms)?;
193                out.push(Segment::One(v));
194            }
195        }
196    }
197    Ok(out)
198}
199
200/// Qualify a symbol name for use inside syntax-quote.
201///
202/// - `foo#` → unique gensym `foo__N__auto__` (same N within one backtick).
203/// - `alias/foo` → resolved through the current ns's `:require :as` aliases
204///   to `real-ns/foo`, exactly like `::alias/kw` and `(binding [alias/*x*
205///   ...])`; not just kept as-is, since call sites (e.g. protocol-metadata
206///   dispatch) key on the *resolved* qualified symbol, not the alias text.
207/// - Special literals (`nil`, `true`, `false`) → kept as-is.
208/// - Special forms (`def`, `if`, `try`, `catch`, `let`, …) → kept as-is.
209/// - Symbols that resolve in the current namespace → qualified with resolved ns.
210/// - Everything else → `current-ns/name`.
211fn qualify_symbol(
212    s: &str,
213    env: &Env,
214    gensyms: &mut std::collections::HashMap<String, Arc<str>>,
215) -> String {
216    // Already qualified (but not the bare `/` division symbol, or a
217    // dangling/leading slash — neither has a real ns part to resolve).
218    if let Some(idx) = s.find('/') {
219        if idx > 0 && idx < s.len() - 1 {
220            let (ns_part, name_part) = (&s[..idx], &s[idx + 1..]);
221            let resolved_ns = env
222                .globals
223                .resolve_alias(&env.current_ns, ns_part)
224                .unwrap_or_else(|| Arc::from(ns_part));
225            return format!("{resolved_ns}/{name_part}");
226        }
227        return s.to_string();
228    }
229    // Special literals.
230    if matches!(s, "nil" | "true" | "false") {
231        return s.to_string();
232    }
233    // Auto-gensym: `foo#`.
234    if let Some(base) = s.strip_suffix('#') {
235        let generated = gensyms.entry(s.to_string()).or_insert_with(|| {
236            let n = crate::env::policy::next_transaction_gensym()
237                .unwrap_or_else(|| GENSYM_COUNTER.fetch_add(1, Ordering::Relaxed));
238            Arc::from(format!("{base}__{n}__auto__"))
239        });
240        return generated.as_ref().to_string();
241    }
242    // Special forms and try-related tokens: never qualify.
243    if SPECIAL_FORMS.contains(&s)
244        || matches!(s, "catch" | "finally" | "Exception" | "Throwable" | "Error")
245    {
246        return s.to_string();
247    }
248    // Resolve through current namespace (interns and refers).
249    // If the symbol resolves to a var, use that var's actual namespace.
250    if let Some(var_ptr) = env.globals.lookup_var_in_ns(&env.current_ns, s) {
251        let var_ns = var_ptr.get().namespace.as_ref().to_string();
252        return format!("{var_ns}/{s}");
253    }
254    // Default: qualify with current namespace.
255    format!("{}/{}", env.current_ns, s)
256}