From 62bab41bed52988d0feeff6d433eab45318e98b3 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Tue, 9 Mar 2021 15:59:40 +0100 Subject: [PATCH] First steps toward schema --- implementations/javascript/src/dictionary.ts | 60 ++-- implementations/javascript/src/fold.ts | 25 ++ implementations/javascript/src/index.ts | 4 + implementations/javascript/src/reader.ts | 10 + implementations/javascript/src/schema/base.ts | 15 + .../javascript/src/schema/block.ts | 160 ++++++++++ .../javascript/src/schema/compiler.ts | 286 ++++++++++++++++++ .../javascript/src/schema/interpreter.ts | 89 ++++++ implementations/javascript/src/schema/meta.ts | 72 +++++ .../javascript/src/schema/reader.ts | 163 ++++++++++ schema/schema.txt | 54 ++++ 11 files changed, 914 insertions(+), 24 deletions(-) create mode 100644 implementations/javascript/src/schema/base.ts create mode 100644 implementations/javascript/src/schema/block.ts create mode 100644 implementations/javascript/src/schema/compiler.ts create mode 100644 implementations/javascript/src/schema/interpreter.ts create mode 100644 implementations/javascript/src/schema/meta.ts create mode 100644 implementations/javascript/src/schema/reader.ts create mode 100644 schema/schema.txt diff --git a/implementations/javascript/src/dictionary.ts b/implementations/javascript/src/dictionary.ts index 0352876..fcc6932 100644 --- a/implementations/javascript/src/dictionary.ts +++ b/implementations/javascript/src/dictionary.ts @@ -10,30 +10,23 @@ import { fromJS } from "./fromjs"; export type DictionaryType = 'Dictionary' | 'Set'; export const DictionaryType = Symbol.for('DictionaryType'); -export class Dictionary extends FlexMap, V> { +export class KeyedDictionary, V, T extends object = DefaultPointer> extends FlexMap { get [DictionaryType](): DictionaryType { return 'Dictionary'; } - static isDictionary(x: any): x is Dictionary { + static isKeyedDictionary, V, T extends object = DefaultPointer>(x: any): x is KeyedDictionary { return x?.[DictionaryType] === 'Dictionary'; } - static fromJS(x: object): Dictionary, T> { - if (Dictionary.isDictionary(x)) return x as Dictionary, T>; - const d = new Dictionary, T>(); - Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value))); - return d; - } - - constructor(items?: readonly [Value, V][]); - constructor(items?: Iterable, V]>); - constructor(items?: Iterable, V]>) { + constructor(items?: readonly [K, V][]); + constructor(items?: Iterable); + constructor(items?: Iterable) { super(canonicalString, items); } - mapEntries(f: (entry: [Value, V]) => [Value, W]): Dictionary { - const result = new Dictionary(); + mapEntries, R extends object = DefaultPointer>(f: (entry: [K, V]) => [S, W]): KeyedDictionary { + const result = new KeyedDictionary(); for (let oldEntry of this.entries()) { const newEntry = f(oldEntry); result.set(newEntry[0], newEntry[1]) @@ -48,8 +41,8 @@ export class Dictionary extends FlexMap { - return new Dictionary(this); + clone(): KeyedDictionary { + return new KeyedDictionary(this); } toString(): string { @@ -81,25 +74,38 @@ export class Dictionary extends FlexMap extends FlexSet> { +export class Dictionary extends KeyedDictionary, V, T> { + static isDictionary(x: any): x is Dictionary { + return x?.[DictionaryType] === 'Dictionary'; + } + + static fromJS(x: object): Dictionary, T> { + if (Dictionary.isDictionary, T>(x)) return x as Dictionary, T>; + const d = new Dictionary, T>(); + Object.entries(x).forEach(([key, value]) => d.set(key, fromJS(value))); + return d; + } +} + +export class KeyedSet, T extends object = DefaultPointer> extends FlexSet { get [DictionaryType](): DictionaryType { return 'Set'; } - static isSet(x: any): x is Set { + static isKeyedSet, T extends object = DefaultPointer>(x: any): x is KeyedSet { return x?.[DictionaryType] === 'Set'; } - constructor(items?: Iterable>) { + constructor(items?: Iterable) { super(canonicalString, items); } - map(f: (value: Value) => Value): Set { - return new Set(_iterMap(this[Symbol.iterator](), f)); + map, R extends object = DefaultPointer>(f: (value: K) => S): KeyedSet { + return new KeyedSet(_iterMap(this[Symbol.iterator](), f)); } - filter(f: (value: Value) => boolean): Set { - const result = new Set(); + filter(f: (value: K) => boolean): KeyedSet { + const result = new KeyedSet(); for (let k of this) if (f(k)) result.add(k); return result; } @@ -122,7 +128,7 @@ export class Set extends FlexSet> { [PreserveOn](encoder: Encoder) { if (encoder.canonical) { - const pieces = Array.from(this).map<[Bytes, Value]>(k => [canonicalEncode(k), k]); + const pieces = Array.from(this).map<[Bytes, K]>(k => [canonicalEncode(k), k]); pieces.sort((a, b) => Bytes.compare(a[0], b[0])); encoder.encodevalues(Tag.Set, pieces.map(e => e[1])); } else { @@ -130,3 +136,9 @@ export class Set extends FlexSet> { } } } + +export class Set extends KeyedSet, T> { + static isSet(x: any): x is Set { + return x?.[DictionaryType] === 'Set'; + } +} diff --git a/implementations/javascript/src/fold.ts b/implementations/javascript/src/fold.ts index 266c67f..7e82afa 100644 --- a/implementations/javascript/src/fold.ts +++ b/implementations/javascript/src/fold.ts @@ -137,3 +137,28 @@ export function mapPointers( { return fold(v, new MapFold(f)); } + +export function isPointer(v: Value): v is T { + return fold(v, { + boolean(_b: boolean): boolean { return false; }, + single(_f: number): boolean { return false; }, + double(_f: number): boolean { return false; }, + integer(_i: number): boolean { return false; }, + string(_s: string): boolean { return false; }, + bytes(_b: Bytes): boolean { return false; }, + symbol(_s: symbol): boolean { return false; }, + + record(_r: Record, Tuple>, T>, _k: Fold): boolean { + return false; + }, + array(_a: Array>, _k: Fold): boolean { return false; }, + set(_s: Set, _k: Fold): boolean { return false; }, + dictionary(_d: Dictionary, T>, _k: Fold): boolean { + return false; + }, + + annotated(_a: Annotated, _k: Fold): boolean { return false; }, + + pointer(_t: T, _k: Fold): boolean { return true; }, + }); +} diff --git a/implementations/javascript/src/index.ts b/implementations/javascript/src/index.ts index 89b73b9..e15c7a4 100644 --- a/implementations/javascript/src/index.ts +++ b/implementations/javascript/src/index.ts @@ -16,3 +16,7 @@ export * from './symbols'; export * from './text'; export * from './values'; export * as Constants from './constants'; + +const _Array = Array; +type _Array = Array; +export { _Array as Array }; diff --git a/implementations/javascript/src/reader.ts b/implementations/javascript/src/reader.ts index 7cd1071..96da83b 100644 --- a/implementations/javascript/src/reader.ts +++ b/implementations/javascript/src/reader.ts @@ -71,6 +71,7 @@ export class Reader { skipws() { while (true) { + if (this.atEnd()) break; if (!isSpace(this.peek())) break; this.index++; } @@ -101,6 +102,15 @@ export class Reader { return u; } + readToEnd(): Array> { + const acc = []; + while (true) { + this.skipws(); + if (this.atEnd()) return acc; + acc.push(this.next()); + } + } + next(): Value { return this.wrap(this._next()); } diff --git a/implementations/javascript/src/schema/base.ts b/implementations/javascript/src/schema/base.ts new file mode 100644 index 0000000..9aa7f7f --- /dev/null +++ b/implementations/javascript/src/schema/base.ts @@ -0,0 +1,15 @@ +import { Record, KeyedDictionary } from '..'; +import { AtomKind, Pattern, Schema } from './meta'; +import * as M from './meta'; + +export const BASE: Schema = Record(M.SCHEMA, [1, new KeyedDictionary([ + [Symbol.for('any'), Record(M.AND, [[]])], + [Symbol.for('bool'), Record(M.ATOM, [M.BOOLEAN as AtomKind])], + [Symbol.for('float'), Record(M.ATOM, [M.FLOAT as AtomKind])], + [Symbol.for('double'), Record(M.ATOM, [M.DOUBLE as AtomKind])], + [Symbol.for('int'), Record(M.ATOM, [M.SIGNEDINTEGER as AtomKind])], + [Symbol.for('string'), Record(M.ATOM, [M.STRING as AtomKind])], + [Symbol.for('bytes'), Record(M.ATOM, [M.BYTESTRING as AtomKind])], + [Symbol.for('symbol'), Record(M.ATOM, [M.SYMBOL as AtomKind])], + [Symbol.for('ref'), Record(M.POINTER, [])], +])]); diff --git a/implementations/javascript/src/schema/block.ts b/implementations/javascript/src/schema/block.ts new file mode 100644 index 0000000..2e10e30 --- /dev/null +++ b/implementations/javascript/src/schema/block.ts @@ -0,0 +1,160 @@ +export type Item = Emittable | string; + +export class Formatter { + width = 80; + indentDelta = ' '; + currentIndent = '\n'; + buffer: Array = []; + + get indentSize(): number { return this.indentDelta.length; } + set indentSize(n: number) { this.indentDelta = new Array(n + 1).join(' '); } + + write(i: Item) { + if (typeof i === 'string') { + this.buffer.push(i); + } else { + i.writeOn(this); + } + } + + newline() { + this.write(this.currentIndent); + } + + toString(): string { + return this.buffer.join(''); + } + + withIndent(f: () => void): void { + const oldIndent = this.currentIndent; + try { + this.currentIndent = this.currentIndent + this.indentDelta; + f(); + } finally { + this.currentIndent = oldIndent; + } + } + + clone(): Formatter { + const f = Object.assign(new Formatter(), this); + f.buffer = []; + return f; + } +} + +export abstract class Emittable { + abstract writeOn(f: Formatter): void; +} + +export class Sequence extends Emittable { + items: Array; + + constructor(items: Array) { + super(); + this.items = items; + } + + get separator(): string { return ''; } + get terminator(): string { return ''; } + + writeOn(f: Formatter): void { + let needSeparator = false; + this.items.forEach(i => { + if (needSeparator) { + f.write(this.separator); + } else { + needSeparator = true; + } + f.write(i); + }); + f.write(this.terminator); + } +} + +export class CommaSequence extends Sequence { + get separator(): string { return ', '; } +} + +export abstract class Grouping extends CommaSequence { + abstract get open(): string; + abstract get close(): string; + + writeHorizontally(f: Formatter): void { + f.write(this.open); + super.writeOn(f); + f.write(this.close); + } + + writeVertically(f: Formatter): void { + f.write(this.open); + if (this.items.length > 0) { + f.withIndent(() => { + this.items.forEach((i, index) => { + f.newline(); + f.write(i); + const delim = index === this.items.length - 1 ? this.terminator : this.separator; + f.write(delim.trimRight()); + }); + }); + f.newline(); + } + f.write(this.close); + } + + writeOn(f: Formatter): void { + const g = f.clone(); + this.writeHorizontally(g); + const s = g.toString(); + if (s.length <= f.width) { + f.write(s); + } else { + this.writeVertically(f); + } + } +} + +export class Parens extends Grouping { + get open(): string { return '('; } + get close(): string { return ')'; } +} + +export class OperatorSequence extends Parens { + operator: string; + + constructor(operator: string, items: Array) { + super(items); + this.operator = operator; + } + + get separator(): string { return this.operator; } +} + +export class Brackets extends Grouping { + get open(): string { return '['; } + get close(): string { return ']'; } +} + +export class AngleBrackets extends Grouping { + get open(): string { return '<'; } + get close(): string { return '>'; } +} + +export class Braces extends Grouping { + get open(): string { return '{'; } + get close(): string { return '}'; } +} + +export class Block extends Braces { + get separator(): string { return '; ' } + get terminator(): string { return ';' } +} + +export const seq = (... items: Item[]) => new Sequence(items); +export const commas = (... items: Item[]) => new CommaSequence(items); +export const parens = (... items: Item[]) => new Parens(items); +export const opseq = (zero: string, op: string, ... items: Item[]) => + (items.length === 0) ? zero : new OperatorSequence(op, items); +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); diff --git a/implementations/javascript/src/schema/compiler.ts b/implementations/javascript/src/schema/compiler.ts new file mode 100644 index 0000000..f5abe71 --- /dev/null +++ b/implementations/javascript/src/schema/compiler.ts @@ -0,0 +1,286 @@ +import { Pattern, NamedPattern, Schema, Input } from "./meta"; +import * as M from './meta'; +import { Annotated, Bytes, Dictionary, Fold, fold, preserves, Record, Tuple, Value } from ".."; +import { Formatter, parens, seq, Item, opseq, block, commas, brackets, anglebrackets } from "./block"; + +function fnblock(... items: Item[]): Item { + return seq('((() => ', block(... items), ')())'); +} + +export type CompileEnvEntry = { + moduleName: string, + modulePath: string, + schema: Schema, + inline: boolean, +}; + +export function compile(env: Array, schema: Schema, preservesModule = 'preserves'): string { + const literals = new Dictionary(); + const types: Array = []; + const predicates: Array = []; + let temps: Array = []; + + function environmentWith(name: symbol, + kLocal: () => R, + kOther: (e: CompileEnvEntry, p: Pattern) => R): R { + if (Schema._.definitions(schema).has(name)) { + return kLocal(); + } + for (const e of env) { + const p = Schema._.definitions(e.schema).get(name); + if (p !== void 0) { + return kOther(e, p); + } + } + throw new Error(`Undefined reference: ${name.description!}`); + } + + function applyPredicate(name: symbol, v: string): Item { + return environmentWith(name, + () => `is${name.description!}(${v})`, + (e, p) => { + if (e.inline) { + return walk(v, p); + } else { + return `${e.moduleName}.is${name.description!}(${v})`; + } + }); + } + + function gentemp(): string { + const varname = '_tmp' + temps.length; + temps.push(varname); + return varname; + } + + function literal(v: Input): Item { + let varname = literals.get(v); + if (varname === void 0) { + varname = '__lit' + literals.size; + literals.set(v, varname); + } + return varname; + } + + function typeFor(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 ${literal(p[0])})`; + case M.REF: + return environmentWith( + p[0], + () => p[0].description!, + (e, pp) => { + if (e.inline) { + return typeFor(pp); + } else { + return `${e.moduleName}.${p[0].description!}`; + } + }); + case M.OR: + return opseq('never', ' | ', ... p[0].map(pp => typeFor(pp))); + case M.AND: + return opseq('_.Value', ' & ', ... p[0].map(pp => typeFor(pp))); + case M.POINTER: + return `any`; // TODO: what to do here? + case M.REC: + return seq('_.Record', anglebrackets(typeFor(p[0]), typeFor(p[1]))); + case M.TUPLE: + return brackets(... p[0].map(pp => typeFor(unname(pp)))); + case M.TUPLESTAR: + if (p[0].length === 0) { + return seq('Array<', typeFor(unname(p[1])), '>'); + } else { + return brackets(... p[0].map(pp => typeFor(unname(pp))), + seq('... Array<', typeFor(unname(p[1])), '>')); + } + case M.SETOF: + return seq('_.KeyedSet<', typeFor(p[0]), '>'); + case M.DICTOF: + return seq('_.KeyedDictionary', anglebrackets(typeFor(p[0]), typeFor(p[1]))); + case M.DICT: + return parens(seq('_.Dictionary<_.Value> & ', block( + ... Array.from(p[0]).map(([k, vp]) => + seq(`get(k: typeof ${literal(k)}): `, typeFor(vp)))))); + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } + } + + function walk(v: string, p: Pattern, 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}, ${literal(p[0])})`; + case M.REF: + return applyPredicate(p[0], v); + case M.OR: + return opseq('false', ' || ', ... p[0].map(pp => walk(v, pp))); + case M.AND: + return opseq('true', ' && ', ... p[0].map(pp => walk(v, pp))); + case M.POINTER: + return `_.isPointer(${v})`; + case M.REC: + return opseq('true', ' && ', + `_.Record.isRecord(${v})`, + walk(`${v}.label`, p[0]), + walk(v, p[1], true)); + case M.TUPLE: + return opseq('true', ' && ', + ... (recordOkAsTuple ? [] + : [`_.Array.isArray(${v})`, `!_.Record.isRecord(${v})`]), + `(${v}.length === ${p[0].length})`, + ... p[0].map((pp, i) => walk(`${v}[${i}]`, unname(pp)))); + case M.TUPLESTAR: + return opseq('true', ' && ', + ... (recordOkAsTuple ? [] + : [`_.Array.isArray(${v})`, `!_.Record.isRecord(${v})`]), + `(${v}.length >= ${p[0].length})`, + seq(`${v}.slice(${p[0].length})`, + `.every(v => `, + parens(walk('v', unname(p[1]))), + `)`), + ... p[0].map((pp, i) => walk(`${v}[${i}]`, unname(pp)))); + case M.SETOF: + return opseq('true', ' && ', + `_.Set.isSet(${v})`, + fnblock( + seq(`for (const vv of ${v}) `, block( + seq('if (!(', walk('vv', p[0]), ')) return false'))), + seq('return true'))); + case M.DICTOF: + return opseq('true', ' && ', + `_.Dictionary.isDictionary(${v})`, + fnblock( + seq(`for (const e of ${v}) `, block( + seq('if (!(', walk('e[0]', p[0]), ')) return false'), + seq('if (!(', walk('e[1]', p[1]), ')) return false'))), + seq('return true'))); + case M.DICT: + return opseq('true', ' && ', + `_.Dictionary.isDictionary(${v})`, + ... Array.from(p[0]).map(([k, vp]) => { + const tmp = gentemp(); + return parens(seq( + `(${tmp} = ${v}.get(${literal(k)})) !== void 0 && `, + walk(tmp, vp))); + })); + default: + ((_p: never) => {})(p); + throw new Error("Unreachable"); + } + } + + function unname(p: NamedPattern): Pattern { + return (p.label === M.NAMED) ? p[1] : p; + } + + for (const [name0, pattern] of Schema._.definitions(schema)) { + const name = name0 as symbol; + temps = []; + const recognizer = walk('v', pattern); + types.push( + seq(`export type ${name.description!} = `, typeFor(pattern))); + predicates.push( + seq('export function ', `is${name.description!}`, + '(v: any): v is ', name.description!, ' ', + block( + ... temps.length > 0 ? [seq('let ', commas(... temps), ': any')] : [], + seq('return ', recognizer)))); + } + + const f = new Formatter(); + f.write(`import * as _ from ${JSON.stringify(preservesModule)};\n`); + f.newline(); + + for (const [lit, varname] of literals) { + f.write(seq(`const ${varname} = `, sourceCodeFor(lit), `;\n`)); + } + f.newline(); + + types.forEach(t => { + f.write(t); + f.newline(); + }); + f.newline(); + + predicates.forEach(p => { + f.write(p); + f.newline(); + f.newline(); + }); + + 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); }, + 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!)})`; }, + + record(r: Record, Tuple>, any>, k: Fold): Item { + return seq(`_.Record`, 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', parens(brackets(... Array.from(s).map(k)))); + }, + dictionary(d: Dictionary, any>, k: Fold): Item { + return seq('new _.Dictionary', parens(brackets(... Array.from(d).map(([kk,vv]) => + brackets(k(kk), k(vv)))))); + }, + + annotated(a: Annotated, k: Fold): Item { + return seq('_.annotate', parens(k(a.item), ... a.annotations.map(k))); + }, + + pointer(t: any, _k: Fold): Item { + throw new Error(preserves`Cannot emit source code for construction of pointer ${t}`); + }, + }); +} + +import fs from 'fs'; +import { readSchema } from "./reader"; +import { Reader } from "../reader"; +import { BASE } from "./base"; +function main() { + const src = fs.readFileSync(__dirname + '/../../../../schema/schema.txt', 'utf-8'); + const sch = readSchema(new Reader(src, { includeAnnotations: true }).readToEnd()); + console.log(compile([{ moduleName: 'BASE', modulePath: 'BASE', schema: BASE, inline: true }], sch, '..')); +} + +main(); diff --git a/implementations/javascript/src/schema/interpreter.ts b/implementations/javascript/src/schema/interpreter.ts new file mode 100644 index 0000000..8fa52be --- /dev/null +++ b/implementations/javascript/src/schema/interpreter.ts @@ -0,0 +1,89 @@ +import { Environment, Pattern, NamedPattern, lookup } from "./meta"; +import * as M from './meta'; +import { Value, Float, Bytes, is, isPointer, Record, Dictionary, Set } from ".."; + +export function validator(env: Environment, p: Pattern): (v: Value) => boolean { + function walk(p: Pattern, v: Value, recordOkAsTuple = false): boolean { + 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, p[0]); + case M.REF: + return walk(lookup(env, p[0]), v); + case M.OR: + for (const pp of p[0]) { + if (walk(pp, v)) return true; + } + return false; + case M.AND: + for (const pp of p[0]) { + if (!walk(pp, v)) return false; + } + return true; + case M.POINTER: + return isPointer(v); + case M.REC: + if (!Record.isRecord(v)) return false; + if (!walk(p[0], v.label)) return false; + return walk(p[1], v, true); + case M.TUPLE: + if (!Array.isArray(v)) return false; + if (!recordOkAsTuple && Record.isRecord(v)) return false; + if (p[0].length !== v.length) return false; + for (let i = 0; i < v.length; i++) { + if (!walknamed(p[0][i], v[i])) return false; + } + return true; + case M.TUPLESTAR: + if (!Array.isArray(v)) return false; + if (!recordOkAsTuple && Record.isRecord(v)) return false; + if (p[0].length > v.length) return false; + for (let i = 0; i < p[0].length; i++) { + if (!walknamed(p[0][i], v[i])) return false; + } + for (let i = p[0].length; i < v.length; i++) { + if (!walknamed(p[1], v[i])) return false; + } + return true; + case M.SETOF: + if (!Set.isSet(v)) return false; + for (const vv of v) { + if (!walk(p[0], vv)) return false; + } + return true; + case M.DICTOF: + if (!Dictionary.isDictionary>(v)) return false; + for (const e of v) { + if (!walk(p[0], e[0])) return false; + if (!walk(p[1], e[1])) return false; + } + return true; + case M.DICT: + if (!Dictionary.isDictionary>(v)) return false; + for (const e of p[0]) { + const vv = v.get(e[0]); + if (vv === void 0) return false; + if (!walk(e[1], vv)) return false; + } + return true; + default: + ((_p: never) => {})(p); + return false; + } + } + + function walknamed(p: NamedPattern, v: Value): boolean { + return (p.label === M.NAMED) ? walk(p[1], v) : walk(p, v); + } + + return v => walk(p, v); +} diff --git a/implementations/javascript/src/schema/meta.ts b/implementations/javascript/src/schema/meta.ts new file mode 100644 index 0000000..ad14523 --- /dev/null +++ b/implementations/javascript/src/schema/meta.ts @@ -0,0 +1,72 @@ +import { Dictionary, KeyedDictionary, Record, Value, preserves } from '..'; + +export type Input = Value; + +export const AND = Symbol.for('and'); +export const ANDSYM = Symbol.for('&'); +export const ATOM = Symbol.for('atom'); +export const BOOLEAN = Symbol.for('Boolean'); +export const BYTESTRING = Symbol.for('ByteString'); +export const DICT = Symbol.for('dict'); +export const DICTOF = Symbol.for('dictof'); +export const DOT = Symbol.for('.'); +export const DOTDOTDOT = Symbol.for('...'); +export const DOUBLE = Symbol.for('Double'); +export const EQUALS = Symbol.for('='); +export const FLOAT = Symbol.for('Float'); +export const LIT = Symbol.for('lit'); +export const NAMED = Symbol.for('named'); +export const OR = Symbol.for('or'); +export const ORSYM = Symbol.for('/'); +export const POINTER = Symbol.for('pointer'); +export const REC = Symbol.for('rec'); +export const REF = Symbol.for('ref'); +export const SCHEMA = Symbol.for('schema'); +export const SETOF = Symbol.for('setof'); +export const SIGNEDINTEGER = Symbol.for('SignedInteger'); +export const STRING = Symbol.for('String'); +export const SYMBOL = Symbol.for('Symbol'); +export const TUPLE = Symbol.for('tuple'); +export const TUPLESTAR = Symbol.for('tuple*'); +export const VERSION = Symbol.for('version'); + +export type Environment = Array; + +export const Schema = Record.makeConstructor<{ + version: number, + definitions: KeyedDictionary, +}>()(Symbol.for('schema'), ['version', 'definitions']); +export type Schema = ReturnType; + +export type AtomKind = + | typeof BOOLEAN + | typeof FLOAT + | typeof DOUBLE + | typeof SIGNEDINTEGER + | typeof STRING + | typeof BYTESTRING + | typeof SYMBOL; + +export type Pattern = + | Record + | Record + | Record + | Record], never> + | Record], never> + | Record + | Record + | Record], never> + | Record, NamedPattern], never> + | Record + | Record + | Record], never>; + +export type NamedPattern = Record | Pattern; + +export function lookup(env: Environment, name: symbol): Pattern { + for (const s of env) { + const p = Schema._.definitions(s).get(name); + if (p !== void 0) return p; + } + throw new Error(preserves`Schema: unbound name ${name}`); +} diff --git a/implementations/javascript/src/schema/reader.ts b/implementations/javascript/src/schema/reader.ts new file mode 100644 index 0000000..564e6df --- /dev/null +++ b/implementations/javascript/src/schema/reader.ts @@ -0,0 +1,163 @@ +import { Annotated, Dictionary, is, KeyedDictionary, peel, preserves, Record, strip, Tuple } from '..'; + +import { Input, NamedPattern, Pattern, Schema } from './meta'; +import * as M from './meta'; + +function splitBy(items: Array, separator: T): Array> { + const groups: Array> = []; + let group: Array = []; + function finish() { + if (group.length > 0) { + groups.push(group); + group = []; + } + } + for (const item of items) { + if (is(item, separator)) { + finish(); + } else { + group.push(item); + } + } + finish(); + return groups; +} + +function invalidClause(clause: Input): never { + throw new Error(preserves`Invalid Schema clause: ${clause}`); +} + +function invalidPattern(name: symbol, item: Input): never { + throw new Error(preserves`Invalid pattern in ${name}: ${item}`); +} + +export function readSchema(toplevelTokens: Array): Schema { + const toplevelClauses = splitBy(peel(toplevelTokens) as Array, M.DOT); + let version: number | undefined = void 0; + let definitions = new KeyedDictionary(); + for (const clause0 of toplevelClauses) { + const clause = clause0.map(peel); + if (!Array.isArray(clause)) { + invalidClause(clause); + } else if (clause.length >= 2 && is(clause[1], M.EQUALS)) { + if (typeof clause[0] !== 'symbol') invalidClause(clause); + const name = clause[0]; + if (definitions.has(name)) { + throw new Error(preserves`Duplicate definition: ${clause}`); + } + definitions.set(name, parseDefinition(name, clause.slice(2).map(peel))); + } else if (clause.length === 2 && is(clause[0], M.VERSION)) { + if (typeof clause[1] !== 'number') invalidClause(clause); + version = clause[1]; + } else { + invalidClause(clause); + } + } + if (version === void 0) { + throw new Error("Schema: missing version declaration."); + } + if (version !== 1) { + throw new Error("Schema: unsupported version " + version); + } + return Record(M.SCHEMA, [version, definitions]); +} + +function parseDefinition(name: symbol, body: Array): Pattern { + return parseOp(body, M.ORSYM, p => parseOp(p, M.ANDSYM, p => parseBase(name, p))); +} + +function parseOp(body: Array, op: Input, k: (p: Array) => Pattern): Pattern { + const pieces = splitBy(body, op); + if (pieces.length === 1) return k(pieces[0]); + switch (op) { + case M.ORSYM: return Record(M.OR, [pieces.map(k)]); + case M.ANDSYM: return Record(M.AND, [pieces.map(k)]); + default: throw new Error("Internal error: unexpected operator"); + } +} + +function findName(x: Input): symbol | false { + if (!Annotated.isAnnotated(x)) return false; + for (const a0 of x.annotations) { + const a = peel(a0); + if (typeof a === 'symbol') return a; + } + return false; +} + +function parseBase(name: symbol, body: Array): Pattern { + body = peel(body) as Array; + if (body.length !== 1) invalidPattern(name, body); + const item = peel(body[0]); + const walk = (b: Input): Pattern => parseBase(name, [b]); + const namedwalk = (b: Input): NamedPattern => { + const name = findName(b); + if (name === false) return walk(b); + return Record(M.NAMED, [name, walk(b)]); + }; + const walkitems = (b: Input): Pattern[] => { + b = peel(b); + if (!Array.isArray(b)) complain(); + return b.map(walk); + }; + + function complain(): never { + invalidPattern(name, item); + } + + if (typeof item === 'symbol') { + const s = item.description; + if (s === void 0) complain(); + if (s[0] === '=') return Record(M.LIT, [Symbol.for(s.slice(1))]); + return Record(M.REF, [item]); + } else if (Record.isRecord, never>(item)) { + const label = item.label; + if (Record.isRecord(label)) { + if (label.length !== 0) complain(); + switch (label.label) { + case M.LIT: + if (item.length !== 1) complain(); + return Record(M.LIT, [item[0]]); + case M.OR: + if (item.length !== 1) complain(); + return Record(M.OR, [walkitems(item[0])]); + case M.AND: + if (item.length !== 1) complain(); + return Record(M.AND, [walkitems(item[0])]); + case M.REC: + if (item.length !== 2) complain(); + return Record(M.REC, [walk(item[0]), walk(item[1])]); + default: + complain(); + } + } else { + return Record(M.REC, [Record(M.LIT, [label]), Record(M.TUPLE, [item.map(namedwalk)])]); + } + } else if (Array.isArray(item)) { + if (is(item[item.length - 1], M.DOTDOTDOT)) { + if (item.length < 2) complain(); + return Record(M.TUPLESTAR, [ + item.slice(0, item.length - 2).map(namedwalk), + namedwalk(item[item.length - 2]), + ]); + } else { + return Record(M.TUPLE, [item.map(namedwalk)]); + } + } else if (Dictionary.isDictionary(item)) { + if (item.size === 2 && item.has(M.DOTDOTDOT)) { + const v = item.clone(); + v.delete(M.DOTDOTDOT); + const [[kp, vp]] = v.entries(); + return Record(M.DICTOF, [walk(kp), walk(vp)]); + } else { + return Record(M.DICT, [item.mapEntries(([k, vp]) => + [strip(k), walk(vp)])]); + } + } else if (Set.isSet(item)) { + if (item.size !== 1) complain(); + const [vp] = item.entries(); + return Record(M.SETOF, [walk(vp)]); + } else { + return Record(M.LIT, [strip(item)]); + } +} diff --git a/schema/schema.txt b/schema/schema.txt new file mode 100644 index 0000000..03fb68c --- /dev/null +++ b/schema/schema.txt @@ -0,0 +1,54 @@ +@ + +version 1 . + +Schema = . + +; version 1 . +Version = 1 . + +Pattern = < [ + ; special builtins or < symbol> + [=Boolean =Float =Double =SignedInteger =String =ByteString =Symbol]>> + + ; =symbol, < any>, or plain non-symbol atom + + + ; symbol + + + ; Pattern / Pattern / ... + ; < [Pattern Pattern ...]> + ; and the empty pattern is < []> + + + ; Pattern & Pattern & ... + ; < [Pattern Pattern ...]> + ; and the universal pattern, "any", is < []> + + + ;