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 +4 -2
- package/dist/cli.cjs +211 -4
- package/dist/index.cjs +210 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +210 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# kei-lisp
|
|
2
2
|
|
|
3
|
+
[](https://github.com/ike-keichan/kei-lisp/actions/workflows/ci.yml)
|
|
3
4
|
[](https://www.npmjs.com/package/kei-lisp)
|
|
4
5
|
[](./LICENSE)
|
|
5
6
|
[](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
|
-
|
|
170
|
-
|
|
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.
|
|
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(
|
|
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, "
|
|
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(
|
|
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, "
|
|
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);
|