Bring TypeScript implementation up-to-date
This commit is contained in:
parent
52a7548bc4
commit
2e9af0f9b3
5 changed files with 472 additions and 301 deletions
32
lib.px
32
lib.px
|
@ -1,10 +1,26 @@
|
|||
"Loading standard library..." pr nl
|
||||
|
||||
(0 gt :n :c c (c) n 1 - times, :n :c) :times
|
||||
(0 gt :n :c ((c) n 1 - timesK) c, :n :c) :timesK
|
||||
(:top drop (top)) :nip
|
||||
|
||||
(0 gt \ :n :c c (c) n 1 - times, :n :c) :times
|
||||
(0 gt \ :n :c ((c) n 1 - timesK) c, :n :c) :timesK
|
||||
|
||||
(
|
||||
%%children :vs
|
||||
shift :k
|
||||
vs size 1 - dup (
|
||||
:n
|
||||
vs n . :v
|
||||
(k (v) ,) schedule!
|
||||
n 1 -
|
||||
) swap times
|
||||
0 ge
|
||||
vs swap . :v
|
||||
(k (v) ,) !
|
||||
) :/
|
||||
|
||||
# aka `reset` perhaps?
|
||||
([dup !] drop drop) :do
|
||||
(:c [c] drop) :do
|
||||
|
||||
# `do` has a similar feel to `forEach`:
|
||||
(:f :vs (vs / f) do) :forEach
|
||||
|
@ -15,18 +31,16 @@
|
|||
# [5 iota] (dup dup) map wr nl
|
||||
# [5 iota] (drop) map wr nl
|
||||
|
||||
(:fc :tc (:#f fc, drop tc) !) :if
|
||||
(:fc :tc (:#f \ fc, drop tc) !) :if
|
||||
(:tc (:#f, drop tc) !) :when
|
||||
(:fc (:#f fc, drop) !) :unless
|
||||
(:fc (:#f \ fc, drop) !) :unless
|
||||
|
||||
# Awkward in languages like this with failing guards rather than
|
||||
# explicit boolean predicates
|
||||
(:f :vs [vs / dup f (drop) unless]) :filter
|
||||
(:g (g drop #t, drop #f)) :guard-to-predicate
|
||||
(:g (g \ drop #t, drop #f)) :guard-to-predicate
|
||||
|
||||
(:top drop top) :nip
|
||||
|
||||
([dup !] nip) :list
|
||||
(:c [c]) :list
|
||||
|
||||
# Doesn't work, because it leaves the last `n` (= 5) on the stack
|
||||
# at the end.
|
||||
|
|
|
@ -251,6 +251,10 @@
|
|||
|
||||
(define-metafunction B
|
||||
push-suspension : suspension cont -> cont
|
||||
;; TODO: If a (@catch [(@closure [()] env)] stack) already exists
|
||||
;; on the top of the continuation, and another one comes
|
||||
;; along (potentially with a different stack), pop the
|
||||
;; existing one before pushing, since it will never trigger.
|
||||
[(push-suspension (@code () env) cont) cont]
|
||||
[(push-suspension (@catch [] stack) cont) cont]
|
||||
[(push-suspension suspension [suspension_k ...]) [suspension suspension_k ...]])
|
||||
|
|
549
src/index.ts
549
src/index.ts
|
@ -9,18 +9,33 @@ export type Word = Pexpr.Positioned<Pexpr.Expr>;
|
|||
export type Words = Pexpr.Compound | Pexpr.Document;
|
||||
export type Code = { words: Words, ip: number };
|
||||
|
||||
export type Obj<T extends Embeddable> = T | Closure<T>;
|
||||
export function EMPTY_CODE(): Code {
|
||||
return { words: new Pexpr.Group(), ip: 0 };
|
||||
}
|
||||
|
||||
export type Obj<T extends Embeddable> = T | Applicable<T>;
|
||||
export type Value<T extends Embeddable> = PreservesValue<Obj<T>>;
|
||||
|
||||
export type Applicable<T extends Embeddable> = Closure<T> | DelimitedContinuation<T>;
|
||||
|
||||
export type Closure<T extends Embeddable> = {
|
||||
readonly [IsEmbedded]: true,
|
||||
branches: Branch<T>[],
|
||||
env: Environment<T>,
|
||||
readonly [IsEmbedded]: true;
|
||||
applicable: 'closure';
|
||||
branches: Branch<T>[];
|
||||
env: Environment<T>;
|
||||
};
|
||||
export type Branch<T extends Embeddable> = {
|
||||
words: Words,
|
||||
template: Scope<T>, // names introduced by patterns immediately contained in `words`
|
||||
};
|
||||
|
||||
export type DelimitedContinuation<T extends Embeddable> = {
|
||||
readonly [IsEmbedded]: true;
|
||||
applicable: 'continuation';
|
||||
stack: Value<T>[];
|
||||
cont: Frame<T>[];
|
||||
};
|
||||
|
||||
export type Primitive<T extends Embeddable> = (vm: VM<T>, pos: Position) => void;
|
||||
|
||||
export type Scope<T extends Embeddable> = { [key: string]: Value<T> | null };
|
||||
|
@ -50,50 +65,47 @@ export class Environment<T extends Embeddable> {
|
|||
}
|
||||
}
|
||||
|
||||
export type Suspension<T extends Embeddable> = {
|
||||
frame: Frame<T>;
|
||||
code: Code;
|
||||
stack: Value<T>[];
|
||||
};
|
||||
|
||||
export function isDelimiter<T extends Embeddable>(s: Suspension<T>): boolean {
|
||||
switch (s.frame.type) {
|
||||
export function isDelimiter<T extends Embeddable>(s: Frame<T>): boolean {
|
||||
switch (s.type) {
|
||||
case 'compound':
|
||||
case 'branch':
|
||||
return true;
|
||||
case 'call':
|
||||
case 'code':
|
||||
case 'pattern':
|
||||
case 'catch':
|
||||
return false;
|
||||
default:
|
||||
unreachable(s.frame, 'isDelimiter');
|
||||
unreachable(s, 'isDelimiter');
|
||||
}
|
||||
}
|
||||
|
||||
export type Frame<T extends Embeddable> =
|
||||
CallFrame<T> | CompoundFrame<T> | BranchFrame<T> | PatternFrame<T>;
|
||||
CodeFrame<T> | CompoundFrame<T> | CatchFrame<T> | PatternFrame<T>;
|
||||
|
||||
export type CallFrame<T extends Embeddable> = {
|
||||
type: 'call';
|
||||
outer_env: Environment<T>;
|
||||
closure: Closure<T>;
|
||||
current_branch: number;
|
||||
};
|
||||
|
||||
export type BranchFrame<T extends Embeddable> = {
|
||||
type: 'branch';
|
||||
completed: Value<T>[];
|
||||
pending: Array<{item: Value<T>, k: DelimitedContinuation<T>}>;
|
||||
};
|
||||
|
||||
export type DelimitedContinuation<T extends Embeddable> = {
|
||||
export type CodeFrame<T extends Embeddable> = {
|
||||
type: 'code';
|
||||
code: Code;
|
||||
stack: Value<T>[];
|
||||
env: Environment<T>;
|
||||
cont: Suspension<T>[];
|
||||
};
|
||||
|
||||
export type CompoundFrame<T extends Embeddable> =
|
||||
{ type: 'compound', accumulator: Accumulator<T> };
|
||||
export type CompoundFrame<T extends Embeddable> = {
|
||||
type: 'compound';
|
||||
accumulator: Accumulator<T>;
|
||||
pending: Value<T>[]; // usually Applicable, but can also be self-evaluating
|
||||
stack: Value<T>[];
|
||||
};
|
||||
|
||||
export type CatchFrame<T extends Embeddable> = {
|
||||
type: 'catch';
|
||||
alternatives: Applicable<T>[];
|
||||
stack: Value<T>[];
|
||||
};
|
||||
|
||||
export type PatternFrame<T extends Embeddable> = {
|
||||
type: 'pattern';
|
||||
pattern: Pattern.ParsedPattern<T>;
|
||||
groupValuePatterns: Pattern.Pattern<T>[];
|
||||
env: Environment<T>;
|
||||
};
|
||||
|
||||
export type Accumulator<T extends Embeddable> =
|
||||
| { type: 'sequence', item: Value<T>[] }
|
||||
|
@ -102,19 +114,87 @@ export type Accumulator<T extends Embeddable> =
|
|||
| { type: 'set', item: Set<Obj<T>> }
|
||||
;
|
||||
|
||||
export type PatternFrame<T extends Embeddable> = {
|
||||
type: 'pattern';
|
||||
pattern: Pattern.ParsedPattern<T>;
|
||||
groupValuePatterns: Pattern.Pattern<T>[];
|
||||
};
|
||||
|
||||
export function isClosure<T extends Embeddable>(v: Value<T>): v is Closure<T> {
|
||||
return isEmbedded(v) && 'branches' in v;
|
||||
export function isApplicable<T extends Embeddable>(v: Value<T>): v is Applicable<T> {
|
||||
return isEmbedded(v) && 'applicable' in v;
|
||||
}
|
||||
|
||||
function stack_sub<T extends Embeddable>(s1: Value<T>[], s0: Value<T>[]): Value<T>[] {
|
||||
const delta = s1.length - s0.length;
|
||||
return delta <= 0 ? [] : s1.slice(-delta);
|
||||
export function copyFrame<T extends Embeddable>(f: Frame<T>): Frame<T> {
|
||||
switch (f.type) {
|
||||
case 'code':
|
||||
return {
|
||||
type: 'code',
|
||||
code: { ... f.code },
|
||||
env: f.env,
|
||||
};
|
||||
case 'compound':
|
||||
return {
|
||||
type: 'compound',
|
||||
accumulator: copyAccumulator(f.accumulator),
|
||||
pending: [ ... f.pending ],
|
||||
stack: [ ... f.stack ],
|
||||
};
|
||||
case 'catch':
|
||||
return {
|
||||
type: 'catch',
|
||||
alternatives: [ ... f.alternatives ],
|
||||
stack: [ ... f.stack ],
|
||||
};
|
||||
case 'pattern':
|
||||
return {
|
||||
type: 'pattern',
|
||||
pattern: f.pattern,
|
||||
groupValuePatterns: f.groupValuePatterns,
|
||||
env: f.env,
|
||||
};
|
||||
default:
|
||||
unreachable(f, "copyFrame");
|
||||
}
|
||||
}
|
||||
|
||||
function copyAccumulator<T extends Embeddable>(a: Accumulator<T>): Accumulator<T> {
|
||||
switch (a.type) {
|
||||
case 'sequence':
|
||||
return { type: 'sequence', item: [ ... a.item ] };
|
||||
case 'set':
|
||||
return { type: 'set', item: a.item.clone() };
|
||||
case 'record': {
|
||||
const item = [ ... a.item ] as Record<Value<T>, Value<T>[], Obj<T>>;
|
||||
item.label = a.item.label;
|
||||
return { type: 'record', item };
|
||||
}
|
||||
case 'block':
|
||||
return { type: 'block', key: a.key, item: a.item.clone() };
|
||||
default:
|
||||
unreachable(a, "copyAccumulator");
|
||||
}
|
||||
}
|
||||
|
||||
function commaSplit(words: Words): Words[] {
|
||||
const groups: Words[] = [];
|
||||
const newGroup = () => groups.push(new Pexpr.Group());
|
||||
newGroup();
|
||||
for (const w of words) {
|
||||
if (Pexpr.Punct.isComma(w.item)) {
|
||||
newGroup();
|
||||
} else {
|
||||
groups[groups.length - 1].push(w);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function closure<T extends Embeddable>(branches: Branch<T>[], env: Environment<T>): Closure<T> {
|
||||
return { [IsEmbedded]: true, applicable: 'closure', branches, env };
|
||||
}
|
||||
|
||||
function isFailSwallow<T extends Embeddable>(f: Frame<T>): f is CatchFrame<T> {
|
||||
if (f.type !== 'catch') return false;
|
||||
if (f.alternatives.length !== 1) return false;
|
||||
const a = f.alternatives[0];
|
||||
if (a.applicable !== 'closure') return false;
|
||||
if (a.branches.length !== 1) return false;
|
||||
const b = a.branches[0];
|
||||
return (b.words.exprs.length === 0);
|
||||
}
|
||||
|
||||
export class VM<T extends Embeddable> {
|
||||
|
@ -124,8 +204,14 @@ export class VM<T extends Embeddable> {
|
|||
code: Code | Words,
|
||||
public stack: Value<T>[] = [],
|
||||
public env: Environment<T> = new Environment<T>(),
|
||||
public cont: Suspension<T>[] = [],
|
||||
public cont: Frame<T>[] = [],
|
||||
) {
|
||||
this.pushFrame({
|
||||
type: 'compound',
|
||||
accumulator: { type: 'sequence', item: [] },
|
||||
pending: [],
|
||||
stack: [],
|
||||
});
|
||||
this.code = ('ip' in code) ? { ... code } : { words: code, ip: 0 };
|
||||
}
|
||||
|
||||
|
@ -133,6 +219,14 @@ export class VM<T extends Embeddable> {
|
|||
this.stack.push(v);
|
||||
}
|
||||
|
||||
pushOrFail(v: Value<T> | undefined): void {
|
||||
if (v === void 0) {
|
||||
this.fail();
|
||||
} else {
|
||||
this.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
pop(): Value<T> | undefined {
|
||||
return this.stack.pop();
|
||||
}
|
||||
|
@ -146,32 +240,45 @@ export class VM<T extends Embeddable> {
|
|||
return i < this.code.words.exprs.length ? this.code.words.get(i) : void 0;
|
||||
}
|
||||
|
||||
suspension(): Suspension<T> | undefined {
|
||||
frame(): Frame<T> | undefined {
|
||||
return this.cont[this.cont.length - 1];
|
||||
}
|
||||
|
||||
step(): boolean {
|
||||
const w = this.peekWord();
|
||||
if (w === void 0) {
|
||||
const s = this.suspension();
|
||||
const s = this.frame();
|
||||
if (s === void 0) {
|
||||
// Undo the implicit sequence collector
|
||||
const vs = this.stack.pop()! as Value<T>[];
|
||||
this.stack.push(... vs);
|
||||
return false;
|
||||
} else {
|
||||
switch (s.frame.type) {
|
||||
case 'call':
|
||||
return this.finishCall(s, s.frame);
|
||||
case 'compound': {
|
||||
this.code = { ... s.code };
|
||||
this.stack = [ ... s.stack, this.finish(s.frame.accumulator, this.stack, s.stack) ];
|
||||
switch (s.type) {
|
||||
case 'code':
|
||||
this.code = s.code;
|
||||
this.env = s.env;
|
||||
this.cont.pop();
|
||||
return true;
|
||||
case 'compound':
|
||||
this._accumulate(s.accumulator, this.stack);
|
||||
if (s.pending.length > 0) {
|
||||
const next = s.pending.pop();
|
||||
this.stack = [];
|
||||
this.apply(next!);
|
||||
} else {
|
||||
this.stack = s.stack;
|
||||
this.push(this.finish(s.accumulator));
|
||||
this.cont.pop();
|
||||
}
|
||||
return true;
|
||||
case 'catch':
|
||||
this.cont.pop();
|
||||
return true;
|
||||
}
|
||||
case 'branch':
|
||||
return this.finishBranch(s, s.frame, stack_sub(this.stack, s.stack));
|
||||
case 'pattern':
|
||||
return this.finishPatternGroup(s, s.frame);
|
||||
return this.finishPatternGroup(s);
|
||||
default:
|
||||
unreachable(s.frame, 'continuation type');
|
||||
unreachable(s, 'continuation type');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -213,8 +320,6 @@ export class VM<T extends Embeddable> {
|
|||
}
|
||||
} else if (w.item instanceof Pexpr.Punct) {
|
||||
switch (w.item.text) {
|
||||
case ',':
|
||||
return this.comma();
|
||||
case ':':
|
||||
return this.pattern();
|
||||
case '::':
|
||||
|
@ -231,7 +336,7 @@ export class VM<T extends Embeddable> {
|
|||
this.push(Symbol.for(n.slice(1)));
|
||||
return true;
|
||||
}
|
||||
return this.apply(n);
|
||||
return this.lookupAndApply(n);
|
||||
} else {
|
||||
this.code.ip++;
|
||||
this.push(w.item);
|
||||
|
@ -271,49 +376,72 @@ export class VM<T extends Embeddable> {
|
|||
return true;
|
||||
}
|
||||
|
||||
closure(words: Words, env: Environment<T>): Closure<T> | undefined {
|
||||
// Important optimization: if `c` is a closure, `(c)` should evaluate just to `c`'s closure
|
||||
closure(words: Words, env: Environment<T>): Value<T> | undefined {
|
||||
// Important optimization: `(x)` becomes just the value of `x`.
|
||||
// This avoids endlessly-nested simple closures.
|
||||
if (words.exprs.length === 1) {
|
||||
const w = words.get(0)!;
|
||||
if (typeof w.item === 'symbol') {
|
||||
if (w.item.description![0] !== '=') {
|
||||
const r = this.env.lookup(w.item.description!);
|
||||
if (typeof r === 'object') {
|
||||
if (isClosure(r.found)) {
|
||||
return r.found;
|
||||
}
|
||||
return r.found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const branches: Branch<T>[] = [];
|
||||
function pushBranch() {
|
||||
branches.push({ words: new Pexpr.Group(), template: {} });
|
||||
for (const g of commaSplit(words)) {
|
||||
const template = {};
|
||||
if (!this.extractNames(g, template)) return void 0;
|
||||
branches.push({ words: g, template });
|
||||
}
|
||||
pushBranch();
|
||||
for (const w of words) {
|
||||
if (Pexpr.Punct.isComma(w.item)) {
|
||||
pushBranch();
|
||||
} else {
|
||||
branches[branches.length - 1].words.push(w);
|
||||
}
|
||||
}
|
||||
for (const b of branches) {
|
||||
if (!this.extractNames(b.words, b.template)) return void 0;
|
||||
}
|
||||
return { [IsEmbedded]: true, branches, env };
|
||||
return closure(branches, env);
|
||||
}
|
||||
|
||||
pushFrame(frame: Frame<T>): Suspension<T> {
|
||||
const s = { frame, stack: [ ... this.stack ], code: { ... this.code } };
|
||||
this.cont.push(s);
|
||||
return s;
|
||||
pushFrame(frame: Frame<T>): void {
|
||||
switch (frame.type) {
|
||||
case 'code':
|
||||
if (frame.code.ip >= frame.code.words.exprs.length) return;
|
||||
break;
|
||||
case 'catch':
|
||||
if (frame.alternatives.length === 0) return;
|
||||
// Slightly more complex case: if we'd end up with two catch-handlers
|
||||
// in a row that just swallow failure, drop the existing one before
|
||||
// pushing the new one, since it's redundant and will never trigger.
|
||||
if (isFailSwallow(frame)) {
|
||||
const existing = this.frame();
|
||||
if (existing !== void 0 && isFailSwallow(existing)) {
|
||||
this.cont.pop();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this.cont.push(frame);
|
||||
}
|
||||
|
||||
compound(words: Words, ip: number, accumulator: Accumulator<T>): true {
|
||||
this.pushFrame({ type: 'compound', accumulator });
|
||||
this.code = { words, ip };
|
||||
const pieces = commaSplit(words);
|
||||
const firstPiece = pieces.shift()!;
|
||||
const pending: Value<T>[] = [];
|
||||
for (const words of pieces) {
|
||||
const c = this.closure(words, this.env);
|
||||
if (c === void 0) return true;
|
||||
pending.push(c);
|
||||
}
|
||||
pending.reverse();
|
||||
this.pushFrame({ type: 'code', code: this.code, env: this.env });
|
||||
this.pushFrame({
|
||||
type: 'compound',
|
||||
accumulator,
|
||||
pending,
|
||||
stack: this.stack,
|
||||
});
|
||||
this.code = { words: firstPiece, ip };
|
||||
this.stack = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -325,22 +453,21 @@ export class VM<T extends Embeddable> {
|
|||
throw new VMError(tag, position ?? this.peekPosition());
|
||||
}
|
||||
|
||||
_accumulate(accumulator: Accumulator<T>, current_stack: Value<T>[], saved_stack: Value<T>[]): void {
|
||||
const items = stack_sub(current_stack, saved_stack);
|
||||
if (items.length > 0) {
|
||||
_accumulate(accumulator: Accumulator<T>, stack: Value<T>[]): void {
|
||||
if (stack.length > 0) {
|
||||
switch (accumulator.type) {
|
||||
case 'sequence':
|
||||
case 'record':
|
||||
accumulator.item.push(... items);
|
||||
accumulator.item.push(... stack);
|
||||
break;
|
||||
case 'block':
|
||||
if (accumulator.key !== void 0) {
|
||||
accumulator.item.set(accumulator.key, items[items.length - 1]);
|
||||
accumulator.item.set(accumulator.key, stack[stack.length - 1]);
|
||||
accumulator.key = void 0;
|
||||
}
|
||||
break;
|
||||
case 'set':
|
||||
for (const i of items) accumulator.item.add(i);
|
||||
for (const i of stack) accumulator.item.add(i);
|
||||
break;
|
||||
default:
|
||||
unreachable(accumulator, 'accumulator type');
|
||||
|
@ -348,8 +475,7 @@ export class VM<T extends Embeddable> {
|
|||
}
|
||||
}
|
||||
|
||||
finish(accumulator: Accumulator<T>, current_stack: Value<T>[], saved_stack: Value<T>[]): Value<T> {
|
||||
this._accumulate(accumulator, current_stack, saved_stack);
|
||||
finish(accumulator: Accumulator<T>): Value<T> {
|
||||
switch (accumulator.type) {
|
||||
case 'sequence':
|
||||
case 'record':
|
||||
|
@ -362,68 +488,25 @@ export class VM<T extends Embeddable> {
|
|||
}
|
||||
}
|
||||
|
||||
finishCall(s: Suspension<T>, f: CallFrame<T>): true {
|
||||
this.code = { ... s.code };
|
||||
this.env = f.outer_env;
|
||||
this.cont.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
finishBranch(s: Suspension<T>, f: BranchFrame<T>, items: Value<T>[]): true {
|
||||
if (f.pending.length > 0) {
|
||||
const next = f.pending.pop()!;
|
||||
this.code = { ... next.k.code };
|
||||
this.stack = [ ... next.k.stack, next.item ];
|
||||
this.env = next.k.env;
|
||||
f.completed.push(... items);
|
||||
this.cont.push(... next.k.cont);
|
||||
} else {
|
||||
this.code = { ... s.code };
|
||||
this.stack = [ ... s.stack, ... f.completed, ... items ];
|
||||
this.cont.pop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
comma(): boolean {
|
||||
const s = this.suspension();
|
||||
if (s === void 0) return this.error('comma-at-toplevel');
|
||||
switch (s.frame.type) {
|
||||
case 'compound':
|
||||
this.code.ip++;
|
||||
this._accumulate(s.frame.accumulator, this.stack, s.stack);
|
||||
this.stack = [ ... s.stack ];
|
||||
return true;
|
||||
case 'branch':
|
||||
return this.finishBranch(s, s.frame, stack_sub(this.stack, s.stack));
|
||||
case 'call':
|
||||
throw new Error("Internal error: call code should have no commas");
|
||||
case 'pattern':
|
||||
throw new Error("Internal error: comma in synthetic pattern frame");
|
||||
default:
|
||||
unreachable(s.frame, 'suspension in comma');
|
||||
}
|
||||
}
|
||||
|
||||
key(): boolean {
|
||||
const s = this.suspension();
|
||||
if (s?.frame.type !== 'compound' || s.frame.accumulator.type !== 'block') {
|
||||
const s = this.frame();
|
||||
if (s?.type !== 'compound' || s.accumulator.type !== 'block') {
|
||||
return this.error('misplaced-key');
|
||||
}
|
||||
const key = this.pop();
|
||||
if (key === void 0) {
|
||||
return this.error('missing-key');
|
||||
}
|
||||
if (s.frame.accumulator.key !== void 0) {
|
||||
if (s.accumulator.key !== void 0) {
|
||||
const value = this.pop();
|
||||
if (value === void 0) {
|
||||
this.push(key);
|
||||
return this.error('missing-value');
|
||||
}
|
||||
s.frame.accumulator.item.set(s.frame.accumulator.key, value);
|
||||
s.accumulator.item.set(s.accumulator.key, value);
|
||||
}
|
||||
this.code.ip++;
|
||||
s.frame.accumulator.key = key;
|
||||
s.accumulator.key = key;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -434,28 +517,28 @@ export class VM<T extends Embeddable> {
|
|||
if (pattern === void 0) return true; // error already signalled
|
||||
this.code.ip += 2;
|
||||
|
||||
const f: PatternFrame<T> = { type: 'pattern', pattern, groupValuePatterns: [] };
|
||||
return this.resumePattern(this.pushFrame(f), f);
|
||||
const f: PatternFrame<T> = { type: 'pattern', pattern, groupValuePatterns: [], env: this.env };
|
||||
this.pushFrame(f);
|
||||
return this.resumePattern(f);
|
||||
}
|
||||
|
||||
finishPatternGroup(s: Suspension<T>, f: PatternFrame<T>): true {
|
||||
finishPatternGroup(f: PatternFrame<T>): true {
|
||||
const v = this.pop();
|
||||
if (v === void 0) return this.error('missing-literal-value');
|
||||
f.groupValuePatterns.push(Pattern.literal(v));
|
||||
return this.resumePattern(s, f);
|
||||
return this.resumePattern(f);
|
||||
}
|
||||
|
||||
resumePattern(s: Suspension<T>, f: PatternFrame<T>): true {
|
||||
resumePattern(f: PatternFrame<T>): true {
|
||||
if (f.pattern.groups.length > f.groupValuePatterns.length) {
|
||||
const cl = this.closure(f.pattern.groups[f.groupValuePatterns.length].item, this.env);
|
||||
const cl = this.closure(f.pattern.groups[f.groupValuePatterns.length].item, f.env);
|
||||
if (cl === void 0) return true;
|
||||
return this.enterClosure(cl);
|
||||
return this.apply(cl);
|
||||
} else {
|
||||
this.cont.pop();
|
||||
this.code = { ... s.code };
|
||||
const value = this.pop();
|
||||
if (value === void 0) return this.error('missing-value');
|
||||
const bindings = match(f.pattern.pattern, value, f.groupValuePatterns);
|
||||
const bindings = Pattern.match(f.pattern.pattern, value, f.groupValuePatterns);
|
||||
if (bindings === void 0) {
|
||||
return this.fail();
|
||||
}
|
||||
|
@ -464,53 +547,74 @@ export class VM<T extends Embeddable> {
|
|||
}
|
||||
}
|
||||
|
||||
enterClosure(closure: Closure<T>): true {
|
||||
this.pushFrame({
|
||||
type: 'call',
|
||||
outer_env: this.env,
|
||||
closure,
|
||||
current_branch: 0,
|
||||
});
|
||||
this.code = { words: closure.branches[0].words, ip: 0 };
|
||||
this.env = new Environment(closure.branches[0].template, closure.env);
|
||||
return true;
|
||||
shift(): DelimitedContinuation<T> {
|
||||
let delim_i = this.cont.length - 1;
|
||||
while (delim_i >= 0 && !isDelimiter(this.cont[delim_i])) delim_i--;
|
||||
const cont: Frame<T>[] = (delim_i >= 0) ? this.cont.splice(delim_i + 1).map(copyFrame) : [];
|
||||
return { [IsEmbedded]: true, applicable: 'continuation', cont, stack: [ ... this.stack ] };
|
||||
}
|
||||
|
||||
apply(v: Value<T>): true {
|
||||
if (isApplicable(v)) {
|
||||
const a = v;
|
||||
switch (a.applicable) {
|
||||
case 'closure':
|
||||
this.pushFrame({ type: 'code', code: this.code, env: this.env });
|
||||
const alternatives: Applicable<T>[] = [];
|
||||
for (let i = 1; i < a.branches.length; i++) {
|
||||
const branch = a.branches[i];
|
||||
alternatives.push(closure([branch], this.env));
|
||||
}
|
||||
this.pushFrame({ type: 'catch', alternatives, stack: [ ... this.stack ] });
|
||||
this.code = { words: a.branches[0].words, ip: 0 };
|
||||
this.env = new Environment(a.branches[0].template, a.env);
|
||||
return true;
|
||||
case 'continuation':
|
||||
this.stack = [ ... a.stack ];
|
||||
a.cont.forEach(f => this.pushFrame(copyFrame(f)));
|
||||
return true;
|
||||
default:
|
||||
unreachable(a, "applyValue");
|
||||
}
|
||||
} else {
|
||||
this.push(v);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
fail(): true {
|
||||
while (true) {
|
||||
const s = this.suspension();
|
||||
const s = this.frame();
|
||||
if (s === void 0) {
|
||||
return this.error('fail');
|
||||
}
|
||||
switch (s.frame.type) {
|
||||
case 'branch':
|
||||
this.code = { ... s.code };
|
||||
return this.finishBranch(s, s.frame, []);
|
||||
case 'call': {
|
||||
const next_branch = s.frame.current_branch + 1;
|
||||
if (next_branch < s.frame.closure.branches.length) {
|
||||
this.code = { words: s.frame.closure.branches[next_branch].words, ip: 0 };
|
||||
this.stack = [ ... s.stack ];
|
||||
this.env = new Environment(s.frame.closure.branches[next_branch].template, s.frame.closure.env);
|
||||
s.frame.current_branch = next_branch;
|
||||
switch (s.type) {
|
||||
case 'code':
|
||||
case 'pattern':
|
||||
this.env = s.env;
|
||||
/* fall through */
|
||||
case 'compound':
|
||||
this.cont.pop();
|
||||
continue;
|
||||
case 'catch':
|
||||
this.stack = [ ... s.stack ];
|
||||
if (s.alternatives.length > 0) {
|
||||
const next = s.alternatives.shift()!;
|
||||
if (s.alternatives.length === 0) this.cont.pop();
|
||||
this.code = EMPTY_CODE();
|
||||
this.apply(next);
|
||||
return true;
|
||||
} else {
|
||||
this.env = s.frame.outer_env;
|
||||
this.cont.pop();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
case 'compound':
|
||||
case 'pattern':
|
||||
this.cont.pop();
|
||||
continue;
|
||||
default:
|
||||
unreachable(s.frame, 'fail-continuation');
|
||||
unreachable(s, 'fail-continuation');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
apply(name: string): boolean {
|
||||
lookupAndApply(name: string): boolean {
|
||||
const r = this.env.lookup(name);
|
||||
switch (r) {
|
||||
case 'uninitialized-variable':
|
||||
|
@ -527,13 +631,8 @@ export class VM<T extends Embeddable> {
|
|||
this.code.ip++;
|
||||
break;
|
||||
}
|
||||
const v = r.found;
|
||||
if (isClosure(v)) {
|
||||
return this.enterClosure(v);
|
||||
} else {
|
||||
this.push(v);
|
||||
return true;
|
||||
}
|
||||
this.apply(r.found);
|
||||
return true;
|
||||
}
|
||||
|
||||
primitiveNamed(name: string): Primitive<T> | undefined {
|
||||
|
@ -542,14 +641,27 @@ export class VM<T extends Embeddable> {
|
|||
|
||||
dumpState(): string {
|
||||
const w = this.peekWord();
|
||||
const stackStr = (vs: Value<T>[]): string => '('+vs.map(sify).join(' ')+')';
|
||||
const contStr = (c: Frame<T>[]): string => '{'+c.map(f => f.type).join('/')+'}';
|
||||
const sify = (v: any) => stringify<Obj<T>>(v, {
|
||||
embeddedWrite: {
|
||||
write(s, v) {
|
||||
if ('branches' in v) {
|
||||
s.pieces.push(
|
||||
'(λ ',
|
||||
v.branches.map(b => sify(b.words.exprs)).join(' '),
|
||||
')');
|
||||
if (isApplicable(v)) {
|
||||
switch (v.applicable) {
|
||||
case 'closure':
|
||||
s.pieces.push(
|
||||
'(λ ',
|
||||
v.branches.map(b => sify(b.words.exprs)).join(' '),
|
||||
')');
|
||||
break;
|
||||
case 'continuation':
|
||||
s.pieces.push(
|
||||
'(κ ',
|
||||
stackStr(v.stack),
|
||||
' ',
|
||||
contStr(v.cont),
|
||||
')');
|
||||
}
|
||||
} else {
|
||||
stringifyEmbeddedWrite.write(s, v);
|
||||
}
|
||||
|
@ -558,52 +670,10 @@ export class VM<T extends Embeddable> {
|
|||
});
|
||||
const sp = w ? `${formatPosition(w.position)} ` : '';
|
||||
const si = w ? `${sify(w.item)} ` : ``;
|
||||
return `${sp}(${this.stack.map(sify).join(' ')}) ${si}{${this.cont.map(f => f.frame.type).join('/')}}`;
|
||||
return `${sp}${stackStr(this.stack)} ${si}${contStr(this.cont)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function match<T extends Embeddable>(
|
||||
pat: Pattern.Pattern<T>,
|
||||
value: Value<T>,
|
||||
groupValuePatterns: Pattern.Pattern<T>[],
|
||||
): Value<T>[] | undefined {
|
||||
const bindings: Value<T>[] = [];
|
||||
function walk(pat: Pattern.Pattern<T>, value: Value<T> | undefined): boolean {
|
||||
if (value === void 0) return false;
|
||||
switch (pat.type) {
|
||||
case 'discard': return true;
|
||||
case 'bind': bindings.push(value); return true;
|
||||
case 'atom': return is(pat.value, value);
|
||||
case 'embedded': return is(pat.value, value);
|
||||
case 'code': return walk(groupValuePatterns[pat.groupIndex], value);
|
||||
case 'record':
|
||||
if (!Record.isRecord<Value<T>, Value<T>[], Obj<T>>(value)) return false;
|
||||
if (!is(pat.label, value.label)) return false;
|
||||
for (let i = 0; i < pat.fields.length; i++) {
|
||||
if (!walk(pat.fields[i], value[i])) return false;
|
||||
}
|
||||
return true;
|
||||
case 'sequence':
|
||||
if (!isSequence(value)) return false;
|
||||
for (let i = 0; i < pat.elements.length; i++) {
|
||||
if (!walk(pat.elements[i], value[i])) return false;
|
||||
}
|
||||
return true;
|
||||
case 'dictionary': {
|
||||
if (!Dictionary.isDictionary<Obj<T>>(value)) return false;
|
||||
const d = new DictionaryMap<Obj<T>>(value);
|
||||
for (const [k, v] of pat.elements) {
|
||||
if (!walk(v, d.get(k))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
unreachable(pat, 'pattern type');
|
||||
}
|
||||
}
|
||||
return walk(pat, value) ? bindings : void 0;
|
||||
}
|
||||
|
||||
export function main(args: string[]) {
|
||||
let verbose = false;
|
||||
if (args[0] === '--verbose') {
|
||||
|
@ -623,4 +693,9 @@ export function main(args: string[]) {
|
|||
do {
|
||||
if (verbose) console.log(vm.dumpState());
|
||||
} while (vm.step());
|
||||
while (true) {
|
||||
const v = vm.pop();
|
||||
if (v === void 0) break;
|
||||
console.log('==>', v);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Atom, Embeddable, Embedded, fold, mapEmbeddeds, Pexpr, Position } from '@preserves/core';
|
||||
import { VMError } from "./error";
|
||||
import { Atom, Dictionary, DictionaryMap, Embeddable, Embedded, fold, is, isSequence, mapEmbeddeds, Pexpr, Position, Record } from '@preserves/core';
|
||||
import { unreachable, VMError } from "./error";
|
||||
import type { Obj, Scope, Value, Word, Words } from './index';
|
||||
|
||||
export type Pattern<T extends Embeddable> =
|
||||
|
@ -25,7 +25,7 @@ function atom<T extends Embeddable>(value: Atom): Pattern<T> {
|
|||
}
|
||||
|
||||
function eraseEmbeddeds<T extends Embeddable>(pos: Position, v: Value<any>): Value<T> {
|
||||
return mapEmbeddeds(v, e => { throw new VMError('invalid-embedded-position', pos); });
|
||||
return (mapEmbeddeds(v, e => { throw new VMError('invalid-embedded-position', pos); }) as Value<T>);
|
||||
}
|
||||
|
||||
export function literal<T extends Embeddable>(v: Value<T>): Pattern<T> {
|
||||
|
@ -101,3 +101,45 @@ export function parse<T extends Embeddable>(pat: Word, scope: Scope<T> = {}): Pa
|
|||
|
||||
return { pattern, scope, names, groups };
|
||||
}
|
||||
|
||||
export function match<T extends Embeddable>(
|
||||
pat: Pattern<T>,
|
||||
value: Value<T>,
|
||||
groupValuePatterns: Pattern<T>[],
|
||||
): Value<T>[] | undefined {
|
||||
const bindings: Value<T>[] = [];
|
||||
function walk(pat: Pattern<T>, value: Value<T> | undefined): boolean {
|
||||
if (value === void 0) return false;
|
||||
switch (pat.type) {
|
||||
case 'discard': return true;
|
||||
case 'bind': bindings.push(value); return true;
|
||||
case 'atom': return is(pat.value, value);
|
||||
case 'embedded': return is(pat.value, value);
|
||||
case 'code': return walk(groupValuePatterns[pat.groupIndex], value);
|
||||
case 'record':
|
||||
if (!Record.isRecord<Value<T>, Value<T>[], Obj<T>>(value)) return false;
|
||||
if (!is(pat.label, value.label)) return false;
|
||||
for (let i = 0; i < pat.fields.length; i++) {
|
||||
if (!walk(pat.fields[i], value[i])) return false;
|
||||
}
|
||||
return true;
|
||||
case 'sequence':
|
||||
if (!isSequence(value)) return false;
|
||||
for (let i = 0; i < pat.elements.length; i++) {
|
||||
if (!walk(pat.elements[i], value[i])) return false;
|
||||
}
|
||||
return true;
|
||||
case 'dictionary': {
|
||||
if (!Dictionary.isDictionary<Obj<T>>(value)) return false;
|
||||
const d = new DictionaryMap<Obj<T>>(value);
|
||||
for (const [k, v] of pat.elements) {
|
||||
if (!walk(v, d.get(k))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
unreachable(pat, 'pattern type');
|
||||
}
|
||||
}
|
||||
return walk(pat, value) ? bindings : void 0;
|
||||
}
|
||||
|
|
|
@ -1,35 +1,40 @@
|
|||
import { isSequence, compare, Pexpr, Position, stringify, isEmbedded } from "@preserves/core";
|
||||
import { isSequence, compare, Pexpr, Position, stringify, isEmbedded, Record, Dictionary, Bytes, Set, DictionaryMap } from "@preserves/core";
|
||||
import { VMError } from "./error";
|
||||
import { BranchFrame, Code, DelimitedContinuation, isClosure, isDelimiter, Primitive, Suspension, Value, VM } from './index';
|
||||
import { Code, DelimitedContinuation, isApplicable, isDelimiter, Primitive, Value, VM } from './index';
|
||||
|
||||
function N(vm: VM<any>, pos: Position): number {
|
||||
const v = vm.pop();
|
||||
function cN(v: Value<any> | undefined, pos: Position): number {
|
||||
if (typeof v !== 'number') throw new VMError('expected-integer', pos);
|
||||
return v;
|
||||
}
|
||||
|
||||
function cV(v: Value<any> | undefined, pos: Position): Value<any> {
|
||||
if (typeof v === 'undefined') throw new VMError('expected-value', pos);
|
||||
return v;
|
||||
}
|
||||
|
||||
function N(vm: VM<any>, pos: Position): number {
|
||||
return cN(vm.pop(), pos);
|
||||
}
|
||||
|
||||
function Np(vm: VM<any>, pos: Position): number {
|
||||
const v = vm.peek();
|
||||
if (typeof v !== 'number') throw new VMError('expected-integer', pos);
|
||||
return v;
|
||||
return cN(vm.peek(), pos);
|
||||
}
|
||||
|
||||
function V(vm: VM<any>, pos: Position): Value<any> {
|
||||
const v = vm.pop();
|
||||
if (v === void 0) throw new VMError('missing-value', pos);
|
||||
return v;
|
||||
return cV(vm.pop(), pos);
|
||||
}
|
||||
|
||||
const OUTPUT_BUFFER: string[] = [];
|
||||
|
||||
export const PRIMITIVES: { [key: string]: Primitive<any> } = {
|
||||
'dup': (vm, pos) => { const v = V(vm, pos); vm.push(v); vm.push(v); },
|
||||
'swap': (vm, pos) => { const a = V(vm, pos); const b = V(vm, pos); vm.push(a); vm.push(b); },
|
||||
'drop': (vm, pos) => V(vm, pos),
|
||||
|
||||
'!': (vm, pos) => {
|
||||
const v = V(vm, pos);
|
||||
if (!isClosure(v)) throw new VMError('expected-closure', pos);
|
||||
vm.enterClosure(v);
|
||||
if (!isApplicable(v)) return vm.push(v);
|
||||
vm.apply(v);
|
||||
},
|
||||
|
||||
'+': (vm, pos) => vm.push(N(vm, pos) + N(vm, pos)),
|
||||
|
@ -43,49 +48,80 @@ export const PRIMITIVES: { [key: string]: Primitive<any> } = {
|
|||
'eq': (vm, pos) => { const d = N(vm, pos); if (!(compare(Np(vm, pos), d) == 0)) vm.fail(); },
|
||||
'ne': (vm, pos) => { const d = N(vm, pos); if (!(compare(Np(vm, pos), d) != 0)) vm.fail(); },
|
||||
|
||||
'/': (vm, pos) => {
|
||||
const vs = vm.pop();
|
||||
if (!isSequence(vs)) throw new VMError('expected-sequence', pos);
|
||||
const branch_code: Code = { words: new Pexpr.Group(), ip: 0 };
|
||||
while (true) {
|
||||
const w = vm.peekWord();
|
||||
if (w == void 0 || Pexpr.Punct.isComma(w.item)) break;
|
||||
branch_code.words.push(w);
|
||||
vm.code.ip++;
|
||||
}
|
||||
const partial_cont: Suspension<any>[] = [];
|
||||
while (vm.cont.length > 0 && !isDelimiter(vm.cont[vm.cont.length - 1])) {
|
||||
partial_cont.push(vm.cont.pop()!);
|
||||
}
|
||||
partial_cont.reverse();
|
||||
if (vs.length > 0) {
|
||||
const s = vm.suspension();
|
||||
let f: BranchFrame<any>;
|
||||
if (s?.frame.type === 'branch') {
|
||||
f = s.frame;
|
||||
} else {
|
||||
f = {
|
||||
type: 'branch',
|
||||
completed: [],
|
||||
pending: [],
|
||||
};
|
||||
vm.pushFrame(f);
|
||||
}
|
||||
const k: DelimitedContinuation<any> = {
|
||||
code: branch_code,
|
||||
stack: [ ... vm.stack ],
|
||||
env: vm.env,
|
||||
cont: partial_cont,
|
||||
};
|
||||
for (let i = vs.length - 1; i > 0; i--) {
|
||||
f.pending.push({ item: vs[i], k });
|
||||
}
|
||||
vm.code = { ... branch_code };
|
||||
vm.cont.push(... partial_cont);
|
||||
vm.push(vs[0]);
|
||||
'size': (vm, pos) => {
|
||||
const v = V(vm, pos);
|
||||
switch (typeof v) {
|
||||
case 'string':
|
||||
return vm.push(v.length);
|
||||
case 'symbol':
|
||||
return vm.push(v.description!.length);
|
||||
case 'object':
|
||||
if (Bytes.isBytes(v)) return vm.push(v.length);
|
||||
if (Record.isRecord(v) || Array.isArray(v)) return vm.push(v.length);
|
||||
if (Dictionary.isDictionary(v) || Set.isSet(v)) return vm.push(v.size);
|
||||
return vm.fail();
|
||||
default:
|
||||
return vm.fail();
|
||||
}
|
||||
},
|
||||
'.': (vm, pos) => {
|
||||
const k = V(vm, pos);
|
||||
const v = V(vm, pos);
|
||||
switch (typeof v) {
|
||||
case 'string':
|
||||
return vm.push(v.charCodeAt(cN(k, pos)));
|
||||
case 'symbol':
|
||||
return vm.push(v.description!.charCodeAt(cN(k, pos)));
|
||||
case 'object':
|
||||
if (Bytes.isBytes(v)) return vm.pushOrFail(v.get(cN(k, pos)));
|
||||
if (Record.isRecord(v) || Array.isArray(v)) return vm.pushOrFail(v[cN(k, pos)]);
|
||||
if (Dictionary.isDictionary(v)) vm.pushOrFail(new DictionaryMap(v).get(k));
|
||||
return vm.fail();
|
||||
default:
|
||||
return vm.fail();
|
||||
}
|
||||
},
|
||||
'%%children': (vm, pos) => {
|
||||
const v = V(vm, pos);
|
||||
if (typeof v !== 'object') return vm.fail();
|
||||
if (Record.isRecord(v) || Array.isArray(v)) return vm.push(v);
|
||||
if (Dictionary.isDictionary(v)) return vm.push(Array.from(new DictionaryMap(v).values()));
|
||||
if (Set.isSet(v)) return vm.push(Array.from(v.values()));
|
||||
vm.fail();
|
||||
},
|
||||
'%%keys': (vm, pos) => {
|
||||
const v = V(vm, pos);
|
||||
if (typeof v !== 'object') return vm.fail();
|
||||
if (Dictionary.isDictionary(v)) return vm.push(Array.from(new DictionaryMap(v).values()));
|
||||
if (Set.isSet(v)) return vm.push(Array.from(v.values()));
|
||||
vm.fail();
|
||||
},
|
||||
'^.': (vm, pos) => {
|
||||
const v = V(vm, pos);
|
||||
if (!Record.isRecord(v)) return vm.fail();
|
||||
vm.push(v.label);
|
||||
},
|
||||
|
||||
'fail': (vm, _pos) => vm.fail(),
|
||||
'shift': (vm, _pos) => vm.push(vm.shift()),
|
||||
'schedule!': (vm, pos) => {
|
||||
const v = V(vm, pos);
|
||||
if (!isApplicable(v)) throw new VMError('expected-closure', pos);
|
||||
for (let i = vm.cont.length - 1; i >= 0; i--) {
|
||||
const f = vm.cont[i];
|
||||
if (f.type === 'compound') {
|
||||
f.pending.push(v);
|
||||
break;
|
||||
}
|
||||
if (isDelimiter(vm.cont[i])) break;
|
||||
}
|
||||
},
|
||||
'\\': (vm, pos) => {
|
||||
const f = vm.frame();
|
||||
if (f === void 0) throw new VMError('expected-frame', pos);
|
||||
if (f.type !== 'catch') throw new VMError('expected-catch', pos);
|
||||
vm.cont.pop();
|
||||
},
|
||||
|
||||
'wr': (vm, pos) => OUTPUT_BUFFER.push(stringify(V(vm, pos))),
|
||||
'pr': (vm, pos) => OUTPUT_BUFFER.push('' + V(vm, pos)),
|
||||
|
|
Loading…
Reference in a new issue