import { EncodableDictionary, KeyedDictionary, Dictionary, Value, is, Record, Float, Bytes, isEmbedded, isSequence, Set, Atom, Embedded, merge as plainMerge, Preservable, PreserveWritable, _iterMap, stringify, fromJS } from '@preserves/core'; import { SchemaDefinition } from './reflection'; import * as M from './meta'; import * as H from './host'; export const UNIT: true = true; export type Parsed = Atom | V | Parsed[] | DictOf | Bindings; export type TopParsed = Atom | V | Parsed[] | DictOf | TopBindings; export type Top = Preservable & PreserveWritable & { __as_preserve__(): Value }; export type DictOf = EncodableDictionary, Parsed, V>; export type BindingName = string; export type Bindings = { [key: BindingName]: Parsed }; export type TopBindings = Bindings & Top; export type SingleConstructor = ((input: any) => Parsed) & { schema(): SchemaDefinition }; export type MultipleConstructors = { [key: string]: SingleConstructor }; export type DefinitionConstructors = SingleConstructor | MultipleConstructors; export namespace Bindings { export function empty(): Bindings { return {}; } export function single(k: BindingName, v: Parsed): Bindings { const bs = empty(); bs[k] = v; return bs; } export function merge(... vs: Bindings[]): Bindings { const acc = empty(); for (const v of vs) { Object.entries(v).forEach(([kw, vw]) => acc[kw] = vw); } return acc; } } export type DynField = | { type: 'simple', value: Parsed } | { type: 'compound', values: Bindings } ; export namespace DynField { export function unwrap(f: DynField): Parsed { if (f.type === 'simple') return f.value; return f.values; } export function unwrap_compound(f: DynField): Bindings { if (f.type === 'simple') throw new Error("Cannot unwrap DynField.simple to compound fields"); return f.values; } export function simple(value: Parsed): DynField { return { type: 'simple', value }; } export function maybeSimple(value: Parsed | null): DynField { return value === null ? compound(Bindings.empty()) : simple(value); } export function compound(values: Bindings): DynField { return { type: 'compound', values }; } export function promote(f: DynField, key?: symbol): Bindings { if (f.type === 'compound') return f.values; return key ? Bindings.single(M.jsId(key.description!), f.value) : Bindings.empty(); } } function optmap(a: A | undefined, f: (a: A) => B): B | undefined { if (a === void 0) return void 0; return f(a); } export type Unparseable = TopParsed; export type Unparser = (v: Parsed) => Value; export type UnparserCompound = (v: Bindings) => Value; export class SchemaInterpreter { activeModule: M.ModulePath = []; unparserCache: { [key: string]: [Unparser] } = {}; constructor ( public env: M.Modules = new KeyedDictionary(), public mergeEmbeddeds: (a: V, b: V) => V | undefined = (_a, _b) => void 0, ) {} _withModule(modulePath: M.ModulePath, f: () => R): R { const saved = this.activeModule; if (modulePath.length > 0) this.activeModule = modulePath; try { return f(); } finally { if (modulePath.length > 0) this.activeModule = saved; } } _findModule(modulePath: M.ModulePath): { resolved: M.ModulePath, schema: M.Schema } { const prefix = this.activeModule.slice(); while (true) { const probe = [... prefix, ... modulePath]; const schema = this.env.get(probe); if (schema !== void 0) { return { resolved: probe, schema }; } if (prefix.length === 0) { throw new Error(`No such preserves-schema module: ${M.formatModulePath(modulePath)}, referred to in module ${M.formatModulePath(this.activeModule)}`); } prefix.pop(); } } _lookup( modulePath: M.ModulePath, name: symbol, f: (d: M.Definition, schema: M.Schema) => R, ): R { const { resolved, schema } = this._findModule(modulePath); return this._withModule(resolved, () => { const definition = schema.definitions.get(name); if (definition === void 0) { throw new Error(`No such preserves-schema definition: ${[... modulePath, name].map(s => s.description!).join('.')}`); } return f(definition, schema); }); } makeTop(modulePath: M.ModulePath, name: symbol, fields: Bindings): TopBindings { const result = fields as TopBindings; result.__as_preserve__ = () => this.unparser(modulePath, name)(result); result.__preserve_on__ = function (e) { e.push(this.__as_preserve__()); }; result.__preserve_text_on__ = function (w) { w.push(this.__as_preserve__()); }; return result; } definitionConstructor( modulePath: M.ModulePath, name: symbol, ): DefinitionConstructors { return this._lookup(modulePath, name, (definition, schema): DefinitionConstructors => { function attachSchema( f: (input: any) => Parsed, variant?: symbol, ): SingleConstructor { const g = f as SingleConstructor; g.schema = () => ({ schema: fromJS(schema), imports: {}, // TODO definitionName: name, variant, }); return g; } const ty = H.definitionType(definition); if (ty._variant === 'union') { const multiple: MultipleConstructors = {}; ty.variants.forEach(v => { const _variant = v.label.description!; switch (v.type._variant) { case 'Field': multiple[_variant] = attachSchema( (value: any) => this.makeTop(modulePath, name, { _variant, value }), v.label); break; case 'Record': multiple[_variant] = attachSchema( (fields: object) => this.makeTop(modulePath, name, { _variant, ... fields }), v.label); break; } }); return multiple; } else { const flatName = M.formatModulePath([... modulePath, name]); switch (ty.value._variant) { case 'Field': { const tmp = { [flatName]: (value: any) => value }; return attachSchema(tmp[flatName]); } case 'Record': { const rec = ty.value.value; if (rec.fields.length > 1) { const tmp = { [flatName]: (fields: Bindings) => this.makeTop(modulePath, name, fields) }; return attachSchema(tmp[flatName]); } else { const tmp = { [flatName]: (field: Parsed) => this.makeTop(modulePath, name, { [M.jsId(rec.fields[0].name.description!)]: field, }) }; return attachSchema(tmp[flatName]); } } } } }); } parse( modulePath: M.ModulePath, name: symbol, input: Value, ): Unparseable { const v = this.tryParse(modulePath, name, input); if (v === void 0) { throw new TypeError( `Invalid ${M.formatModulePath([... modulePath, name])}: ${stringify(input)}`) } return v; } tryParse( modulePath: M.ModulePath, name: symbol, input: Value, ): Unparseable | undefined { return this._lookup(modulePath, name, definition => optmap(this.parseDefinition(definition, input), result0 => { const ty = H.definitionType(definition); if (ty._variant === 'union' || ty.value._variant === 'Record') { return this.makeTop(modulePath, name, result0 as Bindings); } else { return result0 as Exclude, Bindings>; } })); } parseDefinition(d: M.Definition, input: Value): Parsed | undefined { switch (d._variant) { case 'or': return this.parseNamedAlternative(d.pattern0, input) ?? this.parseNamedAlternative(d.pattern1, input) ?? (() => { for (const p of d.patternN) { const r = this.parseNamedAlternative(p, input); if (r !== void 0) return r; } return void 0; })(); case 'and': { const rs = [this.parseNamedPattern(d.pattern0, input), this.parseNamedPattern(d.pattern1, input), ... d.patternN.map(p => this.parseNamedPattern(p, input))]; for (const r of rs) { if (r === void 0) return void 0; } return Bindings.merge(... rs as Bindings[]); } case 'Pattern': return optmap(this.parsePattern(d.value, input), DynField.unwrap); } } parseNamedAlternative(p: M.NamedAlternative, input: Value): Bindings | undefined { return optmap(this.parsePattern(p.pattern, input), w => { const result = DynField.promote(w, Symbol.for('value')); result._variant = p.variantLabel; return result; }); } parseNamedPattern(p: M.NamedPattern, input: Value): Bindings | undefined { switch (p._variant) { case 'named': return optmap(this.parseSimplePattern(p.value.pattern, input), w => DynField.promote(DynField.maybeSimple(w), p.value.name)); case 'anonymous': return optmap(this.parsePattern(p.value, input), w => DynField.promote(w)); } } parseNamedSimplePattern(p: M.NamedSimplePattern, input: Value): DynField | undefined { switch (p._variant) { case 'named': return optmap(this.parseSimplePattern(p.value.pattern, input), w => DynField.compound(DynField.promote(DynField.maybeSimple(w), p.value.name))); case 'anonymous': return optmap(this.parseSimplePattern(p.value, input), DynField.maybeSimple); } } parseSimplePattern(p: M.SimplePattern, input: Value): Parsed | null | undefined { const inputIf = (b: boolean) => b ? input : void 0; switch (p._variant) { case 'any': return input; case 'atom': switch (p.atomKind._variant) { case 'Boolean': return inputIf(typeof input === 'boolean'); case 'Double': return inputIf(Float.isDouble(input)); case 'SignedInteger': return inputIf(typeof input === 'number' || typeof input === 'bigint'); case 'String': return inputIf(typeof input === 'string'); case 'ByteString': return inputIf(Bytes.isBytes(input)); case 'Symbol': return inputIf(typeof input === 'symbol'); } case 'embedded': return isEmbedded(input) ? input.embeddedValue : void 0; case 'lit': return is(input, p.value) ? null : void 0; case 'seqof': { if (!isSequence(input)) return void 0; const result: Parsed[] = []; for (const v of input) { const w = this.parseSimplePattern(p.pattern, v); if (w === void 0) return void 0; if (w !== null) result.push(w); } return result; } case 'setof': { if (!Set.isSet(input)) return void 0; const result: Parsed[] = []; for (const v of input) { const w = this.parseSimplePattern(p.pattern, v); if (w === void 0) return void 0; if (w !== null) result.push(w); } return result; } case 'dictof': { if (!Dictionary.isDictionary(input)) return void 0; const result: DictOf = new EncodableDictionary( this.unparserSimplePattern(p.key), this.unparserSimplePattern(p.value)); for (const [k, v] of input) { const kw = this.parseSimplePattern(p.key, k); if (kw === void 0) return void 0; const vw = this.parseSimplePattern(p.value, v); if (vw === void 0) return void 0; result.set(kw === null ? UNIT : kw, vw === null ? UNIT : vw); } return result; } case 'Ref': return this.tryParse(p.value.module, p.value.name, input); } } parseCompoundPattern(p: M.CompoundPattern, input: Value): Bindings | undefined { switch (p._variant) { case 'rec': if (!Record.isRecord, Array>, V>(input)) return void 0; return optmap(this.parseNamedPattern(p.label, input.label), lw => optmap(this.parseNamedPattern(p.fields, Array.from(input)), fsw => Bindings.merge(lw, fsw))); case 'tuple': { if (!isSequence(input)) return void 0; if (input.length !== p.patterns.length) return void 0; let results: Bindings[] = []; for (let i = 0; i < p.patterns.length; i++) { const w = this.parseNamedPattern(p.patterns[i], input[i]); if (w === void 0) return void 0; results.push(w); } return Bindings.merge(... results); } case 'tuplePrefix': { if (!isSequence(input)) return void 0; if (input.length < p.fixed.length) return void 0; let fixed_results: Bindings[] = []; for (let i = 0; i < p.fixed.length; i++) { const w = this.parseNamedPattern(p.fixed[i], input[i]); if (w === void 0) return void 0; fixed_results.push(w); } const remainder = input.slice(p.fixed.length); return optmap(this.parseNamedSimplePattern(p.variable, remainder), vw => { const variable_results = DynField.unwrap_compound(vw); return Bindings.merge(variable_results, ... fixed_results); }); } case 'dict': { if (!Dictionary.isDictionary(input)) return void 0; const results: Bindings[] = []; for (const [key, vp] of p.entries) { const v = input.get(key); if (v === void 0) return void 0; const vw = this.parseNamedSimplePattern(vp, v); if (vw === void 0) return void 0; results.push(DynField.unwrap_compound(vw)); } return Bindings.merge(... results); } } } parsePattern(p: M.Pattern, input: Value): DynField | undefined { switch (p._variant) { case 'SimplePattern': return optmap(this.parseSimplePattern(p.value, input), DynField.maybeSimple); case 'CompoundPattern': return optmap(this.parseCompoundPattern(p.value, input), DynField.compound); } } unparse( modulePath: M.ModulePath, name: symbol, v: Unparseable, ): Value { return this.unparser(modulePath, name)(v); } unparser(modulePath: M.ModulePath, name: symbol): Unparser { return this._unparser(modulePath, name)[0]; } _unparser(modulePath: M.ModulePath, name: symbol): [Unparser] { const key = [... modulePath.map(n => n.description!), name.description!].join('.'); if (!(key in this.unparserCache)) { const cell: [Unparser] = [null!]; this.unparserCache[key] = cell; cell[0] = this._lookup(modulePath, name, p => this.unparserDefinition(p)); } return this.unparserCache[key]; } unparserDefinition(p: M.Definition): Unparser { switch (p._variant) { case 'or': { const ups = [p.pattern0, p.pattern1, ... p.patternN].map( p => this.unparserNamedAlternative(p)); return v => { const bs = v as Bindings; return ups.find(up => up[0] === bs._variant)![1](bs); }; } case 'and': { const ups = [p.pattern0, p.pattern1, ... p.patternN].map( p => this.unparserNamedPattern(p)); return v => plainMerge(this.mergeEmbeddeds, ups[0](v), ... ups.slice(1).map(up => up(v))); } case 'Pattern': return this.unparserPattern(p.value); } } unparserNamedAlternative(p: M.NamedAlternative): [string, UnparserCompound] { const up = this.unparserPattern(p.pattern); const ty = H.patternType(p.pattern); switch (ty._variant) { case 'Field': return [p.variantLabel, bs => up(bs['value'])]; case 'Record': return [p.variantLabel, up]; } } unparserNamedPattern(p: M.NamedPattern): Unparser { switch (p._variant) { case 'named': { const up = this.unparserSimplePattern(p.value.pattern); const key = M.jsId(p.value.name.description!); return v => up((v as Bindings)[key]); } case 'anonymous': return this.unparserPattern(p.value); } } unparserPattern(p: M.Pattern): Unparser { switch (p._variant) { case 'CompoundPattern': { const up = this.unparserCompoundPattern(p.value); return v => up(v as Bindings); } case 'SimplePattern': return this.unparserSimplePattern(p.value); } } unparserSimplePattern(p: M.SimplePattern): Unparser { switch (p._variant) { case 'any': return v => v as Value; // ?! case 'atom': return v => v as Atom; case 'embedded': return v => new Embedded(v as V); case 'lit': return _v => p.value; case 'seqof': { const up = this.unparserSimplePattern(p.pattern); return vs => (vs as Parsed[]).map(up); } case 'setof': { const up = this.unparserSimplePattern(p.pattern); return vs => new Set((vs as Parsed[]).map(up)); } case 'dictof': { const kp = this.unparserSimplePattern(p.key); const vp = this.unparserSimplePattern(p.value); return vs => new Dictionary(_iterMap( (vs as DictOf).entries(), ([k, v]) => [kp(k), vp(v)])); } case 'Ref': { const up = this._unparser(p.value.module, p.value.name); return v => up[0](v as Bindings); } } } unparserCompoundPattern(p: M.CompoundPattern): UnparserCompound { switch (p._variant) { case 'rec': { const lp = this.unparserNamedPattern(p.label); const fp = this.unparserNamedPattern(p.fields); return bs => Record(lp(bs), fp(bs) as Value[]); } case 'tuple': { const ups = p.patterns.map(p => this.unparserNamedPattern(p)); return bs => ups.map(up => up(bs)); } case 'tuplePrefix': { const fixed = p.fixed.map(p => this.unparserNamedPattern(p)); const variable = this.unparserNamedSimplePattern(p.variable); return bs => [... fixed.map(up => up(bs)), ... variable(bs) as Value[]]; } case 'dict': { const ups: [Value, Unparser][] = Array.from(p.entries.entries()).map( ([key, vp]) => [key, this.unparserNamedSimplePattern(vp)]); return bs => { const result = new Dictionary(); for (const [key, up] of ups) { result.set(key, up(bs)); } return result; }; } } } unparserNamedSimplePattern(p: M.NamedSimplePattern): Unparser { switch (p._variant) { case 'named': { const up = this.unparserSimplePattern(p.value.pattern); const key = M.jsId(p.value.name.description!); return v => up((v as Bindings)[key]); } case 'anonymous': return this.unparserSimplePattern(p.value); } } }