Bring TypeScript implementation up-to-date

This commit is contained in:
Tony Garnock-Jones 2024-05-13 17:13:01 +02:00
parent 52a7548bc4
commit 2e9af0f9b3
5 changed files with 472 additions and 301 deletions

32
lib.px
View file

@ -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.

View file

@ -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 ...]])

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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)),