2022-01-26 13:15:35 +00:00
|
|
|
import { Dictionary, KeyedSet, FlexSet, Position, stringify } from "@preserves/core";
|
2021-03-18 10:15:10 +00:00
|
|
|
import { refPosition } from "../reader";
|
|
|
|
import * as M from "../meta";
|
2022-01-22 22:25:16 +00:00
|
|
|
import { anglebrackets, block, braces, commas, formatItems, Item, keyvalue, seq, opseq } from "./block";
|
2021-10-11 10:56:53 +00:00
|
|
|
import { ANY_TYPE, RefType, Type } from "./type";
|
2021-05-21 13:49:06 +00:00
|
|
|
import { renderType, variantInitFor } from "./rendertype";
|
2021-10-11 10:56:53 +00:00
|
|
|
import { typeForDefinition } from "./gentype";
|
|
|
|
import { SchemaSyntaxError } from "../error";
|
2021-03-18 10:15:10 +00:00
|
|
|
|
|
|
|
export interface CompilerOptions {
|
|
|
|
preservesModule?: string;
|
2021-05-17 12:54:06 +00:00
|
|
|
defaultEmbeddedType?: M.Ref;
|
2021-03-18 10:15:10 +00:00
|
|
|
warn?(message: string, pos: Position | null): void;
|
|
|
|
}
|
|
|
|
|
2021-03-18 21:33:37 +00:00
|
|
|
export interface Capture {
|
|
|
|
fieldName: string;
|
|
|
|
sourceExpr: string;
|
|
|
|
}
|
|
|
|
|
2021-03-23 10:36:55 +00:00
|
|
|
export const RECURSION_LIMIT = 128;
|
|
|
|
|
2021-03-18 10:15:10 +00:00
|
|
|
export class ModuleContext {
|
|
|
|
readonly env: M.Environment;
|
2021-10-11 10:56:53 +00:00
|
|
|
readonly modulePath: M.ModulePath;
|
2021-03-18 10:15:10 +00:00
|
|
|
readonly schema: M.Schema;
|
|
|
|
readonly options: CompilerOptions;
|
2021-10-11 10:56:53 +00:00
|
|
|
readonly embeddedType: Item;
|
2021-03-18 10:15:10 +00:00
|
|
|
|
2021-10-11 10:56:53 +00:00
|
|
|
readonly literals = new Dictionary<M.InputEmbedded, string>();
|
2021-12-09 17:36:45 +00:00
|
|
|
readonly preamble: Item[] = [];
|
2021-03-18 10:15:10 +00:00
|
|
|
readonly typedefs: Item[] = [];
|
|
|
|
readonly functiondefs: Item[] = [];
|
2021-12-12 21:58:00 +00:00
|
|
|
readonly imports = new KeyedSet<[M.ModulePath, string, string, string]>();
|
2021-03-18 10:15:10 +00:00
|
|
|
|
2021-10-11 10:56:53 +00:00
|
|
|
constructor(
|
|
|
|
env: M.Environment,
|
|
|
|
modulePath: M.ModulePath,
|
|
|
|
schema: M.Schema,
|
|
|
|
options: CompilerOptions,
|
|
|
|
) {
|
2021-03-18 10:15:10 +00:00
|
|
|
this.env = env;
|
2021-10-11 10:56:53 +00:00
|
|
|
this.modulePath = modulePath;
|
2021-03-18 10:15:10 +00:00
|
|
|
this.schema = schema;
|
|
|
|
this.options = options;
|
2021-10-11 10:56:53 +00:00
|
|
|
switch (schema.embeddedType._variant) {
|
|
|
|
case 'false':
|
|
|
|
this.embeddedType = '_.GenericEmbedded';
|
|
|
|
break;
|
|
|
|
case 'Ref': {
|
|
|
|
const t = this.resolver()(schema.embeddedType.value);
|
|
|
|
this.embeddedType = t.typeName;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2021-03-18 10:15:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
literal(v: M.Input): Item {
|
|
|
|
let varname = this.literals.get(v);
|
|
|
|
if (varname === void 0) {
|
2022-01-26 13:15:35 +00:00
|
|
|
varname = M.jsId('$' + stringify(v), () => '__lit' + this.literals.size);
|
2021-03-18 10:15:10 +00:00
|
|
|
this.literals.set(v, varname);
|
|
|
|
}
|
|
|
|
return varname;
|
|
|
|
}
|
|
|
|
|
2021-03-23 10:36:55 +00:00
|
|
|
derefPattern(p: M.Definition, refCount = 0): M.Definition {
|
|
|
|
if (refCount > RECURSION_LIMIT) {
|
|
|
|
throw new Error('Recursion limit exceeded');
|
|
|
|
}
|
2021-05-20 20:34:20 +00:00
|
|
|
if (p._variant === 'Pattern' &&
|
|
|
|
p.value._variant === 'SimplePattern' &&
|
|
|
|
p.value.value._variant === 'Ref')
|
2021-03-22 11:13:34 +00:00
|
|
|
{
|
2021-12-12 21:58:00 +00:00
|
|
|
return this.lookup(p.value.value.value,
|
|
|
|
(p, _t) => this.derefPattern(p, refCount + 1),
|
|
|
|
((_modPath, _modId, _modFile, _modExpr, pp, _tt) =>
|
|
|
|
this.derefPattern(pp ?? p, refCount + 1)));
|
2021-03-18 10:15:10 +00:00
|
|
|
} else {
|
2021-03-23 10:36:55 +00:00
|
|
|
return p;
|
2021-03-18 10:15:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-09 17:36:45 +00:00
|
|
|
definePreamble(i: Item): void {
|
|
|
|
this.preamble.push(i);
|
|
|
|
}
|
|
|
|
|
2021-03-18 10:15:10 +00:00
|
|
|
defineType(f: Item): void {
|
|
|
|
this.typedefs.push(f);
|
|
|
|
}
|
|
|
|
|
2022-01-22 22:25:16 +00:00
|
|
|
defineFunctions(definitionName: string, f: (ctx: FunctionContext) => Item[]): void {
|
|
|
|
this.functiondefs.push(... f(new FunctionContext(this, definitionName)));
|
2021-03-18 10:15:10 +00:00
|
|
|
}
|
2021-05-21 13:49:06 +00:00
|
|
|
|
2021-10-11 10:56:53 +00:00
|
|
|
resolver(modulePath?: M.ModulePath): (ref: M.Ref) => RefType {
|
2021-12-09 17:36:45 +00:00
|
|
|
return (ref) => this.lookup(
|
|
|
|
ref,
|
|
|
|
(_p, _t) => Type.ref(ref.name.description!, ref),
|
2021-12-13 11:17:12 +00:00
|
|
|
(modPath, modId, modFile, modExpr, _p, t) => {
|
2021-12-12 21:58:00 +00:00
|
|
|
this.imports.add([modPath, modId, modFile, modExpr]);
|
|
|
|
return Type.ref(`${modId}${modExpr}.${ref.name.description!}`, ref);
|
2021-12-09 17:36:45 +00:00
|
|
|
},
|
|
|
|
modulePath);
|
2021-10-11 10:56:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
lookupType(name: M.Ref, modulePath?: M.ModulePath): Type | null {
|
2021-12-12 21:58:00 +00:00
|
|
|
const t = this.lookup(
|
|
|
|
name,
|
|
|
|
(_p, t) => t,
|
|
|
|
(_modPath, _modId, _modFile, _modExpr, _p, t) => t,
|
|
|
|
modulePath);
|
2021-10-11 10:56:53 +00:00
|
|
|
return t ? t() : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
lookup<R>(name: M.Ref,
|
|
|
|
kLocal: (p: M.Definition, t: () => Type) => R,
|
2021-12-09 17:36:45 +00:00
|
|
|
kOther: (modPath: M.ModulePath,
|
|
|
|
modId: string,
|
|
|
|
modFile: string,
|
2021-12-12 21:58:00 +00:00
|
|
|
modExpr: string,
|
2021-12-09 17:36:45 +00:00
|
|
|
p: M.Definition | null,
|
|
|
|
t: (() => Type) | null) => R,
|
|
|
|
modulePath?: M.ModulePath)
|
|
|
|
: R {
|
2021-10-11 10:56:53 +00:00
|
|
|
const soughtModule = name.module.length ? name.module : (modulePath ?? this.modulePath);
|
|
|
|
|
2021-12-13 11:17:12 +00:00
|
|
|
const e = M.envLookup(this.env, soughtModule);
|
|
|
|
if (e !== null) {
|
|
|
|
const expr = (e.typescriptModuleExpr === null) ? '' : '.' + e.typescriptModuleExpr;
|
|
|
|
if (e.schema === null) {
|
|
|
|
// It's an artificial module, not from a schema. Assume the identifier is present.
|
|
|
|
return kOther(soughtModule,
|
|
|
|
M.modsymFor(e),
|
|
|
|
e.typescriptModulePath,
|
|
|
|
expr,
|
|
|
|
null,
|
|
|
|
null);
|
|
|
|
} else {
|
|
|
|
const p = e.schema.definitions.get(name.name);
|
|
|
|
if (p !== void 0) {
|
|
|
|
let t = () => typeForDefinition(this.resolver(soughtModule), p);
|
|
|
|
if (name.module.length) {
|
|
|
|
return kOther(soughtModule,
|
|
|
|
M.modsymFor(e),
|
|
|
|
e.typescriptModulePath,
|
|
|
|
expr,
|
|
|
|
p,
|
|
|
|
t);
|
|
|
|
} else {
|
|
|
|
return kLocal(p, t);
|
2021-10-11 10:56:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new SchemaSyntaxError(`Undefined reference: ${M.formatRef(name)}`, refPosition(name));
|
|
|
|
}
|
|
|
|
|
|
|
|
genericParameters(): Item {
|
|
|
|
return anglebrackets(seq('_embedded = ', this.embeddedType));
|
|
|
|
}
|
|
|
|
|
|
|
|
genericParametersFor(t: Type): Item {
|
|
|
|
return this.hasEmbedded(t) ? this.genericParameters() : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
genericArgs(): Item {
|
|
|
|
return `<_embedded>`;
|
|
|
|
}
|
|
|
|
|
|
|
|
genericArgsFor(t: Type): Item {
|
|
|
|
return this.hasEmbedded(t) ? this.genericArgs() : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
hasEmbedded(t: Type): boolean {
|
|
|
|
const self = this;
|
|
|
|
const state = new WalkState(this.modulePath);
|
|
|
|
|
|
|
|
function walk(t: Type): boolean {
|
|
|
|
switch (t.kind) {
|
|
|
|
case 'union':
|
|
|
|
for (const v of t.variants.values()) { if (walk(v)) return true; };
|
|
|
|
return false;
|
|
|
|
case 'unit': return false;
|
|
|
|
case 'array': return walk(t.type);
|
|
|
|
case 'set': return true; // because ref to _embedded in renderType()
|
|
|
|
case 'dictionary': return true; // because ref to _embedded in renderType()
|
|
|
|
case 'ref': {
|
|
|
|
if (t.ref === null) {
|
|
|
|
switch (t.typeName) {
|
|
|
|
case '_embedded': return true;
|
|
|
|
case '_.Value': return true;
|
|
|
|
default: return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return state.cycleCheck(
|
|
|
|
t.ref,
|
|
|
|
ref => self.lookupType(ref, state.modulePath),
|
|
|
|
t => t ? walk(t) : false,
|
|
|
|
() => false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case 'record':
|
|
|
|
for (const v of t.fields.values()) { if (walk(v)) return true; };
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return walk(t);
|
2021-05-21 13:49:06 +00:00
|
|
|
}
|
2022-01-22 22:25:16 +00:00
|
|
|
|
|
|
|
withAsPreserveMixinType(name: string, t: Type): Item {
|
|
|
|
if (t.kind === 'unit' || t.kind === 'record' || t.kind === 'union') {
|
|
|
|
return opseq('any', ' & ',
|
|
|
|
seq(name, this.genericArgsFor(t)),
|
|
|
|
braces(seq('__as_preserve__',
|
|
|
|
this.hasEmbedded(t) ? '' : this.genericParameters(),
|
|
|
|
'()',
|
|
|
|
': _.Value', this.genericArgs())));
|
|
|
|
} else {
|
|
|
|
return seq(name, this.genericArgsFor(t));
|
|
|
|
}
|
|
|
|
}
|
2021-03-18 10:15:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export class FunctionContext {
|
|
|
|
readonly mod: ModuleContext;
|
2022-01-22 22:25:16 +00:00
|
|
|
readonly definitionName: string;
|
2021-03-18 10:15:10 +00:00
|
|
|
|
|
|
|
tempCounter = 0;
|
2021-03-18 21:33:37 +00:00
|
|
|
temps: Map<string, { type: Item, names: string[] }> = new Map();
|
2021-03-19 22:42:43 +00:00
|
|
|
|
2021-03-18 21:33:37 +00:00
|
|
|
captures: Capture[] = [];
|
|
|
|
variantName: string | undefined = void 0;
|
2021-03-18 10:15:10 +00:00
|
|
|
|
2022-01-22 22:25:16 +00:00
|
|
|
constructor(mod: ModuleContext, definitionName: string) {
|
2021-03-18 10:15:10 +00:00
|
|
|
this.mod = mod;
|
2022-01-22 22:25:16 +00:00
|
|
|
this.definitionName = definitionName;
|
2021-03-18 10:15:10 +00:00
|
|
|
}
|
|
|
|
|
2021-03-18 21:33:37 +00:00
|
|
|
gentempname(): string {
|
|
|
|
return '_tmp' + this.tempCounter++;
|
2021-03-18 10:15:10 +00:00
|
|
|
}
|
|
|
|
|
2021-03-19 22:42:43 +00:00
|
|
|
gentemp(vartype: Type = ANY_TYPE): string {
|
2021-10-11 10:56:53 +00:00
|
|
|
const typeitem = renderType(this.mod, vartype);
|
2021-03-19 22:42:43 +00:00
|
|
|
const typestr = formatItems([typeitem], Infinity);
|
2021-03-18 21:33:37 +00:00
|
|
|
const varname = this.gentempname();
|
|
|
|
let e = this.temps.get(typestr);
|
|
|
|
if (e === void 0) {
|
2021-03-19 22:42:43 +00:00
|
|
|
e = { type: typeitem, names: [] };
|
2021-03-18 21:33:37 +00:00
|
|
|
this.temps.set(typestr, e);
|
|
|
|
}
|
|
|
|
e.names.push(varname);
|
|
|
|
return varname;
|
2021-03-18 10:15:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
block(f: () => Item[]): Item {
|
|
|
|
const oldTemps = this.temps;
|
2021-03-18 21:33:37 +00:00
|
|
|
this.temps = new Map();
|
2021-03-18 10:15:10 +00:00
|
|
|
const items = f();
|
|
|
|
const ts = this.temps;
|
|
|
|
this.temps = oldTemps;
|
|
|
|
return block(
|
2021-03-18 21:33:37 +00:00
|
|
|
... Array.from(ts).map(([_typestr, { type, names }]) =>
|
2021-03-19 22:42:43 +00:00
|
|
|
seq(`let `, commas(... names), `: (`, type, `) | undefined`)),
|
2021-03-18 10:15:10 +00:00
|
|
|
... items);
|
|
|
|
}
|
2021-03-18 21:33:37 +00:00
|
|
|
|
2021-03-18 21:41:27 +00:00
|
|
|
withCapture<R>(
|
|
|
|
fieldName: string | undefined, sourceExpr: string, ks: (sourceExpr: string) => R): R
|
2021-03-18 21:33:37 +00:00
|
|
|
{
|
|
|
|
if (fieldName !== void 0) this.captures.push({ fieldName, sourceExpr });
|
2021-03-18 21:41:27 +00:00
|
|
|
const result = ks(sourceExpr);
|
2021-03-18 21:33:37 +00:00
|
|
|
if (fieldName !== void 0) this.captures.pop();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2021-03-18 21:41:27 +00:00
|
|
|
convertCapture(
|
2021-05-23 22:15:31 +00:00
|
|
|
fieldName: string | undefined, sourceExpr: string, ks: () => Item[]): Item
|
2021-03-18 21:41:27 +00:00
|
|
|
{
|
|
|
|
return this.withCapture(fieldName, sourceExpr, sourceExpr =>
|
2021-05-23 22:15:31 +00:00
|
|
|
seq(`if (${sourceExpr} !== void 0) `, this.block(() => ks())));
|
2021-03-18 21:41:27 +00:00
|
|
|
}
|
|
|
|
|
2021-03-18 21:33:37 +00:00
|
|
|
buildCapturedCompound(dest: string): Item {
|
2021-03-23 10:36:55 +00:00
|
|
|
const fields = [
|
2021-03-18 21:33:37 +00:00
|
|
|
... variantInitFor(this.variantName),
|
|
|
|
... this.captures.map(({ fieldName, sourceExpr }) =>
|
2022-01-22 22:25:16 +00:00
|
|
|
keyvalue(fieldName, sourceExpr)),
|
|
|
|
seq(`__as_preserve__() `, block(`return from${this.definitionName}(this)`))
|
2021-03-23 10:36:55 +00:00
|
|
|
];
|
2022-01-22 22:25:16 +00:00
|
|
|
return seq(`${dest} = `, braces(... fields));
|
2021-03-18 21:33:37 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-11 10:56:53 +00:00
|
|
|
|
|
|
|
export class WalkState {
|
|
|
|
modulePath: M.ModulePath;
|
|
|
|
readonly seen: FlexSet<M.Ref>;
|
|
|
|
|
|
|
|
constructor(modulePath: M.ModulePath) {
|
|
|
|
this.modulePath = modulePath;
|
|
|
|
this.seen = new FlexSet(refCanonicalizer);
|
|
|
|
}
|
|
|
|
|
|
|
|
cycleCheck<E, R>(
|
|
|
|
r0: M.Ref,
|
|
|
|
step: (ref: M.Ref) => E,
|
|
|
|
ks: (e: E) => R,
|
|
|
|
kf: () => R,
|
|
|
|
): R {
|
|
|
|
const r = M.Ref({
|
|
|
|
module: r0.module.length ? r0.module : this.modulePath,
|
|
|
|
name: r0.name
|
|
|
|
});
|
|
|
|
if (this.seen.has(r)) {
|
|
|
|
return kf();
|
|
|
|
} else {
|
|
|
|
this.seen.add(r);
|
|
|
|
const maybe_e = step(r);
|
|
|
|
const saved = this.modulePath;
|
|
|
|
this.modulePath = r.module;
|
|
|
|
const result = ks(maybe_e);
|
|
|
|
this.modulePath = saved;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function refCanonicalizer(r: M.Ref): string {
|
|
|
|
return stringify([... r.module, r.name]);
|
|
|
|
}
|