1#!/usr/bin/env python3
2"""A self-contained Scheme interpreter in Python, built around Pair and nil."""
3
4from __future__ import annotations
5
6import json
7import math
8import operator
9import sys
10from dataclasses import dataclass
11from functools import reduce
12from pathlib import Path
13from typing import Any, Callable, Iterable, Iterator
14
15
16# 1. Errors & Basic Types ------------------------------------------------
17
18
19class SchemeError(Exception):
20 """Base exception for Scheme evaluation and syntax errors."""
21
22
23class SchemeSyntaxError(SchemeError):
24 pass
25
26
27class IncompleteInput(SchemeSyntaxError):
28 pass
29
30
31class String(str):
32 """A Scheme string literal (distinct from a Scheme symbol, which is a plain str)."""
33
34 def __repr__(self) -> str:
35 return json.dumps(str(self), ensure_ascii=False)
36
37
38Symbol = str
39
40
41class _Unspecified:
42 def __repr__(self) -> str:
43 return "UNSPECIFIED"
44
45
46UNSPECIFIED = _Unspecified()
47
48
49# 2. Pair and nil (S-Expression Linked Lists) ----------------------------
50
51
52class _Nil:
53 """The empty list () in Scheme."""
54
55 def __repr__(self) -> str:
56 return "nil"
57
58 def __str__(self) -> str:
59 return "()"
60
61 def __len__(self) -> int:
62 return 0
63
64 def __iter__(self) -> Iterator[Any]:
65 return iter(())
66
67 def __bool__(self) -> bool:
68 return False
69
70 def map(self, fn: Callable[[Any], Any]) -> _Nil:
71 return self
72
73
74nil = _Nil()
75
76
77class Pair:
78 """A cons cell (first, second) representing Scheme pairs and lists."""
79
80 def __init__(self, first: Any, second: Any = nil) -> None:
81 self.first = first
82 self.second = second
83
84 def __repr__(self) -> str:
85 return f"Pair({self.first!r}, {self.second!r})"
86
87 def __str__(self) -> str:
88 return to_scheme(self)
89
90 def __len__(self) -> int:
91 count = 0
92 current: Any = self
93 while isinstance(current, Pair):
94 count += 1
95 current = current.second
96 if current is not nil:
97 raise SchemeError("length requires a proper list")
98 return count
99
100 def __iter__(self) -> Iterator[Any]:
101 current: Any = self
102 while isinstance(current, Pair):
103 yield current.first
104 current = current.second
105 if current is not nil:
106 raise SchemeError("expected a proper list, got dotted pair")
107
108 def __getitem__(self, index: int) -> Any:
109 if index < 0:
110 raise IndexError("negative index not supported")
111 for i, item in enumerate(self):
112 if i == index:
113 return item
114 raise IndexError("Pair index out of range")
115
116 def __eq__(self, other: object) -> bool:
117 return (
118 isinstance(other, Pair)
119 and self.first == other.first
120 and self.second == other.second
121 )
122
123 def map(self, fn: Callable[[Any], Any]) -> Pair:
124 if not isinstance(self.second, (Pair, _Nil)):
125 raise SchemeError("map requires a proper list")
126 return Pair(fn(self.first), self.second.map(fn))
127
128
129def as_scheme_list(*items: Any) -> Pair | _Nil:
130 """Convert a sequence of Python values into a Scheme linked list."""
131 result: Pair | _Nil = nil
132 for item in reversed(items):
133 result = Pair(item, result)
134 return result
135
136
137def is_symbol(value: Any) -> bool:
138 return isinstance(value, str) and not isinstance(value, String)
139
140
141def is_proper_list(value: Any) -> bool:
142 while isinstance(value, Pair):
143 value = value.second
144 return value is nil
145
146
147def to_scheme(value: Any) -> str:
148 """Format a runtime value using standard Scheme notation."""
149 if value is True:
150 return "#t"
151 if value is False:
152 return "#f"
153 if value is nil:
154 return "()"
155 if value is UNSPECIFIED:
156 return ""
157 if isinstance(value, String):
158 return json.dumps(str(value), ensure_ascii=False)
159 if isinstance(value, str):
160 return value
161 if isinstance(value, Pair):
162 parts: list[str] = []
163 current: Any = value
164 while isinstance(current, Pair):
165 parts.append(to_scheme(current.first))
166 current = current.second
167 if current is not nil:
168 parts.append(".")
169 parts.append(to_scheme(current))
170 return "(" + " ".join(parts) + ")"
171 if isinstance(value, Procedure):
172 return "<procedure>"
173 if callable(value):
174 return "<builtin-procedure>"
175 return str(value)
176
177
178# 3. Lexer & Parser (Read) -----------------------------------------------
179
180
181def tokenize(source: str) -> list[str]:
182 """Split Scheme source code into tokens, ignoring comments and whitespace."""
183 tokens: list[str] = []
184 i = 0
185
186 while i < len(source):
187 char = source[i]
188
189 if char.isspace():
190 i += 1
191 continue
192
193 if char == ";":
194 newline = source.find("\n", i)
195 i = len(source) if newline == -1 else newline + 1
196 continue
197
198 if char in "()'":
199 tokens.append(char)
200 i += 1
201 continue
202
203 if char == '"':
204 start = i
205 i += 1
206 escaped = False
207 while i < len(source):
208 if escaped:
209 escaped = False
210 elif source[i] == "\\":
211 escaped = True
212 elif source[i] == '"':
213 i += 1
214 tokens.append(source[start:i])
215 break
216 i += 1
217 else:
218 raise IncompleteInput("unterminated string literal")
219 continue
220
221 start = i
222 while i < len(source) and not source[i].isspace() and source[i] not in "()'\";":
223 i += 1
224 tokens.append(source[start:i])
225
226 return tokens
227
228
229def atom(token: str) -> Any:
230 """Convert an atomic token into a boolean, String, number, or symbol (str)."""
231 if token == "#t":
232 return True
233 if token == "#f":
234 return False
235 if token.startswith('"'):
236 try:
237 return String(json.loads(token))
238 except json.JSONDecodeError as err:
239 raise SchemeSyntaxError(f"invalid string literal: {token}") from err
240 try:
241 return int(token)
242 except ValueError:
243 try:
244 return float(token)
245 except ValueError:
246 return token
247
248
249class Buffer:
250 """A simple token stream with lookahead for recursive-descent parsing."""
251
252 def __init__(self, tokens: Iterable[str]) -> None:
253 self.tokens = list(tokens)
254 self.index = 0
255
256 @property
257 def more(self) -> bool:
258 return self.index < len(self.tokens)
259
260 def current(self) -> str | None:
261 return self.tokens[self.index] if self.more else None
262
263 def pop_first(self) -> str:
264 if not self.more:
265 raise IncompleteInput("unexpected end of input")
266 token = self.tokens[self.index]
267 self.index += 1
268 return token
269
270
271def scheme_read(src: Buffer) -> Any:
272 """Read the next complete Scheme expression from token buffer src."""
273 if not src.more:
274 raise IncompleteInput("unexpected end of input")
275
276 token = src.pop_first()
277 if token == "(":
278 return read_tail(src)
279 if token == "'":
280 return Pair("quote", Pair(scheme_read(src), nil))
281 if token in (")", "."):
282 raise SchemeSyntaxError(f"unexpected token: {token}")
283 return atom(token)
284
285
286def read_tail(src: Buffer) -> Pair | _Nil:
287 """Read the remainder of a parenthesized list until the matching ')'."""
288 if not src.more:
289 raise IncompleteInput("unexpected end of input while reading list")
290
291 if src.current() == ")":
292 src.pop_first()
293 return nil
294
295 if src.current() == ".":
296 src.pop_first()
297 rest = scheme_read(src)
298 if not src.more:
299 raise IncompleteInput("unexpected end of input after '.'")
300 if src.pop_first() != ")":
301 raise SchemeSyntaxError("expected ')' after dotted tail")
302 return rest
303
304 first = scheme_read(src)
305 rest = read_tail(src)
306 return Pair(first, rest)
307
308
309def parse_program(source: str) -> list[Any]:
310 """Parse all top-level Scheme expressions from source string."""
311 src = Buffer(tokenize(source))
312 expressions: list[Any] = []
313 while src.more:
314 expressions.append(scheme_read(src))
315 return expressions
316
317
318# 4. Environments & Procedures -------------------------------------------
319
320
321class Env(dict[Symbol, Any]):
322 """An environment frame mapping symbols to values, chained to outer."""
323
324 def __init__(
325 self,
326 params: Iterable[Symbol] = (),
327 args: Iterable[Any] = (),
328 outer: Env | None = None,
329 ) -> None:
330 params_list = list(params)
331 args_list = list(args)
332 if len(params_list) != len(args_list):
333 raise SchemeError(
334 f"expected {len(params_list)} arguments, got {len(args_list)}"
335 )
336 super().__init__(zip(params_list, args_list))
337 self.outer = outer
338
339 def find(self, name: Symbol) -> Env:
340 """Return the innermost frame in which name is bound."""
341 if name in self:
342 return self
343 if self.outer is not None:
344 return self.outer.find(name)
345 raise SchemeError(f"unbound symbol: {name}")
346
347
348@dataclass
349class Procedure:
350 """A user-defined Scheme procedure (closure)."""
351
352 params: list[Symbol]
353 body: Pair
354 env: Env
355
356 def __call__(self, *args: Any) -> Any:
357 local_env = Env(self.params, args, self.env)
358 return eval_all(self.body, local_env)
359
360
361# 5. Built-in Procedures & Standard Environment --------------------------
362
363
364def require(condition: bool, message: str) -> None:
365 if not condition:
366 raise SchemeError(message)
367
368
369def is_true(value: Any) -> bool:
370 """In Scheme, only #f is false; everything else (including 0 and ()) is true."""
371 return value is not False
372
373
374def calc_add(*args: Any) -> Any:
375 return sum(args)
376
377
378def calc_sub(*args: Any) -> Any:
379 require(len(args) >= 1, "- requires at least 1 argument")
380 if len(args) == 1:
381 return -args[0]
382 return reduce(operator.sub, args[1:], args[0])
383
384
385def calc_mul(*args: Any) -> Any:
386 return reduce(operator.mul, args, 1)
387
388
389def calc_div(*args: Any) -> Any:
390 require(len(args) >= 1, "/ requires at least 1 argument")
391 try:
392 if len(args) == 1:
393 return 1 / args[0]
394 return reduce(operator.truediv, args[1:], args[0])
395 except ZeroDivisionError as err:
396 raise SchemeError("division by zero") from err
397
398
399def compare(name: str, op: Callable[[Any, Any], bool], *args: Any) -> bool:
400 require(len(args) >= 2, f"{name} requires at least 2 arguments")
401 return all(op(a, b) for a, b in zip(args, args[1:]))
402
403
404def builtin_car(pair: Any) -> Any:
405 require(isinstance(pair, Pair), "car requires a pair")
406 return pair.first
407
408
409def builtin_cdr(pair: Any) -> Any:
410 require(isinstance(pair, Pair), "cdr requires a pair")
411 return pair.second
412
413
414def builtin_append(*lists: Any) -> Pair | _Nil:
415 items: list[Any] = []
416 for lst in lists:
417 require(is_proper_list(lst), "append requires proper lists")
418 items.extend(lst)
419 return as_scheme_list(*items)
420
421
422def builtin_list_ref(lst: Any, index: int) -> Any:
423 require(isinstance(lst, Pair), "list-ref requires a non-empty list")
424 try:
425 return lst[index]
426 except IndexError as err:
427 raise SchemeError(str(err)) from err
428
429
430def builtin_display(value: Any) -> _Unspecified:
431 if isinstance(value, String):
432 print(str(value), end="")
433 else:
434 print(to_scheme(value), end="")
435 return UNSPECIFIED
436
437
438def builtin_newline() -> _Unspecified:
439 print()
440 return UNSPECIFIED
441
442
443def scheme_apply(procedure: Any, args: Pair | _Nil) -> Any:
444 """Apply a built-in function or user-defined Procedure to a Scheme list of args."""
445 require(callable(procedure), f"{to_scheme(procedure)} is not callable")
446 py_args = list(args)
447 try:
448 return procedure(*py_args)
449 except SchemeError:
450 raise
451 except (ArithmeticError, TypeError, ValueError) as err:
452 raise SchemeError(str(err)) from err
453
454
455def standard_env() -> Env:
456 """Create the default global environment with built-in procedures."""
457 env = Env()
458 env.update(
459 {
460 "+": calc_add,
461 "-": calc_sub,
462 "*": calc_mul,
463 "/": calc_div,
464 "abs": abs,
465 "max": max,
466 "min": min,
467 "round": round,
468 "sqrt": math.sqrt,
469 "expt": pow,
470 "quotient": lambda a, b: math.trunc(a / b),
471 "modulo": operator.mod,
472 "=": lambda *xs: compare("=", operator.eq, *xs),
473 "<": lambda *xs: compare("<", operator.lt, *xs),
474 "<=": lambda *xs: compare("<=", operator.le, *xs),
475 ">": lambda *xs: compare(">", operator.gt, *xs),
476 ">=": lambda *xs: compare(">=", operator.ge, *xs),
477 "not": lambda x: x is False,
478 "eq?": lambda a, b: a is b if isinstance(a, Pair) else a == b,
479 "equal?": operator.eq,
480 "number?": lambda x: isinstance(x, (int, float))
481 and not isinstance(x, bool),
482 "symbol?": is_symbol,
483 "string?": lambda x: isinstance(x, String),
484 "boolean?": lambda x: isinstance(x, bool),
485 "procedure?": callable,
486 "pair?": lambda x: isinstance(x, Pair),
487 "null?": lambda x: x is nil,
488 "list?": is_proper_list,
489 "cons": Pair,
490 "car": builtin_car,
491 "cdr": builtin_cdr,
492 "list": as_scheme_list,
493 "append": builtin_append,
494 "length": len,
495 "list-ref": builtin_list_ref,
496 "apply": lambda proc, args: scheme_apply(proc, args),
497 "map": lambda proc, lst: lst.map(
498 lambda x: scheme_apply(proc, Pair(x, nil))
499 ),
500 "filter": lambda pred, lst: as_scheme_list(
501 *[x for x in lst if is_true(scheme_apply(pred, Pair(x, nil)))]
502 ),
503 "display": builtin_display,
504 "newline": builtin_newline,
505 "pi": math.pi,
506 }
507 )
508 return env
509
510
511# 6. Evaluator & Special Forms (Eval) ------------------------------------
512
513
514def check_form(expr: Pair, min_len: int, max_len: int | None = None) -> None:
515 if not is_proper_list(expr):
516 raise SchemeSyntaxError(f"malformed expression: {to_scheme(expr)}")
517 length = len(expr)
518 if length < min_len or (max_len is not None and length > max_len):
519 raise SchemeSyntaxError(f"bad syntax in {to_scheme(expr)}")
520
521
522def check_params(params: Any) -> list[Symbol]:
523 require(is_proper_list(params), "parameter list must be a proper list")
524 result: list[Symbol] = []
525 for p in params:
526 require(is_symbol(p), f"invalid parameter name: {to_scheme(p)}")
527 require(p not in result, f"duplicate parameter name: {p}")
528 result.append(p)
529 return result
530
531
532def eval_all(expressions: Pair | _Nil, env: Env) -> Any:
533 """Evaluate a sequence of expressions in order and return the last value."""
534 result: Any = UNSPECIFIED
535 for expr in expressions:
536 result = scheme_eval(expr, env)
537 return result
538
539
540def scheme_eval(expr: Any, env: Env) -> Any:
541 """Evaluate a Scheme expression in environment env."""
542 # 1. Symbols are looked up in the environment
543 if is_symbol(expr):
544 return env.find(expr)[expr]
545
546 # 2. Self-evaluating atoms (numbers, booleans, String literals)
547 if not isinstance(expr, Pair):
548 if expr is nil:
549 raise SchemeSyntaxError("cannot evaluate an empty list ()")
550 return expr
551
552 check_form(expr, 1)
553 first = expr.first
554 rest = expr.second
555 form = first if is_symbol(first) else None
556
557 # 3. Special forms
558 if form == "quote":
559 check_form(expr, 2, 2)
560 return rest.first
561
562 if form == "if":
563 check_form(expr, 3, 4)
564 if is_true(scheme_eval(rest.first, env)):
565 return scheme_eval(rest.second.first, env)
566 if len(expr) == 4:
567 return scheme_eval(rest.second.second.first, env)
568 return UNSPECIFIED
569
570 if form == "define":
571 check_form(expr, 3)
572 target = rest.first
573 if is_symbol(target):
574 check_form(expr, 3, 3)
575 env[target] = scheme_eval(rest.second.first, env)
576 return UNSPECIFIED
577 if isinstance(target, Pair) and is_symbol(target.first):
578 name = target.first
579 params = check_params(target.second)
580 env[name] = Procedure(params, rest.second, env)
581 return UNSPECIFIED
582 raise SchemeSyntaxError(f"invalid define target: {to_scheme(target)}")
583
584 if form == "set!":
585 check_form(expr, 3, 3)
586 target = rest.first
587 require(is_symbol(target), "set! target must be a symbol")
588 env.find(target)[target] = scheme_eval(rest.second.first, env)
589 return UNSPECIFIED
590
591 if form == "lambda":
592 check_form(expr, 3)
593 params = check_params(rest.first)
594 return Procedure(params, rest.second, env)
595
596 if form == "begin":
597 return eval_all(rest, env)
598
599 if form == "let":
600 check_form(expr, 3)
601 bindings = rest.first
602 require(is_proper_list(bindings), "let bindings must be a list")
603 names: list[Symbol] = []
604 values: list[Any] = []
605 for binding in bindings:
606 require(
607 isinstance(binding, Pair) and is_proper_list(binding) and len(binding) == 2,
608 f"invalid let binding: {to_scheme(binding)}",
609 )
610 name = binding.first
611 require(is_symbol(name), "let binding name must be a symbol")
612 require(name not in names, f"duplicate let binding: {name}")
613 names.append(name)
614 values.append(scheme_eval(binding.second.first, env))
615 return eval_all(rest.second, Env(names, values, env))
616
617 if form == "and":
618 result: Any = True
619 for sub_expr in rest:
620 result = scheme_eval(sub_expr, env)
621 if not is_true(result):
622 return False
623 return result
624
625 if form == "or":
626 for sub_expr in rest:
627 result = scheme_eval(sub_expr, env)
628 if is_true(result):
629 return result
630 return False
631
632 if form == "cond":
633 for idx, clause in enumerate(rest):
634 require(
635 isinstance(clause, Pair) and is_proper_list(clause),
636 f"invalid cond clause: {to_scheme(clause)}",
637 )
638 is_else = is_symbol(clause.first) and clause.first == "else"
639 if is_else:
640 require(idx == len(rest) - 1, "else must be the last clause in cond")
641 test_val: Any = True
642 else:
643 test_val = scheme_eval(clause.first, env)
644 if is_true(test_val):
645 if clause.second is nil:
646 return test_val
647 return eval_all(clause.second, env)
648 return UNSPECIFIED
649
650 # 4. Ordinary procedure call: evaluate operator and all operands, then apply
651 operator_proc = scheme_eval(first, env)
652 operands = rest.map(lambda sub_expr: scheme_eval(sub_expr, env))
653 return scheme_apply(operator_proc, operands)
654
655
656# 7. REPL & File Execution -----------------------------------------------
657
658
659def evaluate(source: str, env: Env | None = None) -> tuple[list[Any], Env]:
660 """Parse and evaluate all expressions in source string."""
661 if env is None:
662 env = standard_env()
663 results = [scheme_eval(expr, env) for expr in parse_program(source)]
664 return results, env
665
666
667def repl(env: Env | None = None) -> None:
668 """Run an interactive Read-Eval-Print Loop."""
669 if env is None:
670 env = standard_env()
671 buffer_text = ""
672
673 while True:
674 try:
675 prompt = "scm> " if not buffer_text else "... "
676 buffer_text += input(prompt) + "\n"
677 try:
678 expressions = parse_program(buffer_text)
679 except IncompleteInput:
680 continue
681
682 for expr in expressions:
683 value = scheme_eval(expr, env)
684 if value is not UNSPECIFIED:
685 print(to_scheme(value))
686 buffer_text = ""
687 except SchemeError as err:
688 print(f"{type(err).__name__}: {err}")
689 buffer_text = ""
690 except (EOFError, KeyboardInterrupt):
691 print("\nbye")
692 return
693
694
695def run_file(filename: str, env: Env | None = None) -> Env:
696 if env is None:
697 env = standard_env()
698 source = Path(filename).read_text(encoding="utf-8")
699 for expr in parse_program(source):
700 scheme_eval(expr, env)
701 return env
702
703
704if __name__ == "__main__":
705 if len(sys.argv) > 2:
706 raise SystemExit(f"Usage: {sys.argv[0]} [file.scm]")
707 global_env = standard_env()
708 if len(sys.argv) == 2:
709 run_file(sys.argv[1], global_env)
710 else:
711 repl(global_env)