kei-lisp 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # kei-lisp
2
2
 
3
+ [![CI](https://github.com/ike-keichan/kei-lisp/actions/workflows/ci.yml/badge.svg)](https://github.com/ike-keichan/kei-lisp/actions/workflows/ci.yml)
3
4
  [![npm version](https://img.shields.io/npm/v/kei-lisp.svg)](https://www.npmjs.com/package/kei-lisp)
4
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
5
6
  [![Node.js](https://img.shields.io/badge/node-%3E%3D24.0.0-brightgreen.svg)](https://nodejs.org/)
@@ -10,6 +11,7 @@ an interactive REPL, or embed it in your application as a library.
10
11
  ## Features
11
12
 
12
13
  - Common Lisp-inspired syntax (`setq`, `defun`, `let`, `cond`, ...)
14
+ - Macros: `defmacro` with backquote/unquote (`` ` ``, `,`, `,@`) and `macroexpand`
13
15
  - CLI tool **and** embeddable library
14
16
  - ESM and CommonJS dual output with TypeScript types
15
17
  - Zero runtime dependencies
@@ -166,8 +168,8 @@ Runnable TypeScript examples live in [`examples/`](./examples/):
166
168
 
167
169
  ```sh
168
170
  pnpm build # build the package once
169
- pnpm exec tsx examples/basic-eval.ts
170
- pnpm exec tsx examples/exit-handling.ts
171
+ node --experimental-strip-types examples/basic-eval.ts
172
+ node --experimental-strip-types examples/exit-handling.ts
171
173
  ```
172
174
 
173
175
  ## Reference
package/dist/cli.cjs CHANGED
@@ -27,7 +27,7 @@ node_v8 = __toESM(node_v8, 1);
27
27
  let node_vm = require("node:vm");
28
28
  node_vm = __toESM(node_vm, 1);
29
29
  //#region package.json
30
- var version = "2.2.0";
30
+ var version = "2.3.0";
31
31
  //#endregion
32
32
  //#region src/runtime/Table/index.ts
33
33
  /**
@@ -699,6 +699,31 @@ var Parser = class Parser extends Object {
699
699
  return 0;
700
700
  }
701
701
  /**
702
+ * Recognizes a backquote (`` ` ``), wraps the following form into `(quasiquote form)`, and returns the token number; invoked from NextState.
703
+ * @return 0
704
+ */
705
+ quasiquote() {
706
+ const anObject = new Cons(this.nextToken(), Cons.nil);
707
+ this.token = new Cons(InterpretedSymbol.of("quasiquote"), anObject);
708
+ return 0;
709
+ }
710
+ /**
711
+ * Recognizes a comma and wraps the following form into `(unquote form)`, or
712
+ * `(unquote-splicing form)` when the comma is immediately followed by `@`
713
+ * (i.e. `,@`); invoked from NextState.
714
+ * @return 0
715
+ */
716
+ unquote() {
717
+ let aSymbol = InterpretedSymbol.of("unquote");
718
+ if (this.peekChar() === "@") {
719
+ this.nextChar();
720
+ aSymbol = InterpretedSymbol.of("unquote-splicing");
721
+ }
722
+ const anObject = new Cons(this.nextToken(), Cons.nil);
723
+ this.token = new Cons(aSymbol, anObject);
724
+ return 0;
725
+ }
726
+ /**
702
727
  * Returns the token number for a quote or for a 0-origin String-type (pseudo-Character); invoked from NextState.
703
728
  * @return the next state number
704
729
  */
@@ -818,7 +843,7 @@ var Parser = class Parser extends Object {
818
843
  aTable.set(String(41), this.nextState(-1, null));
819
844
  aTable.set(String(42), this.nextState(8, "symbolToken"));
820
845
  aTable.set(String(43), this.nextState(7, "sign"));
821
- aTable.set(String(44), this.nextState(8, "symbolToken"));
846
+ aTable.set(String(44), this.nextState(0, "unquote"));
822
847
  aTable.set(String(45), this.nextState(7, "sign"));
823
848
  aTable.set(String(46), this.nextState(-1, null));
824
849
  aTable.set(String(47), this.nextState(8, "symbolToken"));
@@ -830,7 +855,7 @@ var Parser = class Parser extends Object {
830
855
  aTable.set(String(93), this.nextState(-1, null));
831
856
  aTable.set(String(94), this.nextState(8, "symbolToken"));
832
857
  aTable.set(String(95), this.nextState(8, "symbolToken"));
833
- aTable.set(String(96), this.nextState(0, "quote"));
858
+ aTable.set(String(96), this.nextState(0, "quasiquote"));
834
859
  for (const index of IntStream.rangeClosed(97, 122)) aTable.set(String(index), this.nextState(8, "symbolToken"));
835
860
  aTable.set(String(123), this.nextState(-1, "parseList"));
836
861
  aTable.set(String(124), this.nextState(8, "symbolToken"));
@@ -2879,6 +2904,11 @@ var Evaluator = class Evaluator extends Object {
2879
2904
  */
2880
2905
  static buildInFunctions = Evaluator.setup();
2881
2906
  /**
2907
+ * Marker symbol stored as the car of the Cons that represents a macro binding,
2908
+ * distinguishing macros from ordinary `lambda` closures in the environment.
2909
+ */
2910
+ static macroMarker = InterpretedSymbol.of("macro");
2911
+ /**
2882
2912
  * The variable binding environment used during evaluation.
2883
2913
  */
2884
2914
  environment;
@@ -3024,6 +3054,85 @@ var Evaluator = class Evaluator extends Object {
3024
3054
  return variable;
3025
3055
  }
3026
3056
  /**
3057
+ * Implementation of the Lisp `defmacro` special form. Defines a macro: a
3058
+ * transformer whose body receives its arguments unevaluated and returns a
3059
+ * form that is then evaluated in the caller's environment.
3060
+ * @param aCons the argument Cons containing the macro name, parameter list, and body
3061
+ * @return the macro name symbol
3062
+ */
3063
+ defmacro(aCons) {
3064
+ const variable = aCons.car;
3065
+ const lambda = Evaluator.eval(new Cons(InterpretedSymbol.of("lambda"), aCons.cdr), new Table(this.environment), this.streamManager, this.depth, this.plugins);
3066
+ const macro = new Cons(Evaluator.macroMarker, new Cons(lambda, Cons.nil));
3067
+ this.environment.set(variable, macro);
3068
+ return variable;
3069
+ }
3070
+ /**
3071
+ * Returns the macro transformer (a lambda Cons) bound to the given symbol, or
3072
+ * null when the symbol is not bound to a macro. Special-form symbols are never
3073
+ * treated as macros.
3074
+ * @param car the operator position of a call form
3075
+ * @return the macro's lambda Cons, or null
3076
+ */
3077
+ lookupMacro(car) {
3078
+ if (Cons.isNotSymbol(car) || Evaluator.buildInFunctions.has(car)) return null;
3079
+ const value = this.environment.get(car);
3080
+ if (Cons.isCons(value) && value.car === Evaluator.macroMarker) return value.nth(2);
3081
+ return null;
3082
+ }
3083
+ /**
3084
+ * Expands a macro call exactly once by applying its transformer to the
3085
+ * unevaluated argument forms in the macro's captured environment.
3086
+ * @param form the call form whose car names the macro
3087
+ * @param macroLambda the macro's transformer lambda Cons
3088
+ * @return the expansion form
3089
+ */
3090
+ expandMacro1(form, macroLambda) {
3091
+ const capturedEnvironment = macroLambda.last().car;
3092
+ return Applier.apply(macroLambda, form.cdr, capturedEnvironment, this.streamManager, this.depth, this.plugins);
3093
+ }
3094
+ /**
3095
+ * Expands a macro call once and evaluates the resulting form in the current
3096
+ * environment.
3097
+ * @param form the call form whose car names the macro
3098
+ * @param macroLambda the macro's transformer lambda Cons
3099
+ * @return the result of evaluating the expansion
3100
+ */
3101
+ evalMacroCall(form, macroLambda) {
3102
+ const expansion = this.expandMacro1(form, macroLambda);
3103
+ return Evaluator.eval(expansion, this.environment, this.streamManager, this.depth, this.plugins);
3104
+ }
3105
+ /**
3106
+ * Implementation of the Lisp `macroexpand-1` special form. Evaluates its
3107
+ * argument to obtain a form and, when that form is a macro call, expands it
3108
+ * exactly once without evaluating the result.
3109
+ * @param aCons the argument Cons whose car evaluates to the form to expand
3110
+ * @return the once-expanded form, or the form unchanged when it is not a macro call
3111
+ */
3112
+ macroexpand_1(aCons) {
3113
+ const form = Evaluator.eval(aCons.car, this.environment, this.streamManager, this.depth, this.plugins);
3114
+ if (Cons.isNotCons(form)) return form;
3115
+ const macroLambda = this.lookupMacro(form.car);
3116
+ if (macroLambda == null) return form;
3117
+ return this.expandMacro1(form, macroLambda);
3118
+ }
3119
+ /**
3120
+ * Implementation of the Lisp `macroexpand` special form. Evaluates its
3121
+ * argument to obtain a form and repeatedly expands it until the result is no
3122
+ * longer a macro call, without evaluating the result.
3123
+ * @param aCons the argument Cons whose car evaluates to the form to expand
3124
+ * @return the fully expanded form
3125
+ */
3126
+ macroexpand(aCons) {
3127
+ let form = Evaluator.eval(aCons.car, this.environment, this.streamManager, this.depth, this.plugins);
3128
+ while (Cons.isCons(form)) {
3129
+ const macroLambda = this.lookupMacro(form.car);
3130
+ if (macroLambda == null) break;
3131
+ form = this.expandMacro1(form, macroLambda);
3132
+ }
3133
+ return form;
3134
+ }
3135
+ /**
3027
3136
  * Implementation of the Lisp `do` special form (parallel binding update).
3028
3137
  * @param aCons the argument Cons containing bindings, termination clause, and body
3029
3138
  * @return the value of the termination clause's result form
@@ -3135,6 +3244,10 @@ var Evaluator = class Evaluator extends Object {
3135
3244
  if (Cons.isNil(form) || Cons.isNotList(form)) return form;
3136
3245
  const formCons = form;
3137
3246
  if (Cons.isSymbol(formCons.car) && Evaluator.buildInFunctions.has(formCons.car)) return this.specialForm(formCons);
3247
+ if (Cons.isSymbol(formCons.car)) {
3248
+ const macroLambda = this.lookupMacro(formCons.car);
3249
+ if (macroLambda != null) return this.evalMacroCall(formCons, macroLambda);
3250
+ }
3138
3251
  if (Cons.isSymbol(formCons.car) && this.plugins.length > 0) {
3139
3252
  const symbol = formCons.car;
3140
3253
  const plugin = this.plugins.find((p) => p.has(symbol));
@@ -3385,6 +3498,94 @@ var Evaluator = class Evaluator extends Object {
3385
3498
  return aCons.car;
3386
3499
  }
3387
3500
  /**
3501
+ * Implementation of the Lisp `quasiquote` (`` ` ``) special form. Returns the
3502
+ * template with every `unquote` (`,`) and `unquote-splicing` (`,@`) at the
3503
+ * matching nesting level replaced by the evaluation of its operand. Nested
3504
+ * quasiquotes increase the level so inner unquotes are preserved.
3505
+ * @param aCons the argument Cons whose car is the template
3506
+ * @return the constructed form
3507
+ */
3508
+ quasiquote(aCons) {
3509
+ return this.quasiquoteExpand(aCons.car, 1);
3510
+ }
3511
+ /**
3512
+ * Recursively expands a quasiquote template at the given nesting level.
3513
+ * @param template the template to expand
3514
+ * @param level the current quasiquote nesting level (1 is the outermost)
3515
+ * @return the expanded value
3516
+ */
3517
+ quasiquoteExpand(template, level) {
3518
+ if (Cons.isNotCons(template)) return template;
3519
+ const aCons = template;
3520
+ if (aCons.car === InterpretedSymbol.of("unquote")) {
3521
+ if (level === 1) return Evaluator.eval(aCons.nth(2), this.environment, this.streamManager, this.depth, this.plugins);
3522
+ return new Cons(InterpretedSymbol.of("unquote"), new Cons(this.quasiquoteExpand(aCons.nth(2), level - 1), Cons.nil));
3523
+ }
3524
+ if (aCons.car === InterpretedSymbol.of("quasiquote")) return new Cons(InterpretedSymbol.of("quasiquote"), new Cons(this.quasiquoteExpand(aCons.nth(2), level + 1), Cons.nil));
3525
+ return this.quasiquoteList(aCons, level);
3526
+ }
3527
+ /**
3528
+ * Expands the elements of a quasiquoted list, handling `unquote-splicing`
3529
+ * (`,@`) elements and a possible dotted `unquote` (`,`) tail.
3530
+ * @param template the list template to expand
3531
+ * @param level the current quasiquote nesting level
3532
+ * @return the constructed list
3533
+ */
3534
+ quasiquoteList(template, level) {
3535
+ const parts = [];
3536
+ let tail = Cons.nil;
3537
+ let current = template;
3538
+ while (Cons.isCons(current)) {
3539
+ if (current.car === InterpretedSymbol.of("unquote")) {
3540
+ tail = this.quasiquoteExpand(current, level);
3541
+ current = Cons.nil;
3542
+ break;
3543
+ }
3544
+ const head = current.car;
3545
+ if (Cons.isCons(head) && head.car === InterpretedSymbol.of("unquote-splicing")) if (level === 1) this.spliceInto(parts, Evaluator.eval(head.nth(2), this.environment, this.streamManager, this.depth, this.plugins));
3546
+ else parts.push(new Cons(InterpretedSymbol.of("unquote-splicing"), new Cons(this.quasiquoteExpand(head.nth(2), level - 1), Cons.nil)));
3547
+ else parts.push(this.quasiquoteExpand(head, level));
3548
+ current = current.cdr;
3549
+ }
3550
+ if (Cons.isNotNil(current)) tail = current;
3551
+ let result = tail;
3552
+ for (let index = parts.length - 1; index >= 0; index--) result = new Cons(parts[index], result);
3553
+ return result;
3554
+ }
3555
+ /**
3556
+ * Appends the elements of a spliced value (`,@`) onto the accumulator. The
3557
+ * value must be a proper list (or nil); an atom or an improper (dotted) list
3558
+ * is rejected rather than silently dropping the dotted tail.
3559
+ * @param parts the accumulator of list elements
3560
+ * @param value the value produced by an `unquote-splicing` operand
3561
+ */
3562
+ spliceInto(parts, value) {
3563
+ if (Cons.isNil(value)) return null;
3564
+ if (Cons.isNotCons(value)) throw new EvalError(cannotApply("unquote-splicing", value));
3565
+ let current = value;
3566
+ while (Cons.isCons(current)) {
3567
+ parts.push(current.car);
3568
+ current = current.cdr;
3569
+ }
3570
+ if (Cons.isNotNil(current)) throw new EvalError(cannotApply("unquote-splicing", value));
3571
+ return null;
3572
+ }
3573
+ /**
3574
+ * Implementation of the Lisp `unquote` (`,`) special form. Signals an error
3575
+ * because unquote is only meaningful inside a `quasiquote` template.
3576
+ */
3577
+ unquote() {
3578
+ throw new EvalError("unquote (\",\") is only valid inside a quasiquote (\"`\")");
3579
+ }
3580
+ /**
3581
+ * Implementation of the Lisp `unquote-splicing` (`,@`) special form. Signals
3582
+ * an error because unquote-splicing is only meaningful inside a `quasiquote`
3583
+ * template.
3584
+ */
3585
+ unquoteSplicing() {
3586
+ throw new EvalError("unquote-splicing (\",@\") is only valid inside a quasiquote (\"`\")");
3587
+ }
3588
+ /**
3388
3589
  * Implementation of the Lisp `rplaca` special form; destructively replaces the car of a Cons.
3389
3590
  * @param args the argument Cons containing the target Cons expression and the new car value
3390
3591
  * @return the modified Cons
@@ -3462,6 +3663,7 @@ var Evaluator = class Evaluator extends Object {
3462
3663
  ["apply", "apply_lisp"],
3463
3664
  ["bind", "bind"],
3464
3665
  ["cond", "cond"],
3666
+ ["defmacro", "defmacro"],
3465
3667
  ["defun", "defun"],
3466
3668
  ["do", "do_"],
3467
3669
  ["dolist", "doList"],
@@ -3473,6 +3675,8 @@ var Evaluator = class Evaluator extends Object {
3473
3675
  ["lambda", "lambda"],
3474
3676
  ["let", "let"],
3475
3677
  ["let*", "letStar"],
3678
+ ["macroexpand", "macroexpand"],
3679
+ ["macroexpand-1", "macroexpand_1"],
3476
3680
  ["not", "not"],
3477
3681
  ["notrace", "notrace"],
3478
3682
  ["or", "or"],
@@ -3481,6 +3685,7 @@ var Evaluator = class Evaluator extends Object {
3481
3685
  ["princ", "princ"],
3482
3686
  ["print", "print"],
3483
3687
  ["push", "push_"],
3688
+ ["quasiquote", "quasiquote"],
3484
3689
  ["quote", "quote"],
3485
3690
  ["rplaca", "rplaca"],
3486
3691
  ["rplacd", "rplacd"],
@@ -3490,6 +3695,8 @@ var Evaluator = class Evaluator extends Object {
3490
3695
  ["time", "time"],
3491
3696
  ["trace", "trace"],
3492
3697
  ["unless", "unless"],
3698
+ ["unquote", "unquote"],
3699
+ ["unquote-splicing", "unquoteSplicing"],
3493
3700
  ["when", "when"]
3494
3701
  ].map(([key, value]) => [InterpretedSymbol.of(key), value]));
3495
3702
  } catch {
@@ -3684,7 +3891,7 @@ var LispInterpreter = class extends Object {
3684
3891
  const aList = [];
3685
3892
  const aTable = new Table();
3686
3893
  aTable.setRoot(true);
3687
- aList.push("abs", "add", "and", "apply", "assoc", "atom", "bind", "car", "cdr", "characterp", "cond", "ceiling", "concatenate", "cons", "consp", "copy", "cos", "count", "floatp", "floor", "defun", "divide", "do", "do*", "dolist", "doublep", "elt", "eq", "equal", "eval", "evenp", "every", "exit", "exp", "expt", "find", "format", "gc", "gensym", "if", "integerp", "lambda", "let", "let*", "last", "length", "list", "listp", "mapcan", "mapcar", "max", "member", "memq", "min", "minusp", "mod", "multiply", "napier", "neq", "nequal", "not", "notrace", "nth", "null", "numberp", "oddp", "or", "pi", "plusp", "pop", "princ", "print", "progn", "push", "quote", "random", "reduce", "round", "rplaca", "rplacd", "setq", "set-allq", "sin", "some", "sort", "sqrt", "string-downcase", "string-trim", "string-upcase", "stringp", "subseq", "substring", "subtract", "symbolp", "tan", "terpri", "time", "trace", "truncate", "unless", "when", "zerop", "1+", "1-", "+", "-", "*", "/", "//", "=", "==", "~=", "~~", "<", "<=", ">", ">=");
3894
+ aList.push("abs", "add", "and", "apply", "assoc", "atom", "bind", "car", "cdr", "characterp", "cond", "ceiling", "concatenate", "cons", "consp", "copy", "cos", "count", "floatp", "floor", "defmacro", "defun", "divide", "do", "do*", "dolist", "doublep", "elt", "eq", "equal", "eval", "evenp", "every", "exit", "exp", "expt", "find", "format", "gc", "gensym", "if", "integerp", "lambda", "let", "let*", "last", "length", "list", "listp", "macroexpand", "macroexpand-1", "mapcan", "mapcar", "max", "member", "memq", "min", "minusp", "mod", "multiply", "napier", "neq", "nequal", "not", "notrace", "nth", "null", "numberp", "oddp", "or", "pi", "plusp", "pop", "princ", "print", "progn", "push", "quasiquote", "quote", "random", "reduce", "round", "rplaca", "rplacd", "setq", "set-allq", "sin", "some", "sort", "sqrt", "string-downcase", "string-trim", "string-upcase", "stringp", "subseq", "substring", "subtract", "symbolp", "tan", "terpri", "time", "trace", "truncate", "unless", "unquote", "unquote-splicing", "when", "zerop", "1+", "1-", "+", "-", "*", "/", "//", "=", "==", "~=", "~~", "<", "<=", ">", ">=");
3688
3895
  for (const each of aList) {
3689
3896
  const aSymbol = InterpretedSymbol.of(each);
3690
3897
  aTable.set(aSymbol, aSymbol);
package/dist/index.cjs CHANGED
@@ -696,6 +696,31 @@ var Parser = class Parser extends Object {
696
696
  return 0;
697
697
  }
698
698
  /**
699
+ * Recognizes a backquote (`` ` ``), wraps the following form into `(quasiquote form)`, and returns the token number; invoked from NextState.
700
+ * @return 0
701
+ */
702
+ quasiquote() {
703
+ const anObject = new Cons(this.nextToken(), Cons.nil);
704
+ this.token = new Cons(InterpretedSymbol.of("quasiquote"), anObject);
705
+ return 0;
706
+ }
707
+ /**
708
+ * Recognizes a comma and wraps the following form into `(unquote form)`, or
709
+ * `(unquote-splicing form)` when the comma is immediately followed by `@`
710
+ * (i.e. `,@`); invoked from NextState.
711
+ * @return 0
712
+ */
713
+ unquote() {
714
+ let aSymbol = InterpretedSymbol.of("unquote");
715
+ if (this.peekChar() === "@") {
716
+ this.nextChar();
717
+ aSymbol = InterpretedSymbol.of("unquote-splicing");
718
+ }
719
+ const anObject = new Cons(this.nextToken(), Cons.nil);
720
+ this.token = new Cons(aSymbol, anObject);
721
+ return 0;
722
+ }
723
+ /**
699
724
  * Returns the token number for a quote or for a 0-origin String-type (pseudo-Character); invoked from NextState.
700
725
  * @return the next state number
701
726
  */
@@ -815,7 +840,7 @@ var Parser = class Parser extends Object {
815
840
  aTable.set(String(41), this.nextState(-1, null));
816
841
  aTable.set(String(42), this.nextState(8, "symbolToken"));
817
842
  aTable.set(String(43), this.nextState(7, "sign"));
818
- aTable.set(String(44), this.nextState(8, "symbolToken"));
843
+ aTable.set(String(44), this.nextState(0, "unquote"));
819
844
  aTable.set(String(45), this.nextState(7, "sign"));
820
845
  aTable.set(String(46), this.nextState(-1, null));
821
846
  aTable.set(String(47), this.nextState(8, "symbolToken"));
@@ -827,7 +852,7 @@ var Parser = class Parser extends Object {
827
852
  aTable.set(String(93), this.nextState(-1, null));
828
853
  aTable.set(String(94), this.nextState(8, "symbolToken"));
829
854
  aTable.set(String(95), this.nextState(8, "symbolToken"));
830
- aTable.set(String(96), this.nextState(0, "quote"));
855
+ aTable.set(String(96), this.nextState(0, "quasiquote"));
831
856
  for (const index of IntStream.rangeClosed(97, 122)) aTable.set(String(index), this.nextState(8, "symbolToken"));
832
857
  aTable.set(String(123), this.nextState(-1, "parseList"));
833
858
  aTable.set(String(124), this.nextState(8, "symbolToken"));
@@ -2876,6 +2901,11 @@ var Evaluator = class Evaluator extends Object {
2876
2901
  */
2877
2902
  static buildInFunctions = Evaluator.setup();
2878
2903
  /**
2904
+ * Marker symbol stored as the car of the Cons that represents a macro binding,
2905
+ * distinguishing macros from ordinary `lambda` closures in the environment.
2906
+ */
2907
+ static macroMarker = InterpretedSymbol.of("macro");
2908
+ /**
2879
2909
  * The variable binding environment used during evaluation.
2880
2910
  */
2881
2911
  environment;
@@ -3021,6 +3051,85 @@ var Evaluator = class Evaluator extends Object {
3021
3051
  return variable;
3022
3052
  }
3023
3053
  /**
3054
+ * Implementation of the Lisp `defmacro` special form. Defines a macro: a
3055
+ * transformer whose body receives its arguments unevaluated and returns a
3056
+ * form that is then evaluated in the caller's environment.
3057
+ * @param aCons the argument Cons containing the macro name, parameter list, and body
3058
+ * @return the macro name symbol
3059
+ */
3060
+ defmacro(aCons) {
3061
+ const variable = aCons.car;
3062
+ const lambda = Evaluator.eval(new Cons(InterpretedSymbol.of("lambda"), aCons.cdr), new Table(this.environment), this.streamManager, this.depth, this.plugins);
3063
+ const macro = new Cons(Evaluator.macroMarker, new Cons(lambda, Cons.nil));
3064
+ this.environment.set(variable, macro);
3065
+ return variable;
3066
+ }
3067
+ /**
3068
+ * Returns the macro transformer (a lambda Cons) bound to the given symbol, or
3069
+ * null when the symbol is not bound to a macro. Special-form symbols are never
3070
+ * treated as macros.
3071
+ * @param car the operator position of a call form
3072
+ * @return the macro's lambda Cons, or null
3073
+ */
3074
+ lookupMacro(car) {
3075
+ if (Cons.isNotSymbol(car) || Evaluator.buildInFunctions.has(car)) return null;
3076
+ const value = this.environment.get(car);
3077
+ if (Cons.isCons(value) && value.car === Evaluator.macroMarker) return value.nth(2);
3078
+ return null;
3079
+ }
3080
+ /**
3081
+ * Expands a macro call exactly once by applying its transformer to the
3082
+ * unevaluated argument forms in the macro's captured environment.
3083
+ * @param form the call form whose car names the macro
3084
+ * @param macroLambda the macro's transformer lambda Cons
3085
+ * @return the expansion form
3086
+ */
3087
+ expandMacro1(form, macroLambda) {
3088
+ const capturedEnvironment = macroLambda.last().car;
3089
+ return Applier.apply(macroLambda, form.cdr, capturedEnvironment, this.streamManager, this.depth, this.plugins);
3090
+ }
3091
+ /**
3092
+ * Expands a macro call once and evaluates the resulting form in the current
3093
+ * environment.
3094
+ * @param form the call form whose car names the macro
3095
+ * @param macroLambda the macro's transformer lambda Cons
3096
+ * @return the result of evaluating the expansion
3097
+ */
3098
+ evalMacroCall(form, macroLambda) {
3099
+ const expansion = this.expandMacro1(form, macroLambda);
3100
+ return Evaluator.eval(expansion, this.environment, this.streamManager, this.depth, this.plugins);
3101
+ }
3102
+ /**
3103
+ * Implementation of the Lisp `macroexpand-1` special form. Evaluates its
3104
+ * argument to obtain a form and, when that form is a macro call, expands it
3105
+ * exactly once without evaluating the result.
3106
+ * @param aCons the argument Cons whose car evaluates to the form to expand
3107
+ * @return the once-expanded form, or the form unchanged when it is not a macro call
3108
+ */
3109
+ macroexpand_1(aCons) {
3110
+ const form = Evaluator.eval(aCons.car, this.environment, this.streamManager, this.depth, this.plugins);
3111
+ if (Cons.isNotCons(form)) return form;
3112
+ const macroLambda = this.lookupMacro(form.car);
3113
+ if (macroLambda == null) return form;
3114
+ return this.expandMacro1(form, macroLambda);
3115
+ }
3116
+ /**
3117
+ * Implementation of the Lisp `macroexpand` special form. Evaluates its
3118
+ * argument to obtain a form and repeatedly expands it until the result is no
3119
+ * longer a macro call, without evaluating the result.
3120
+ * @param aCons the argument Cons whose car evaluates to the form to expand
3121
+ * @return the fully expanded form
3122
+ */
3123
+ macroexpand(aCons) {
3124
+ let form = Evaluator.eval(aCons.car, this.environment, this.streamManager, this.depth, this.plugins);
3125
+ while (Cons.isCons(form)) {
3126
+ const macroLambda = this.lookupMacro(form.car);
3127
+ if (macroLambda == null) break;
3128
+ form = this.expandMacro1(form, macroLambda);
3129
+ }
3130
+ return form;
3131
+ }
3132
+ /**
3024
3133
  * Implementation of the Lisp `do` special form (parallel binding update).
3025
3134
  * @param aCons the argument Cons containing bindings, termination clause, and body
3026
3135
  * @return the value of the termination clause's result form
@@ -3132,6 +3241,10 @@ var Evaluator = class Evaluator extends Object {
3132
3241
  if (Cons.isNil(form) || Cons.isNotList(form)) return form;
3133
3242
  const formCons = form;
3134
3243
  if (Cons.isSymbol(formCons.car) && Evaluator.buildInFunctions.has(formCons.car)) return this.specialForm(formCons);
3244
+ if (Cons.isSymbol(formCons.car)) {
3245
+ const macroLambda = this.lookupMacro(formCons.car);
3246
+ if (macroLambda != null) return this.evalMacroCall(formCons, macroLambda);
3247
+ }
3135
3248
  if (Cons.isSymbol(formCons.car) && this.plugins.length > 0) {
3136
3249
  const symbol = formCons.car;
3137
3250
  const plugin = this.plugins.find((p) => p.has(symbol));
@@ -3382,6 +3495,94 @@ var Evaluator = class Evaluator extends Object {
3382
3495
  return aCons.car;
3383
3496
  }
3384
3497
  /**
3498
+ * Implementation of the Lisp `quasiquote` (`` ` ``) special form. Returns the
3499
+ * template with every `unquote` (`,`) and `unquote-splicing` (`,@`) at the
3500
+ * matching nesting level replaced by the evaluation of its operand. Nested
3501
+ * quasiquotes increase the level so inner unquotes are preserved.
3502
+ * @param aCons the argument Cons whose car is the template
3503
+ * @return the constructed form
3504
+ */
3505
+ quasiquote(aCons) {
3506
+ return this.quasiquoteExpand(aCons.car, 1);
3507
+ }
3508
+ /**
3509
+ * Recursively expands a quasiquote template at the given nesting level.
3510
+ * @param template the template to expand
3511
+ * @param level the current quasiquote nesting level (1 is the outermost)
3512
+ * @return the expanded value
3513
+ */
3514
+ quasiquoteExpand(template, level) {
3515
+ if (Cons.isNotCons(template)) return template;
3516
+ const aCons = template;
3517
+ if (aCons.car === InterpretedSymbol.of("unquote")) {
3518
+ if (level === 1) return Evaluator.eval(aCons.nth(2), this.environment, this.streamManager, this.depth, this.plugins);
3519
+ return new Cons(InterpretedSymbol.of("unquote"), new Cons(this.quasiquoteExpand(aCons.nth(2), level - 1), Cons.nil));
3520
+ }
3521
+ if (aCons.car === InterpretedSymbol.of("quasiquote")) return new Cons(InterpretedSymbol.of("quasiquote"), new Cons(this.quasiquoteExpand(aCons.nth(2), level + 1), Cons.nil));
3522
+ return this.quasiquoteList(aCons, level);
3523
+ }
3524
+ /**
3525
+ * Expands the elements of a quasiquoted list, handling `unquote-splicing`
3526
+ * (`,@`) elements and a possible dotted `unquote` (`,`) tail.
3527
+ * @param template the list template to expand
3528
+ * @param level the current quasiquote nesting level
3529
+ * @return the constructed list
3530
+ */
3531
+ quasiquoteList(template, level) {
3532
+ const parts = [];
3533
+ let tail = Cons.nil;
3534
+ let current = template;
3535
+ while (Cons.isCons(current)) {
3536
+ if (current.car === InterpretedSymbol.of("unquote")) {
3537
+ tail = this.quasiquoteExpand(current, level);
3538
+ current = Cons.nil;
3539
+ break;
3540
+ }
3541
+ const head = current.car;
3542
+ if (Cons.isCons(head) && head.car === InterpretedSymbol.of("unquote-splicing")) if (level === 1) this.spliceInto(parts, Evaluator.eval(head.nth(2), this.environment, this.streamManager, this.depth, this.plugins));
3543
+ else parts.push(new Cons(InterpretedSymbol.of("unquote-splicing"), new Cons(this.quasiquoteExpand(head.nth(2), level - 1), Cons.nil)));
3544
+ else parts.push(this.quasiquoteExpand(head, level));
3545
+ current = current.cdr;
3546
+ }
3547
+ if (Cons.isNotNil(current)) tail = current;
3548
+ let result = tail;
3549
+ for (let index = parts.length - 1; index >= 0; index--) result = new Cons(parts[index], result);
3550
+ return result;
3551
+ }
3552
+ /**
3553
+ * Appends the elements of a spliced value (`,@`) onto the accumulator. The
3554
+ * value must be a proper list (or nil); an atom or an improper (dotted) list
3555
+ * is rejected rather than silently dropping the dotted tail.
3556
+ * @param parts the accumulator of list elements
3557
+ * @param value the value produced by an `unquote-splicing` operand
3558
+ */
3559
+ spliceInto(parts, value) {
3560
+ if (Cons.isNil(value)) return null;
3561
+ if (Cons.isNotCons(value)) throw new EvalError(cannotApply("unquote-splicing", value));
3562
+ let current = value;
3563
+ while (Cons.isCons(current)) {
3564
+ parts.push(current.car);
3565
+ current = current.cdr;
3566
+ }
3567
+ if (Cons.isNotNil(current)) throw new EvalError(cannotApply("unquote-splicing", value));
3568
+ return null;
3569
+ }
3570
+ /**
3571
+ * Implementation of the Lisp `unquote` (`,`) special form. Signals an error
3572
+ * because unquote is only meaningful inside a `quasiquote` template.
3573
+ */
3574
+ unquote() {
3575
+ throw new EvalError("unquote (\",\") is only valid inside a quasiquote (\"`\")");
3576
+ }
3577
+ /**
3578
+ * Implementation of the Lisp `unquote-splicing` (`,@`) special form. Signals
3579
+ * an error because unquote-splicing is only meaningful inside a `quasiquote`
3580
+ * template.
3581
+ */
3582
+ unquoteSplicing() {
3583
+ throw new EvalError("unquote-splicing (\",@\") is only valid inside a quasiquote (\"`\")");
3584
+ }
3585
+ /**
3385
3586
  * Implementation of the Lisp `rplaca` special form; destructively replaces the car of a Cons.
3386
3587
  * @param args the argument Cons containing the target Cons expression and the new car value
3387
3588
  * @return the modified Cons
@@ -3459,6 +3660,7 @@ var Evaluator = class Evaluator extends Object {
3459
3660
  ["apply", "apply_lisp"],
3460
3661
  ["bind", "bind"],
3461
3662
  ["cond", "cond"],
3663
+ ["defmacro", "defmacro"],
3462
3664
  ["defun", "defun"],
3463
3665
  ["do", "do_"],
3464
3666
  ["dolist", "doList"],
@@ -3470,6 +3672,8 @@ var Evaluator = class Evaluator extends Object {
3470
3672
  ["lambda", "lambda"],
3471
3673
  ["let", "let"],
3472
3674
  ["let*", "letStar"],
3675
+ ["macroexpand", "macroexpand"],
3676
+ ["macroexpand-1", "macroexpand_1"],
3473
3677
  ["not", "not"],
3474
3678
  ["notrace", "notrace"],
3475
3679
  ["or", "or"],
@@ -3478,6 +3682,7 @@ var Evaluator = class Evaluator extends Object {
3478
3682
  ["princ", "princ"],
3479
3683
  ["print", "print"],
3480
3684
  ["push", "push_"],
3685
+ ["quasiquote", "quasiquote"],
3481
3686
  ["quote", "quote"],
3482
3687
  ["rplaca", "rplaca"],
3483
3688
  ["rplacd", "rplacd"],
@@ -3487,6 +3692,8 @@ var Evaluator = class Evaluator extends Object {
3487
3692
  ["time", "time"],
3488
3693
  ["trace", "trace"],
3489
3694
  ["unless", "unless"],
3695
+ ["unquote", "unquote"],
3696
+ ["unquote-splicing", "unquoteSplicing"],
3490
3697
  ["when", "when"]
3491
3698
  ].map(([key, value]) => [InterpretedSymbol.of(key), value]));
3492
3699
  } catch {
@@ -3681,7 +3888,7 @@ var LispInterpreter = class extends Object {
3681
3888
  const aList = [];
3682
3889
  const aTable = new Table();
3683
3890
  aTable.setRoot(true);
3684
- aList.push("abs", "add", "and", "apply", "assoc", "atom", "bind", "car", "cdr", "characterp", "cond", "ceiling", "concatenate", "cons", "consp", "copy", "cos", "count", "floatp", "floor", "defun", "divide", "do", "do*", "dolist", "doublep", "elt", "eq", "equal", "eval", "evenp", "every", "exit", "exp", "expt", "find", "format", "gc", "gensym", "if", "integerp", "lambda", "let", "let*", "last", "length", "list", "listp", "mapcan", "mapcar", "max", "member", "memq", "min", "minusp", "mod", "multiply", "napier", "neq", "nequal", "not", "notrace", "nth", "null", "numberp", "oddp", "or", "pi", "plusp", "pop", "princ", "print", "progn", "push", "quote", "random", "reduce", "round", "rplaca", "rplacd", "setq", "set-allq", "sin", "some", "sort", "sqrt", "string-downcase", "string-trim", "string-upcase", "stringp", "subseq", "substring", "subtract", "symbolp", "tan", "terpri", "time", "trace", "truncate", "unless", "when", "zerop", "1+", "1-", "+", "-", "*", "/", "//", "=", "==", "~=", "~~", "<", "<=", ">", ">=");
3891
+ aList.push("abs", "add", "and", "apply", "assoc", "atom", "bind", "car", "cdr", "characterp", "cond", "ceiling", "concatenate", "cons", "consp", "copy", "cos", "count", "floatp", "floor", "defmacro", "defun", "divide", "do", "do*", "dolist", "doublep", "elt", "eq", "equal", "eval", "evenp", "every", "exit", "exp", "expt", "find", "format", "gc", "gensym", "if", "integerp", "lambda", "let", "let*", "last", "length", "list", "listp", "macroexpand", "macroexpand-1", "mapcan", "mapcar", "max", "member", "memq", "min", "minusp", "mod", "multiply", "napier", "neq", "nequal", "not", "notrace", "nth", "null", "numberp", "oddp", "or", "pi", "plusp", "pop", "princ", "print", "progn", "push", "quasiquote", "quote", "random", "reduce", "round", "rplaca", "rplacd", "setq", "set-allq", "sin", "some", "sort", "sqrt", "string-downcase", "string-trim", "string-upcase", "stringp", "subseq", "substring", "subtract", "symbolp", "tan", "terpri", "time", "trace", "truncate", "unless", "unquote", "unquote-splicing", "when", "zerop", "1+", "1-", "+", "-", "*", "/", "//", "=", "==", "~=", "~~", "<", "<=", ">", ">=");
3685
3892
  for (const each of aList) {
3686
3893
  const aSymbol = InterpretedSymbol.of(each);
3687
3894
  aTable.set(aSymbol, aSymbol);