From 7d8453a806bb67215f81c389b28f2f538e1d45e2 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Tue, 23 Mar 2021 11:36:55 +0100 Subject: [PATCH] Unconverter --- .../packages/schema/src/compiler.ts | 43 +--- .../packages/schema/src/compiler/context.ts | 31 ++- .../schema/src/compiler/genconverter.ts | 7 +- .../packages/schema/src/compiler/gentype.ts | 10 +- .../schema/src/compiler/genunconverter.ts | 206 ++++++++++++++++++ .../packages/schema/src/compiler/value.ts | 38 ++++ .../javascript/packages/schema/src/reader.ts | 25 ++- 7 files changed, 294 insertions(+), 66 deletions(-) create mode 100644 implementations/javascript/packages/schema/src/compiler/genunconverter.ts create mode 100644 implementations/javascript/packages/schema/src/compiler/value.ts diff --git a/implementations/javascript/packages/schema/src/compiler.ts b/implementations/javascript/packages/schema/src/compiler.ts index 8d82244..3f6c8e3 100644 --- a/implementations/javascript/packages/schema/src/compiler.ts +++ b/implementations/javascript/packages/schema/src/compiler.ts @@ -6,6 +6,8 @@ import { typeForDefinition } from "./compiler/gentype"; import { converterForDefinition, converterForSimple } from "./compiler/genconverter"; import { EMPTY_TYPE, renderType } from "./compiler/type"; import { genConstructor } from "./compiler/genctor"; +import { unconverterForDefinition } from "./compiler/genunconverter"; +import { sourceCodeFor } from "./compiler/value"; export function compile(env: M.Environment, schema: M.Schema, options: CompilerOptions = {}): string { const mod = new ModuleContext(env, schema, options); @@ -65,6 +67,11 @@ export function compile(env: M.Environment, schema: M.Schema, options: CompilerO ctx.block(() => [seq(`let result: undefined | `, name.description!), ... converterForDefinition(ctx, def, 'v', 'result'), seq(`return result`)]))); + + mod.defineFunction(ctx => + seq(`export function from${name.description!}`, + '(v: ', name.description!, '): _val ', + ctx.block(() => unconverterForDefinition(ctx, name.description!, def, 'v')))); } const f = new Formatter(); @@ -96,39 +103,3 @@ export function compile(env: M.Environment, schema: M.Schema, options: CompilerO return f.toString(); } - -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 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(${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)))); - }, - array(a: Array>, k: Fold): Item { - return brackets(... a.map(k)); - }, - set(s: Set, k: Fold): Item { - return seq('new _.Set<_val>', parens(brackets(... Array.from(s).map(k)))); - }, - dictionary(d: Dictionary, k: Fold): Item { - return seq('new _.Dictionary<_ptr>', parens(brackets(... Array.from(d).map(([kk,vv]) => - brackets(k(kk), k(vv)))))); - }, - - annotated(a: Annotated, k: Fold): Item { - return seq('_.annotate<_ptr>', parens(k(a.item), ... a.annotations.map(k))); - }, - - pointer(t: any, _k: Fold): Item { - throw new Error(`Cannot emit source code for construction of pointer ${stringify(t)}`); - }, - }); -} diff --git a/implementations/javascript/packages/schema/src/compiler/context.ts b/implementations/javascript/packages/schema/src/compiler/context.ts index f8bc812..81c119d 100644 --- a/implementations/javascript/packages/schema/src/compiler/context.ts +++ b/implementations/javascript/packages/schema/src/compiler/context.ts @@ -15,6 +15,8 @@ export interface Capture { sourceExpr: string; } +export const RECURSION_LIMIT = 128; + export class ModuleContext { readonly env: M.Environment; readonly schema: M.Schema; @@ -40,17 +42,22 @@ export class ModuleContext { return varname; } - derefPattern([_name, p]: [string, M.Alternative]): M.Definition { - if (p._variant === 'Pattern' && - p.value._variant === 'SimplePattern' && - p.value.value._variant === 'Ref') + derefPattern(p: M.Definition, refCount = 0): M.Definition { + if (refCount > RECURSION_LIMIT) { + throw new Error('Recursion limit exceeded'); + } + if (p._variant === 'Alternative' && + p.value._variant === 'Pattern' && + p.value.value._variant === 'SimplePattern' && + p.value.value.value._variant === 'Ref') { - return M.lookup(refPosition(p.value.value.value), p.value.value.value, this.env, - (p) => p, - (_modId, _modPath, pp) => pp ?? M.Definition.Alternative( - M.Alternative.Pattern(p.value))); + return M.lookup(refPosition(p.value.value.value.value), + p.value.value.value.value, + this.env, + (p) => this.derefPattern(p, refCount + 1), + (_modId, _modPath, pp) => this.derefPattern(pp ?? p, refCount + 1)); } else { - return M.Definition.Alternative(p); + return p; } } @@ -122,9 +129,11 @@ export class FunctionContext { } buildCapturedCompound(dest: string): Item { - return seq(`${dest} = `, braces( + const fields = [ ... variantInitFor(this.variantName), ... this.captures.map(({ fieldName, sourceExpr }) => - keyvalue(fieldName, sourceExpr)))); + keyvalue(fieldName, sourceExpr)) + ]; + return seq(`${dest} = `, fields.length === 0 ? `null` : braces(... fields)); } } diff --git a/implementations/javascript/packages/schema/src/compiler/genconverter.ts b/implementations/javascript/packages/schema/src/compiler/genconverter.ts index 43631ea..8f194b9 100644 --- a/implementations/javascript/packages/schema/src/compiler/genconverter.ts +++ b/implementations/javascript/packages/schema/src/compiler/genconverter.ts @@ -184,16 +184,17 @@ export function converterForSimple( return [`${dest} = ${src}`]; case 'atom': { let test: Item; + let valexp: Item = `${src}`; switch (p.atomKind._variant) { case 'Boolean': test = `typeof ${src} === 'boolean'`; break; - case 'Float': test = `_.Float.isSingle(${src})`; break; - case 'Double': test =`_.Float.isDouble(${src})`; break; + case 'Float': test = `_.Float.isSingle(${src})`; valexp = `${src}.value`; break; + case 'Double': test =`_.Float.isDouble(${src})`; valexp = `${src}.value`; break; case 'SignedInteger': test = `typeof ${src} === 'number'`; break; case 'String': test = `typeof ${src} === 'string'`; break; case 'ByteString': test = `_.Bytes.isBytes(${src})`; break; case 'Symbol': test = `typeof ${src} === 'symbol'`; break; } - return [seq(`${dest} = `, test, ` ? ${src} : void 0`)]; + return [seq(`${dest} = `, test, ` ? `, valexp, ` : void 0`)]; } case 'lit': return [`${dest} = _.is(${src}, ${ctx.mod.literal(p.value)}) ? null : void 0`]; diff --git a/implementations/javascript/packages/schema/src/compiler/gentype.ts b/implementations/javascript/packages/schema/src/compiler/gentype.ts index 2626902..cb825f4 100644 --- a/implementations/javascript/packages/schema/src/compiler/gentype.ts +++ b/implementations/javascript/packages/schema/src/compiler/gentype.ts @@ -12,11 +12,11 @@ export function typeForDefinition(mod: ModuleContext, d: M.Definition): Type { } } -function typeForAlternative(mod: ModuleContext, a: M.Alternative): SimpleType { +export function typeForAlternative(mod: ModuleContext, a: M.Alternative): SimpleType { if (a._variant === 'and') { const fs = new Map(); a.patterns.forEach(n => gatherFields(fs, mod, n)); - return Type.record(fs); + return fs.size > 0 ? Type.record(fs) : Type.unit(); } else { return typeFor(mod, a.value); } @@ -47,7 +47,7 @@ export function typeFor(mod: ModuleContext, p: M.Pattern): SimpleType { if (arrayType === void 0) { const fs = new Map(); compoundFields(fs, mod, p.value); - return Type.record(fs); + return fs.size > 0 ? Type.record(fs) : Type.unit(); } else { return Type.array(simpleType(mod, arrayType)); } @@ -63,8 +63,8 @@ export function simpleType(mod: ModuleContext, p: M.SimplePattern): AtomicType { case 'atom': switch (p.atomKind._variant) { case 'Boolean': return Type.ref(`boolean`); - case 'Float': return Type.ref(`_.SingleFloat`); - case 'Double': return Type.ref(`_.DoubleFloat`); + case 'Float': return Type.ref(`number`); + case 'Double': return Type.ref(`number`); case 'SignedInteger': return Type.ref(`number`); case 'String': return Type.ref(`string`); case 'ByteString': return Type.ref(`_.Bytes`); diff --git a/implementations/javascript/packages/schema/src/compiler/genunconverter.ts b/implementations/javascript/packages/schema/src/compiler/genunconverter.ts new file mode 100644 index 0000000..0b37bc2 --- /dev/null +++ b/implementations/javascript/packages/schema/src/compiler/genunconverter.ts @@ -0,0 +1,206 @@ +import { SchemaSyntaxError } from '../error'; +import { refPosition } from '../reader'; +import * as M from '../meta'; +import { block, brackets, formatItems, Item, parens, seq } from './block'; +import { FunctionContext } from "./context"; +import { typeForAlternative } from './gentype'; +import { FieldType, renderType, SimpleType } from './type'; + +export function unconverterForDefinition( + ctx: FunctionContext, + name: string, + def: M.Definition, + src: string): Item[] +{ + if (def._variant === 'or') { + return [seq(`switch (${src}._variant) `, block( + ... def.patterns.map(p => + seq(`case `, JSON.stringify(p.variantLabel), `: `, ctx.block(() => { + const hasValueField = + p.alternative._variant === 'Pattern' && + p.alternative.value._variant === 'SimplePattern'; + return [seq(`return `, unconverterForAlternative( + ctx, name, p.alternative, hasValueField ? `${src}.value` : src))]; + })))))]; + } else { + return [seq(`return `, unconverterForAlternative(ctx, name, def.value, `${src}`))]; + } +} + +export class InsufficientInformationError extends Error {} + +function unconverterForAlternative( + ctx: FunctionContext, + name: string, + a: M.Alternative, + src: string): Item +{ + const t = typeForAlternative(ctx.mod, a); + if (a._variant === 'and') { + const errs: [string, InsufficientInformationError][] = []; + const cs = a.patterns.flatMap(p => { + if (p._variant === 'anonymous' && p.value._variant === 'SimplePattern') { + return []; + } else { + try { + return [unconverterForNamed(ctx, p, src, t)]; + } catch (e) { + if (e instanceof InsufficientInformationError) { + errs.push([name + '/' + (M.nameFor(p) ?? ''), e]); + return []; + } + throw e; + } + } + }); + if (cs.length === 0 || errs.length > 0) { + throw new Error(`Cannot produce unconverter for ${name}: ` + + errs.map(e => `${e[0]}: ${e[1].message}`).join(', ')); + } + return (cs.length === 1) ? cs[0] : seq(`_.merge`, parens(`(a, b) => (a === b) ? a : void 0`, ... cs)); + } else { + return unconverterFor(ctx, a.value, src, t); + } +} + +function stepSource( + src: string, + t: SimpleType, + key: string): { steppedSrc: string, steppedType: FieldType } +{ + if (t.kind !== 'record' || !t.fields.has(key)) { + throw new Error( + `Internal error: attempt to step type ` + + `${formatItems([renderType(t)])} with key ${key}`); + } + return { + steppedSrc: `${src}[${JSON.stringify(key)}]`, + steppedType: t.fields.get(key)! + }; +} + +function unconverterFor(ctx: FunctionContext, p: M.Pattern, src: string, t: SimpleType): Item { + switch (p._variant) { + case 'SimplePattern': + return ((p: M.SimplePattern) => { + if (p._variant !== 'lit' && t.kind === 'record') { + throw new InsufficientInformationError(`A Cannot produce unconverter TODO ${src} ${formatItems([renderType(t)])}`); + } + switch (p._variant) { + case 'any': + return `${src}`; + case 'atom': + switch (p.atomKind._variant) { + case 'Float': return `_.Single(${src})`; + case 'Double': return `_.Double(${src})`; + default: return `${src}`; + } + case 'lit': + return ctx.mod.literal(p.value); + case 'pointer': + return `_fromPtr(${src})`; + case 'Ref': + return M.lookup( + refPosition(p.value), p.value, ctx.mod.env, + (_p) => `from${p.value.name.description!}(${src})`, + (modId, modPath, _p) => { + ctx.mod.imports.add([modId, modPath]); + return `${modId}.from${p.value.name.description!}(${src})`; + }); + } + })(p.value); + case 'CompoundPattern': + return ((p: M.CompoundPattern) => { + switch (p._variant) { + case 'rec': + return seq(`_.Record`, parens( + unconverterForNamed(ctx, p.label, src, t), + unconverterForNamed(ctx, p.fields, src, t))); + case 'tuple': + return brackets(... p.patterns.map(pp => + unconverterForNamed(ctx, pp, src, t))); + case 'tuple*': { + let varexp: Item; + if (p.variable._variant === 'named') { + const { steppedSrc, steppedType } = + stepSource(src, t, p.variable.value.name.description!); + if (steppedType.kind !== 'array') { + throw new Error( + `Internal error: attempt to visit element type of ` + + `${formatItems([renderType(steppedType)])} after ` + + `stepping by key ${p.variable.value.name.description!}`); + } + varexp = seq(steppedSrc, `.map`, parens( + seq(`v => `, unconverterFor( + ctx, + M.Pattern.SimplePattern(p.variable.value.pattern), + `v`, + steppedType.type)))); + } else { + if (t.kind !== 'array') { + throw new InsufficientInformationError( + `B Cannot produce unconverter TODO ${src} ${formatItems([renderType(t)])}`); + } + varexp = seq(src, `.map`, parens( + seq(`v => `, unconverterFor( + ctx, + M.Pattern.SimplePattern(p.variable.value), + `v`, + t.type)))); + } + if (p.fixed.length === 0) { + return varexp; + } else { + return brackets( + ... p.fixed.map(pp => unconverterForNamed(ctx, pp, src, t)), + seq(`... `, varexp)); + } + } + case 'setof': + if (t.kind !== 'set') { + throw new InsufficientInformationError( + `C Cannot produce unconverter TODO ${src} ${formatItems([renderType(t)])}`); + } + return seq(`new _.Set<_ptr>`, parens( + `_.Array.from(${src}.values()).map(v => `, + unconverterFor(ctx, M.Pattern.SimplePattern(p.pattern), 'v', t), + `)`)); + case 'dictof': + if (t.kind !== 'dictionary') { + throw new InsufficientInformationError( + `D Cannot produce unconverter TODO ${src} ${formatItems([renderType(t)])}`); + } + return seq(`new _.Dictionary<_ptr>`, parens(seq( + `_.Array.from(${src}.entries()).map(([k, v]) => `, + brackets( + unconverterFor(ctx, M.Pattern.SimplePattern(p.key), 'k', t), + unconverterFor(ctx, M.Pattern.SimplePattern(p.value), 'v', t)), + `)`))); + case 'dict': + return seq(`new _.Dictionary<_ptr>`, parens( + brackets(... Array.from(p.entries.entries()).map(([k, n]) => + brackets( + ctx.mod.literal(k), + unconverterForNamedSimple(ctx, M.addNameIfAbsent(n, k), src, t)))))); + } + })(p.value); + } +} + +function unconverterForNamed(ctx: FunctionContext, p: M.NamedPattern, src: string, t: SimpleType): Item { + if (p._variant === 'named') { + const { steppedSrc, steppedType } = stepSource(src, t, p.value.name.description!); + return unconverterFor(ctx, M.Pattern.SimplePattern(p.value.pattern), steppedSrc, steppedType); + } else { + return unconverterFor(ctx, p.value, src, t); + } +} + +function unconverterForNamedSimple(ctx: FunctionContext, p: M.NamedSimplePattern, src: string, t: SimpleType): Item { + if (p._variant === 'named') { + const { steppedSrc, steppedType } = stepSource(src, t, p.value.name.description!); + return unconverterFor(ctx, M.Pattern.SimplePattern(p.value.pattern), steppedSrc, steppedType); + } else { + return unconverterFor(ctx, M.Pattern.SimplePattern(p.value), src, t); + } +} diff --git a/implementations/javascript/packages/schema/src/compiler/value.ts b/implementations/javascript/packages/schema/src/compiler/value.ts new file mode 100644 index 0000000..2383b4a --- /dev/null +++ b/implementations/javascript/packages/schema/src/compiler/value.ts @@ -0,0 +1,38 @@ +import { Annotated, Bytes, Dictionary, Fold, fold, Record, Tuple, Value, stringify } from "@preserves/core"; +import { brackets, Item, parens, seq } from "./block"; + +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 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(${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)))); + }, + array(a: Array>, k: Fold): Item { + return brackets(... a.map(k)); + }, + set(s: Set, k: Fold): Item { + return seq('new _.Set<_val>', parens(brackets(... Array.from(s).map(k)))); + }, + dictionary(d: Dictionary, k: Fold): Item { + return seq('new _.Dictionary<_ptr>', parens(brackets(... Array.from(d).map(([kk,vv]) => + brackets(k(kk), k(vv)))))); + }, + + annotated(a: Annotated, k: Fold): Item { + return seq('_.annotate<_ptr>', parens(k(a.item), ... a.annotations.map(k))); + }, + + pointer(t: any, _k: Fold): Item { + throw new Error(`Cannot emit source code for construction of pointer ${stringify(t)}`); + }, + }); +} diff --git a/implementations/javascript/packages/schema/src/reader.ts b/implementations/javascript/packages/schema/src/reader.ts index 9f948d1..ed7a7af 100644 --- a/implementations/javascript/packages/schema/src/reader.ts +++ b/implementations/javascript/packages/schema/src/reader.ts @@ -170,7 +170,7 @@ function parseDefinition(name: symbol, body: Array): Definition { } // TODO: deal with situation where there's an or of ands, where - // the branches of the and arenamed. The parsing is ambiguous, and + // the branches of the and are named. The parsing is ambiguous, and // with the current code I think (?) you end up with the same name // attached to the or-branch as to the leftmost and-branch. @@ -261,6 +261,17 @@ function parsePattern(name: symbol, body0: Array): Pattern { const maybeNamedSimple = _maybeNamed(M.NamedSimplePattern.named, M.NamedSimplePattern.anonymous, walkSimple); + function parseArrayLike(item: Array): CompoundPattern { + if (is(item[item.length - 1], M.DOTDOTDOT)) { + if (item.length < 2) complain(); + return M.CompoundPattern.tuple$STAR$( + item.slice(0, item.length - 2).map(maybeNamed), + maybeNamedSimple(item[item.length - 2])); + } else { + return M.CompoundPattern.tuple(item.map(maybeNamed)); + } + } + if (Record.isRecord, never>(item)) { const label = item.label; if (Record.isRecord(label)) { @@ -275,18 +286,10 @@ function parsePattern(name: symbol, body0: Array): Pattern { } else { return M.CompoundPattern.rec( M.NamedPattern.anonymous(M.Pattern.SimplePattern(M.SimplePattern.lit(label))), - M.NamedPattern.anonymous(M.Pattern.CompoundPattern( - M.CompoundPattern.tuple(item.map(maybeNamed))))); + M.NamedPattern.anonymous(M.Pattern.CompoundPattern(parseArrayLike(item)))); } } else if (Array.isArray(item)) { - if (is(item[item.length - 1], M.DOTDOTDOT)) { - if (item.length < 2) complain(); - return M.CompoundPattern.tuple$STAR$( - item.slice(0, item.length - 2).map(maybeNamed), - maybeNamedSimple(item[item.length - 2])); - } else { - return M.CompoundPattern.tuple(item.map(maybeNamed)); - } + return parseArrayLike(item); } else if (Dictionary.isDictionary(item)) { if (item.size === 2 && item.has(M.DOTDOTDOT)) { const v = item.clone();