1use smol_str::SmolStr;
2
3use super::host::{self, Str};
4use super::{list, list_elements, rc_exp, rc_val, Exp, Prim1, Prim2, RcExp, Val};
5
6fn err(tag: &str, detail: impl Into<String>) -> Val {
9 Val::Pair(
10 rc_val(Val::Sym(SmolStr::new(tag))),
11 rc_val(Str::val(detail.into())),
12 )
13}
14
15enum Tok {
18 Open,
19 Close,
20 Mark(&'static str),
23 Datum(Val),
24}
25
26struct Lexer<'a> {
27 rest: &'a str,
28}
29
30impl<'a> Lexer<'a> {
31 fn skip_trivia(&mut self) {
32 loop {
33 let trimmed = self.rest.trim_start();
34 if let Some(after) = trimmed.strip_prefix(';') {
35 self.rest = after.split_once('\n').map_or("", |(_, t)| t);
36 continue;
37 }
38 self.rest = trimmed;
39 return;
40 }
41 }
42
43 fn next_tok(&mut self) -> Result<Option<Tok>, Val> {
44 self.skip_trivia();
45 let mut chars = self.rest.chars();
46 let c = match chars.next() {
47 None => return Ok(None),
48 Some(c) => c,
49 };
50 match c {
51 '(' => {
52 self.rest = chars.as_str();
53 Ok(Some(Tok::Open))
54 }
55 ')' => {
56 self.rest = chars.as_str();
57 Ok(Some(Tok::Close))
58 }
59 '\'' => {
60 self.rest = chars.as_str();
61 Ok(Some(Tok::Mark("quote")))
62 }
63 '`' => {
64 self.rest = chars.as_str();
65 Ok(Some(Tok::Mark("quasiquote")))
66 }
67 ',' => {
68 self.rest = chars.as_str();
69 match chars.next() {
70 Some('@') => {
71 self.rest = chars.as_str();
72 Ok(Some(Tok::Mark("unquote-splicing")))
73 }
74 _ => Ok(Some(Tok::Mark("unquote"))),
75 }
76 }
77 '"' => {
78 self.rest = chars.as_str();
79 self.string_literal().map(|v| Some(Tok::Datum(v)))
80 }
81 _ => {
82 let end = self
83 .rest
84 .find(|c: char| c.is_whitespace() || "()';\"`,".contains(c))
85 .unwrap_or(self.rest.len());
86 let (word, tail) = self.rest.split_at(end);
87 self.rest = tail;
88 Ok(Some(Tok::Datum(atom(word))))
89 }
90 }
91 }
92
93 fn string_literal(&mut self) -> Result<Val, Val> {
94 let mut out = String::new();
95 let mut chars = self.rest.chars();
96 loop {
97 match chars.next() {
98 None => return Err(err("read-error", "unterminated string")),
99 Some('"') => {
100 self.rest = chars.as_str();
101 return Ok(Str::val(out));
102 }
103 Some('\\') => match chars.next() {
104 Some('n') => out.push('\n'),
105 Some('t') => out.push('\t'),
106 Some(c @ ('\\' | '"')) => out.push(c),
107 Some(c) => return Err(err("read-error", format!("bad escape \\{c}"))),
108 None => return Err(err("read-error", "unterminated string")),
109 },
110 Some(c) => out.push(c),
111 }
112 }
113 }
114}
115
116fn atom(word: &str) -> Val {
119 if let Ok(n) = word.parse::<i64>() {
120 return Val::Num(n);
121 }
122 if let Ok(x) = word.parse::<f64>() {
123 return Val::Flo(x);
124 }
125 Val::Sym(SmolStr::new(word))
126}
127
128struct Frame {
132 items: Vec<Val>,
133 marks: Vec<&'static str>,
134}
135
136impl Frame {
137 fn accept(&mut self, mut v: Val) {
138 for m in self.marks.drain(..).rev() {
139 v = list(&[Val::Sym(SmolStr::new(m)), v]);
140 }
141 self.items.push(v);
142 }
143}
144
145fn close(items: Vec<Val>) -> Result<Val, Val> {
148 let dot = items
149 .iter()
150 .position(|v| matches!(v, Val::Sym(s) if s == "."));
151 let (elems, tail) = match dot {
152 None => (&items[..], Val::Nil),
153 Some(i) if i + 2 == items.len() && i > 0 => (&items[..i], items[i + 1].clone()),
154 Some(_) => return Err(err("read-error", "misplaced .")),
155 };
156 Ok(elems
157 .iter()
158 .rev()
159 .fold(tail, |t, h| Val::Pair(rc_val(h.clone()), rc_val(t))))
160}
161
162pub fn read(src: &str) -> Result<Vec<Val>, Val> {
164 let mut lex = Lexer { rest: src };
165 let mut stack = vec![Frame {
166 items: Vec::new(),
167 marks: Vec::new(),
168 }];
169
170 while let Some(tok) = lex.next_tok()? {
171 match tok {
172 Tok::Open => stack.push(Frame {
173 items: Vec::new(),
174 marks: Vec::new(),
175 }),
176 Tok::Close => {
177 let done = stack.pop().expect("the base frame is never closed");
178 let v = close(done.items)?;
179 match stack.last_mut() {
180 Some(f) => f.accept(v),
181 None => return Err(err("read-error", "unbalanced )")),
182 }
183 }
184 Tok::Mark(m) => stack.last_mut().expect("a frame is open").marks.push(m),
185 Tok::Datum(v) => stack.last_mut().expect("a frame is open").accept(v),
186 }
187 }
188
189 if stack.len() != 1 {
190 return Err(err("read-error", "unbalanced ("));
191 }
192 let base = stack.pop().expect("the base frame");
193 if !base.marks.is_empty() {
194 return Err(err("read-error", "quote with nothing to quote"));
195 }
196 Ok(base.items)
197}
198
199const SPECIAL: &[&str] = &[
205 "lambda", "let", "if", "quote", "lift", "lift-ref", "run", "code?", "evalms", "catch", "throw",
206 "apply", "lift-fun",
207];
208
209fn prim1(name: &str) -> Option<Prim1> {
210 Prim1::from_name(name)
211}
212
213fn prim2(name: &str) -> Option<Prim2> {
214 Prim2::from_name(name)
215}
216
217enum Job {
218 Lower(Val),
219 Quote(Val),
220 Bind(SmolStr),
221 Unbind(usize),
222 Build(Build),
223}
224
225enum Build {
227 Lam(u16),
228 Let,
229 If,
230 App(usize),
231 Apply,
232 LiftFun,
233 P1(Prim1),
234 P2(Prim2),
235 Lift,
236 LiftRef,
237 Run,
238 IsCode,
239 Evalms,
240 Catch,
241 Throw,
242 Op(u16, usize),
243}
244
245pub fn trans(src: &Val, names: &Val) -> Result<Val, Val> {
248 let mut env = name_list(names)?;
249 let mut jobs = vec![Job::Lower(src.clone())];
250 let mut out: Vec<RcExp> = Vec::new();
251
252 while let Some(job) = jobs.pop() {
253 match job {
254 Job::Bind(n) => env.push(n),
255 Job::Unbind(k) => env.truncate(env.len() - k),
256 Job::Build(b) => build(b, &mut out),
257 Job::Quote(v) => quote(v, &mut jobs, &mut out),
258 Job::Lower(v) => lower(v, &env, &mut jobs, &mut out)?,
259 }
260 }
261
262 match out.pop() {
263 Some(e) if out.is_empty() => Ok(Val::Code(e)),
264 _ => Err(err("trans", "malformed source")),
265 }
266}
267
268fn name_list(v: &Val) -> Result<Vec<SmolStr>, Val> {
269 let mut out = Vec::new();
270 let mut cur = v;
271 loop {
272 match cur {
273 Val::Nil => return Ok(out),
274 Val::Pair(h, t) => {
275 match &**h {
276 Val::Sym(s) => out.push(s.clone()),
277 _ => out.push(SmolStr::new("")),
280 }
281 cur = t;
282 }
283 _ => return Err(err("trans", "environment is not a list")),
284 }
285 }
286}
287
288fn elements(v: &Val) -> Result<Vec<Val>, Val> {
289 list_elements(v).ok_or_else(|| err("trans", "improper form"))
290}
291
292pub fn form_arity(name: &str) -> Option<usize> {
295 match name {
296 "quote" | "lift" | "catch" | "throw" => Some(1),
297 "lambda" | "let" | "lift-ref" | "run" | "code?" | "evalms" | "apply" | "lift-fun" => {
298 Some(2)
299 }
300 "if" => Some(3),
301 _ if prim1(name).is_some() => Some(1),
302 _ if prim2(name).is_some() => Some(2),
303 _ => {
304 let r = host::registry();
305 r.lookup(name).map(|i| r.get(i).arity)
306 }
307 }
308}
309
310fn build(b: Build, out: &mut Vec<RcExp>) {
311 let pop = |out: &mut Vec<RcExp>| out.pop().expect("build: operand");
312 let e = match b {
313 Build::Lam(n) => Exp::Lam(n, pop(out)),
314 Build::Catch => Exp::Catch(pop(out)),
315 Build::Throw => Exp::Throw(pop(out)),
316 Build::Lift => Exp::Lift(pop(out)),
317 Build::P1(op) => Exp::Prim1(op, pop(out)),
318 Build::Let => {
319 let (body, init) = (pop(out), pop(out));
320 Exp::Let(init, body)
321 }
322 Build::App(n) => {
323 let args: Box<[RcExp]> = out.split_off(out.len() - n).into();
324 Exp::App(pop(out), args)
325 }
326 Build::P2(op) => {
327 let (b2, a) = (pop(out), pop(out));
328 Exp::Prim2(op, a, b2)
329 }
330 Build::Apply => {
331 let (args, f) = (pop(out), pop(out));
332 Exp::Apply(f, args)
333 }
334 Build::LiftFun => {
335 let (f, n) = (pop(out), pop(out));
336 Exp::LiftFun(n, f)
337 }
338 Build::LiftRef => {
339 let (b2, a) = (pop(out), pop(out));
340 Exp::LiftRef(a, b2)
341 }
342 Build::Run => {
343 let (e, b2) = (pop(out), pop(out));
344 Exp::Run(b2, e)
345 }
346 Build::IsCode => {
347 let (b2, a) = (pop(out), pop(out));
348 Exp::IsCode(a, b2)
349 }
350 Build::Evalms => {
351 let (e, env) = (pop(out), pop(out));
352 Exp::Evalms(env, e)
353 }
354 Build::If => {
355 let (f, t, c) = (pop(out), pop(out), pop(out));
356 Exp::If(c, t, f)
357 }
358 Build::Op(idx, n) => {
359 let at = out.len() - n;
360 Exp::Op(idx, out.split_off(at))
361 }
362 };
363 out.push(rc_exp(e));
364}
365
366fn quote(v: Val, jobs: &mut Vec<Job>, out: &mut Vec<RcExp>) {
369 match v {
370 Val::Pair(h, t) => {
371 jobs.push(Job::Build(Build::P2(Prim2::Cons)));
372 jobs.push(Job::Quote((*t).clone()));
373 jobs.push(Job::Quote((*h).clone()));
374 }
375 Val::Sym(s) => out.push(rc_exp(Exp::Sym(s))),
376 other => out.push(rc_exp(datum(other))),
377 }
378}
379
380fn datum(v: Val) -> Exp {
384 match v {
385 Val::Num(n) => Exp::Lit(n),
386 Val::Flo(x) => Exp::Flo(x),
387 Val::Atom(a) => Exp::Atom(a),
388 Val::Nil => Exp::Nil,
389 Val::Code(e) => RcExp::unwrap_or_clone(e),
390 other => Exp::Proc(rc_val(other)),
391 }
392}
393
394fn lower(v: Val, env: &[SmolStr], jobs: &mut Vec<Job>, out: &mut Vec<RcExp>) -> Result<(), Val> {
395 let (head, tail) = match v {
396 Val::Sym(ref s) => {
397 return match env.iter().rposition(|n| n == s) {
398 Some(i) => {
399 out.push(rc_exp(Exp::Var(i)));
400 Ok(())
401 }
402 None => Err(err("trans", format!("unbound variable {s}"))),
403 }
404 }
405 Val::Pair(h, t) => ((*h).clone(), (*t).clone()),
406 other => {
407 out.push(rc_exp(datum(other)));
408 return Ok(());
409 }
410 };
411
412 let args = elements(&tail)?;
413 if let Val::Sym(s) = &head {
414 if SPECIAL.contains(&s.as_str()) {
415 return special(s, &args, jobs);
416 }
417 if !env.iter().any(|n| n == s) {
420 if let Some(()) = table(s, &args, jobs)? {
421 return Ok(());
422 }
423 }
424 }
425
426 jobs.push(Job::Build(Build::App(args.len())));
427 for a in args.iter().rev() {
428 jobs.push(Job::Lower(a.clone()));
429 }
430 jobs.push(Job::Lower(head));
431 Ok(())
432}
433
434fn table(name: &str, args: &[Val], jobs: &mut Vec<Job>) -> Result<Option<()>, Val> {
435 if let Some(op) = prim1(name) {
436 let arg = match (op, args.len()) {
438 (Prim1::Receive, 0) => Val::Nil,
439 (_, 1) => args[0].clone(),
440 _ => return Err(err("trans", format!("{name} takes one argument"))),
441 };
442 jobs.push(Job::Build(Build::P1(op)));
443 jobs.push(Job::Lower(arg));
444 return Ok(Some(()));
445 }
446 if let Some(op) = prim2(name) {
447 let [a, b] = args else {
448 return Err(err("trans", format!("{name} takes two arguments")));
449 };
450 jobs.push(Job::Build(Build::P2(op)));
451 jobs.push(Job::Lower(b.clone()));
452 jobs.push(Job::Lower(a.clone()));
453 return Ok(Some(()));
454 }
455 if let Some(idx) = host::registry().lookup(name) {
456 let def = host::registry().get(idx);
457 if args.len() != def.arity {
458 return Err(err(
459 "trans",
460 format!("{name} takes {} arguments", def.arity),
461 ));
462 }
463 jobs.push(Job::Build(Build::Op(idx, args.len())));
464 for a in args.iter().rev() {
465 jobs.push(Job::Lower(a.clone()));
466 }
467 return Ok(Some(()));
468 }
469 Ok(None)
470}
471
472fn special(name: &str, args: &[Val], jobs: &mut Vec<Job>) -> Result<(), Val> {
473 let fixed = |n: usize| -> Result<(), Val> {
474 if args.len() == n {
475 Ok(())
476 } else {
477 Err(err("trans", format!("{name} takes {n} arguments")))
478 }
479 };
480 let mut unary = |b: Build| -> Result<(), Val> {
481 fixed(1)?;
482 jobs.push(Job::Build(b));
483 jobs.push(Job::Lower(args[0].clone()));
484 Ok(())
485 };
486 match name {
487 "quote" => {
488 fixed(1)?;
489 jobs.push(Job::Quote(args[0].clone()));
490 Ok(())
491 }
492 "lift" => unary(Build::Lift),
493 "catch" => unary(Build::Catch),
494 "throw" => unary(Build::Throw),
495
496 "lambda" => {
499 fixed(2)?;
500 let params = elements(&args[0])?;
501 let Some((f, xs)) = params.split_first() else {
502 return Err(err("trans", "lambda takes (self arg ...)"));
503 };
504 let arity = u16::try_from(xs.len())
505 .map_err(|_| err("trans", "lambda takes too many parameters"))?;
506 jobs.push(Job::Build(Build::Lam(arity)));
507 jobs.push(Job::Unbind(xs.len() + 1));
508 jobs.push(Job::Lower(args[1].clone()));
509 for x in xs.iter().rev() {
510 jobs.push(Job::Bind(binder(x)?));
511 }
512 jobs.push(Job::Bind(binder(f)?));
513 Ok(())
514 }
515
516 "let" => {
517 fixed(2)?;
518 let binding = elements(&args[0])?;
519 let [x, init] = binding.as_slice() else {
520 return Err(err("trans", "let takes (name init)"));
521 };
522 jobs.push(Job::Build(Build::Let));
523 jobs.push(Job::Unbind(1));
524 jobs.push(Job::Lower(args[1].clone()));
525 jobs.push(Job::Bind(binder(x)?));
526 jobs.push(Job::Lower(init.clone()));
527 Ok(())
528 }
529
530 "if" => {
531 fixed(3)?;
532 jobs.push(Job::Build(Build::If));
533 for a in args.iter().rev() {
534 jobs.push(Job::Lower(a.clone()));
535 }
536 Ok(())
537 }
538
539 _ => {
540 let b = match name {
541 "lift-ref" => Build::LiftRef,
542 "run" => Build::Run,
543 "code?" => Build::IsCode,
544 "evalms" => Build::Evalms,
545 "apply" => Build::Apply,
546 "lift-fun" => Build::LiftFun,
547 _ => unreachable!("SPECIAL and this match agree"),
548 };
549 fixed(2)?;
550 jobs.push(Job::Build(b));
551 jobs.push(Job::Lower(args[1].clone()));
552 jobs.push(Job::Lower(args[0].clone()));
553
554 Ok(())
555 }
556 }
557}
558
559fn binder(v: &Val) -> Result<SmolStr, Val> {
560 match v {
561 Val::Sym(s) if SPECIAL.contains(&s.as_str()) => {
562 Err(err("trans", format!("{s} is reserved")))
563 }
564 Val::Sym(s) => Ok(s.clone()),
565 _ => Err(err("trans", "binder is not a symbol")),
566 }
567}
568
569pub fn read_op(a: &[Val]) -> host::OpResult {
572 match Str::of(&a[0]) {
573 Some(s) => read(s).map(|forms| list(&forms)),
574 None => Err(err("wrong-type", "read")),
575 }
576}
577
578pub fn trans_op(a: &[Val]) -> host::OpResult {
579 trans(&a[0], &a[1])
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 fn one(src: &str) -> Val {
587 let forms = read(src).expect("reads");
588 assert_eq!(forms.len(), 1);
589 forms[0].clone()
590 }
591
592 fn code(src: &str, names: &[&str]) -> Exp {
593 let names = list(
594 &names
595 .iter()
596 .map(|n| Val::Sym(SmolStr::new(n)))
597 .collect::<Vec<_>>(),
598 );
599 match trans(&one(src), &names).expect("lowers") {
600 Val::Code(e) => RcExp::unwrap_or_clone(e),
601 other => panic!("expected code, got {other}"),
602 }
603 }
604
605 #[test]
606 fn a_comment_and_a_string_do_not_confuse_each_other() {
607 assert_eq!(
608 read("; (not a form\n \"a ; b\" 1").expect("reads"),
609 vec![Str::val("a ; b"), Val::Num(1)]
610 );
611 }
612
613 #[test]
614 fn quote_applies_to_the_next_datum_however_deep() {
615 assert_eq!(one("''x"), one("(quote (quote x))"));
616 assert_eq!(one("'(a . b)"), one("(quote (a . b))"));
617 }
618
619 #[test]
620 fn a_word_is_a_symbol_unless_it_is_a_number() {
621 assert_eq!(one("-"), Val::Sym("-".into()));
622 assert_eq!(one("-2"), Val::Num(-2));
623 assert_eq!(one("1.5"), Val::Flo(1.5));
624 }
625
626 #[test]
627 fn binders_resolve_positionally_and_shadow() {
628 let e = code("(lambda (f x) (let (x (+ x 1)) x))", &[]);
630 assert_eq!(
631 e,
632 Exp::Lam(
633 1,
634 rc_exp(Exp::Let(
635 rc_exp(Exp::Prim2(
636 Prim2::Plus,
637 rc_exp(Exp::Var(1)),
638 rc_exp(Exp::Lit(1))
639 )),
640 rc_exp(Exp::Var(2)),
641 ))
642 )
643 );
644 }
645
646 #[test]
647 fn an_ambient_environment_shifts_every_index() {
648 assert_eq!(code("(cons a b)", &["a", "b"]), {
649 Exp::Prim2(Prim2::Cons, rc_exp(Exp::Var(0)), rc_exp(Exp::Var(1)))
650 });
651 }
652
653 #[test]
654 fn a_primitive_name_is_shadowable_but_a_special_form_is_not() {
655 assert_eq!(
657 code("(car x)", &["car", "x"]),
658 Exp::App(rc_exp(Exp::Var(0)), Box::new([rc_exp(Exp::Var(1))]))
659 );
660 assert!(trans(&one("(lambda (f if) if)"), &Val::Nil).is_err());
661 }
662
663 #[test]
664 fn a_registered_op_lowers_to_an_indexed_call() {
665 let idx = host::registry().lookup("str-len").expect("op present");
666 assert_eq!(
667 code("(str-len \"narju\")", &[]),
668 Exp::Op(
669 idx,
670 vec![rc_exp(Exp::Atom(match Str::val("narju") {
671 Val::Atom(a) => a,
672 _ => unreachable!(),
673 }))]
674 )
675 );
676 }
677
678 #[test]
679 fn a_quoted_list_lowers_to_the_conses_that_build_it() {
680 assert_eq!(code("'(a b)", &[]), code("(cons 'a (cons 'b ()))", &[]));
681 }
682
683 #[test]
684 fn a_live_value_splices_in_as_a_persisted_one() {
685 let cell = Val::Cell(3);
686 let src = list(&[Val::Sym("car".into()), cell.clone()]);
687 let got = match trans(&src, &Val::Nil).expect("lowers") {
688 Val::Code(e) => RcExp::unwrap_or_clone(e),
689 other => panic!("expected code, got {other}"),
690 };
691 assert_eq!(got, Exp::Prim1(Prim1::Car, rc_exp(Exp::Proc(rc_val(cell)))));
692 }
693
694 #[test]
695 fn an_unbound_variable_is_a_catchable_value() {
696 assert!(matches!(
697 trans(&one("(car nope)"), &Val::Nil),
698 Err(Val::Pair(_, _))
699 ));
700 }
701}