313 lines
13 KiB
TypeScript
313 lines
13 KiB
TypeScript
import { Pattern, NamedPattern, Schema, Input } from "./meta";
|
|
import * as M from './meta';
|
|
import { Annotated, Bytes, Dictionary, Fold, fold, preserves, Record, Tuple, Value } from "@preserves/core";
|
|
import { Formatter, parens, seq, Item, opseq, block, commas, brackets, anglebrackets, braces } from "./block";
|
|
|
|
function fnblock(... items: Item[]): Item {
|
|
return seq('((() => ', block(... items), ')())');
|
|
}
|
|
|
|
export type CompileEnvEntry = {
|
|
moduleName: string,
|
|
modulePath: string,
|
|
schema: Schema,
|
|
inline: boolean,
|
|
};
|
|
|
|
export function compile(env: Array<CompileEnvEntry>, schema: Schema, preservesModule = '@preserves/core'): string {
|
|
const literals = new Dictionary<string, never>();
|
|
const types: Array<Item> = [];
|
|
const predicates: Array<Item> = [];
|
|
let temps: Array<string> = [];
|
|
|
|
function environmentWith<R>(name: symbol,
|
|
kLocal: () => R,
|
|
kOther: (e: CompileEnvEntry, p: Pattern) => R): R {
|
|
if (Schema._.definitions(schema).has(name)) {
|
|
return kLocal();
|
|
}
|
|
for (const e of env) {
|
|
const p = Schema._.definitions(e.schema).get(name);
|
|
if (p !== void 0) {
|
|
return kOther(e, p);
|
|
}
|
|
}
|
|
throw new Error(`Undefined reference: ${name.description!}`);
|
|
}
|
|
|
|
function applyPredicate(name: symbol, v: string): Item {
|
|
return environmentWith(name,
|
|
() => `is${name.description!}(${v})`,
|
|
(e, p) => {
|
|
if (e.inline) {
|
|
return walk(v, p);
|
|
} else {
|
|
return `${e.moduleName}.is${name.description!}(${v})`;
|
|
}
|
|
});
|
|
}
|
|
|
|
function gentemp(): string {
|
|
const varname = '_tmp' + temps.length;
|
|
temps.push(varname);
|
|
return varname;
|
|
}
|
|
|
|
function literal(v: Input): Item {
|
|
let varname = literals.get(v);
|
|
if (varname === void 0) {
|
|
const s = '___' + (
|
|
v.asPreservesText()
|
|
.replace('_', '__')
|
|
.replace('*', '_STAR_')
|
|
);
|
|
varname = M.isValidToken(s, true) ? s : '__lit' + literals.size;
|
|
literals.set(v, varname);
|
|
}
|
|
return varname;
|
|
}
|
|
|
|
function typeFor(p: Pattern): Item {
|
|
switch (p.label) {
|
|
case M.___atom:
|
|
switch (p[0]) {
|
|
case M.___Boolean: return `boolean`;
|
|
case M.___Float: return `_.SingleFloat`;
|
|
case M.___Double: return `_.DoubleFloat`;
|
|
case M.___SignedInteger: return `number`;
|
|
case M.___String: return `string`;
|
|
case M.___ByteString: return `_.Bytes`;
|
|
case M.___Symbol: return `symbol`;
|
|
}
|
|
case M.___lit:
|
|
return `(typeof ${literal(p[0])})`;
|
|
case M.___ref:
|
|
return environmentWith(
|
|
p[0],
|
|
() => p[0].description!,
|
|
(e, pp) => {
|
|
if (e.inline) {
|
|
return typeFor(pp);
|
|
} else {
|
|
return `${e.moduleName}.${p[0].description!}`;
|
|
}
|
|
});
|
|
case M.___or:
|
|
return opseq('never', ' | ', ... p[0].map(pp => typeFor(pp)));
|
|
case M.___and:
|
|
return opseq('_.Value', ' & ', ... p[0].map(pp => typeFor(pp)));
|
|
case M.___pointer:
|
|
return `any`; // TODO: what to do here?
|
|
case M.___rec:
|
|
return seq('_.Record', anglebrackets(typeFor(p[0]), typeFor(p[1])));
|
|
case M.___tuple:
|
|
return brackets(... p[0].map(pp => typeFor(unname(pp))));
|
|
case M.___tuple_STAR_:
|
|
if (p[0].length === 0) {
|
|
return seq('Array<', typeFor(unname(p[1])), '>');
|
|
} else {
|
|
return brackets(... p[0].map(pp => typeFor(unname(pp))),
|
|
seq('... Array<', typeFor(unname(p[1])), '>'));
|
|
}
|
|
case M.___setof:
|
|
return seq('_.KeyedSet<', typeFor(p[0]), '>');
|
|
case M.___dictof:
|
|
return seq('_.KeyedDictionary', anglebrackets(typeFor(p[0]), typeFor(p[1])));
|
|
case M.___dict:
|
|
return parens(seq(
|
|
block(
|
|
... Array.from(p[0]).map(([k, vp]) =>
|
|
seq(`get(k: typeof ${literal(k)}): `, typeFor(vp))),
|
|
... Array.from(p[0]).map(([k, _vp]) =>
|
|
seq(`has(k: typeof ${literal(k)}): true`))),
|
|
' & _.Dictionary<_.Value>'));
|
|
default:
|
|
((_p: never) => {})(p);
|
|
throw new Error("Unreachable");
|
|
}
|
|
}
|
|
|
|
function walk(v: string, p: Pattern, recordOkAsTuple = false): Item {
|
|
switch (p.label) {
|
|
case M.___atom:
|
|
switch (p[0]) {
|
|
case M.___Boolean: return `typeof ${v} === 'boolean'`;
|
|
case M.___Float: return `_.Float.isSingle(${v})`;
|
|
case M.___Double: return `_.Float.isDouble(${v})`;
|
|
case M.___SignedInteger: return `typeof ${v} === 'number'`;
|
|
case M.___String: return `typeof ${v} === 'string'`;
|
|
case M.___ByteString: return `_.Bytes.isBytes(${v})`;
|
|
case M.___Symbol: return `typeof ${v} === 'symbol'`;
|
|
}
|
|
case M.___lit:
|
|
switch (typeof p[0]) {
|
|
case 'boolean':
|
|
case 'number':
|
|
case 'string':
|
|
case 'symbol':
|
|
return `${v} === ${literal(p[0])}`;
|
|
default:
|
|
return `_.is(${v}, ${literal(p[0])})`;
|
|
}
|
|
case M.___ref:
|
|
return applyPredicate(p[0], v);
|
|
case M.___or:
|
|
return opseq('false', ' || ', ... p[0].map(pp => walk(v, pp)));
|
|
case M.___and:
|
|
return opseq('true', ' && ', ... p[0].map(pp => walk(v, pp)));
|
|
case M.___pointer:
|
|
return `_.isPointer(${v})`;
|
|
case M.___rec:
|
|
return opseq('true', ' && ',
|
|
`_.Record.isRecord(${v})`,
|
|
walk(`${v}.label`, p[0]),
|
|
walk(v, p[1], true));
|
|
case M.___tuple:
|
|
return opseq('true', ' && ',
|
|
... (recordOkAsTuple ? []
|
|
: [`_.Array.isArray(${v})`, `!_.Record.isRecord(${v})`]),
|
|
`(${v}.length === ${p[0].length})`,
|
|
... p[0].map((pp, i) => walk(`${v}[${i}]`, unname(pp))));
|
|
case M.___tuple_STAR_:
|
|
return opseq('true', ' && ',
|
|
... (recordOkAsTuple ? []
|
|
: [`_.Array.isArray(${v})`, `!_.Record.isRecord(${v})`]),
|
|
`(${v}.length >= ${p[0].length})`,
|
|
seq(`${v}.slice(${p[0].length})`,
|
|
`.every(v => `,
|
|
parens(walk('v', unname(p[1]))),
|
|
`)`),
|
|
... p[0].map((pp, i) => walk(`${v}[${i}]`, unname(pp))));
|
|
case M.___setof:
|
|
return opseq('true', ' && ',
|
|
`_.Set.isSet(${v})`,
|
|
fnblock(
|
|
seq(`for (const vv of ${v}) `, block(
|
|
seq('if (!(', walk('vv', p[0]), ')) return false'))),
|
|
seq('return true')));
|
|
case M.___dictof:
|
|
return opseq('true', ' && ',
|
|
`_.Dictionary.isDictionary(${v})`,
|
|
fnblock(
|
|
seq(`for (const e of ${v}) `, block(
|
|
seq('if (!(', walk('e[0]', p[0]), ')) return false'),
|
|
seq('if (!(', walk('e[1]', p[1]), ')) return false'))),
|
|
seq('return true')));
|
|
case M.___dict:
|
|
return opseq('true', ' && ',
|
|
`_.Dictionary.isDictionary(${v})`,
|
|
... Array.from(p[0]).map(([k, vp]) => {
|
|
const tmp = gentemp();
|
|
return parens(seq(
|
|
`(${tmp} = ${v}.get(${literal(k)})) !== void 0 && `,
|
|
walk(tmp, vp)));
|
|
}));
|
|
default:
|
|
((_p: never) => {})(p);
|
|
throw new Error("Unreachable");
|
|
}
|
|
}
|
|
|
|
function unname(p: NamedPattern): Pattern {
|
|
return (p.label === M.___named) ? p[1] : p;
|
|
}
|
|
|
|
function fieldName(np: NamedPattern, index: number): string {
|
|
return (np.label === M.___named) ? np[0].description! : `_field${index}`;
|
|
}
|
|
|
|
function fieldEntry(np: NamedPattern, index: number): Item {
|
|
return seq(JSON.stringify(fieldName(np, index)), ': ', typeFor(unname(np)));
|
|
}
|
|
|
|
for (const [name0, pattern] of Schema._.definitions(schema)) {
|
|
const name = name0 as symbol;
|
|
temps = [];
|
|
const recognizer = walk('v', pattern);
|
|
if (pattern.label === M.___rec &&
|
|
pattern[0].label === M.___lit &&
|
|
pattern[1].label === M.___tuple)
|
|
{
|
|
types.push(
|
|
seq(`export const ${name.description!} = _.Record.makeConstructor<`,
|
|
braces(... pattern[1][0].map(fieldEntry)),
|
|
`>()(${literal(pattern[0][0])}, `,
|
|
JSON.stringify(pattern[1][0].map(fieldName)), `);`));
|
|
}
|
|
types.push(
|
|
seq(`export type ${name.description!} = `, typeFor(pattern), `;`));
|
|
predicates.push(
|
|
seq('export function ', `is${name.description!}`,
|
|
'(v: any): v is ', name.description!, ' ',
|
|
block(
|
|
... temps.length > 0 ? [seq('let ', commas(... temps), ': any')] : [],
|
|
seq('return ', recognizer))));
|
|
}
|
|
|
|
const f = new Formatter();
|
|
f.write(`import * as _ from ${JSON.stringify(preservesModule)};\n`);
|
|
f.newline();
|
|
|
|
const sortedLiterals = Array.from(literals);
|
|
sortedLiterals.sort((a, b) => a[1] < b[1] ? -1 : a[1] === b[1] ? 0 : 1);
|
|
for (const [lit, varname] of sortedLiterals) {
|
|
f.write(seq(`export const ${varname} = `, sourceCodeFor(lit), `;\n`));
|
|
}
|
|
f.newline();
|
|
|
|
types.forEach(t => {
|
|
f.write(t);
|
|
f.newline();
|
|
f.newline();
|
|
});
|
|
f.newline();
|
|
|
|
predicates.forEach(p => {
|
|
f.write(p);
|
|
f.newline();
|
|
f.newline();
|
|
});
|
|
|
|
return f.toString();
|
|
}
|
|
|
|
export function stringSource(s: string) {
|
|
return JSON.stringify(s);
|
|
}
|
|
|
|
export function sourceCodeFor(v: Value<any>): Item {
|
|
return fold(v, {
|
|
boolean(b: boolean): Item { return b.toString(); },
|
|
single(f: number): Item { return f.toString(); },
|
|
double(f: number): Item { return f.toString(); },
|
|
integer(i: number): Item { return i.toString(); },
|
|
string(s: string): Item { return stringSource(s); },
|
|
bytes(b: Bytes): Item {
|
|
return seq(`Uint8Array.from(`, brackets(... Array.from(b).map(b => b.toString())), `)`);
|
|
},
|
|
symbol(s: symbol): Item { return `Symbol.for(${stringSource(s.description!)})`; },
|
|
|
|
record(r: Record<Value<any>, Tuple<Value<any>>, any>, k: Fold<any, Item>): Item {
|
|
return seq(`_.Record`, parens(k(r.label), brackets(... r.map(k))));
|
|
},
|
|
array(a: Array<Value<any>>, k: Fold<any, Item>): Item {
|
|
return brackets(... a.map(k));
|
|
},
|
|
set(s: Set<any>, k: Fold<any, Item>): Item {
|
|
return seq('new _.Set', parens(brackets(... Array.from(s).map(k))));
|
|
},
|
|
dictionary(d: Dictionary<Value<any>, any>, k: Fold<any, Item>): Item {
|
|
return seq('new _.Dictionary', parens(brackets(... Array.from(d).map(([kk,vv]) =>
|
|
brackets(k(kk), k(vv))))));
|
|
},
|
|
|
|
annotated(a: Annotated<any>, k: Fold<any, Item>): Item {
|
|
return seq('_.annotate', parens(k(a.item), ... a.annotations.map(k)));
|
|
},
|
|
|
|
pointer(t: any, _k: Fold<any, Item>): Item {
|
|
throw new Error(preserves`Cannot emit source code for construction of pointer ${t}`);
|
|
},
|
|
});
|
|
}
|