import { Dictionary, KeyedSet, FlexSet, Position, stringify } from "@preserves/core"; import { refPosition } from "../reader"; import * as M from "../meta"; import { anglebrackets, block, braces, commas, formatItems, Item, keyvalue, seq, opseq } from "./block"; import { ANY_TYPE, RefType, Type } from "./type"; import { renderType, variantInitFor } from "./rendertype"; import { typeForDefinition } from "./gentype"; import { SchemaSyntaxError } from "../error"; export interface CompilerOptions { preservesModule?: string; defaultEmbeddedType?: M.Ref; warn?(message: string, pos: Position | null): void; } export interface Capture { fieldName: string; sourceExpr: string; } export const RECURSION_LIMIT = 128; export class ModuleContext { readonly env: M.Environment; readonly modulePath: M.ModulePath; readonly schema: M.Schema; readonly options: CompilerOptions; readonly embeddedType: Item; readonly literals = new Dictionary(); readonly preamble: Item[] = []; readonly typedefs: Item[] = []; readonly functiondefs: Item[] = []; readonly imports = new KeyedSet<[M.ModulePath, string, string, string]>(); constructor( env: M.Environment, modulePath: M.ModulePath, schema: M.Schema, options: CompilerOptions, ) { this.env = env; this.modulePath = modulePath; this.schema = schema; this.options = options; switch (schema.embeddedType._variant) { case 'false': this.embeddedType = '_.GenericEmbedded'; break; case 'Ref': { const t = this.resolver()(schema.embeddedType.value); this.embeddedType = t.typeName; break; } } } literal(v: M.Input): Item { let varname = this.literals.get(v); if (varname === void 0) { varname = M.jsId('$' + stringify(v), () => '__lit' + this.literals.size); this.literals.set(v, varname); } return varname; } derefPattern(p: M.Definition, refCount = 0): M.Definition { if (refCount > RECURSION_LIMIT) { throw new Error('Recursion limit exceeded'); } if (p._variant === 'Pattern' && p.value._variant === 'SimplePattern' && p.value.value._variant === 'Ref') { 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))); } else { return p; } } definePreamble(i: Item): void { this.preamble.push(i); } defineType(f: Item): void { this.typedefs.push(f); } defineFunctions(definitionName: string, f: (ctx: FunctionContext) => Item[]): void { this.functiondefs.push(... f(new FunctionContext(this, definitionName))); } resolver(modulePath?: M.ModulePath): (ref: M.Ref) => RefType { return (ref) => this.lookup( ref, (_p, _t) => Type.ref(ref.name.description!, ref), (modPath, modId, modFile, modExpr, _p, t) => { this.imports.add([modPath, modId, modFile, modExpr]); return Type.ref(`${modId}${modExpr}.${ref.name.description!}`, ref); }, modulePath); } lookupType(name: M.Ref, modulePath?: M.ModulePath): Type | null { const t = this.lookup( name, (_p, t) => t, (_modPath, _modId, _modFile, _modExpr, _p, t) => t, modulePath); return t ? t() : null; } lookup(name: M.Ref, kLocal: (p: M.Definition, t: () => Type) => R, kOther: (modPath: M.ModulePath, modId: string, modFile: string, modExpr: string, p: M.Definition | null, t: (() => Type) | null) => R, modulePath?: M.ModulePath) : R { const soughtModule = name.module.length ? name.module : (modulePath ?? this.modulePath); 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); } } } } 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); } 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)); } } } export class FunctionContext { readonly mod: ModuleContext; readonly definitionName: string; tempCounter = 0; temps: Map = new Map(); captures: Capture[] = []; variantName: string | undefined = void 0; constructor(mod: ModuleContext, definitionName: string) { this.mod = mod; this.definitionName = definitionName; } gentempname(): string { return '_tmp' + this.tempCounter++; } gentemp(vartype: Type = ANY_TYPE): string { const typeitem = renderType(this.mod, vartype); const typestr = formatItems([typeitem], Infinity); const varname = this.gentempname(); let e = this.temps.get(typestr); if (e === void 0) { e = { type: typeitem, names: [] }; this.temps.set(typestr, e); } e.names.push(varname); return varname; } block(f: () => Item[]): Item { const oldTemps = this.temps; this.temps = new Map(); const items = f(); const ts = this.temps; this.temps = oldTemps; return block( ... Array.from(ts).map(([_typestr, { type, names }]) => seq(`let `, commas(... names), `: (`, type, `) | undefined`)), ... items); } withCapture( fieldName: string | undefined, sourceExpr: string, ks: (sourceExpr: string) => R): R { if (fieldName !== void 0) this.captures.push({ fieldName, sourceExpr }); const result = ks(sourceExpr); if (fieldName !== void 0) this.captures.pop(); return result; } convertCapture( fieldName: string | undefined, sourceExpr: string, ks: () => Item[]): Item { return this.withCapture(fieldName, sourceExpr, sourceExpr => seq(`if (${sourceExpr} !== void 0) `, this.block(() => ks()))); } buildCapturedCompound(dest: string): Item { const fields = [ ... variantInitFor(this.variantName), ... this.captures.map(({ fieldName, sourceExpr }) => keyvalue(fieldName, sourceExpr)), seq(`__as_preserve__() `, block(`return from${this.definitionName}(this)`)) ]; return seq(`${dest} = `, braces(... fields)); } } export class WalkState { modulePath: M.ModulePath; readonly seen: FlexSet; constructor(modulePath: M.ModulePath) { this.modulePath = modulePath; this.seen = new FlexSet(refCanonicalizer); } cycleCheck( 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]); }