From 8d7e7c6d95772e48d6b7af665cd995a2b82e30bb Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Thu, 11 Mar 2021 17:59:40 +0100 Subject: [PATCH] Pointers; improved diagnostics --- .../schema/src/bin/preserves-schema-ts.ts | 106 ++++++++++---- .../packages/schema/src/compiler.ts | 48 ++++--- .../javascript/packages/schema/src/error.ts | 2 +- .../packages/schema/src/gen/schema.ts | 135 +++++++++++------- .../packages/schema/src/interpreter.ts | 2 +- .../javascript/packages/schema/src/meta.ts | 38 +++-- .../javascript/packages/schema/src/reader.ts | 64 +++++---- schema/schema.prs | 3 + 8 files changed, 265 insertions(+), 133 deletions(-) diff --git a/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts b/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts index a0576dc..71fff8d 100644 --- a/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts +++ b/implementations/javascript/packages/schema/src/bin/preserves-schema-ts.ts @@ -5,9 +5,8 @@ import { glob } from 'glob'; import minimatch from 'minimatch'; import yargs from 'yargs/yargs'; import * as M from '../meta'; -import { SchemaSyntaxError } from '../error'; import chalk from 'chalk'; -import { formatPosition } from '@preserves/core'; +import { formatPosition, Position } from '@preserves/core'; export type CommandLineArguments = { input: string; @@ -15,12 +14,20 @@ export type CommandLineArguments = { output: string | undefined; core: string; watch: boolean; + traceback: boolean; + module: string[]; +}; + +export interface Diagnostic { + type: 'warn' | 'error'; + file: string; + detail: Error | { message: string, pos: Position | null }; }; export type CompilationResult = { options: CommandLineArguments, inputFiles: Array, - failures: Array<[string, Error]>, + failures: Array, base: string, output: string, }; @@ -51,22 +58,32 @@ export function computeBase(paths: string[]): string { } } +function failureCount(type: 'warn' | 'error', r: CompilationResult): number { + return r.failures.filter(f => f.type === type).length; +} + export function run(options: CommandLineArguments): void { if (!options.watch) { - if (runOnce(options).failures.length !== 0) { + if (failureCount('error', runOnce(options)) > 0) { process.exit(1); } } else { function runWatch() { console.clear(); - console.log(chalk.yellow(new Date().toISOString()) + + console.log(chalk.gray(new Date().toISOString()) + ' Compiling Schemas in watch mode...\n'); const r = runOnce(options); - const hasErrors = r.failures.length > 0; - const errorSummary = (hasErrors ? chalk.redBright : chalk.greenBright)( - `${r.failures.length} error(s)`); - console.log(chalk.yellow(new Date().toISOString()) + - ` Processed ${r.inputFiles.length} file(s) with ${errorSummary}. Waiting for changes.`); + const warningCount = failureCount('warn', r); + const errorCount = failureCount('error', r); + const wMsg = (warningCount > 0) && chalk.yellowBright(`${warningCount} warning(s)`); + const eMsg = (errorCount > 0) && chalk.redBright(`${errorCount} error(s)`); + const errorSummary = + (wMsg && eMsg) ? `with ${eMsg} and ${wMsg}` : + (wMsg) ? `with ${wMsg}` : + (eMsg) ? `with ${eMsg}` : + chalk.greenBright('successfully'); + console.log(chalk.gray(new Date().toISOString()) + + ` Processed ${r.inputFiles.length} file(s) ${errorSummary}. Waiting for changes.`); const watcher = fs.watch(r.base, { recursive: true }, (_event, filename) => { filename = path.join(r.base, filename); if (minimatch(filename, options.input)) { @@ -92,11 +109,23 @@ export function modulePathTo(file1: string, file2: string): string | null { export function runOnce(options: CommandLineArguments): CompilationResult { const matches = glob.sync(options.input); - const failures: Array<[string, Error]> = []; + const failures: Array = []; const base = options.base ?? computeBase(matches); const output = options.output ?? base; + const extensionEnv: M.Environment = options.module.map(arg => { + const i = arg.indexOf('='); + if (i === -1) throw new Error(`--module argument must be Namespace=path: ${arg}`); + const ns = arg.slice(0, i); + const path = arg.slice(i + 1); + return { + schema: null, + schemaModulePath: ns.split('.').map(Symbol.for), + typescriptModulePath: path, + }; + }); + const inputFiles: Array = matches.flatMap(inputFilePath => { if (!inputFilePath.startsWith(base)) { throw new Error(`Input filename ${inputFilePath} falls outside base ${base}`); @@ -109,31 +138,47 @@ export function runOnce(options: CommandLineArguments): CompilationResult { const schemaPath = relPath.split('/').map(p => p.split('.')[0]).map(Symbol.for); return [{ inputFilePath, outputFilePath, schemaPath, schema }]; } catch (e) { - failures.push([inputFilePath, e]); + failures.push({ type: 'error', file: inputFilePath, detail: e }); return []; } }); inputFiles.forEach(c => { - const env: M.Environment = inputFiles.map(cc => ({ - schema: cc.schema, - schemaModulePath: cc.schemaPath, - typescriptModulePath: modulePathTo(c.outputFilePath, cc.outputFilePath), - })); + const env: M.Environment = [ + ... extensionEnv.flatMap(e => { + if (e.typescriptModulePath === null) return []; + const p = modulePathTo(c.outputFilePath, e.typescriptModulePath); + if (p === null) return []; + return [{... e, typescriptModulePath: p}]; + }), + ... inputFiles.map(cc => ({ + schema: cc.schema, + schemaModulePath: cc.schemaPath, + typescriptModulePath: modulePathTo(c.outputFilePath, cc.outputFilePath), + })), + ]; fs.mkdirSync(path.dirname(c.outputFilePath), { recursive: true }); try { - fs.writeFileSync(c.outputFilePath, compile(env, c.schema, options.core), 'utf-8'); + fs.writeFileSync(c.outputFilePath, compile(env, c.schema, { + preservesModule: options.core, + warn: (message: string, pos: Position | null) => + failures.push({ type: 'warn', file: c.inputFilePath, detail: { message, pos } }), + }), 'utf-8'); } catch (e) { - failures.push([c.inputFilePath, e]); + failures.push({ type: 'error', file: c.inputFilePath, detail: e }); } }); - for (const [inputFile, err] of failures) { - if (err instanceof SchemaSyntaxError) { - console.log(chalk.blueBright(formatPosition(err.pos ?? inputFile)) + ': ' + err.message); - } else { - console.log(chalk.blueBright(inputFile) + ': ' + err.message); - } + for (const d of failures) { + console.log( + (d.type === 'error' ? chalk.redBright('[ERROR]') : chalk.yellowBright('[WARNING]')) + + ' ' + + chalk.blueBright(formatPosition((d.detail as any).pos ?? d.file)) + + ': ' + + d.detail.message + + (options.traceback && (d.detail instanceof Error) + ? '\n' + d.detail.stack + : '')); } if (failures.length > 0) { console.log(); @@ -169,6 +214,17 @@ export function main(argv: Array) { type: 'boolean', descripion: 'Watch base directory for changes', default: false, + }) + .option('traceback', { + type: 'boolean', + description: 'Include stack traces in compiler errors', + default: false, + }) + .option('module', { + type: 'string', + array: true, + description: 'Additional Namespace=path imports', + default: [], }), argv => argv) .argv; diff --git a/implementations/javascript/packages/schema/src/compiler.ts b/implementations/javascript/packages/schema/src/compiler.ts index 3963e31..00257f4 100644 --- a/implementations/javascript/packages/schema/src/compiler.ts +++ b/implementations/javascript/packages/schema/src/compiler.ts @@ -1,19 +1,26 @@ import { Pattern, NamedPattern, Schema, Input, Environment, Ref, lookup } from "./meta"; import * as M from './meta'; -import { Annotated, Bytes, Dictionary, Fold, fold, KeyedSet, preserves, Record, Set, Tuple, Value } from "@preserves/core"; +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"; +export interface CompilerOptions { + preservesModule?: string; + defaultPointer?: Ref; + warn?(message: string, pos: Position | null): void; +}; + function fnblock(... items: Item[]): Item { return seq('((() => ', block(... items), ')())'); } -export function compile(env: Environment, schema: Schema, preservesModule = '@preserves/core'): string { +export function compile(env: Environment, schema: Schema, options: CompilerOptions = {}): string { const literals = new Dictionary(); const types: Array = []; const functions: Array = []; const imports = new KeyedSet<[string, string]>(); let temps: Array = []; + const pointerName = Schema._.details(schema).get(M.$pointer); function applyPredicate(name: Ref, v: string): Item { return lookup(refPosition(name), name, env, @@ -68,11 +75,11 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr case M.$or: return opseq('never', ' | ', ... p[0].map(pp => typeFor(pp))); case M.$and: - return opseq('_.Value', ' & ', ... p[0].map(pp => typeFor(pp))); + return opseq('_val', ' & ', ... p[0].map(pp => typeFor(pp))); case M.$pointer: - return `any`; // TODO: what to do here? + return `_ptr`; case M.$rec: - return seq('_.Record', anglebrackets(typeFor(p[0]), typeFor(p[1]))); + return seq('_.Record', anglebrackets(typeFor(p[0]), typeFor(p[1]), '_ptr')); case M.$tuple: return brackets(... p[0].map(pp => typeFor(unname(pp)))); case M.$tuple_STAR_: @@ -93,7 +100,7 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr seq(`get(k: typeof ${literal(k)}): `, typeFor(vp))), ... Array.from(p[0]).map(([k, _vp]) => seq(`has(k: typeof ${literal(k)}): true`))), - ' & _.Dictionary<_.Value>')); + ' & _.Dictionary<_val, _ptr>')); default: ((_p: never) => {})(p); throw new Error("Unreachable"); @@ -124,19 +131,19 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr return `_.isPointer(${v})`; case M.$rec: return opseq('true', ' && ', - `_.Record.isRecord(${v})`, + `_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`, walk(`${v}.label`, p[0]), walk(v, p[1], true)); case M.$tuple: return opseq('true', ' && ', ... (recordOkAsTuple ? [] - : [`_.Array.isArray(${v})`, `!_.Record.isRecord(${v})`]), + : [`_.Array.isArray(${v})`, `!_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`]), `(${v}.length === ${p[0].length})`, ... p[0].map((pp, i) => walk(`${v}[${i}]`, unname(pp)))); case M.$tuple_STAR_: return opseq('true', ' && ', ... (recordOkAsTuple ? [] - : [`_.Array.isArray(${v})`, `!_.Record.isRecord(${v})`]), + : [`_.Array.isArray(${v})`, `!_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(${v})`]), `(${v}.length >= ${p[0].length})`, seq(`${v}.slice(${p[0].length})`, `.every(v => `, @@ -145,14 +152,14 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr ... p[0].map((pp, i) => walk(`${v}[${i}]`, unname(pp)))); case M.$setof: return opseq('true', ' && ', - `_.Set.isSet(${v})`, + `_.Set.isSet<_val>(${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})`, + `_.Dictionary.isDictionary<_val, _ptr>(${v})`, fnblock( seq(`for (const e of ${v}) `, block( seq('if (!(', walk('e[0]', p[0]), ')) return false'), @@ -160,7 +167,7 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr seq('return true'))); case M.$dict: return opseq('true', ' && ', - `_.Dictionary.isDictionary(${v})`, + `_.Dictionary.isDictionary<_val, _ptr>(${v})`, ... Array.from(p[0]).map(([k, vp]) => { const tmp = gentemp(); return parens(seq( @@ -196,7 +203,7 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr types.push( seq(`export const ${name.description!} = _.Record.makeConstructor<`, braces(... pattern[1][0].map(fieldEntry)), - `>()(${literal(pattern[0][0])}, `, + `, _ptr>()(${literal(pattern[0][0])}, `, JSON.stringify(pattern[1][0].map(fieldName)), `);`)); } types.push( @@ -212,13 +219,16 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr '(v: any): ', name.description!, ' ', block( seq(`if (!is${name.description!}(v)) `, - block(`throw new TypeError("Invalid ${name.description!}")`), + block(`throw new TypeError(\`Invalid ${name.description!}: \${_.stringify(v)}\`)`), ' else ', block(`return v`))))); } + types.push(seq('export type _ptr = ', pointerName === false ? 'never' : typeFor(pointerName), `;`)); + types.push(`export type _val = _.Value<_ptr>;`); + const f = new Formatter(); - f.write(`import * as _ from ${JSON.stringify(preservesModule)};\n`); + f.write(`import * as _ from ${JSON.stringify(options.preservesModule ?? '@preserves/core')};\n`); imports.forEach(([identifier, path]) => { f.write(`import * as ${identifier} from ${JSON.stringify(path)};\n`); }); @@ -264,21 +274,21 @@ export function sourceCodeFor(v: Value): Item { 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)))); + 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', parens(brackets(... Array.from(s).map(k)))); + return seq('new _.Set<_val>', 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]) => + return seq('new _.Dictionary<_val, _ptr>', 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))); + return seq('_.annotate<_ptr>', parens(k(a.item), ... a.annotations.map(k))); }, pointer(t: any, _k: Fold): Item { diff --git a/implementations/javascript/packages/schema/src/error.ts b/implementations/javascript/packages/schema/src/error.ts index 5dd8dfd..6d4943e 100644 --- a/implementations/javascript/packages/schema/src/error.ts +++ b/implementations/javascript/packages/schema/src/error.ts @@ -1,4 +1,4 @@ -import { Position, formatPosition } from '@preserves/core'; +import { Position } from '@preserves/core'; export class SchemaSyntaxError extends Error { readonly pos: Position | null; diff --git a/implementations/javascript/packages/schema/src/gen/schema.ts b/implementations/javascript/packages/schema/src/gen/schema.ts index 299e3de..75e6a7a 100644 --- a/implementations/javascript/packages/schema/src/gen/schema.ts +++ b/implementations/javascript/packages/schema/src/gen/schema.ts @@ -1,5 +1,8 @@ import * as _ from "@preserves/core"; +export type _ptr = never; +export type _val = _.Value<_ptr>; + export const $1 = 1; export const $Boolean = Symbol.for("Boolean"); export const $ByteString = Symbol.for("ByteString"); @@ -25,17 +28,20 @@ export const $thisModule = Symbol.for("thisModule"); export const $tuple = Symbol.for("tuple"); export const $tuple_STAR_ = Symbol.for("tuple*"); export const $version = Symbol.for("version"); +export const __lit5 = false; export const Schema = _.Record.makeConstructor<{ "details": ( { get(k: typeof $version): Version; + get(k: typeof $pointer): PointerName; get(k: typeof $definitions): _.KeyedDictionary; has(k: typeof $version): true; + has(k: typeof $pointer): true; has(k: typeof $definitions): true; - } & _.Dictionary<_.Value> + } & _.Dictionary<_val, _ptr> ) -}>()($schema, ["details"]); +}, _ptr>()($schema, ["details"]); export type Schema = _.Record< (typeof $schema), @@ -43,16 +49,21 @@ export type Schema = _.Record< ( { get(k: typeof $version): Version; + get(k: typeof $pointer): PointerName; get(k: typeof $definitions): _.KeyedDictionary; has(k: typeof $version): true; + has(k: typeof $pointer): true; has(k: typeof $definitions): true; - } & _.Dictionary<_.Value> + } & _.Dictionary<_val, _ptr> ) - ] + ], + _ptr >; export type Version = (typeof $1); +export type PointerName = (Ref | (typeof __lit5)); + export type Pattern = ( _.Record< (typeof $atom), @@ -66,26 +77,27 @@ export type Pattern = ( (typeof $ByteString) | (typeof $Symbol) ) - ] + ], + _ptr > | - _.Record<(typeof $pointer), []> | - _.Record<(typeof $lit), [_.Value]> | + _.Record<(typeof $pointer), [], _ptr> | + _.Record<(typeof $lit), [_val], _ptr> | Ref | - _.Record<(typeof $or), [Array]> | - _.Record<(typeof $and), [Array]> | - _.Record<(typeof $rec), [Pattern, Pattern]> | - _.Record<(typeof $tuple), [Array]> | - _.Record<(typeof $tuple_STAR_), [Array, NamedPattern]> | - _.Record<(typeof $setof), [Pattern]> | - _.Record<(typeof $dictof), [Pattern, Pattern]> | - _.Record<(typeof $dict), [_.KeyedDictionary<_.Value, Pattern>]> + _.Record<(typeof $or), [Array], _ptr> | + _.Record<(typeof $and), [Array], _ptr> | + _.Record<(typeof $rec), [Pattern, Pattern], _ptr> | + _.Record<(typeof $tuple), [Array], _ptr> | + _.Record<(typeof $tuple_STAR_), [Array, NamedPattern], _ptr> | + _.Record<(typeof $setof), [Pattern], _ptr> | + _.Record<(typeof $dictof), [Pattern, Pattern], _ptr> | + _.Record<(typeof $dict), [_.KeyedDictionary<_val, Pattern>], _ptr> ); -export type NamedPattern = (_.Record<(typeof $named), [symbol, Pattern]> | Pattern); +export type NamedPattern = (_.Record<(typeof $named), [symbol, Pattern], _ptr> | Pattern); -export const Ref = _.Record.makeConstructor<{"module": ModuleRef, "name": symbol}>()($ref, ["module","name"]); +export const Ref = _.Record.makeConstructor<{"module": ModuleRef, "name": symbol}, _ptr>()($ref, ["module","name"]); -export type Ref = _.Record<(typeof $ref), [ModuleRef, symbol]>; +export type Ref = _.Record<(typeof $ref), [ModuleRef, symbol], _ptr>; export type ModuleRef = ((typeof $thisModule) | ModulePath); @@ -93,20 +105,21 @@ export type ModulePath = Array; export function isSchema(v: any): v is Schema { - let _tmp0, _tmp1: any; + let _tmp0, _tmp1, _tmp2: any; return ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $schema) && ( (v.length === 1) && ( - _.Dictionary.isDictionary(v[0]) && + _.Dictionary.isDictionary<_val, _ptr>(v[0]) && ((_tmp0 = v[0].get($version)) !== void 0 && isVersion(_tmp0)) && + ((_tmp1 = v[0].get($pointer)) !== void 0 && isPointerName(_tmp1)) && ( - (_tmp1 = v[0].get($definitions)) !== void 0 && ( - _.Dictionary.isDictionary(_tmp1) && + (_tmp2 = v[0].get($definitions)) !== void 0 && ( + _.Dictionary.isDictionary<_val, _ptr>(_tmp2) && ((() => { - for (const e of _tmp1) { + for (const e of _tmp2) { if (!(typeof e[0] === 'symbol')) return false; if (!(isPattern(e[1]))) return false; }; @@ -119,16 +132,26 @@ export function isSchema(v: any): v is Schema { ); } -export function asSchema(v: any): Schema {if (!isSchema(v)) {throw new TypeError("Invalid Schema");} else {return v;};} +export function asSchema(v: any): Schema { + if (!isSchema(v)) {throw new TypeError(`Invalid Schema: ${_.stringify(v)}`);} else {return v;}; +} export function isVersion(v: any): v is Version {return _.is(v, $1);} -export function asVersion(v: any): Version {if (!isVersion(v)) {throw new TypeError("Invalid Version");} else {return v;};} +export function asVersion(v: any): Version { + if (!isVersion(v)) {throw new TypeError(`Invalid Version: ${_.stringify(v)}`);} else {return v;}; +} + +export function isPointerName(v: any): v is PointerName {return (isRef(v) || _.is(v, __lit5));} + +export function asPointerName(v: any): PointerName { + if (!isPointerName(v)) {throw new TypeError(`Invalid PointerName: ${_.stringify(v)}`);} else {return v;}; +} export function isPattern(v: any): v is Pattern { return ( ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $atom) && ( (v.length === 1) && @@ -143,61 +166,69 @@ export function isPattern(v: any): v is Pattern { ) ) ) || - (_.Record.isRecord(v) && _.is(v.label, $pointer) && ((v.length === 0))) || - (_.Record.isRecord(v) && _.is(v.label, $lit) && ((v.length === 1) && true)) || + ( + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && + _.is(v.label, $pointer) && + ((v.length === 0)) + ) || + ( + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && + _.is(v.label, $lit) && + ((v.length === 1) && true) + ) || isRef(v) || ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $or) && ( (v.length === 1) && ( _.Array.isArray(v[0]) && - !_.Record.isRecord(v[0]) && + !_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v[0]) && (v[0].length >= 0) && v[0].slice(0).every(v => (isPattern(v))) ) ) ) || ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $and) && ( (v.length === 1) && ( _.Array.isArray(v[0]) && - !_.Record.isRecord(v[0]) && + !_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v[0]) && (v[0].length >= 0) && v[0].slice(0).every(v => (isPattern(v))) ) ) ) || ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $rec) && ((v.length === 2) && isPattern(v[0]) && isPattern(v[1])) ) || ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $tuple) && ( (v.length === 1) && ( _.Array.isArray(v[0]) && - !_.Record.isRecord(v[0]) && + !_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v[0]) && (v[0].length >= 0) && v[0].slice(0).every(v => (isNamedPattern(v))) ) ) ) || ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $tuple_STAR_) && ( (v.length === 2) && ( _.Array.isArray(v[0]) && - !_.Record.isRecord(v[0]) && + !_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v[0]) && (v[0].length >= 0) && v[0].slice(0).every(v => (isNamedPattern(v))) ) && @@ -205,22 +236,22 @@ export function isPattern(v: any): v is Pattern { ) ) || ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $setof) && ((v.length === 1) && isPattern(v[0])) ) || ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $dictof) && ((v.length === 2) && isPattern(v[0]) && isPattern(v[1])) ) || ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $dict) && ( (v.length === 1) && ( - _.Dictionary.isDictionary(v[0]) && + _.Dictionary.isDictionary<_val, _ptr>(v[0]) && ((() => { for (const e of v[0]) {if (!(true)) return false; if (!(isPattern(e[1]))) return false;}; return true; @@ -231,12 +262,14 @@ export function isPattern(v: any): v is Pattern { ); } -export function asPattern(v: any): Pattern {if (!isPattern(v)) {throw new TypeError("Invalid Pattern");} else {return v;};} +export function asPattern(v: any): Pattern { + if (!isPattern(v)) {throw new TypeError(`Invalid Pattern: ${_.stringify(v)}`);} else {return v;}; +} export function isNamedPattern(v: any): v is NamedPattern { return ( ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $named) && ((v.length === 2) && typeof v[0] === 'symbol' && isPattern(v[1])) ) || @@ -245,35 +278,37 @@ export function isNamedPattern(v: any): v is NamedPattern { } export function asNamedPattern(v: any): NamedPattern { - if (!isNamedPattern(v)) {throw new TypeError("Invalid NamedPattern");} else {return v;}; + if (!isNamedPattern(v)) {throw new TypeError(`Invalid NamedPattern: ${_.stringify(v)}`);} else {return v;}; } export function isRef(v: any): v is Ref { return ( - _.Record.isRecord(v) && + _.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && _.is(v.label, $ref) && ((v.length === 2) && isModuleRef(v[0]) && typeof v[1] === 'symbol') ); } -export function asRef(v: any): Ref {if (!isRef(v)) {throw new TypeError("Invalid Ref");} else {return v;};} +export function asRef(v: any): Ref { + if (!isRef(v)) {throw new TypeError(`Invalid Ref: ${_.stringify(v)}`);} else {return v;}; +} export function isModuleRef(v: any): v is ModuleRef {return (_.is(v, $thisModule) || isModulePath(v));} export function asModuleRef(v: any): ModuleRef { - if (!isModuleRef(v)) {throw new TypeError("Invalid ModuleRef");} else {return v;}; + if (!isModuleRef(v)) {throw new TypeError(`Invalid ModuleRef: ${_.stringify(v)}`);} else {return v;}; } export function isModulePath(v: any): v is ModulePath { return ( _.Array.isArray(v) && - !_.Record.isRecord(v) && + !_.Record.isRecord<_val, _.Tuple<_val>, _ptr>(v) && (v.length >= 0) && v.slice(0).every(v => (typeof v === 'symbol')) ); } export function asModulePath(v: any): ModulePath { - if (!isModulePath(v)) {throw new TypeError("Invalid ModulePath");} else {return v;}; + if (!isModulePath(v)) {throw new TypeError(`Invalid ModulePath: ${_.stringify(v)}`);} else {return v;}; } diff --git a/implementations/javascript/packages/schema/src/interpreter.ts b/implementations/javascript/packages/schema/src/interpreter.ts index 7b9a19d..0478267 100644 --- a/implementations/javascript/packages/schema/src/interpreter.ts +++ b/implementations/javascript/packages/schema/src/interpreter.ts @@ -22,7 +22,7 @@ export function validator(env: Environment, p: Pattern): (v: Value) => bool return lookup(refPosition(p), p, env, (p) => walk(p, v), (p) => walk(p, v), - (_mod, _modPath, p) => walk(p, v)); + (_mod, _modPath, p) => walk(p!, v)); case M.$or: for (const pp of p[0]) { if (walk(pp, v)) return true; diff --git a/implementations/javascript/packages/schema/src/meta.ts b/implementations/javascript/packages/schema/src/meta.ts index d8a583c..939e9e8 100644 --- a/implementations/javascript/packages/schema/src/meta.ts +++ b/implementations/javascript/packages/schema/src/meta.ts @@ -21,32 +21,44 @@ export const DOTDOTDOT = Symbol.for('...'); export const EQUALS = Symbol.for('='); export const ORSYM = Symbol.for('/'); -export type SchemaEnvEntry = { - schemaModulePath: ModulePath, - typescriptModulePath: string | null, // null means it's $thisModule in disguise - schema: Schema, -}; +export type SchemaEnvEntry = { schemaModulePath: ModulePath } & ( + ({ + typescriptModulePath: string | null, // null means it's $thisModule in disguise + schema: Schema, + }) | ({ + typescriptModulePath: string, + schema: null, + }) +); export type Environment = Array; +function modsymFor(e: SchemaEnvEntry): string { + return '_i_' + e.schemaModulePath.map(s => s.description!).join('$'); +} + export function lookup(namePos: Position | null, name: Ref, env: Environment, kLocal: (p: Pattern) => R, kBase: (p: Pattern) => R, - kOther: (mod: string, modPath: string, p: Pattern) => R): R + kOther: (mod: string, modPath: string, p: Pattern | null) => R): R { for (const e of env) { if (is(e.schemaModulePath, Ref._.module(name)) || (e.typescriptModulePath === null && Ref._.module(name) === $thisModule)) { - const p = Schema._.details(e.schema).get($definitions).get(Ref._.name(name)); - if (p !== void 0) { - if (e.typescriptModulePath === null) { - return kLocal(p); - } else { - const modsym = '_i_' + e.schemaModulePath.map(s => s.description!).join('$'); - return kOther(modsym, e.typescriptModulePath, p); + 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._.details(e.schema).get($definitions).get(Ref._.name(name)); + if (p !== void 0) { + if (e.typescriptModulePath === null) { + return kLocal(p); + } else { + return kOther(modsymFor(e), e.typescriptModulePath, p); + } } } } diff --git a/implementations/javascript/packages/schema/src/reader.ts b/implementations/javascript/packages/schema/src/reader.ts index 56b8f3e..f139c32 100644 --- a/implementations/javascript/packages/schema/src/reader.ts +++ b/implementations/javascript/packages/schema/src/reader.ts @@ -1,6 +1,7 @@ -import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Value, Position, position, formatPosition, ReaderOptions } from '@preserves/core'; +import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Value, Position, position, ReaderOptions, stringify } from '@preserves/core'; import { Input, NamedPattern, Pattern, Schema } from './meta'; import * as M from './meta'; +import { SchemaSyntaxError } from './error'; const positionTable = new WeakMap(); @@ -35,11 +36,12 @@ function splitBy(items: Array, separator: T): Array> { } function invalidClause(clause: Array): never { - throw new Error(formatPosition(position(clause[0] ?? false)) + preserves`: Invalid Schema clause: ${clause}`); + throw new SchemaSyntaxError(preserves`Invalid Schema clause: ${clause}`, + position(clause[0] ?? false)); } -function invalidPattern(name: symbol, item: Input, pos: Position | null): never { - throw new Error(formatPosition(pos) + preserves`: Invalid pattern in ${name}: ${item}`); +function invalidPattern(name: string, item: Input, pos: Position | null): never { + throw new SchemaSyntaxError(`Invalid pattern in ${name}: ${stringify(item)}`, pos); } export function readSchema(source: string, options?: ReaderOptions): Schema { @@ -50,6 +52,7 @@ export function readSchema(source: string, options?: ReaderOptions): Sche export function parseSchema(toplevelTokens: Array): Schema { const toplevelClauses = splitBy(peel(toplevelTokens) as Array, M.DOT); let version: M.Version | undefined = void 0; + let pointer: M.PointerName = false; let definitions = new Dictionary(); for (const clause of toplevelClauses) { if (!Array.isArray(clause)) { @@ -58,25 +61,35 @@ export function parseSchema(toplevelTokens: Array): Schema { const name = peel(clause[0]); if (typeof name !== 'symbol') invalidClause(clause); if (!M.isValidToken(name.description!)) { - throw new Error(preserves`Invalid definition name: ${name}`); + throw new SchemaSyntaxError(preserves`Invalid definition name: ${name}`, + position(clause[0])); } if (definitions.has(name)) { - throw new Error(preserves`Duplicate definition: ${clause}`); + throw new SchemaSyntaxError(preserves`Duplicate definition: ${clause}`, + position(clause[0])); } definitions.set(name, parseDefinition(name, clause.slice(2))); } else if (clause.length === 2 && is(clause[0], M.$version)) { version = M.asVersion(peel(clause[1])); + } else if (clause.length === 2 && is(clause[0], M.$pointer)) { + const pos = position(clause[1]); + const stx = peel(clause[1]); + const quasiName = 'pointer name specification'; + pointer = M.asPointerName((stx === false) ? stx + : (typeof stx === 'symbol') ? parseRef(quasiName, pos, stx) + : invalidPattern(quasiName, stx, pos)); } else { invalidClause(clause); } } if (version === void 0) { - throw new Error("Schema: missing version declaration."); + throw new SchemaSyntaxError("Schema: missing version declaration.", null); } - return Record(M.$schema, [new Dictionary([ + return M.asSchema(Record(M.$schema, [new Dictionary([ [M.$version, version], + [M.$pointer, pointer], [M.$definitions, definitions], - ])]); + ])])); } function parseDefinition(name: symbol, body: Array): Pattern { @@ -102,10 +115,25 @@ function findName(x: Input): symbol | false { return false; } +function parseRef(name: string, pos: Position | null, item: symbol): Pattern { + const s = item.description; + if (s === void 0) invalidPattern(name, item, pos); + if (s[0] === '=') return Record(M.$lit, [Symbol.for(s.slice(1))]); + const pieces = s.split('.'); + if (pieces.length === 1) { + return recordPosition(Record(M.$ref, [M.$thisModule, item]), pos); + } else { + return recordPosition(Record(M.$ref, [ + pieces.slice(0, pieces.length - 1).map(Symbol.for), + Symbol.for(pieces[pieces.length - 1]) + ]), pos); + } +} + function parseBase(name: symbol, body: Array): Pattern { body = peel(body) as Array; if (body.length !== 1) { - invalidPattern(name, body, body.length > 0 ? position(body[0]) : position(body)); + invalidPattern(stringify(name), body, body.length > 0 ? position(body[0]) : position(body)); } const pos = position(body[0]); @@ -124,23 +152,11 @@ function parseBase(name: symbol, body: Array): Pattern { }; function complain(): never { - invalidPattern(name, item, pos); + invalidPattern(stringify(name), item, pos); } if (typeof item === 'symbol') { - const pos = position(body[0]); - const s = item.description; - if (s === void 0) complain(); - if (s[0] === '=') return Record(M.$lit, [Symbol.for(s.slice(1))]); - const pieces = s.split('.'); - if (pieces.length === 1) { - return recordPosition(Record(M.$ref, [M.$thisModule, item]), pos); - } else { - return recordPosition(Record(M.$ref, [ - pieces.slice(0, pieces.length - 1).map(Symbol.for), - Symbol.for(pieces[pieces.length - 1]) - ]), pos); - } + return parseRef(stringify(name), position(body[0]), item); } else if (Record.isRecord, never>(item)) { const label = item.label; if (Record.isRecord(label)) { diff --git a/schema/schema.prs b/schema/schema.prs index 318187d..0ed31b4 100644 --- a/schema/schema.prs +++ b/schema/schema.prs @@ -4,12 +4,15 @@ version 1 . Schema = . ; version 1 . Version = 1 . +PointerName = Ref / #f. + Pattern = < [ ; special builtins or < symbol> [=Boolean =Float =Double =SignedInteger =String =ByteString =Symbol]>>