Skip to main content

sema_eval/
special_forms.rs

1use std::cell::Cell;
2use std::rc::Rc;
3
4use sema_core::{
5    intern, resolve, Agent, Env, EvalContext, Lambda, Macro, Record, SemaError, Spur, Thunk,
6    ToolDefinition, Value,
7};
8
9use crate::eval::{self, Trampoline};
10
11/// Pre-interned `Spur` handles for all special form names.
12///
13/// Special form dispatch is the hottest path in the evaluator — every list expression
14/// checks whether its head is a special form. By caching the interned `Spur` (u32) for
15/// each name, we compare integers instead of resolving strings. This also avoids calling
16/// `resolve()` which allocates a `String` from the thread-local interner on every lookup.
17struct SpecialFormSpurs {
18    // Core language
19    and: Spur,
20    begin: Spur,
21    case: Spur,
22    catch: Spur,
23    cond: Spur,
24    define: Spur,
25    define_record_type: Spur,
26    defmacro: Spur,
27    defun: Spur,
28    delay: Spur,
29    do_: Spur,
30    else_: Spur,
31    eval: Spur,
32    export: Spur,
33    fn_: Spur,
34    force: Spur,
35    if_: Spur,
36    lambda: Spur,
37    let_: Spur,
38    let_star: Spur,
39    letrec: Spur,
40    macroexpand: Spur,
41    or: Spur,
42    quasiquote: Spur,
43    quote: Spur,
44    set_bang: Spur,
45    throw: Spur,
46    try_: Spur,
47    unless: Spur,
48    when: Spur,
49
50    // Modules
51    import: Spur,
52    load: Spur,
53    module: Spur,
54
55    // LLM primitives
56    defagent: Spur,
57    deftool: Spur,
58    message: Spur,
59    prompt: Spur,
60}
61
62impl SpecialFormSpurs {
63    fn init() -> Self {
64        Self {
65            // Core language
66            and: intern("and"),
67            begin: intern("begin"),
68            case: intern("case"),
69            catch: intern("catch"),
70            cond: intern("cond"),
71            define: intern("define"),
72            define_record_type: intern("define-record-type"),
73            defmacro: intern("defmacro"),
74            defun: intern("defun"),
75            delay: intern("delay"),
76            do_: intern("do"),
77            else_: intern("else"),
78            eval: intern("eval"),
79            export: intern("export"),
80            fn_: intern("fn"),
81            force: intern("force"),
82            if_: intern("if"),
83            lambda: intern("lambda"),
84            let_: intern("let"),
85            let_star: intern("let*"),
86            letrec: intern("letrec"),
87            macroexpand: intern("macroexpand"),
88            or: intern("or"),
89            quasiquote: intern("quasiquote"),
90            quote: intern("quote"),
91            set_bang: intern("set!"),
92            throw: intern("throw"),
93            try_: intern("try"),
94            unless: intern("unless"),
95            when: intern("when"),
96
97            // Modules
98            import: intern("import"),
99            load: intern("load"),
100            module: intern("module"),
101
102            // LLM primitives
103            defagent: intern("defagent"),
104            deftool: intern("deftool"),
105            message: intern("message"),
106            prompt: intern("prompt"),
107        }
108    }
109}
110
111thread_local! {
112    static SF: Cell<Option<&'static SpecialFormSpurs>> = const { Cell::new(None) };
113}
114
115fn special_forms() -> &'static SpecialFormSpurs {
116    SF.with(|cell| match cell.get() {
117        Some(sf) => sf,
118        None => {
119            let sf: &'static SpecialFormSpurs = Box::leak(Box::new(SpecialFormSpurs::init()));
120            cell.set(Some(sf));
121            sf
122        }
123    })
124}
125
126/// Canonical list of all special form names recognized by the evaluator.
127///
128/// This is the single source of truth — used by the REPL for completion,
129/// the LSP for highlighting, and anywhere else that needs to enumerate special forms.
130pub const SPECIAL_FORM_NAMES: &[&str] = &[
131    // Core language
132    "and",
133    "begin",
134    "case",
135    "cond",
136    "define",
137    "define-record-type",
138    "defmacro",
139    "defun",
140    "delay",
141    "do",
142    "eval",
143    "fn",
144    "force",
145    "if",
146    "lambda",
147    "let",
148    "let*",
149    "letrec",
150    "macroexpand",
151    "or",
152    "quasiquote",
153    "quote",
154    "set!",
155    "throw",
156    "try",
157    "unless",
158    "when",
159    // Modules
160    "export",
161    "import",
162    "load",
163    "module",
164    // LLM primitives
165    "defagent",
166    "deftool",
167    "message",
168    "prompt",
169];
170
171/// Evaluate a special form. Returns Some(result) if the head is a special form, None otherwise.
172pub fn try_eval_special(
173    head_spur: Spur,
174    args: &[Value],
175    env: &Env,
176    ctx: &EvalContext,
177) -> Option<Result<Trampoline, SemaError>> {
178    let sf = special_forms();
179
180    // Core language — hot path forms first (if, define, let, begin, lambda)
181    if head_spur == sf.if_ {
182        Some(eval_if(args, env, ctx))
183    } else if head_spur == sf.define {
184        Some(eval_define(args, env, ctx))
185    } else if head_spur == sf.let_ {
186        Some(eval_let(args, env, ctx))
187    } else if head_spur == sf.begin {
188        Some(eval_begin(args, env, ctx))
189    } else if head_spur == sf.lambda || head_spur == sf.fn_ {
190        Some(eval_lambda(args, env, None))
191    } else if head_spur == sf.and {
192        Some(eval_and(args, env, ctx))
193    } else if head_spur == sf.case {
194        Some(eval_case(args, env, ctx))
195    } else if head_spur == sf.cond {
196        Some(eval_cond(args, env, ctx))
197    } else if head_spur == sf.define_record_type {
198        Some(eval_define_record_type(args, env))
199    } else if head_spur == sf.defmacro {
200        Some(eval_defmacro(args, env))
201    } else if head_spur == sf.defun {
202        Some(eval_defun(args, env, ctx))
203    } else if head_spur == sf.delay {
204        Some(eval_delay(args, env))
205    } else if head_spur == sf.do_ {
206        Some(eval_do(args, env, ctx))
207    } else if head_spur == sf.eval {
208        Some(eval_eval(args, env, ctx))
209    } else if head_spur == sf.force {
210        Some(eval_force(args, env, ctx))
211    } else if head_spur == sf.let_star {
212        Some(eval_let_star(args, env, ctx))
213    } else if head_spur == sf.letrec {
214        Some(eval_letrec(args, env, ctx))
215    } else if head_spur == sf.macroexpand {
216        Some(eval_macroexpand(args, env, ctx))
217    } else if head_spur == sf.or {
218        Some(eval_or(args, env, ctx))
219    } else if head_spur == sf.quasiquote {
220        Some(eval_quasiquote(args, env, ctx))
221    } else if head_spur == sf.quote {
222        Some(eval_quote(args))
223    } else if head_spur == sf.set_bang {
224        Some(eval_set(args, env, ctx))
225    } else if head_spur == sf.throw {
226        Some(eval_throw(args, env, ctx))
227    } else if head_spur == sf.try_ {
228        Some(eval_try(args, env, ctx))
229    } else if head_spur == sf.unless {
230        Some(eval_unless(args, env, ctx))
231    } else if head_spur == sf.when {
232        Some(eval_when(args, env, ctx))
233
234    // Modules
235    } else if head_spur == sf.import {
236        Some(eval_import(args, env, ctx))
237    } else if head_spur == sf.load {
238        Some(eval_load(args, env, ctx))
239    } else if head_spur == sf.module {
240        Some(eval_module(args, env, ctx))
241
242    // LLM primitives
243    } else if head_spur == sf.defagent {
244        Some(eval_defagent(args, env, ctx))
245    } else if head_spur == sf.deftool {
246        Some(eval_deftool(args, env, ctx))
247    } else if head_spur == sf.message {
248        Some(eval_message(args, env, ctx))
249    } else if head_spur == sf.prompt {
250        Some(eval_prompt(args, env, ctx))
251    } else {
252        None
253    }
254}
255
256fn eval_quote(args: &[Value]) -> Result<Trampoline, SemaError> {
257    if args.len() != 1 {
258        return Err(SemaError::arity("quote", "1", args.len()));
259    }
260    Ok(Trampoline::Value(args[0].clone()))
261}
262
263fn eval_if(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
264    if args.len() < 2 || args.len() > 3 {
265        return Err(SemaError::arity("if", "2 or 3", args.len()));
266    }
267    let cond = eval::eval_value(ctx, &args[0], env)?;
268    if cond.is_truthy() {
269        Ok(Trampoline::Eval(args[1].clone(), env.clone()))
270    } else if args.len() == 3 {
271        Ok(Trampoline::Eval(args[2].clone(), env.clone()))
272    } else {
273        Ok(Trampoline::Value(Value::nil()))
274    }
275}
276
277fn eval_cond(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
278    let sf = special_forms();
279    for clause in args {
280        let items = clause
281            .as_list()
282            .ok_or_else(|| SemaError::eval("cond clause must be a list"))?;
283        if items.is_empty() {
284            return Err(SemaError::eval("cond clause must not be empty"));
285        }
286        // (else body...) or (test body...)
287        let is_else = items[0].as_symbol_spur().is_some_and(|s| s == sf.else_);
288        if is_else || eval::eval_value(ctx, &items[0], env)?.is_truthy() {
289            if items.len() == 1 {
290                return Ok(Trampoline::Value(Value::bool(true)));
291            }
292            // Eval all but last, tail-call last
293            for expr in &items[1..items.len() - 1] {
294                eval::eval_value(ctx, expr, env)?;
295            }
296            return Ok(Trampoline::Eval(items.last().unwrap().clone(), env.clone()));
297        }
298    }
299    Ok(Trampoline::Value(Value::nil()))
300}
301
302fn eval_define(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
303    if args.is_empty() {
304        return Err(SemaError::arity("define", "2+", 0));
305    }
306    if let Some(spur) = args[0].as_symbol_spur() {
307        // (define x expr)
308        if args.len() != 2 {
309            return Err(SemaError::arity("define", "2", args.len()));
310        }
311        let val = eval::eval_value(ctx, &args[1], env)?;
312        env.set(spur, val);
313        Ok(Trampoline::Value(Value::nil()))
314    } else if let Some(sig) = args[0].as_list() {
315        // (define (f x y) body...) => (define f (lambda (x y) body...))
316        if sig.is_empty() {
317            return Err(SemaError::eval("define: empty function signature"));
318        }
319        let name_spur = sig[0]
320            .as_symbol_spur()
321            .ok_or_else(|| SemaError::eval("define: function name must be a symbol"))?;
322        let param_names: Vec<Spur> = sig[1..]
323            .iter()
324            .map(|v| {
325                v.as_symbol_spur()
326                    .ok_or_else(|| SemaError::eval("define: parameter must be a symbol"))
327            })
328            .collect::<Result<_, _>>()?;
329        let (params, rest_param) = parse_params(&param_names);
330        let body = args[1..].to_vec();
331        if body.is_empty() {
332            return Err(SemaError::eval("define: function body cannot be empty"));
333        }
334        let lambda = Value::lambda(Lambda {
335            params,
336            rest_param,
337            body,
338            env: env.clone(),
339            name: Some(name_spur),
340        });
341        env.set(name_spur, lambda);
342        Ok(Trampoline::Value(Value::nil()))
343    } else {
344        Err(SemaError::type_error("symbol or list", args[0].type_name()))
345    }
346}
347
348/// (defun name (params...) body...) => (define (name params...) body...)
349fn eval_defun(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
350    if args.len() < 3 {
351        return Err(SemaError::arity("defun", "3+", args.len()));
352    }
353    if args[0].as_symbol_spur().is_none() {
354        return Err(SemaError::type_error("symbol", args[0].type_name()));
355    }
356    let name = args[0].clone();
357    let params = args[1]
358        .as_list_rc()
359        .ok_or_else(|| SemaError::type_error("list", args[1].type_name()))?;
360    // Build (name params...) signature list
361    let mut sig = vec![name];
362    sig.extend(params.iter().cloned());
363    // Build transformed args: [(name params...), body...]
364    let mut define_args = vec![Value::list(sig)];
365    define_args.extend_from_slice(&args[2..]);
366    eval_define(&define_args, env, ctx)
367}
368
369fn eval_set(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
370    if args.len() != 2 {
371        return Err(SemaError::arity("set!", "2", args.len()));
372    }
373    let spur = args[0]
374        .as_symbol_spur()
375        .ok_or_else(|| SemaError::eval("set!: first argument must be a symbol"))?;
376    let val = eval::eval_value(ctx, &args[1], env)?;
377    if !env.set_existing(spur, val) {
378        return Err(SemaError::Unbound(resolve(spur)));
379    }
380    Ok(Trampoline::Value(Value::nil()))
381}
382
383fn eval_lambda(args: &[Value], env: &Env, name: Option<Spur>) -> Result<Trampoline, SemaError> {
384    if args.len() < 2 {
385        return Err(SemaError::arity("lambda", "2+", args.len()));
386    }
387    let param_list = if let Some(params) = args[0].as_list() {
388        params.to_vec()
389    } else if let Some(params) = args[0].as_vector() {
390        params.to_vec()
391    } else {
392        return Err(SemaError::type_error("list or vector", args[0].type_name()));
393    };
394    let param_names: Vec<Spur> = param_list
395        .iter()
396        .map(|v| {
397            v.as_symbol_spur()
398                .ok_or_else(|| SemaError::eval("lambda: parameter must be a symbol"))
399        })
400        .collect::<Result<_, _>>()?;
401    let (params, rest_param) = parse_params(&param_names);
402    let body = args[1..].to_vec();
403    Ok(Trampoline::Value(Value::lambda(Lambda {
404        params,
405        rest_param,
406        body,
407        env: env.clone(),
408        name,
409    })))
410}
411
412fn eval_let(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
413    if args.len() < 2 {
414        return Err(SemaError::arity("let", "2+", args.len()));
415    }
416
417    // Named let: (let name ((var init) ...) body...)
418    if let Some(loop_name_spur) = args[0].as_symbol_spur() {
419        if args.len() < 3 {
420            return Err(SemaError::arity("named let", "3+", args.len()));
421        }
422        let bindings_list = args[1]
423            .as_list()
424            .ok_or_else(|| SemaError::eval("named let: bindings must be a list"))?;
425
426        let mut params: Vec<Spur> = Vec::new();
427        let mut init_vals = Vec::new();
428        for binding in bindings_list {
429            let pair = binding
430                .as_list()
431                .ok_or_else(|| SemaError::eval("named let: each binding must be a list"))?;
432            if pair.len() != 2 {
433                return Err(SemaError::eval(
434                    "named let: each binding must have 2 elements",
435                ));
436            }
437            let pname = pair[0]
438                .as_symbol_spur()
439                .ok_or_else(|| SemaError::eval("named let: binding name must be a symbol"))?;
440            let val = eval::eval_value(ctx, &pair[1], env)?;
441            params.push(pname);
442            init_vals.push(val);
443        }
444
445        let body = args[2..].to_vec();
446        let lambda = Lambda {
447            params: params.clone(),
448            rest_param: None,
449            body,
450            env: env.clone(),
451            name: Some(loop_name_spur),
452        };
453
454        // Build env with params bound + self-reference
455        let new_env = Env::with_parent(Rc::new(env.clone()));
456        for (p, v) in params.iter().zip(init_vals.iter()) {
457            new_env.set(*p, v.clone());
458        }
459        new_env.set(
460            loop_name_spur,
461            Value::lambda(Lambda {
462                params: lambda.params.clone(),
463                rest_param: None,
464                body: lambda.body.clone(),
465                env: env.clone(),
466                name: lambda.name,
467            }),
468        );
469
470        // Tail-call on last body expr
471        let body_ref = &args[2..];
472        for expr in &body_ref[..body_ref.len() - 1] {
473            eval::eval_value(ctx, expr, &new_env)?;
474        }
475        return Ok(Trampoline::Eval(body_ref.last().unwrap().clone(), new_env));
476    }
477
478    // Regular let
479    let bindings = args[0]
480        .as_list()
481        .ok_or_else(|| SemaError::eval("let: bindings must be a list"))?;
482
483    let new_env = Env::with_parent(Rc::new(env.clone()));
484
485    for binding in bindings {
486        let pair = binding
487            .as_list()
488            .ok_or_else(|| SemaError::eval("let: each binding must be a list"))?;
489        if pair.len() != 2 {
490            return Err(SemaError::eval("let: each binding must have 2 elements"));
491        }
492        let name_spur = pair[0]
493            .as_symbol_spur()
494            .ok_or_else(|| SemaError::eval("let: binding name must be a symbol"))?;
495        // Evaluate in the OUTER env for let (not let*)
496        let val = eval::eval_value(ctx, &pair[1], env)?;
497        new_env.set(name_spur, val);
498    }
499
500    // Eval body with tail call on last expr
501    for expr in &args[1..args.len() - 1] {
502        eval::eval_value(ctx, expr, &new_env)?;
503    }
504    Ok(Trampoline::Eval(args.last().unwrap().clone(), new_env))
505}
506
507fn eval_let_star(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
508    if args.len() < 2 {
509        return Err(SemaError::arity("let*", "2+", args.len()));
510    }
511    let bindings = args[0]
512        .as_list()
513        .ok_or_else(|| SemaError::eval("let*: bindings must be a list"))?;
514
515    let new_env = Env::with_parent(Rc::new(env.clone()));
516
517    for binding in bindings {
518        let pair = binding
519            .as_list()
520            .ok_or_else(|| SemaError::eval("let*: each binding must be a list"))?;
521        if pair.len() != 2 {
522            return Err(SemaError::eval("let*: each binding must have 2 elements"));
523        }
524        let name_spur = pair[0]
525            .as_symbol_spur()
526            .ok_or_else(|| SemaError::eval("let*: binding name must be a symbol"))?;
527        // Evaluate in the NEW env (sequential binding)
528        let val = eval::eval_value(ctx, &pair[1], &new_env)?;
529        new_env.set(name_spur, val);
530    }
531
532    for expr in &args[1..args.len() - 1] {
533        eval::eval_value(ctx, expr, &new_env)?;
534    }
535    Ok(Trampoline::Eval(args.last().unwrap().clone(), new_env))
536}
537
538fn eval_letrec(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
539    if args.len() < 2 {
540        return Err(SemaError::arity("letrec", "2+", args.len()));
541    }
542    let bindings = args[0]
543        .as_list()
544        .ok_or_else(|| SemaError::eval("letrec: bindings must be a list"))?;
545
546    let new_env = Env::with_parent(Rc::new(env.clone()));
547
548    // Pass 1: bind all names to Nil (placeholders)
549    let mut name_spurs = Vec::new();
550    for binding in bindings {
551        let pair = binding
552            .as_list()
553            .ok_or_else(|| SemaError::eval("letrec: each binding must be a list"))?;
554        if pair.len() != 2 {
555            return Err(SemaError::eval("letrec: each binding must have 2 elements"));
556        }
557        let spur = pair[0]
558            .as_symbol_spur()
559            .ok_or_else(|| SemaError::eval("letrec: binding name must be a symbol"))?;
560        name_spurs.push(spur);
561        new_env.set(spur, Value::nil());
562    }
563
564    // Pass 2: evaluate init exprs in new env, update bindings
565    for (i, binding) in bindings.iter().enumerate() {
566        let pair = binding.as_list().unwrap();
567        let val = eval::eval_value(ctx, &pair[1], &new_env)?;
568        new_env.set(name_spurs[i], val);
569    }
570
571    // Eval body with tail call on last expr
572    for expr in &args[1..args.len() - 1] {
573        eval::eval_value(ctx, expr, &new_env)?;
574    }
575    Ok(Trampoline::Eval(args.last().unwrap().clone(), new_env))
576}
577
578fn eval_begin(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
579    if args.is_empty() {
580        return Ok(Trampoline::Value(Value::nil()));
581    }
582    for expr in &args[..args.len() - 1] {
583        eval::eval_value(ctx, expr, env)?;
584    }
585    Ok(Trampoline::Eval(args.last().unwrap().clone(), env.clone()))
586}
587
588fn eval_and(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
589    if args.is_empty() {
590        return Ok(Trampoline::Value(Value::bool(true)));
591    }
592    for expr in &args[..args.len() - 1] {
593        let val = eval::eval_value(ctx, expr, env)?;
594        if !val.is_truthy() {
595            return Ok(Trampoline::Value(val));
596        }
597    }
598    Ok(Trampoline::Eval(args.last().unwrap().clone(), env.clone()))
599}
600
601fn eval_or(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
602    if args.is_empty() {
603        return Ok(Trampoline::Value(Value::bool(false)));
604    }
605    for expr in &args[..args.len() - 1] {
606        let val = eval::eval_value(ctx, expr, env)?;
607        if val.is_truthy() {
608            return Ok(Trampoline::Value(val));
609        }
610    }
611    Ok(Trampoline::Eval(args.last().unwrap().clone(), env.clone()))
612}
613
614fn eval_when(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
615    if args.len() < 2 {
616        return Err(SemaError::arity("when", "2+", args.len()));
617    }
618    let cond = eval::eval_value(ctx, &args[0], env)?;
619    if cond.is_truthy() {
620        for expr in &args[1..args.len() - 1] {
621            eval::eval_value(ctx, expr, env)?;
622        }
623        Ok(Trampoline::Eval(args.last().unwrap().clone(), env.clone()))
624    } else {
625        Ok(Trampoline::Value(Value::nil()))
626    }
627}
628
629fn eval_unless(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
630    if args.len() < 2 {
631        return Err(SemaError::arity("unless", "2+", args.len()));
632    }
633    let cond = eval::eval_value(ctx, &args[0], env)?;
634    if !cond.is_truthy() {
635        for expr in &args[1..args.len() - 1] {
636            eval::eval_value(ctx, expr, env)?;
637        }
638        Ok(Trampoline::Eval(args.last().unwrap().clone(), env.clone()))
639    } else {
640        Ok(Trampoline::Value(Value::nil()))
641    }
642}
643
644fn eval_defmacro(args: &[Value], env: &Env) -> Result<Trampoline, SemaError> {
645    if args.len() < 3 {
646        return Err(SemaError::arity("defmacro", "3+", args.len()));
647    }
648    let name_spur = args[0]
649        .as_symbol_spur()
650        .ok_or_else(|| SemaError::eval("defmacro: name must be a symbol"))?;
651    let param_list = args[1]
652        .as_list()
653        .ok_or_else(|| SemaError::eval("defmacro: params must be a list"))?;
654    let param_names: Vec<Spur> = param_list
655        .iter()
656        .map(|v| {
657            v.as_symbol_spur()
658                .ok_or_else(|| SemaError::eval("defmacro: parameter must be a symbol"))
659        })
660        .collect::<Result<_, _>>()?;
661    let (params, rest_param) = parse_params(&param_names);
662    let body = args[2..].to_vec();
663
664    let mac = Value::macro_val(Macro {
665        params,
666        rest_param,
667        body,
668        name: name_spur,
669    });
670    env.set(name_spur, mac);
671    Ok(Trampoline::Value(Value::nil()))
672}
673
674fn eval_quasiquote(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
675    if args.len() != 1 {
676        return Err(SemaError::arity("quasiquote", "1", args.len()));
677    }
678    let result = expand_quasiquote(&args[0], env, ctx)?;
679    Ok(Trampoline::Value(result))
680}
681
682fn expand_quasiquote(val: &Value, env: &Env, ctx: &EvalContext) -> Result<Value, SemaError> {
683    if let Some(items) = val.as_list() {
684        if items.is_empty() {
685            return Ok(val.clone());
686        }
687        // Check for (unquote x)
688        if let Some(sym) = items[0].as_symbol() {
689            if sym == "unquote" {
690                if items.len() != 2 {
691                    return Err(SemaError::arity("unquote", "1", items.len() - 1));
692                }
693                return eval::eval_value(ctx, &items[1], env);
694            }
695        }
696        // Expand each element, handling splicing
697        let mut result = Vec::new();
698        for item in items.iter() {
699            if let Some(inner) = item.as_list() {
700                if !inner.is_empty() {
701                    if let Some(sym) = inner[0].as_symbol() {
702                        if sym == "unquote-splicing" {
703                            if inner.len() != 2 {
704                                return Err(SemaError::arity(
705                                    "unquote-splicing",
706                                    "1",
707                                    inner.len() - 1,
708                                ));
709                            }
710                            let splice_val = eval::eval_value(ctx, &inner[1], env)?;
711                            if let Some(splice_items) = splice_val.as_list() {
712                                result.extend(splice_items.iter().cloned());
713                            } else {
714                                return Err(SemaError::type_error("list", splice_val.type_name()));
715                            }
716                            continue;
717                        }
718                    }
719                }
720            }
721            result.push(expand_quasiquote(item, env, ctx)?);
722        }
723        Ok(Value::list(result))
724    } else if let Some(items) = val.as_vector() {
725        let mut result = Vec::new();
726        for item in items.iter() {
727            result.push(expand_quasiquote(item, env, ctx)?);
728        }
729        Ok(Value::vector(result))
730    } else {
731        Ok(val.clone())
732    }
733}
734
735fn eval_prompt(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
736    use sema_core::{Message, Prompt, Role};
737
738    let role_names = ["system", "user", "assistant", "tool"];
739
740    let mut messages = Vec::new();
741    for arg in args {
742        // Check if this is a (role content...) form BEFORE evaluating
743        if let Some(items) = arg.as_list() {
744            if !items.is_empty() {
745                if let Some(role_str) = items[0].as_symbol() {
746                    if role_names.contains(&role_str.as_str()) {
747                        let role = match role_str.as_str() {
748                            "system" => Role::System,
749                            "user" => Role::User,
750                            "assistant" => Role::Assistant,
751                            "tool" => Role::Tool,
752                            _ => unreachable!(),
753                        };
754                        // Evaluate and concatenate the content parts
755                        let mut content = String::new();
756                        for part in &items[1..] {
757                            let part_val = eval::eval_value(ctx, part, env)?;
758                            if let Some(s) = part_val.as_str() {
759                                content.push_str(s);
760                            } else {
761                                content.push_str(&part_val.to_string());
762                            }
763                        }
764                        messages.push(Message {
765                            role,
766                            content,
767                            images: Vec::new(),
768                        });
769                        continue;
770                    }
771                }
772            }
773        }
774        // Not a role form — evaluate it and check if it's a Message value
775        let val = eval::eval_value(ctx, arg, env)?;
776        if let Some(msg) = val.as_message_rc() {
777            messages.push((*msg).clone());
778        } else {
779            return Err(SemaError::eval(
780                "prompt: expected (role content...) or message value",
781            ));
782        }
783    }
784    Ok(Trampoline::Value(Value::prompt(Prompt { messages })))
785}
786
787fn eval_message(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
788    use sema_core::{Message, Role};
789
790    if args.len() < 2 {
791        return Err(SemaError::arity("message", "2+", args.len()));
792    }
793    let role_val = eval::eval_value(ctx, &args[0], env)?;
794    let role = if let Some(spur) = role_val.as_keyword_spur() {
795        let s = resolve(spur);
796        match s.as_str() {
797            "system" => Role::System,
798            "user" => Role::User,
799            "assistant" => Role::Assistant,
800            "tool" => Role::Tool,
801            other => return Err(SemaError::eval(format!("message: unknown role '{other}'"))),
802        }
803    } else {
804        return Err(SemaError::type_error("keyword", role_val.type_name()));
805    };
806    let mut content = String::new();
807    for part in &args[1..] {
808        let val = eval::eval_value(ctx, part, env)?;
809        if let Some(s) = val.as_str() {
810            content.push_str(s);
811        } else {
812            content.push_str(&val.to_string());
813        }
814    }
815    Ok(Trampoline::Value(Value::message(Message {
816        role,
817        content,
818        images: Vec::new(),
819    })))
820}
821
822/// (deftool name "description" {:param {:type :string :description "..."}} handler-expr)
823fn eval_deftool(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
824    if args.len() < 4 {
825        return Err(SemaError::arity("deftool", "4", args.len()));
826    }
827    let name = args[0]
828        .as_symbol()
829        .ok_or_else(|| SemaError::eval("deftool: name must be a symbol"))?
830        .to_string();
831    let description = eval::eval_value(ctx, &args[1], env)?;
832    let description = description
833        .as_str()
834        .ok_or_else(|| SemaError::type_error("string", description.type_name()))?
835        .to_string();
836    let parameters = eval::eval_value(ctx, &args[2], env)?;
837    let handler = eval::eval_value(ctx, &args[3], env)?;
838
839    let tool = Value::tool_def(ToolDefinition {
840        name: name.clone(),
841        description,
842        parameters,
843        handler,
844    });
845    env.set(intern(&name), tool.clone());
846    Ok(Trampoline::Value(tool))
847}
848
849/// (defagent name {:system "..." :tools [...] :max-turns N :model "..."})
850fn eval_defagent(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
851    if args.len() != 2 {
852        return Err(SemaError::arity("defagent", "2", args.len()));
853    }
854    let name = args[0]
855        .as_symbol()
856        .ok_or_else(|| SemaError::eval("defagent: name must be a symbol"))?
857        .to_string();
858    let opts = eval::eval_value(ctx, &args[1], env)?;
859    let opts_map = opts
860        .as_map_rc()
861        .ok_or_else(|| SemaError::type_error("map", opts.type_name()))?;
862
863    let system = opts_map
864        .get(&Value::keyword("system"))
865        .and_then(|v| v.as_str().map(|s| s.to_string()))
866        .unwrap_or_default();
867
868    let tools = opts_map
869        .get(&Value::keyword("tools"))
870        .map(|v| {
871            if let Some(l) = v.as_list() {
872                l.to_vec()
873            } else if let Some(v) = v.as_vector() {
874                v.to_vec()
875            } else {
876                vec![]
877            }
878        })
879        .unwrap_or_default();
880
881    let max_turns = opts_map
882        .get(&Value::keyword("max-turns"))
883        .and_then(|v| v.as_int())
884        .unwrap_or(10) as usize;
885
886    let model = opts_map
887        .get(&Value::keyword("model"))
888        .and_then(|v| v.as_str().map(|s| s.to_string()))
889        .unwrap_or_default();
890
891    let agent = Value::agent(Agent {
892        name: name.clone(),
893        system,
894        tools,
895        max_turns,
896        model,
897    });
898    env.set(intern(&name), agent.clone());
899    Ok(Trampoline::Value(agent))
900}
901
902/// (throw value) — raise a user exception
903fn eval_throw(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
904    if args.len() != 1 {
905        return Err(SemaError::arity("throw", "1", args.len()));
906    }
907    let val = eval::eval_value(ctx, &args[0], env)?;
908    Err(SemaError::UserException(val))
909}
910
911/// (try body... (catch e handler-body...))
912fn eval_try(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
913    let sf = special_forms();
914    if args.is_empty() {
915        return Err(SemaError::arity("try", "1+", 0));
916    }
917
918    // Last arg must be (catch var handler...)
919    let last = &args[args.len() - 1];
920    let catch_form = last
921        .as_list()
922        .ok_or_else(|| SemaError::eval("try: last argument must be (catch var handler...)"))?;
923    if catch_form.is_empty() {
924        return Err(SemaError::eval("try: catch form is empty"));
925    }
926    let is_catch = catch_form[0]
927        .as_symbol_spur()
928        .is_some_and(|s| s == sf.catch);
929    if !is_catch {
930        return Err(SemaError::eval(
931            "try: last argument must be (catch var handler...)",
932        ));
933    }
934    if catch_form.len() < 3 {
935        return Err(SemaError::eval("try: catch needs (catch var handler...)"));
936    }
937    let catch_var = catch_form[1]
938        .as_symbol()
939        .ok_or_else(|| SemaError::eval("try: catch variable must be a symbol"))?
940        .to_string();
941    let handler_body = &catch_form[2..];
942
943    // Evaluate body exprs (not the catch form)
944    let body = &args[..args.len() - 1];
945    let body_result: Result<Value, SemaError> = (|| {
946        let mut result = Value::nil();
947        for expr in body {
948            result = eval::eval_value(ctx, expr, env)?;
949        }
950        Ok(result)
951    })();
952
953    match body_result {
954        Ok(val) => Ok(Trampoline::Value(val)),
955        Err(err) => {
956            // Convert error to a Sema value (map)
957            let err_val = error_to_value(&err);
958            let catch_env = Env::with_parent(Rc::new(env.clone()));
959            catch_env.set(intern(&catch_var), err_val);
960            // Eval handler with TCO on last expr
961            for expr in &handler_body[..handler_body.len() - 1] {
962                eval::eval_value(ctx, expr, &catch_env)?;
963            }
964            Ok(Trampoline::Eval(
965                handler_body.last().unwrap().clone(),
966                catch_env,
967            ))
968        }
969    }
970}
971
972/// Convert a SemaError into a Sema map value
973fn error_to_value(err: &SemaError) -> Value {
974    use std::collections::BTreeMap;
975
976    // Extract stack trace if present, then work with the inner error
977    let trace = err.stack_trace().cloned();
978    let inner = err.inner();
979
980    let mut map = BTreeMap::new();
981    match inner {
982        SemaError::Reader { message, span } => {
983            map.insert(Value::keyword("type"), Value::keyword("reader"));
984            map.insert(
985                Value::keyword("message"),
986                Value::string(&format!("{message} at {span}")),
987            );
988        }
989        SemaError::Eval(msg) => {
990            map.insert(Value::keyword("type"), Value::keyword("eval"));
991            map.insert(Value::keyword("message"), Value::string(msg));
992        }
993        SemaError::Type { expected, got } => {
994            map.insert(Value::keyword("type"), Value::keyword("type-error"));
995            map.insert(
996                Value::keyword("message"),
997                Value::string(&format!("expected {expected}, got {got}")),
998            );
999            map.insert(Value::keyword("expected"), Value::string(expected));
1000            map.insert(Value::keyword("got"), Value::string(got));
1001        }
1002        SemaError::Arity {
1003            name,
1004            expected,
1005            got,
1006        } => {
1007            map.insert(Value::keyword("type"), Value::keyword("arity"));
1008            map.insert(
1009                Value::keyword("message"),
1010                Value::string(&format!("{name} expects {expected} args, got {got}")),
1011            );
1012        }
1013        SemaError::Unbound(name) => {
1014            map.insert(Value::keyword("type"), Value::keyword("unbound"));
1015            map.insert(
1016                Value::keyword("message"),
1017                Value::string(&format!("Unbound variable: {name}")),
1018            );
1019            map.insert(Value::keyword("name"), Value::string(name));
1020        }
1021        SemaError::Llm(msg) => {
1022            map.insert(Value::keyword("type"), Value::keyword("llm"));
1023            map.insert(Value::keyword("message"), Value::string(msg));
1024        }
1025        SemaError::Io(msg) => {
1026            map.insert(Value::keyword("type"), Value::keyword("io"));
1027            map.insert(Value::keyword("message"), Value::string(msg));
1028        }
1029        SemaError::PermissionDenied {
1030            function,
1031            capability,
1032        } => {
1033            map.insert(Value::keyword("type"), Value::keyword("permission-denied"));
1034            map.insert(
1035                Value::keyword("message"),
1036                Value::string(&format!(
1037                    "Permission denied: {function} requires '{capability}' capability"
1038                )),
1039            );
1040            map.insert(Value::keyword("function"), Value::string(function));
1041            map.insert(Value::keyword("capability"), Value::string(capability));
1042        }
1043        SemaError::UserException(val) => {
1044            map.insert(Value::keyword("type"), Value::keyword("user"));
1045            map.insert(Value::keyword("message"), Value::string(&val.to_string()));
1046            map.insert(Value::keyword("value"), val.clone());
1047        }
1048        SemaError::PathDenied { function, path } => {
1049            map.insert(Value::keyword("type"), Value::keyword("permission-denied"));
1050            map.insert(
1051                Value::keyword("message"),
1052                Value::string(&format!(
1053                    "Permission denied: {function} — path '{path}' is outside allowed directories"
1054                )),
1055            );
1056            map.insert(Value::keyword("function"), Value::string(function));
1057            map.insert(Value::keyword("path"), Value::string(path));
1058        }
1059        SemaError::WithTrace { .. } => unreachable!("inner() already unwraps WithTrace"),
1060        SemaError::WithContext { .. } => unreachable!("inner() already unwraps WithContext"),
1061    }
1062
1063    // Add stack trace as list of maps
1064    if let Some(st) = trace {
1065        let frames: Vec<Value> =
1066            st.0.iter()
1067                .map(|frame| {
1068                    let mut fm = BTreeMap::new();
1069                    fm.insert(Value::keyword("name"), Value::string(&frame.name));
1070                    if let Some(ref file) = frame.file {
1071                        fm.insert(
1072                            Value::keyword("file"),
1073                            Value::string(&file.to_string_lossy()),
1074                        );
1075                    }
1076                    if let Some(ref span) = frame.span {
1077                        fm.insert(Value::keyword("line"), Value::int(span.line as i64));
1078                        fm.insert(Value::keyword("col"), Value::int(span.col as i64));
1079                    }
1080                    Value::map(fm)
1081                })
1082                .collect();
1083        map.insert(Value::keyword("stack-trace"), Value::list(frames));
1084    }
1085
1086    Value::map(map)
1087}
1088
1089/// (module name (export sym1 sym2 ...) body...)
1090fn eval_module(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
1091    let sf = special_forms();
1092    if args.len() < 2 {
1093        return Err(SemaError::arity("module", "2+", args.len()));
1094    }
1095    // Parse module name (just for documentation, not used in resolution)
1096    let _name = args[0]
1097        .as_symbol()
1098        .ok_or_else(|| SemaError::eval("module: name must be a symbol"))?;
1099
1100    // Parse export list: (export sym1 sym2 ...)
1101    let export_list = args[1]
1102        .as_list()
1103        .ok_or_else(|| SemaError::eval("module: second argument must be (export sym1 sym2 ...)"))?;
1104    if export_list.is_empty() || export_list[0].as_symbol_spur() != Some(sf.export) {
1105        return Err(SemaError::eval(
1106            "module: second argument must start with 'export'",
1107        ));
1108    }
1109    let export_names: Vec<String> = export_list[1..]
1110        .iter()
1111        .map(|v| {
1112            v.as_symbol()
1113                .map(|s| s.to_string())
1114                .ok_or_else(|| SemaError::eval("module: export names must be symbols"))
1115        })
1116        .collect::<Result<_, _>>()?;
1117
1118    ctx.set_module_exports(export_names);
1119
1120    // Evaluate body
1121    let body = &args[2..];
1122    if body.is_empty() {
1123        return Ok(Trampoline::Value(Value::nil()));
1124    }
1125    for expr in &body[..body.len() - 1] {
1126        eval::eval_value(ctx, expr, env)?;
1127    }
1128    Ok(Trampoline::Eval(body.last().unwrap().clone(), env.clone()))
1129}
1130
1131/// (import "path.sema") or (import "path.sema" sym1 sym2)
1132fn eval_import(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
1133    if args.is_empty() {
1134        return Err(SemaError::arity("import", "1+", 0));
1135    }
1136    let path_val = eval::eval_value(ctx, &args[0], env)?;
1137    let path_str = path_val
1138        .as_str()
1139        .ok_or_else(|| SemaError::type_error("string", path_val.type_name()))?;
1140
1141    // Resolve path relative to current file
1142    let resolved = if std::path::Path::new(path_str).is_absolute() {
1143        std::path::PathBuf::from(path_str)
1144    } else if let Some(dir) = ctx.current_file_dir() {
1145        dir.join(path_str)
1146    } else {
1147        std::path::PathBuf::from(path_str)
1148    };
1149
1150    // Selective import names
1151    let selective: Vec<String> = args[1..]
1152        .iter()
1153        .map(|v| {
1154            v.as_symbol()
1155                .map(|s| s.to_string())
1156                .ok_or_else(|| SemaError::eval("import: selective names must be symbols"))
1157        })
1158        .collect::<Result<_, _>>()?;
1159
1160    // Check cache for preloaded modules (before canonicalize, which requires a real file).
1161    if let Some(cached) = ctx.get_cached_module(&resolved) {
1162        copy_exports_to_env(&cached, &selective, env)?;
1163        return Ok(Trampoline::Value(Value::nil()));
1164    }
1165
1166    let canonical = resolved
1167        .canonicalize()
1168        .map_err(|e| SemaError::Io(format!("import {path_str}: {e}")))?;
1169
1170    // Check cache for on-disk modules
1171    if let Some(cached) = ctx.get_cached_module(&canonical) {
1172        copy_exports_to_env(&cached, &selective, env)?;
1173        return Ok(Trampoline::Value(Value::nil()));
1174    }
1175
1176    ctx.begin_module_load(&canonical)?;
1177
1178    let load_result: Result<std::collections::BTreeMap<String, Value>, SemaError> = (|| {
1179        // Load and evaluate the module
1180        let content = std::fs::read_to_string(&canonical)
1181            .map_err(|e| SemaError::Io(format!("import {path_str}: {e}")))?;
1182        let (exprs, spans) = sema_reader::read_many_with_spans(&content)?;
1183        ctx.merge_span_table(spans);
1184
1185        let module_env = eval::create_module_env(env);
1186        ctx.push_file_path(canonical.clone());
1187        ctx.clear_module_exports();
1188
1189        let eval_result = (|| {
1190            for expr in &exprs {
1191                eval::eval_value(ctx, expr, &module_env)?;
1192            }
1193            Ok(())
1194        })();
1195
1196        ctx.pop_file_path();
1197
1198        // Always pop export scope, even if module evaluation fails.
1199        let declared = ctx.take_module_exports();
1200        eval_result?;
1201
1202        Ok(collect_module_exports(&module_env, declared.as_deref()))
1203    })();
1204
1205    ctx.end_module_load(&canonical);
1206    let exports = load_result?;
1207
1208    // Cache
1209    ctx.cache_module(canonical, exports.clone());
1210
1211    // Copy to caller env
1212    copy_exports_to_env(&exports, &selective, env)?;
1213
1214    Ok(Trampoline::Value(Value::nil()))
1215}
1216
1217/// Collect exported bindings from a module env
1218fn collect_module_exports(
1219    module_env: &Env,
1220    declared: Option<&[String]>,
1221) -> std::collections::BTreeMap<String, Value> {
1222    let bindings = module_env.bindings.borrow();
1223    match declared {
1224        Some(names) => {
1225            let mut exports = std::collections::BTreeMap::new();
1226            for name in names {
1227                let spur = intern(name);
1228                if let Some(val) = bindings.get(&spur) {
1229                    exports.insert(name.clone(), val.clone());
1230                }
1231            }
1232            exports
1233        }
1234        None => {
1235            let mut exports = std::collections::BTreeMap::new();
1236            for (spur, val) in bindings.iter() {
1237                exports.insert(resolve(*spur), val.clone());
1238            }
1239            exports
1240        }
1241    }
1242}
1243
1244/// Copy exports into the caller environment
1245fn copy_exports_to_env(
1246    exports: &std::collections::BTreeMap<String, Value>,
1247    selective: &[String],
1248    env: &Env,
1249) -> Result<(), SemaError> {
1250    if selective.is_empty() {
1251        for (name, val) in exports {
1252            env.set(intern(name), val.clone());
1253        }
1254    } else {
1255        for name in selective {
1256            let val = exports.get(name).ok_or_else(|| {
1257                SemaError::eval(format!("import: module does not export '{name}'"))
1258            })?;
1259            env.set(intern(name), val.clone());
1260        }
1261    }
1262    Ok(())
1263}
1264
1265/// (load "file.sema") — read and evaluate a file in the current environment
1266fn eval_load(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
1267    if args.len() != 1 {
1268        return Err(SemaError::arity("load", "1", args.len()));
1269    }
1270    ctx.sandbox.check(sema_core::Caps::FS_READ, "load")?;
1271    let path_val = eval::eval_value(ctx, &args[0], env)?;
1272    let path_str = path_val
1273        .as_str()
1274        .ok_or_else(|| SemaError::type_error("string", path_val.type_name()))?;
1275
1276    // Resolve path relative to current file
1277    let resolved = if std::path::Path::new(path_str).is_absolute() {
1278        std::path::PathBuf::from(path_str)
1279    } else if let Some(dir) = ctx.current_file_dir() {
1280        dir.join(path_str)
1281    } else {
1282        std::path::PathBuf::from(path_str)
1283    };
1284
1285    let content = std::fs::read_to_string(&resolved)
1286        .map_err(|e| SemaError::Io(format!("load {}: {e}", resolved.display())))?;
1287    let (exprs, spans) = sema_reader::read_many_with_spans(&content)?;
1288    ctx.merge_span_table(spans);
1289
1290    // Push the file path so nested load/import resolves correctly
1291    let canonical = resolved.canonicalize().ok();
1292    if let Some(path) = canonical.clone() {
1293        ctx.push_file_path(path);
1294    }
1295
1296    let mut result = Value::nil();
1297    let eval_result = (|| {
1298        for expr in &exprs {
1299            result = eval::eval_value(ctx, expr, env)?;
1300        }
1301        Ok(result.clone())
1302    })();
1303
1304    // Pop regardless of success/failure
1305    if canonical.is_some() {
1306        ctx.pop_file_path();
1307    }
1308
1309    eval_result?;
1310    Ok(Trampoline::Value(result))
1311}
1312
1313/// (case key-expr ((datum ...) body ...) ... (else body ...))
1314fn eval_case(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
1315    let sf = special_forms();
1316    if args.len() < 2 {
1317        return Err(SemaError::arity("case", "2+", args.len()));
1318    }
1319    let key = eval::eval_value(ctx, &args[0], env)?;
1320
1321    for clause in &args[1..] {
1322        let items = clause
1323            .as_list()
1324            .ok_or_else(|| SemaError::eval("case: clause must be a list"))?;
1325        if items.is_empty() {
1326            return Err(SemaError::eval("case: clause must not be empty"));
1327        }
1328        // (else body...)
1329        let is_else = items[0].as_symbol_spur() == Some(sf.else_);
1330        if is_else {
1331            if items.len() == 1 {
1332                return Ok(Trampoline::Value(Value::nil()));
1333            }
1334            for expr in &items[1..items.len() - 1] {
1335                eval::eval_value(ctx, expr, env)?;
1336            }
1337            return Ok(Trampoline::Eval(items.last().unwrap().clone(), env.clone()));
1338        }
1339        // ((datum ...) body...)
1340        let datums = items[0]
1341            .as_list()
1342            .ok_or_else(|| SemaError::eval("case: datums must be a list"))?;
1343        if datums.contains(&key) {
1344            if items.len() == 1 {
1345                return Ok(Trampoline::Value(Value::nil()));
1346            }
1347            for expr in &items[1..items.len() - 1] {
1348                eval::eval_value(ctx, expr, env)?;
1349            }
1350            return Ok(Trampoline::Eval(items.last().unwrap().clone(), env.clone()));
1351        }
1352    }
1353    Ok(Trampoline::Value(Value::nil()))
1354}
1355
1356/// (eval expr) — evaluate a quoted expression
1357fn eval_eval(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
1358    if args.len() != 1 {
1359        return Err(SemaError::arity("eval", "1", args.len()));
1360    }
1361    let expr = eval::eval_value(ctx, &args[0], env)?;
1362    Ok(Trampoline::Eval(expr, env.clone()))
1363}
1364
1365/// (macroexpand '(macro-call args...)) — expand a macro once without evaluating
1366fn eval_macroexpand(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
1367    if args.len() != 1 {
1368        return Err(SemaError::arity("macroexpand", "1", args.len()));
1369    }
1370    let form = eval::eval_value(ctx, &args[0], env)?;
1371    // Check if it's a list starting with a macro name
1372    if let Some(items) = form.as_list() {
1373        if !items.is_empty() {
1374            if let Some(spur) = items[0].as_symbol_spur() {
1375                if let Some(mac_val) = env.get(spur) {
1376                    if let Some(mac) = mac_val.as_macro_rc() {
1377                        let expanded = eval::apply_macro(ctx, &mac, &items[1..], env)?;
1378                        return Ok(Trampoline::Value(expanded));
1379                    }
1380                }
1381            }
1382        }
1383    }
1384    // Not a macro form — return as-is
1385    Ok(Trampoline::Value(form))
1386}
1387
1388/// (do ((var init step) ...) (test result ...) body ...)
1389fn eval_do(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
1390    if args.len() < 2 {
1391        return Err(SemaError::arity("do", "2+", args.len()));
1392    }
1393
1394    let bindings = args[0]
1395        .as_list()
1396        .ok_or_else(|| SemaError::eval("do: bindings must be a list"))?;
1397    let test_clause = args[1]
1398        .as_list()
1399        .ok_or_else(|| SemaError::eval("do: test clause must be a list"))?;
1400    if test_clause.is_empty() {
1401        return Err(SemaError::eval("do: test clause must not be empty"));
1402    }
1403    let body = &args[2..];
1404
1405    let mut var_names = Vec::new();
1406    let mut step_exprs: Vec<Option<Value>> = Vec::new();
1407
1408    let loop_env = Env::with_parent(Rc::new(env.clone()));
1409
1410    for binding in bindings {
1411        let parts = binding
1412            .as_list()
1413            .ok_or_else(|| SemaError::eval("do: each binding must be a list"))?;
1414        if parts.len() < 2 || parts.len() > 3 {
1415            return Err(SemaError::eval(
1416                "do: binding must be (var init) or (var init step)",
1417            ));
1418        }
1419        let name_spur = parts[0]
1420            .as_symbol_spur()
1421            .ok_or_else(|| SemaError::eval("do: variable name must be a symbol"))?;
1422        let init_val = eval::eval_value(ctx, &parts[1], env)?;
1423        let step = if parts.len() == 3 {
1424            Some(parts[2].clone())
1425        } else {
1426            None
1427        };
1428        loop_env.set(name_spur, init_val);
1429        var_names.push(name_spur);
1430        step_exprs.push(step);
1431    }
1432
1433    let mut new_vals: Vec<Option<Value>> = vec![None; var_names.len()];
1434    loop {
1435        let test_result = eval::eval_value(ctx, &test_clause[0], &loop_env)?;
1436        if test_result.is_truthy() {
1437            if test_clause.len() == 1 {
1438                return Ok(Trampoline::Value(Value::nil()));
1439            }
1440            for expr in &test_clause[1..test_clause.len() - 1] {
1441                eval::eval_value(ctx, expr, &loop_env)?;
1442            }
1443            return Ok(Trampoline::Eval(
1444                test_clause.last().unwrap().clone(),
1445                loop_env,
1446            ));
1447        }
1448
1449        for expr in body {
1450            eval::eval_value(ctx, expr, &loop_env)?;
1451        }
1452
1453        // Compute all step values before updating (parallel assignment)
1454        for (i, step) in step_exprs.iter().enumerate() {
1455            new_vals[i] = match step {
1456                Some(expr) => Some(eval::eval_value(ctx, expr, &loop_env)?),
1457                None => None,
1458            };
1459        }
1460
1461        for (i, name) in var_names.iter().enumerate() {
1462            if let Some(val) = new_vals[i].take() {
1463                loop_env.set(*name, val);
1464            }
1465        }
1466    }
1467}
1468
1469/// (delay expr) — create a lazy promise
1470fn eval_delay(args: &[Value], env: &Env) -> Result<Trampoline, SemaError> {
1471    if args.len() != 1 {
1472        return Err(SemaError::arity("delay", "1", args.len()));
1473    }
1474    let lambda = Value::lambda(Lambda {
1475        params: vec![],
1476        rest_param: None,
1477        body: vec![args[0].clone()],
1478        env: env.clone(),
1479        name: None,
1480    });
1481    let thunk = Thunk {
1482        body: lambda,
1483        forced: std::cell::RefCell::new(None),
1484    };
1485    Ok(Trampoline::Value(Value::thunk(thunk)))
1486}
1487
1488/// (force promise) — evaluate a promise, memoizing the result
1489fn eval_force(args: &[Value], env: &Env, ctx: &EvalContext) -> Result<Trampoline, SemaError> {
1490    if args.len() != 1 {
1491        return Err(SemaError::arity("force", "1", args.len()));
1492    }
1493    let val = eval::eval_value(ctx, &args[0], env)?;
1494    if let Some(thunk) = val.as_thunk_rc() {
1495        if let Some(cached) = &*thunk.forced.borrow() {
1496            return Ok(Trampoline::Value(cached.clone()));
1497        }
1498        let result = if let Some(lambda) = thunk.body.as_lambda_rc() {
1499            let thunk_env = Env::with_parent(Rc::new(lambda.env.clone()));
1500            let mut res = Value::nil();
1501            for expr in &lambda.body {
1502                res = eval::eval_value(ctx, expr, &thunk_env)?;
1503            }
1504            res
1505        } else {
1506            eval::eval_value(ctx, &thunk.body, env)?
1507        };
1508        *thunk.forced.borrow_mut() = Some(result.clone());
1509        Ok(Trampoline::Value(result))
1510    } else {
1511        Ok(Trampoline::Value(val))
1512    }
1513}
1514
1515/// (define-record-type <name> (<ctor> <field> ...) <pred> (<field> <accessor> [<mutator>]) ...)
1516fn eval_define_record_type(args: &[Value], env: &Env) -> Result<Trampoline, SemaError> {
1517    if args.len() < 3 {
1518        return Err(SemaError::eval(
1519            "define-record-type: requires at least type name, constructor, and predicate",
1520        ));
1521    }
1522
1523    let type_name = args[0]
1524        .as_symbol()
1525        .ok_or_else(|| SemaError::eval("define-record-type: type name must be a symbol"))?;
1526    let type_tag = intern(&type_name);
1527
1528    let ctor_spec = args[1]
1529        .as_list()
1530        .ok_or_else(|| SemaError::eval("define-record-type: constructor spec must be a list"))?;
1531    if ctor_spec.is_empty() {
1532        return Err(SemaError::eval(
1533            "define-record-type: constructor spec must have a name",
1534        ));
1535    }
1536    let ctor_name = ctor_spec[0]
1537        .as_symbol()
1538        .ok_or_else(|| SemaError::eval("define-record-type: constructor name must be a symbol"))?;
1539    let field_names: Vec<String> = ctor_spec[1..]
1540        .iter()
1541        .map(|v| {
1542            v.as_symbol()
1543                .ok_or_else(|| SemaError::eval("define-record-type: field name must be a symbol"))
1544        })
1545        .collect::<Result<_, _>>()?;
1546    let field_count = field_names.len();
1547
1548    let pred_name = args[2]
1549        .as_symbol()
1550        .ok_or_else(|| SemaError::eval("define-record-type: predicate must be a symbol"))?;
1551
1552    let ctor_name_clone = ctor_name.clone();
1553    env.set_str(
1554        &ctor_name,
1555        Value::native_fn(sema_core::NativeFn::simple(
1556            ctor_name.clone(),
1557            move |args: &[Value]| {
1558                if args.len() != field_count {
1559                    return Err(SemaError::arity(
1560                        &ctor_name_clone,
1561                        field_count.to_string(),
1562                        args.len(),
1563                    ));
1564                }
1565                Ok(Value::record(Record {
1566                    type_tag,
1567                    fields: args.to_vec(),
1568                }))
1569            },
1570        )),
1571    );
1572
1573    let pred_name_for_closure = pred_name.clone();
1574    let pred_name_for_set = pred_name.clone();
1575    env.set_str(
1576        &pred_name_for_set,
1577        Value::native_fn(sema_core::NativeFn::simple(
1578            pred_name,
1579            move |args: &[Value]| {
1580                if args.len() != 1 {
1581                    return Err(SemaError::arity(&pred_name_for_closure, "1", args.len()));
1582                }
1583                Ok(Value::bool(
1584                    args[0].as_record().is_some_and(|r| r.type_tag == type_tag),
1585                ))
1586            },
1587        )),
1588    );
1589
1590    for field_spec_val in &args[3..] {
1591        let field_spec = field_spec_val
1592            .as_list()
1593            .ok_or_else(|| SemaError::eval("define-record-type: field spec must be a list"))?;
1594        if field_spec.len() < 2 {
1595            return Err(SemaError::eval(
1596                "define-record-type: field spec must have at least (field-name accessor)",
1597            ));
1598        }
1599
1600        let field_name = field_spec[0]
1601            .as_symbol()
1602            .ok_or_else(|| SemaError::eval("define-record-type: field name must be a symbol"))?;
1603
1604        let field_idx = field_names
1605            .iter()
1606            .position(|n| n == &field_name)
1607            .ok_or_else(|| {
1608                SemaError::eval(format!(
1609                    "define-record-type: field '{field_name}' not in constructor"
1610                ))
1611            })?;
1612
1613        let accessor_name = field_spec[1]
1614            .as_symbol()
1615            .ok_or_else(|| SemaError::eval("define-record-type: accessor must be a symbol"))?;
1616
1617        let accessor_name_for_closure = accessor_name.clone();
1618        let accessor_name_for_set = accessor_name.clone();
1619        let type_name_for_err = type_name.clone();
1620        env.set_str(
1621            &accessor_name_for_set,
1622            Value::native_fn(sema_core::NativeFn::simple(
1623                accessor_name,
1624                move |args: &[Value]| {
1625                    if args.len() != 1 {
1626                        return Err(SemaError::arity(
1627                            &accessor_name_for_closure,
1628                            "1",
1629                            args.len(),
1630                        ));
1631                    }
1632                    match args[0].as_record() {
1633                        Some(r) if r.type_tag == type_tag => Ok(r.fields[field_idx].clone()),
1634                        _ => Err(SemaError::type_error(
1635                            &type_name_for_err,
1636                            args[0].type_name(),
1637                        )),
1638                    }
1639                },
1640            )),
1641        );
1642    }
1643
1644    Ok(Trampoline::Value(Value::nil()))
1645}
1646
1647/// Parse parameter list, handling rest params (e.g., `(a b . rest)`)
1648fn parse_params(names: &[Spur]) -> (Vec<Spur>, Option<Spur>) {
1649    let dot = intern(".");
1650    if let Some(pos) = names.iter().position(|s| *s == dot) {
1651        let params = names[..pos].to_vec();
1652        let rest = if pos + 1 < names.len() {
1653            Some(names[pos + 1])
1654        } else {
1655            None
1656        };
1657        (params, rest)
1658    } else {
1659        (names.to_vec(), None)
1660    }
1661}