diff --git a/implementations/javascript/packages/schema/src/compiler.ts b/implementations/javascript/packages/schema/src/compiler.ts index 847ceca..b3b5ca7 100644 --- a/implementations/javascript/packages/schema/src/compiler.ts +++ b/implementations/javascript/packages/schema/src/compiler.ts @@ -1,485 +1,17 @@ -import { Pattern, NamedPattern, Schema, Input, Environment, Ref, lookup } from "./meta"; -import * as M from './meta'; -import { Annotated, Bytes, Dictionary, Fold, fold, KeyedSet, Position, preserves, Record, Set, Tuple, Value } from "@preserves/core"; -import { Formatter, parens, seq, Item, opseq, block, commas, brackets, anglebrackets, braces } from "./block"; -import { refPosition } from "./reader"; -import { Alternative, Definition } from "gen/schema"; +import { Annotated, Bytes, Dictionary, Fold, fold, Record, stringify, Tuple, Value } from "@preserves/core"; +import * as M from "./meta"; +import { CompilerOptions, ModuleContext } from "./compiler/context"; +import { brackets, Formatter, Item, parens, seq } from "./compiler/block"; +import { typeForDefinition } from "./compiler/type"; +import { decoderFor } from "./compiler/decoder"; +import { converterForDefinition } from "./compiler/converter"; -export interface CompilerOptions { - preservesModule?: string; - defaultPointer?: Ref; - warn?(message: string, pos: Position | null): void; -}; - -function fnblock(... items: Item[]): Item { - return seq('((() => ', block(... items), ')())'); -} - -class ModuleContext { - readonly env: Environment; - readonly schema: Schema; - readonly options: CompilerOptions; - - readonly literals = new Dictionary(); - readonly typedefs: Item[] = []; - readonly functiondefs: Item[] = []; - readonly imports = new KeyedSet<[string, string]>(); - - constructor(env: Environment, schema: Schema, options: CompilerOptions) { - this.env = env; - this.schema = schema; - this.options = options; - } - - literal(v: Input): Item { - let varname = this.literals.get(v); - if (varname === void 0) { - const s = v.asPreservesText() - .replace('_', '__') - .replace('*', '_STAR_'); - varname = M.isValidToken('_' + s, true) ? '$' + s : '__lit' + this.literals.size; - this.literals.set(v, varname); - } - return varname; - } - - derefPattern([_name, p]: [string, Alternative]): Definition { - if (p.label === M.$ref) { - return lookup(refPosition(p), p, this.env, - (p) => p, - (p) => p, - (_modId, _modPath, pp) => pp ?? p); - } else { - return p; - } - } - - defineType(f: Item): void { - this.typedefs.push(f); - } - - defineFunction(f: (ctx: FunctionContext) => Item): void { - this.functiondefs.push(f(new FunctionContext(this))); - } -} - -class FunctionContext { - readonly mod: ModuleContext; - - tempCounter = 0; - temps: string[] = []; - - constructor(mod: ModuleContext) { - this.mod = mod; - } - - gentemp(): string { - const varname = '_tmp' + this.tempCounter++; - this.temps.push(varname); - return varname; - } - - gentemps(n: number): string[] { - const temps = []; - while (temps.length < n) temps.push(this.gentemp()); - return temps; - } - - block(f: () => Item[]): Item { - const oldTemps = this.temps; - this.temps = []; - const items = f(); - const ts = this.temps; - this.temps = oldTemps; - return block( - ... ts.length > 0 ? [seq(`let `, commas(... ts), ': any')] : [], - ... items); - } -} - -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 accumulateCompound(ctx: FunctionContext, - p: Pattern, - kFail: () => Item[], - kAcc: (temp: string) => Item[]): Item -{ - const t = ctx.gentemp(); - return seq(`while (!d.closeCompound()) `, ctx.block(() => [ - seq(`${t} = void 0`), - ... decoderFor(ctx, p, t), - seq(`if (${t} === void 0) `, block(... kFail(), seq(`break`))), - ... kAcc(t)])); -} - -function decoderForTuple(ctx: FunctionContext, - tuplePattern: Pattern, - ps: Pattern[], - dest: string, - recordFields: boolean, - variablePattern: Pattern | undefined): Item[] -{ - const temps = ctx.gentemps(ps.length); - - function loop(i: number): Item[] { - if (i < ps.length) { - return [... decoderFor(ctx, ps[i], temps[i]), - seq(`if (${temps[i]} !== void 0) `, ctx.block(() => loop(i + 1)))]; - } else { - if (variablePattern === void 0) { - return [seq(`if (d.closeCompound()) ${dest} = `, brackets(... temps), - ` as `, typeFor(ctx.mod, tuplePattern))]; - } else { - return [block( - seq(`let vN: `, typeFor(ctx.mod, tuplePattern), - ` | undefined = `, brackets(... temps)), - accumulateCompound(ctx, - variablePattern, - () => [`vN = void 0`], - (t) => [`vN.push(${t})`]), - seq(`${dest} = vN`))]; - } - } - } - - return recordFields - ? loop(0) - : [seq(`if (d.openSequence()) `, ctx.block(() => loop(0)))]; -} - -function decoderFor(ctx: FunctionContext, p: Definition, dest: string, recordFields = false): Item[] -{ - switch (p.label) { - case M.$atom: - switch (p[0]) { - case M.$Boolean: return [`${dest} = d.nextBoolean()`]; - case M.$Float: return [`${dest} = d.nextFloat()`]; - case M.$Double: return [`${dest} = d.nextDouble()`]; - case M.$SignedInteger: return [`${dest} = d.nextSignedInteger()`]; - case M.$String: return [`${dest} = d.nextString()`]; - case M.$ByteString: return [`${dest} = d.nextByteString()`]; - case M.$Symbol: return [`${dest} = d.nextSymbol()`]; - } - case M.$lit: { - let n: string; - switch (typeof p[0]) { - case 'boolean': n = `d.nextBoolean()`; break; - case 'string': n = `d.nextString()`; break; - case 'number': n = `d.nextSignedInteger()`; break; - case 'symbol': n = `d.nextSymbol()`; break; - default: n = `d.next()`; break; - } - return [`${dest} = _.asLiteral(${n}, ${ctx.mod.literal(p[0])})`]; - } - case M.$ref: - return lookup(refPosition(p), p, ctx.mod.env, - (_p) => [`${dest} = decode${p[1].description!}(d)`], - (p) => decoderFor(ctx, p, dest), - (modId, modPath,_p) => { - ctx.mod.imports.add([modId, modPath]); - return [`${dest} = ${modId}.decode${p[1].description!}(d)`]; - }); - case M.$or: { - const alts = p[0]; - const recs = alts.map(p => ctx.mod.derefPattern(p)); - if (recs.length > 1 && recs.every(pp => pp.label === M.$rec)) { - // Hoist the record check up. - // This is pretty hacky. If we lift the level of - // discourse a little, we can do this - // automatically and generically... - return [seq(`if (d.openRecord()) `, ctx.block(() => { - const label = ctx.gentemp(); - const mark = ctx.gentemp(); - function loop(i: number): Item[] { - const alt = recs[i]; - if (alt.label !== M.$rec) throw new Error("Internal error"); // avoid a cast - return [ - seq(`if (`, predicateFor(ctx, label, unname(alt[0])), `) `, ctx.block(() => { - const fs = ctx.gentemp(); - return [... decoderFor(ctx, unname(alt[1]), fs, true), - seq(`if (${fs} !== void 0) ${dest} = _.Record`, - anglebrackets(typeFor(ctx.mod, unname(alt[0])), - typeFor(ctx.mod, unname(alt[1]))), - parens(seq(label, ` as any`), - seq(fs, ` as any`)))]; - })), - ... (i < recs.length - 1) - ? [seq(`if (${dest} === void 0) `, - ctx.block(() => [`d.restoreMark(${mark})`, ... loop(i + 1)]))] - : [], - ]; - } - return [seq(`${label} = d.next()`), - seq(`${mark} = d.mark()`), - ... loop(0)]; - }))]; - } else { - switch (alts.length) { - case 0: return []; // assume dest is already void 0 - case 1: return decoderFor(ctx, alts[0][1], dest); - default: { - const mark = ctx.gentemp(); - function loop(i: number): Item[] { - return [ - ... decoderFor(ctx, alts[i][1], dest), - ... (i < alts.length - 1) - ? [seq(`if (${dest} === void 0) `, ctx.block(() => - [`d.restoreMark(${mark})`, ... loop(i + 1)]))] - : [], - ]; - } - return [`${mark} = d.mark()`, ... loop(0)]; - } - } - } - } - case M.$and: - switch (p[0].length) { - case 0: return [`${dest} = d.next()`]; - case 1: return decoderFor(ctx, unname(p[0][0]), dest); - default: { - const [pp0, ... ppN] = p[0]; - return [... decoderFor(ctx, unname(pp0), dest), - seq(`if (!`, opseq('true', ' && ', - ... ppN.map(pp => - predicateFor(ctx, dest, unname(pp)))), - `) ${dest} = void 0`)]; - } - } - case M.$pointer: - return [`${dest} = _decodePtr(d)`]; - case M.$rec: - // assume dest is already void 0 - return [seq(`if (d.openRecord()) `, ctx.block(() => { - const label = ctx.gentemp(); - return [... decoderFor(ctx, unname(p[0]), label), - seq(`if (${label} !== void 0) `, ctx.block(() => { - const fs = ctx.gentemp(); - return [... decoderFor(ctx, unname(p[1]), fs, true), - seq(`if (${fs} !== void 0) ${dest} = _.Record`, - anglebrackets(typeFor(ctx.mod, unname(p[0])), - typeFor(ctx.mod, unname(p[1]))), - parens(seq(label, ` as any`), - seq(fs, ` as any`)))]; - }))]; - }))]; - case M.$tuple: - // assume dest is already void 0 - return decoderForTuple(ctx, p, p[0].map(unname), dest, recordFields, void 0); - case M.$tuple_STAR_: - // assume dest is already void 0 - return decoderForTuple(ctx, p, p[0].map(unname), dest, recordFields, unname(p[1])); - case M.$setof: - // assume dest is already void 0 - return [seq(`if (d.openSet()) `, ctx.block(() => [ - seq(`let r: `, typeFor(ctx.mod, p), ` | undefined = new _.KeyedSet()`), - accumulateCompound(ctx, - p[0], - () => [`r = void 0`], - (t) => [`r.add(${t})`]), - `${dest} = r`]))]; - case M.$dictof: - // assume dest is already void 0 - return [seq(`if (d.openDictionary()) `, ctx.block(() => [ - seq(`let r: `, typeFor(ctx.mod, p), ` | undefined = new _.KeyedDictionary()`), - seq(`while (!d.closeCompound()) `, ctx.block(() => [ - seq(`let K: undefined | `, typeFor(ctx.mod, p[0]), ` = void 0`), - ... decoderFor(ctx, p[0], 'K'), - seq(`if (K === void 0) { r = void 0; break; }`), - seq(`let V: undefined | `, typeFor(ctx.mod, p[1]), ` = void 0`), - ... decoderFor(ctx, p[1], 'V'), - seq(`if (V === void 0) { r = void 0; break; }`), - seq(`r.set(K, V)`)])), - seq(`${dest} = r`)]))]; - case M.$dict: - return [seq(`${dest} = d.next()`), - seq(`if (${dest} !== void 0 && !(`, predicateFor(ctx, dest, p), - `)) ${dest} = void 0`)]; - default: - ((_p: never) => {})(p); - throw new Error("Unreachable"); - } -} - -function typeFor(mod: ModuleContext, 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 ${mod.literal(p[0])})`; - case M.$ref: - return lookup(refPosition(p), p, mod.env, - (_p) => p[1].description!, - (p) => typeForAlternative(mod, p), - (modId, modPath,_p) => { - mod.imports.add([modId, modPath]); - return `${modId}.${p[1].description!}`; - }); - case M.$pointer: - return `_ptr`; - case M.$rec: - return seq('_.Record', anglebrackets(typeFor(mod, unname(p[0])), - typeFor(mod, unname(p[1])), '_ptr')); - case M.$tuple: - return brackets(... p[0].map(pp => typeFor(mod, unname(pp)))); - case M.$tuple_STAR_: - if (p[0].length === 0) { - return seq('Array<', typeFor(mod, unname(p[1])), '>'); - } else { - return brackets(... p[0].map(pp => typeFor(mod, unname(pp))), - seq('... Array<', typeFor(mod, unname(p[1])), '>')); - } - case M.$setof: - return seq('_.KeyedSet', anglebrackets(typeFor(mod, p[0]), '_ptr')); - case M.$dictof: - return seq('_.KeyedDictionary', anglebrackets( - typeFor(mod, p[0]), - typeFor(mod, p[1]), - '_ptr')); - case M.$dict: - return parens(seq( - block( - ... Array.from(p[0]).map(([k, vp]) => - seq(`get(k: typeof ${mod.literal(k)}): `, typeFor(mod, unname(vp)))), - ... Array.from(p[0]).map(([k, _vp]) => - seq(`has(k: typeof ${mod.literal(k)}): true`))), - ' & _.Dictionary<_ptr>')); - default: - ((_p: never) => {})(p); - throw new Error("Unreachable"); - } -} - -function typeForDefinition(mod: ModuleContext, _name: symbol, d: Definition): Item { - if (d.label === M.$or) { - return opseq('never', ' | ', ... d[0].map(a => typeForAlternative(mod, a[1]))); - } else { - return typeForAlternative(mod, d); - } -} - -function typeForAlternative(mod: ModuleContext, a: Alternative): Item { - if (a.label === M.$and) { - return opseq('_val', ' & ', ... a[0].map(p => typeFor(mod, unname(p)))); - } else { - return typeFor(mod, a); - } -} - -function predicateFor(ctx: FunctionContext, v: string, p: Definition, 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: - return `_.is(${v}, ${ctx.mod.literal(p[0])})`; - case M.$ref: - return lookup(refPosition(p), p, ctx.mod.env, - (_p) => `is${Ref._.name(p).description!}(${v})`, - (pp) => predicateFor(ctx, v, pp), - (modId, modPath, _p) => { - ctx.mod.imports.add([modId, modPath]); - return `${modId}.is${Ref._.name(p).description!}(${v})`; - }); - case M.$or: { - const alts = p[0]; - const recs = alts.map(p => ctx.mod.derefPattern(p)); - if (recs.length > 1 && recs.every(pp => pp.label === M.$rec)) { - return seq( - `_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v}) && `, - parens(opseq('false', ' || ', - ... recs.map(r => - (r.label !== M.$rec) ? '' : parens(seq( - predicateFor(ctx, `${v}.label`, unname(r[0])), - ' && ', - predicateFor(ctx, v, unname(r[1]), true))))))); - } else { - return opseq('false', ' || ', ... p[0].map(pp => predicateFor(ctx, v, pp[1]))); - } - } - case M.$and: - return opseq('true', ' && ', ... p[0].map(pp => predicateFor(ctx, v, unname(pp)))); - case M.$pointer: - return `_.isPointer(${v})`; - case M.$rec: - return opseq('true', ' && ', - `_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`, - predicateFor(ctx, `${v}.label`, unname(p[0])), - predicateFor(ctx, v, unname(p[1]), true)); - case M.$tuple: - return opseq('true', ' && ', - ... (recordOkAsTuple ? [] - : [`_.Array.isArray(${v})`, `!_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`]), - `(${v}.length === ${p[0].length})`, - ... p[0].map((pp, i) => predicateFor(ctx, `${v}[${i}]`, unname(pp)))); - case M.$tuple_STAR_: - return opseq('true', ' && ', - ... (recordOkAsTuple ? [] - : [`_.Array.isArray(${v})`, `!_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`]), - `(${v}.length >= ${p[0].length})`, - seq(p[0].length > 0 ? `${v}.slice(${p[0].length})` : v, - `.every(v => `, - parens(predicateFor(ctx, 'v', unname(p[1]))), - `)`), - ... p[0].map((pp, i) => predicateFor(ctx, `${v}[${i}]`, unname(pp)))); - case M.$setof: - return opseq('true', ' && ', - `_.Set.isSet<_val>(${v})`, - fnblock( - seq(`for (const vv of ${v}) `, block( - seq('if (!(', predicateFor(ctx, 'vv', p[0]), ')) return false'))), - seq('return true'))); - case M.$dictof: - return opseq('true', ' && ', - `_.Dictionary.isDictionary<_ptr>(${v})`, - fnblock( - seq(`for (const e of ${v}) `, block( - seq('if (!(', predicateFor(ctx, 'e[0]', p[0]), ')) return false'), - seq('if (!(', predicateFor(ctx, 'e[1]', p[1]), ')) return false'))), - seq('return true'))); - case M.$dict: - return opseq('true', ' && ', - `_.Dictionary.isDictionary<_ptr>(${v})`, - ... Array.from(p[0]).map(([k, vp]) => { - const tmp = ctx.gentemp(); - return parens(seq( - `(${tmp} = ${v}.get(${ctx.mod.literal(k)})) !== void 0 && `, - predicateFor(ctx, tmp, unname(vp)))); - })); - default: - ((_p: never) => {})(p); - throw new Error("Unreachable"); - } -} - -export function compile(env: Environment, schema: Schema, options: CompilerOptions = {}): string { +export function compile(env: M.Environment, schema: M.Schema, options: CompilerOptions = {}): string { const mod = new ModuleContext(env, schema, options); - const pointerName = Schema._._field0(schema).get(M.$pointer); + const pointerName = M.Schema._._field0(schema).get(M.$pointer); mod.defineType(seq(`export type _ptr = `, - pointerName === false ? 'never' : typeFor(mod, pointerName), + pointerName === false ? 'never' : typeForDefinition(mod, pointerName), `;`)); mod.defineType(`export type _val = _.Value<_ptr>;`); @@ -493,47 +25,33 @@ export function compile(env: Environment, schema: Schema, options: CompilerOptio seq(`return result`)]))), `;`)); - for (const [name0, def] of Schema._._field0(schema).get(M.$definitions)) { - const name = name0 as symbol; - - if (def.label === M.$rec && - def[0].label === M.$lit && - def[1].label === M.$tuple) - { - function fieldEntry(np: NamedPattern, index: number): Item { - return seq(JSON.stringify(fieldName(np, index)), ': ', typeFor(mod, unname(np))); - } - mod.defineType( - seq(`export const ${name.description!} = _.Record.makeConstructor<`, - braces(... def[1][0].map(fieldEntry)), - `, _ptr>()(${mod.literal(def[0][0])}, `, - JSON.stringify(def[1][0].map(fieldName)), `);`)); - } - + for (const [name, def] of M.Schema._._field0(schema).get(M.$definitions)) { mod.defineType( - seq(`export type ${name.description!} = `, typeForDefinition(mod, name, def), `;`)); + seq(`export type ${stringify(name)} = `, typeForDefinition(mod, def), `;`)); } - for (const [name0, def] of Schema._._field0(schema).get(M.$definitions)) { + for (const [name0, def] of M.Schema._._field0(schema).get(M.$definitions)) { const name = name0 as symbol; - mod.defineFunction(ctx => - seq(`export function is${name.description!}`, - '(v: any): v is ', name.description!, ' ', - ctx.block(() => [seq('return ', predicateFor(ctx, 'v', def))]))); - mod.defineFunction(ctx => seq(`export function as${name.description!}`, '(v: any): ', name.description!, ' ', ctx.block(() => [ - seq(`if (!is${name.description!}(v)) `, - block(`throw new TypeError(\`Invalid ${name.description!}: \${_.stringify(v)}\`)`), - ' else ', - block(`return v`))]))); + seq(`let result = to${name.description!}(v)`), + seq(`if (result === void 0) `, + `throw new TypeError(\`Invalid ${name.description!}: \${_.stringify(v)}\`)`), + seq(`return result`)]))); + + mod.defineFunction(ctx => + seq(`export function to${name.description!}`, + '(v: any): ', name.description!, ' | undefined ', + ctx.block(() => [seq(`let result`), + ... converterForDefinition(ctx, def, 'v', 'result'), + seq(`return result`)]))); mod.defineFunction(ctx => seq(`export function decode${name.description!}`, - `(d: _.TypedDecoder<_ptr>): ${name.description!} | undefined `, + `(d: _.TypedDecoder<_ptr>): `, name.description!, ` | undefined `, ctx.block(() => [seq(`let result`), ... decoderFor(ctx, def, 'result'), seq(`return result`)]))); @@ -569,21 +87,17 @@ export function compile(env: Environment, schema: Schema, options: CompilerOptio return f.toString(); } -export function stringSource(s: string) { - return JSON.stringify(s); -} - export function sourceCodeFor(v: Value): 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); }, + string(s: string): Item { return JSON.stringify(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!)})`; }, + symbol(s: symbol): Item { return `Symbol.for(${JSON.stringify(s.description!)})`; }, record(r: Record, Tuple>, any>, k: Fold): Item { return seq(`_.Record<_val, _.Tuple<_val>, _ptr>`, parens(k(r.label), brackets(... r.map(k)))); @@ -604,7 +118,7 @@ export function sourceCodeFor(v: Value): Item { }, pointer(t: any, _k: Fold): Item { - throw new Error(preserves`Cannot emit source code for construction of pointer ${t}`); + throw new Error(`Cannot emit source code for construction of pointer ${stringify(t)}`); }, }); } diff --git a/implementations/javascript/packages/schema/src/block.ts b/implementations/javascript/packages/schema/src/compiler/block.ts similarity index 95% rename from implementations/javascript/packages/schema/src/block.ts rename to implementations/javascript/packages/schema/src/compiler/block.ts index 2e10e30..4461408 100644 --- a/implementations/javascript/packages/schema/src/block.ts +++ b/implementations/javascript/packages/schema/src/compiler/block.ts @@ -51,6 +51,7 @@ export class Sequence extends Emittable { constructor(items: Array) { super(); + if (items.some(i => i === void 0)) throw new Error('aiee'); this.items = items; } @@ -158,3 +159,5 @@ export const brackets = (... items: Item[]) => new Brackets(items); export const anglebrackets = (... items: Item[]) => new AngleBrackets(items); export const braces = (... items: Item[]) => new Braces(items); export const block = (... items: Item[]) => new Block(items); +export const fnblock = (... items: Item[]) => seq('((() => ', block(... items), ')())'); +export const keyvalue = (k: Item, v: Item) => seq(k, ': ', v); diff --git a/implementations/javascript/packages/schema/src/compiler/context.ts b/implementations/javascript/packages/schema/src/compiler/context.ts new file mode 100644 index 0000000..36743be --- /dev/null +++ b/implementations/javascript/packages/schema/src/compiler/context.ts @@ -0,0 +1,92 @@ +import { Dictionary, KeyedSet, Position } from "@preserves/core"; +import { refPosition } from "../reader"; +import * as M from "../meta"; +import { block, commas, Item, seq } from "./block"; + +export interface CompilerOptions { + preservesModule?: string; + defaultPointer?: M.Ref; + warn?(message: string, pos: Position | null): void; +} + +export class ModuleContext { + readonly env: M.Environment; + readonly schema: M.Schema; + readonly options: CompilerOptions; + + readonly literals = new Dictionary(); + readonly typedefs: Item[] = []; + readonly functiondefs: Item[] = []; + readonly imports = new KeyedSet<[string, string]>(); + + constructor(env: M.Environment, schema: M.Schema, options: CompilerOptions) { + this.env = env; + this.schema = schema; + this.options = options; + } + + literal(v: M.Input): Item { + let varname = this.literals.get(v); + if (varname === void 0) { + const s = v.asPreservesText() + .replace('_', '__') + .replace('*', '_STAR_'); + varname = M.isValidToken('_' + s, true) ? '$' + s : '__lit' + this.literals.size; + this.literals.set(v, varname); + } + return varname; + } + + derefPattern([_name, p]: [string, M.Alternative]): M.Definition { + if (p.label === M.$ref) { + return M.lookup(refPosition(p), p, this.env, + (p) => p, + (p) => p, + (_modId, _modPath, pp) => pp ?? p); + } else { + return p; + } + } + + defineType(f: Item): void { + this.typedefs.push(f); + } + + defineFunction(f: (ctx: FunctionContext) => Item): void { + this.functiondefs.push(f(new FunctionContext(this))); + } +} + +export class FunctionContext { + readonly mod: ModuleContext; + + tempCounter = 0; + temps: string[] = []; + + constructor(mod: ModuleContext) { + this.mod = mod; + } + + gentemp(): string { + const varname = '_tmp' + this.tempCounter++; + this.temps.push(varname); + return varname; + } + + gentemps(n: number): string[] { + const temps = []; + while (temps.length < n) temps.push(this.gentemp()); + return temps; + } + + block(f: () => Item[]): Item { + const oldTemps = this.temps; + this.temps = []; + const items = f(); + const ts = this.temps; + this.temps = oldTemps; + return block( + ... ts.length > 0 ? [seq(`let `, commas(... ts), ': any')] : [], + ... items); + } +} diff --git a/implementations/javascript/packages/schema/src/compiler/converter.ts b/implementations/javascript/packages/schema/src/compiler/converter.ts new file mode 100644 index 0000000..155788a --- /dev/null +++ b/implementations/javascript/packages/schema/src/compiler/converter.ts @@ -0,0 +1,292 @@ +import { FunctionContext } from "./context"; +import * as M from '../meta'; +import { block, braces, Item, keyvalue, seq } from "./block"; +import { typeFor, variantFor, variantInitFor } from "./type"; +import { refPosition } from "../reader"; +import { stringify } from "@preserves/core"; + +function converterForTuple(ctx: FunctionContext, + ps: M.NamedPattern[], + src: string, + dest: string, + variantName: string | undefined, + recordFields: boolean, + variablePattern: M.NamedSimplePattern | undefined): Item[] +{ + const temps = ctx.gentemps(ps.length); + + function loop(i: number): Item[] { + if (i < ps.length) { + return [...converterFor(ctx, M.unname(ps[i]), `${src}[${i}]`, temps[i]), + seq(`if (${temps[i]} !== void 0) `, ctx.block(() => loop(i + 1)))]; + } else { + if (variablePattern === void 0) { + return [seq(`${dest} = `, braces( + ... variantInitFor(variantName), + ... ps.flatMap((pp, i) => converterField(pp, temps[i]))))]; + } else { + return [ps.length > 0 ? `let vN = ${src}.slice(${ps.length})` : `let vN = ${src}`, + converterForArray(ctx, M.unname(variablePattern), 'vN', dest, false)]; + } + } + } + + const lengthCheck = variablePattern === void 0 + ? seq(`${src}.length === ${ps.length}`) + : seq(`${src}.length >= ${ps.length}`); + + return recordFields + ? loop(0) + : [seq(`if (_.Array.isArray(${src}) && `, lengthCheck, `) `, ctx.block(() => loop(0)))]; +} + +export function converterForDefinition( + ctx: FunctionContext, + p: M.Definition, + src: string, + dest: string): Item[] +{ + if (p.label === M.$or) { + const alts = p[0]; + switch (alts.length) { + case 0: return []; // assume dest is already void 0 + case 1: return converterForAlternative(ctx, alts[0][1], src, dest, alts[0][0]); + default: { + function loop(i: number): Item[] { + return [ + ... converterForAlternative(ctx, alts[i][1], src, dest, alts[i][0]), + ... (i < alts.length - 1) + ? [seq(`if (${dest} === void 0) `, ctx.block(() => loop(i + 1)))] + : []]; + } + return loop(0); + } + } + } else { + return converterForAlternative(ctx, p, src, dest, void 0); + } +} + +function converterForAlternative(ctx: FunctionContext, p: M.Alternative, src: string, dest: string, variantName: string | undefined): Item[] { + if (p.label === M.$and) { + switch (p[0].length) { + case 0: return [`${dest} = ${src}`]; + case 1: { + return converterFor(ctx, M.unname(p[0][0]), src, dest, variantName); + } + default: { + const alts = p[0]; + const temps = ctx.gentemps(alts.length); + function loop(i: number): Item[] { + return (i < temps.length) + ? [...converterFor(ctx, M.unname(alts[i]), src, temps[i]), + seq(`if (${temps[i]} !== void 0) `, ctx.block(() => loop(i + 1)))] + : [seq(`${dest} = `, braces( + ... variantInitFor(variantName), + ... alts.flatMap((pp, i) => converterField(pp, temps[i]))))]; + } + return loop(0); + } + } + } else { + return converterFor(ctx, p, src, dest, variantName); + } +} + +function converterForArray(ctx: FunctionContext, + arrayType: M.SimplePattern, + src: string, + dest: string, + checkArray: boolean): Item +{ + const postCheck = () => [ + seq(`let r: Array<`, typeFor(ctx.mod, arrayType), `> | undefined = []`), + seq(`for (const v of ${src}) `, ctx.block(() => [ + seq(`let vv`), + ... converterFor(ctx, arrayType, 'v', 'vv'), + seq(`if (vv === void 0) { r = void 0; break; }`), + seq(`r.push(vv)`)])), + seq(`${dest} = r`)]; + return (checkArray + ? seq(`if (_.Array.isArray(${src})) `, ctx.block(postCheck)) + : block(... postCheck())); +} + +function converterFor( + ctx: FunctionContext, + p: M.Pattern, + src: string, + dest: string, + variantName?: string, + recordFields = false): Item[] +{ + let converterItem: Item[]; + const unlabeled = variantName === void 0 ? dest : ctx.gentemp(); + + if (M.isSimplePattern(p)) { + converterItem = converterForSimple(ctx, p, src, unlabeled); + } else { + switch (p.label) { + case M.$setof: + // assume dest is already void 0 + converterItem = [ + seq(`if (_.Set.isSet(${src})) `, ctx.block(() => [ + seq(`let r: `, typeFor(ctx.mod, p), ` | undefined = new _.KeyedSet()`), + seq(`for (const v of ${src}) `, ctx.block(() => [ + seq(`let vv`), + ... converterFor(ctx, p[0], 'v', 'vv'), + seq(`if (vv === void 0) { r = void 0; break; }`), + seq(`r.add(vv)`)])), + seq(`${unlabeled} = r`)]))]; + break; + case M.$dictof: + // assume dest is already void 0 + converterItem = [ + seq(`if (_.Dictionary.isDictionary(${src})) `, ctx.block(() => [ + seq(`let r: `, typeFor(ctx.mod, p), ` | undefined = new _.KeyedDictionary()`), + seq(`for (const [k, v] of ${src}) `, ctx.block(() => [ + seq(`let kk`), + ... converterFor(ctx, p[0], 'k', 'kk'), + seq(`if (kk === void 0) { r = void 0; break; }`), + seq(`let vv`), + ... converterFor(ctx, p[1], 'v', 'vv'), + seq(`if (vv === void 0) { r = void 0; break; }`), + seq(`r.set(kk, vv)`)])), + seq(`${unlabeled} = r`)]))]; + break; + default: { + const arrayType = M.simpleArray(p); + if (arrayType === void 0) { + return converterForCompound(ctx, p, src, dest, variantName, recordFields); + } else { + converterItem = [ + converterForArray(ctx, arrayType, src, unlabeled, !recordFields)]; + break; + } + } + } + } + + if (variantName === void 0) { + return converterItem; + } else { + return [... converterItem, + seq(`if (${unlabeled} !== void 0) ${dest} = `, braces( + variantFor(variantName), + keyvalue('value', unlabeled)))]; + } +} + +function converterForSimple( + ctx: FunctionContext, + p: M.SimplePattern, + src: string, + dest: string): Item[] +{ + switch (p.label) { + case M.$atom: { + let test: Item; + switch (p[0]) { + case M.$Boolean: test = `typeof ${src} === 'boolean'`; break; + case M.$Float: test = `_.Float.isSingle(${src})`; break; + case M.$Double: test =`_.Float.isDouble(${src})`; break; + case M.$SignedInteger: test = `typeof ${src} === 'number'`; break; + case M.$String: test = `typeof ${src} === 'string'`; break; + case M.$ByteString: test = `_.Bytes.isBytes(${src})`; break; + case M.$Symbol: test = `typeof ${src} === 'symbol'`; break; + } + return [seq(`${dest} = `, test, ` ? ${src} : void 0`)]; + } + case M.$lit: + return [`${dest} = _.is(${src}, ${ctx.mod.literal(p[0])}) ? ${src} : void 0`]; + case M.$ref: + return M.lookup(refPosition(p), p, ctx.mod.env, + (_p) => [`${dest} = to${p[1].description!}(${src})`], + (p) => converterForAlternative(ctx, p, src, dest, void 0), + (modId, modPath,_p) => { + ctx.mod.imports.add([modId, modPath]); + return [`${dest} = ${modId}.decode${p[1].description!}(${src})`]; + }); + case M.$pointer: + return [`${dest} = _toPtr(${src})`]; + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } +} + +function converterForCompound( + ctx: FunctionContext, + p: M.CompoundPattern, + src: string, + dest: string, + variantName: string | undefined, + recordFields: boolean): Item[] +{ + switch (p.label) { + case M.$rec: + // assume dest is already void 0 + return [seq(`if (_.Record.isRecord(${src})) `, ctx.block(() => { + const label = ctx.gentemp(); + return [...converterFor(ctx, M.unname(p[0]), `${src}.label`, label), + seq(`if (${label} !== void 0) `, ctx.block(() => { + const fs = ctx.gentemp(); + return [...converterFor(ctx, M.unname(p[1]), src, fs, void 0, true), + seq(`if (${fs} !== void 0) ${dest} = `, + braces(... variantInitFor(variantName), + ... converterField(p[0], label), + `... ${fs}`))]; + }))]; + }))]; + case M.$tuple: + // assume dest is already void 0 + return converterForTuple(ctx, p[0], src, dest, variantName, recordFields, void 0); + case M.$tuple_STAR_: + // assume dest is already void 0 + return converterForTuple(ctx, p[0], src, dest, variantName, recordFields, p[1]); + case M.$setof: + case M.$dictof: + throw new Error('Internal error: setof and dictof are handled in converterFor()'); + case M.$dict: { + const entries = Array.from(p[0]); + const temps = ctx.gentemps(entries.length); + function loop(i: number): Item[] { + if (i < entries.length) { + const [k, n] = entries[i]; + const tmpSrc = ctx.gentemp(); + return [ + seq(`if ((${tmpSrc} = ${src}.get(${ctx.mod.literal(k)})) !== void 0) `, + ctx.block(() => [ + ...converterFor(ctx, M.unname(n), tmpSrc, temps[i]), + seq(`if (${temps[i]} !== void 0) `, ctx.block(() => + loop(i + 1)))]))]; + } else { + return [ + seq(`${dest} = `, braces( + ... variantInitFor(variantName), + ... entries.flatMap(([k, n], i) => converterField(n, temps[i], k))))]; + } + } + return [seq(`if (_.Dictionary.isDictionary(${src})) `, ctx.block(() => loop(0)))]; + } + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } +} + +function converterField(n: M.NamedPattern, src: string, k?: M.Input): Item[] { + if (n.label === M.$named) { + return [keyvalue(stringify(n[0]), src)]; + } + if (k !== void 0) { + const s = M.namelike(k); + if (s !== void 0) { + return [keyvalue(JSON.stringify(s), src)]; + } + } + if (M.isCompoundPattern(n)) { + return [`... ${src}`]; + } + return []; +} diff --git a/implementations/javascript/packages/schema/src/compiler/decoder.ts b/implementations/javascript/packages/schema/src/compiler/decoder.ts new file mode 100644 index 0000000..c4ec381 --- /dev/null +++ b/implementations/javascript/packages/schema/src/compiler/decoder.ts @@ -0,0 +1,208 @@ +import { FunctionContext } from "./context"; +import * as M from '../meta'; +import { anglebrackets, block, brackets, Item, opseq, parens, seq } from "./block"; +import { typeFor } from './type'; +import { refPosition } from "../reader"; +import { predicateFor } from "./predicate"; + +function decodeCompound(ctx: FunctionContext, + p: M.Pattern, + kFail: () => Item[], + kAcc: (temp: string) => Item[]): Item +{ + const t = ctx.gentemp(); + return seq(`while (!d.closeCompound()) `, ctx.block(() => [ + seq(`${t} = void 0`), + ... decoderFor(ctx, p, t), + seq(`if (${t} === void 0) `, block(... kFail(), seq(`break`))), + ... kAcc(t)])); +} + +function decoderForTuple(ctx: FunctionContext, + tuplePattern: M.Pattern, + ps: M.Pattern[], + dest: string, + recordFields: boolean, + variablePattern: M.Pattern | undefined): Item[] +{ + const temps = ctx.gentemps(ps.length); + + function loop(i: number): Item[] { + if (i < ps.length) { + return [... decoderFor(ctx, ps[i], temps[i]), + seq(`if (${temps[i]} !== void 0) `, ctx.block(() => loop(i + 1)))]; + } else { + if (variablePattern === void 0) { + return [seq(`if (d.closeCompound()) ${dest} = `, brackets(... temps), + ` as `, typeFor(ctx.mod, tuplePattern))]; + } else { + return [block( + seq(`let vN: `, typeFor(ctx.mod, tuplePattern), + ` | undefined = `, brackets(... temps)), + decodeCompound(ctx, + variablePattern, + () => [`vN = void 0`], + (t) => [`vN.push(${t})`]), + seq(`${dest} = vN`))]; + } + } + } + + return recordFields + ? loop(0) + : [seq(`if (d.openSequence()) `, ctx.block(() => loop(0)))]; +} + +export function decoderFor(ctx: FunctionContext, p: M.Definition, dest: string, recordFields = false): Item[] +{ + switch (p.label) { + case M.$atom: + switch (p[0]) { + case M.$Boolean: return [`${dest} = d.nextBoolean()`]; + case M.$Float: return [`${dest} = d.nextFloat()`]; + case M.$Double: return [`${dest} = d.nextDouble()`]; + case M.$SignedInteger: return [`${dest} = d.nextSignedInteger()`]; + case M.$String: return [`${dest} = d.nextString()`]; + case M.$ByteString: return [`${dest} = d.nextByteString()`]; + case M.$Symbol: return [`${dest} = d.nextSymbol()`]; + } + case M.$lit: { + let n: string; + switch (typeof p[0]) { + case 'boolean': n = `d.nextBoolean()`; break; + case 'string': n = `d.nextString()`; break; + case 'number': n = `d.nextSignedInteger()`; break; + case 'symbol': n = `d.nextSymbol()`; break; + default: n = `d.next()`; break; + } + return [`${dest} = _.asLiteral(${n}, ${ctx.mod.literal(p[0])})`]; + } + case M.$ref: + return M.lookup(refPosition(p), p, ctx.mod.env, + (_p) => [`${dest} = decode${p[1].description!}(d)`], + (p) => decoderFor(ctx, p, dest), + (modId, modPath,_p) => { + ctx.mod.imports.add([modId, modPath]); + return [`${dest} = ${modId}.decode${p[1].description!}(d)`]; + }); + case M.$or: { + const alts = p[0]; + const recs = alts.map(p => ctx.mod.derefPattern(p)); + if (recs.length > 1 && recs.every(pp => pp.label === M.$rec)) { + // Hoist the record check up. + // This is pretty hacky. If we lift the level of + // discourse a little, we can do this + // automatically and generically... + return [seq(`if (d.openRecord()) `, ctx.block(() => { + const label = ctx.gentemp(); + const mark = ctx.gentemp(); + function loop(i: number): Item[] { + const alt = recs[i]; + if (alt.label !== M.$rec) throw new Error("Internal error"); // avoid a cast + return [ + seq(`if (`, predicateFor(ctx, label, M.unname(alt[0])), `) `, ctx.block(() => { + const fs = ctx.gentemp(); + return [...decoderFor(ctx, M.unname(alt[1]), fs, true), + seq(`if (${fs} !== void 0) ${dest} = _.Record`, + anglebrackets(typeFor(ctx.mod, M.unname(alt[0])), + typeFor(ctx.mod, M.unname(alt[1]))), + parens(seq(label, ` as any`), + seq(fs, ` as any`)))]; + })), + ... (i < recs.length - 1) + ? [seq(`if (${dest} === void 0) `, + ctx.block(() => [`d.restoreMark(${mark})`, ... loop(i + 1)]))] + : [], + ]; + } + return [seq(`${label} = d.next()`), + seq(`${mark} = d.mark()`), + ... loop(0)]; + }))]; + } else { + switch (alts.length) { + case 0: return []; // assume dest is already void 0 + case 1: return decoderFor(ctx, alts[0][1], dest); + default: { + const mark = ctx.gentemp(); + function loop(i: number): Item[] { + return [ + ... decoderFor(ctx, alts[i][1], dest), + ... (i < alts.length - 1) + ? [seq(`if (${dest} === void 0) `, ctx.block(() => + [`d.restoreMark(${mark})`, ... loop(i + 1)]))] + : [], + ]; + } + return [`${mark} = d.mark()`, ... loop(0)]; + } + } + } + } + case M.$and: + switch (p[0].length) { + case 0: return [`${dest} = d.next()`]; + case 1: return decoderFor(ctx, M.unname(p[0][0]), dest); + default: { + const [pp0, ... ppN] = p[0]; + return [...decoderFor(ctx, M.unname(pp0), dest), + seq(`if (!`, opseq('true', ' && ', + ... ppN.map(pp => + predicateFor(ctx, dest, M.unname(pp)))), + `) ${dest} = void 0`)]; + } + } + case M.$pointer: + return [`${dest} = _decodePtr(d)`]; + case M.$rec: + // assume dest is already void 0 + return [seq(`if (d.openRecord()) `, ctx.block(() => { + const label = ctx.gentemp(); + return [...decoderFor(ctx, M.unname(p[0]), label), + seq(`if (${label} !== void 0) `, ctx.block(() => { + const fs = ctx.gentemp(); + return [...decoderFor(ctx, M.unname(p[1]), fs, true), + seq(`if (${fs} !== void 0) ${dest} = _.Record`, + anglebrackets(typeFor(ctx.mod, M.unname(p[0])), + typeFor(ctx.mod, M.unname(p[1]))), + parens(seq(label, ` as any`), + seq(fs, ` as any`)))]; + }))]; + }))]; + case M.$tuple: + // assume dest is already void 0 + return decoderForTuple(ctx, p, p[0].map(M.unname), dest, recordFields, void 0); + case M.$tuple_STAR_: + // assume dest is already void 0 + return decoderForTuple(ctx, p, p[0].map(M.unname), dest, recordFields, M.unname(p[1])); + case M.$setof: + // assume dest is already void 0 + return [seq(`if (d.openSet()) `, ctx.block(() => [ + seq(`let r: `, typeFor(ctx.mod, p), ` | undefined = new _.KeyedSet()`), + decodeCompound(ctx, + p[0], + () => [`r = void 0`], + (t) => [`r.add(${t})`]), + `${dest} = r`]))]; + case M.$dictof: + // assume dest is already void 0 + return [seq(`if (d.openDictionary()) `, ctx.block(() => [ + seq(`let r: `, typeFor(ctx.mod, p), ` | undefined = new _.KeyedDictionary()`), + seq(`while (!d.closeCompound()) `, ctx.block(() => [ + seq(`let K: undefined | `, typeFor(ctx.mod, p[0]), ` = void 0`), + ... decoderFor(ctx, p[0], 'K'), + seq(`if (K === void 0) { r = void 0; break; }`), + seq(`let V: undefined | `, typeFor(ctx.mod, p[1]), ` = void 0`), + ... decoderFor(ctx, p[1], 'V'), + seq(`if (V === void 0) { r = void 0; break; }`), + seq(`r.set(K, V)`)])), + seq(`${dest} = r`)]))]; + case M.$dict: + return [seq(`${dest} = d.next()`), + seq(`if (${dest} !== void 0 && !(`, predicateFor(ctx, dest, p), + `)) ${dest} = void 0`)]; + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } +} diff --git a/implementations/javascript/packages/schema/src/compiler/predicate.ts b/implementations/javascript/packages/schema/src/compiler/predicate.ts new file mode 100644 index 0000000..706e811 --- /dev/null +++ b/implementations/javascript/packages/schema/src/compiler/predicate.ts @@ -0,0 +1,98 @@ +import { refPosition } from '../reader'; +import * as M from '../meta'; +import { block, fnblock, Item, opseq, parens, seq } from './block'; +import { FunctionContext } from './context'; + +export function predicateFor(ctx: FunctionContext, v: string, p: M.Definition, 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: + return `_.is(${v}, ${ctx.mod.literal(p[0])})`; + case M.$ref: + return M.lookup(refPosition(p), p, ctx.mod.env, + (_p) => `is${M.Ref._.name(p).description!}(${v})`, + (pp) => predicateFor(ctx, v, pp), + (modId, modPath, _p) => { + ctx.mod.imports.add([modId, modPath]); + return `${modId}.is${M.Ref._.name(p).description!}(${v})`; + }); + case M.$or: { + const alts = p[0]; + const recs = alts.map(p => ctx.mod.derefPattern(p)); + if (recs.length > 1 && recs.every(pp => pp.label === M.$rec)) { + return seq( + `_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v}) && `, + parens(opseq('false', ' || ', + ... recs.map(r => + (r.label !== M.$rec) ? '' : parens(seq( + predicateFor(ctx, `${v}.label`, M.unname(r[0])), + ' && ', + predicateFor(ctx, v, M.unname(r[1]), true))))))); + } else { + return opseq('false', ' || ', ... p[0].map(pp => predicateFor(ctx, v, pp[1]))); + } + } + case M.$and: + return opseq('true', ' && ', ...p[0].map(pp => predicateFor(ctx, v, M.unname(pp)))); + case M.$pointer: + return `_.isPointer(${v})`; + case M.$rec: + return opseq('true', ' && ', + `_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`, + predicateFor(ctx, `${v}.label`, M.unname(p[0])), + predicateFor(ctx, v, M.unname(p[1]), true)); + case M.$tuple: + return opseq('true', ' && ', + ... (recordOkAsTuple ? [] + : [`_.Array.isArray(${v})`, `!_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`]), + `(${v}.length === ${p[0].length})`, + ...p[0].map((pp, i) => predicateFor(ctx, `${v}[${i}]`, M.unname(pp)))); + case M.$tuple_STAR_: + return opseq('true', ' && ', + ... (recordOkAsTuple ? [] + : [`_.Array.isArray(${v})`, `!_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`]), + `(${v}.length >= ${p[0].length})`, + seq(p[0].length > 0 ? `${v}.slice(${p[0].length})` : v, + `.every(v => `, + parens(predicateFor(ctx, 'v', M.unname(p[1]))), + `)`), + ...p[0].map((pp, i) => predicateFor(ctx, `${v}[${i}]`, M.unname(pp)))); + case M.$setof: + return opseq('true', ' && ', + `_.Set.isSet<_val>(${v})`, + fnblock( + seq(`for (const vv of ${v}) `, block( + seq('if (!(', predicateFor(ctx, 'vv', p[0]), ')) return false'))), + seq('return true'))); + case M.$dictof: + return opseq('true', ' && ', + `_.Dictionary.isDictionary<_ptr>(${v})`, + fnblock( + seq(`for (const e of ${v}) `, block( + seq('if (!(', predicateFor(ctx, 'e[0]', p[0]), ')) return false'), + seq('if (!(', predicateFor(ctx, 'e[1]', p[1]), ')) return false'))), + seq('return true'))); + case M.$dict: + return opseq('true', ' && ', + `_.Dictionary.isDictionary<_ptr>(${v})`, + ... Array.from(p[0]).map(([k, vp]) => { + const tmp = ctx.gentemp(); + return parens(seq( + `(${tmp} = ${v}.get(${ctx.mod.literal(k)})) !== void 0 && `, + predicateFor(ctx, tmp, M.unname(vp)))); + })); + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } +} diff --git a/implementations/javascript/packages/schema/src/compiler/type.ts b/implementations/javascript/packages/schema/src/compiler/type.ts new file mode 100644 index 0000000..ff017fd --- /dev/null +++ b/implementations/javascript/packages/schema/src/compiler/type.ts @@ -0,0 +1,140 @@ +import { refPosition } from "../reader"; +import * as M from "../meta"; +import { anglebrackets, braces, Item, keyvalue, opseq, seq } from "./block"; +import { ModuleContext } from "./context"; +import { stringify } from "@preserves/core"; + +export function typeFor(mod: ModuleContext, p: M.Pattern, variantName?: string): Item { + let typeItem: Item; + + if (M.isSimplePattern(p)) { + typeItem = typeForSimple(mod, p); + } else { + switch (p.label) { + case M.$setof: + typeItem = seq(`_.KeyedSet`, anglebrackets(typeForSimple(mod, p[0]), '_ptr')); + break; + case M.$dictof: + typeItem = seq(`_.KeyedDictionary`, anglebrackets(typeForSimple(mod, p[0]), + typeForSimple(mod, p[1]), + '_ptr')); + break; + default: { + const arrayType = M.simpleArray(p); + if (arrayType === void 0) { + return braces(... variantInitFor(variantName), + ... typeForCompound(mod, p)); + } else { + typeItem = seq('Array<', typeForSimple(mod, arrayType), '>'); + break; + } + } + } + } + + if (variantName === void 0) { + return typeItem; + } else { + return braces(variantFor(variantName), keyvalue('value', typeItem)); + } +} + +function typeForSimple(mod: ModuleContext, p: M.SimplePattern): 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.$pointer: + return `_ptr`; + case M.$lit: + return `(typeof ${mod.literal(p[0])})`; + case M.$ref: + return M.lookup(refPosition(p), p, mod.env, + (_p) => p[1].description!, + (p) => typeForAlternative(mod, p, void 0), + (modId, modPath,_p) => { + mod.imports.add([modId, modPath]); + return `${modId}.${p[1].description!}`; + }); + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } +} + +function typeField(mod: ModuleContext, n: M.NamedPattern): Item[] { + return (n.label === M.$named) + ? [keyvalue(stringify(n[0]), typeForSimple(mod, n[1]))] + : (M.isCompoundPattern(n) + ? typeForCompound(mod, n) + : []); +} + +function typeForCompound(mod: ModuleContext, p: M.CompoundPattern): Item[] { + switch (p.label) { + case M.$rec: + return [... typeField(mod, p[0]), ... typeField(mod, p[1])]; + case M.$tuple: + return p[0].flatMap(pp => typeField(mod, pp)); + case M.$tuple_STAR_: { + const n = p[1]; + return [... p[0].flatMap(pp => typeField(mod, pp)), + ... ((n.label === M.$named) + ? [keyvalue(stringify(n[0]), + seq('Array<', typeForSimple(mod, n[1]), '>'))] + : [])]; + } + case M.$setof: + case M.$dictof: + return []; + case M.$dict: + return Array.from(p[0]).flatMap(([k, n]) => { + if (n.label === M.$named) { + return typeField(mod, n); + } else { + const s = M.namelike(k); + if (s !== void 0) { + return [keyvalue(JSON.stringify(s), typeForSimple(mod, n))]; + } else { + return []; + } + } + }); + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } +} + +export function typeForDefinition(mod: ModuleContext, d: M.Definition): Item { + if (d.label === M.$or) { + return opseq('never', ' | ', ... d[0].map(a => typeForAlternative(mod, a[1], a[0]))); + } else { + return typeForAlternative(mod, d, void 0); + } +} + +export function variantInitFor(variantName: string | undefined) : Item[] { + return variantName === void 0 ? [] : [variantFor(variantName)]; +} + +export function variantFor(variantName: string): Item { + return keyvalue('_variant', JSON.stringify(variantName)); +} + +function typeForAlternative(mod: ModuleContext, a: M.Alternative, variantName: string | undefined): Item { + if (a.label === M.$and) { + return opseq('_val', ' & ', + ... variantName === void 0 ? [] : [braces(variantFor(variantName))], + ...a[0].map(p => typeFor(mod, M.unname(p)))); + } else { + return typeFor(mod, a, variantName); + } +} diff --git a/implementations/javascript/packages/schema/src/meta.ts b/implementations/javascript/packages/schema/src/meta.ts index 2b548fc..1a8b618 100644 --- a/implementations/javascript/packages/schema/src/meta.ts +++ b/implementations/javascript/packages/schema/src/meta.ts @@ -1,5 +1,5 @@ -import { Value, is, Position } from '@preserves/core'; -import { ModulePath, Ref, Schema, $definitions, Definition, Alternative } from './gen/schema'; +import { Value, is, Position, stringify } from '@preserves/core'; +import * as M from './gen/schema'; import { BASE } from './base'; import { SchemaSyntaxError } from './error'; @@ -22,10 +22,10 @@ export const EQUALS = Symbol.for('='); export const INCLUDE = Symbol.for('include'); export const ORSYM = Symbol.for('/'); -export type SchemaEnvEntry = { schemaModulePath: ModulePath } & ( +export type SchemaEnvEntry = { schemaModulePath: M.ModulePath } & ( ({ typescriptModulePath: string | null, // null means it's "this module" in disguise - schema: Schema, + schema: M.Schema, }) | ({ typescriptModulePath: string, schema: null, @@ -39,21 +39,21 @@ function modsymFor(e: SchemaEnvEntry): string { } export function lookup(namePos: Position | null, - name: Ref, + name: M.Ref, env: Environment, - kLocal: (p: Definition) => R, - kBase: (p: Alternative) => R, - kOther: (modId: string, modPath: string, p: Definition | null) => R): R + kLocal: (p: M.Definition) => R, + kBase: (p: M.Alternative) => R, + kOther: (modId: string, modPath: string, p: M.Definition | null) => R): R { for (const e of env) { - if (is(e.schemaModulePath, Ref._.module(name)) || - (e.typescriptModulePath === null && Ref._.module(name).length === 0)) + if (is(e.schemaModulePath, M.Ref._.module(name)) || + (e.typescriptModulePath === null && M.Ref._.module(name).length === 0)) { if (e.schema === null) { // It's an artificial module, not from a schema. Assume the identifier is present. return kOther(modsymFor(e), e.typescriptModulePath, null); } else { - const p = Schema._._field0(e.schema).get($definitions).get(Ref._.name(name)); + const p = M.Schema._._field0(e.schema).get(M.$definitions).get(M.Ref._.name(name)); if (p !== void 0) { if (e.typescriptModulePath === null) { return kLocal(p); @@ -65,14 +65,37 @@ export function lookup(namePos: Position | null, } } - if (Ref._.module(name).length === 0) { - const p = Schema._._field0(BASE).get($definitions).get(Ref._.name(name)); - if (p !== void 0) return kBase(p as Alternative); + if (M.Ref._.module(name).length === 0) { + const p = M.Schema._._field0(BASE).get(M.$definitions).get(M.Ref._.name(name)); + if (p !== void 0) return kBase(p as M.Alternative); } throw new SchemaSyntaxError(`Undefined reference: ${formatRef(name)}`, namePos); } -export function formatRef(r: Ref): string { +export function formatRef(r: M.Ref): string { return [... r[0], r[1]].map(s => s.description!).join('.'); } + +export function unname( + p: M.NamedSimplePattern_ | R): M.SimplePattern | R +{ + return (p.label === M.$named) ? p[1] : p; +} + +// Simple arrays at toplevel for convenience +// +export function simpleArray(p: M.CompoundPattern): M.SimplePattern | undefined { + if (p.label === M.$tuple_STAR_ && p[0].length === 0 && p[1].label !== M.$named) { + return p[1]; + } else { + return void 0; + } +} + +export function namelike(x: Input): string | undefined { + if (typeof x === 'string') return x; + if (typeof x === 'symbol') return stringify(x); + if (typeof x === 'number') return '' + x; + return void 0; +} diff --git a/implementations/javascript/packages/schema/src/reader.ts b/implementations/javascript/packages/schema/src/reader.ts index 3f30d5a..4984315 100644 --- a/implementations/javascript/packages/schema/src/reader.ts +++ b/implementations/javascript/packages/schema/src/reader.ts @@ -141,7 +141,9 @@ function parseDefinition(name: symbol, body: Array): Definition { switch (typeof p[0]) { case 'symbol': return [p[0].description!, p]; case 'string': return [p[0], p]; - case 'number': return ['' + p[0], p]; + case 'boolean': + case 'number': + return ['' + p[0], p]; default: break; } @@ -198,10 +200,20 @@ function parsePattern(name: symbol, body0: Array): Pattern { }); const walk = (b: Input): Pattern => parsePattern(name, [b]); - function _maybeNamed(recur: (b: Input) => R): (b: Input) => M.NamedSimplePattern_ | R { + function _maybeNamed( + recur: (b: Input) => R, + literalName?: Input): (b: Input) => M.NamedSimplePattern_ | R + { return (b: Input) => { - const name = findName(b); - if (name === false) return recur(b); + let name = findName(b); + if (name === false) { + if (literalName !== void 0 && typeof literalName === 'symbol') { + name = literalName; + } + } + if (name === false) { + return recur(b); + } return Record(M.$named, [name, parseSimple(b, () => { throw new SchemaSyntaxError(`Named patterns must be Simple patterns`, position(b)); })]); @@ -242,7 +254,7 @@ function parsePattern(name: symbol, body0: Array): Pattern { return Record(M.$dictof, [walkSimple(kp), walkSimple(vp)]); } else { return Record(M.$dict, [item.mapEntries( - ([k, vp]) => [strip(k), maybeNamedSimple(vp)])]); + ([k, vp]) => [strip(k), _maybeNamed(walkSimple, k)(vp)])]); } } else if (Set.isSet(item)) { if (item.size !== 1) complain();