Position tracking in Reader; major driver improvements in schema compiler

This commit is contained in:
Tony Garnock-Jones 2021-03-11 14:43:06 +01:00
parent 94f6f9af9d
commit c8f564aea4
12 changed files with 355 additions and 164 deletions

View File

@ -5,12 +5,61 @@ import { DefaultPointer, Value } from "./values";
import { is, isAnnotated, IsPreservesAnnotated } from "./is";
import { stringify } from "./text";
export interface Position {
line?: number;
column?: number;
pos: number;
name?: string;
}
export function newPosition(name?: string): Position {
return { line: 1, column: 0, pos: 0, name };
}
export function updatePosition(p: Position, ch: string): boolean {
p.pos++;
if (p.line === void 0) {
return false;
} else {
let advancedLine = false;
switch (ch) {
case '\t':
p.column = (p.column! + 8) & ~7;
break;
case '\n':
p.column = 0;
p.line++;
advancedLine = true;
break;
case '\r':
p.column = 0;
break;
default:
p.column!++;
break;
}
return advancedLine;
}
}
export function formatPosition(p: Position | null | string): string {
if (p === null) {
return '<unknown>';
} else if (typeof p === 'string') {
return p;
} else {
return `${p.name ?? ''}:${p.line ?? ''}:${p.column ?? ''}:${p.pos}`;
}
}
export class Annotated<T = DefaultPointer> {
readonly annotations: Array<Value<T>>;
readonly pos: Position | null;
readonly item: Value<T>;
constructor(item: Value<T>) {
constructor(item: Value<T>, pos?: Position) {
this.annotations = [];
this.pos = pos ?? null;
this.item = item;
}
@ -59,3 +108,7 @@ export function annotate<T = DefaultPointer>(v0: Value<T>, ...anns: Value<T>[]):
anns.forEach((a) => v.annotations.push(a));
return v;
}
export function position<T = DefaultPointer>(v: Value<T>): Position | null {
return Annotated.isAnnotated<T>(v) ? v.pos : null;
}

View File

@ -1,5 +1,7 @@
// Preserves Binary codec.
import { Position } from "./annotated";
export type ErrorType = 'DecodeError' | 'EncodeError' | 'ShortPacket';
export const ErrorType = Symbol.for('ErrorType');
@ -12,8 +14,15 @@ export abstract class PreservesCodecError {
}
export class DecodeError extends Error {
readonly pos: Position | undefined;
get [ErrorType](): ErrorType { return 'DecodeError' }
constructor(message: string, pos?: Position) {
super(message);
this.pos = pos;
}
static isDecodeError(e: any): e is DecodeError {
return PreservesCodecError.isCodecError(e, 'DecodeError');
}

View File

@ -7,13 +7,14 @@ import { unannotate } from './strip';
import { Bytes, unhexDigit } from './bytes';
import { decode } from './decoder';
import { Record } from './record';
import { annotate, Annotated } from './annotated';
import { Annotated, newPosition, Position, updatePosition } from './annotated';
import { Double, DoubleFloat, Single, SingleFloat } from './float';
import { stringify } from './text';
export interface ReaderOptions<T> {
includeAnnotations?: boolean;
decodePointer?: (v: Value<T>) => T;
name?: string | Position;
}
type IntOrFloat = 'int' | 'float';
@ -22,12 +23,18 @@ type IntContinuation = (kind: IntOrFloat, acc: string) => Numeric;
export class Reader<T> {
buffer: string;
pos: Position;
index: number;
discarded = 0;
options: ReaderOptions<T>;
constructor(buffer: string = '', options: ReaderOptions<T> = {}) {
this.buffer = buffer;
switch (typeof options.name) {
case 'undefined': this.pos = newPosition(); break;
case 'string': this.pos = newPosition(options.name); break;
case 'object': this.pos = { ... options.name }; break;
}
this.index = 0;
this.options = options;
}
@ -46,9 +53,8 @@ export class Reader<T> {
this.index = 0;
}
error(message: string, index = this.index): never {
throw new DecodeError(
`${message} (position ${this.discarded + index})`);
error(message: string, pos: Position): never {
throw new DecodeError(message, { ... pos });
}
atEnd(): boolean {
@ -56,48 +62,58 @@ export class Reader<T> {
}
peek(): string {
if (this.atEnd()) throw new ShortPacket("Short term");
if (this.atEnd()) throw new ShortPacket("Short term", this.pos);
return this.buffer[this.index];
}
advance(): number {
const n = this.index++;
updatePosition(this.pos, this.buffer[n]);
return n;
}
nextchar(): string {
if (this.atEnd()) throw new ShortPacket("Short term");
return this.buffer[this.index++];
if (this.atEnd()) throw new ShortPacket("Short term", this.pos);
return this.buffer[this.advance()];
}
nextcharcode(): number {
if (this.atEnd()) throw new ShortPacket("Short term");
return this.buffer.charCodeAt(this.index++);
if (this.atEnd()) throw new ShortPacket("Short term", this.pos);
return this.buffer.charCodeAt(this.advance());
}
skipws() {
while (true) {
if (this.atEnd()) break;
if (!isSpace(this.peek())) break;
this.index++;
this.advance();
}
}
readCommentLine(): Value<T> {
const startPos = { ... this.pos };
let acc = '';
while (true) {
const c = this.nextchar();
if (c === '\n' || c === '\r') {
return this.wrap(acc);
return this.wrap(acc, startPos);
}
acc = acc + c;
}
}
wrap(v: Value<T>): Value<T> {
if (this.includeAnnotations) {
return annotate(v);
} else {
return v;
wrap(v: Value<T>, pos: Position): Value<T> {
if (this.includeAnnotations && !Annotated.isAnnotated(v)) {
v = new Annotated(v, pos);
}
return v;
}
annotateNextWith(v: Value<T>): Value<T> {
this.skipws();
if (this.atEnd()) {
throw new DecodeError("Trailing annotations and comments are not permitted", this.pos);
}
const u = this.next();
if (this.includeAnnotations) (u as Annotated<T>).annotations.unshift(v);
return u;
@ -113,79 +129,82 @@ export class Reader<T> {
}
next(): Value<T> {
return this.wrap(this._next());
}
_next(): Value<T> {
this.skipws();
const startPos = this.index;
const c = this.nextchar();
switch (c) {
case '-':
return this.readIntpart('-', this.nextchar());
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
return this.readIntpart('', c);
case '"':
return this.readString('"');
case '|':
return Symbol.for(this.readString('|'));
case ';':
return this.annotateNextWith(this.readCommentLine());
case '@':
return this.annotateNextWith(this.next());
case ':':
this.error('Unexpected key/value separator between items', startPos);
case '#': {
const c = this.nextchar();
switch (c) {
case 'f': return false;
case 't': return true;
case '{': return this.seq(new Set<T>(), (v, s) => s.add(v), '}');
case '"': return this.readLiteralBinary();
case 'x':
if (this.nextchar() !== '"') {
this.error('Expected open-quote at start of hex ByteString', startPos);
const startPos = { ... this.pos };
const unwrapped = (() => {
const c = this.nextchar();
switch (c) {
case '-':
return this.readIntpart('-', this.nextchar());
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
return this.readIntpart('', c);
case '"':
return this.readString('"');
case '|':
return Symbol.for(this.readString('|'));
case ';':
return this.annotateNextWith(this.readCommentLine());
case '@':
return this.annotateNextWith(this.next());
case ':':
this.error('Unexpected key/value separator between items', startPos);
case '#': {
const c = this.nextchar();
switch (c) {
case 'f': return false;
case 't': return true;
case '{': return this.seq(new Set<T>(), (v, s) => s.add(v), '}');
case '"': return this.readLiteralBinary();
case 'x':
if (this.nextchar() !== '"') {
this.error('Expected open-quote at start of hex ByteString',
startPos);
}
return this.readHexBinary();
case '[': return this.readBase64Binary();
case '=': {
const bs = unannotate(this.next());
if (!Bytes.isBytes(bs)) this.error('ByteString must follow #=',
startPos);
return decode<T>(bs, {
decodePointer: this.options.decodePointer,
includeAnnotations: this.options.includeAnnotations,
});
}
return this.readHexBinary();
case '[': return this.readBase64Binary();
case '=': {
const bs = unannotate(this.next());
if (!Bytes.isBytes(bs)) this.error('ByteString must follow #=', startPos);
return decode<T>(bs, {
decodePointer: this.options.decodePointer,
includeAnnotations: this.options.includeAnnotations,
});
case '!': {
const d = this.options.decodePointer;
if (d === void 0) {
this.error("No decodePointer function supplied", startPos);
}
return d(this.next());
}
default:
this.error(`Invalid # syntax: ${c}`, startPos);
}
case '!': {
const d = this.options.decodePointer;
if (d === void 0) this.error("No decodePointer function supplied");
return d(this.next());
}
default:
this.error(`Invalid # syntax: ${c}`, startPos);
}
case '<': {
const label = this.next();
const fields = this.readSequence('>');
return Record(label, fields);
}
case '[': return this.readSequence(']');
case '{': return this.readDictionary();
case '>': this.error('Unexpected >', startPos);
case ']': this.error('Unexpected ]', startPos);
case '}': this.error('Unexpected }', startPos);
default:
return this.readRawSymbol(c);
}
case '<': {
const label = this.next();
const fields = this.readSequence('>');
return Record(label, fields);
}
case '[': return this.readSequence(']');
case '{': return this.readDictionary();
case '>': this.error('Unexpected >', startPos);
case ']': this.error('Unexpected ]', startPos);
case '}': this.error('Unexpected }', startPos);
default:
return this.readRawSymbol(c);
}
})();
return this.wrap(unwrapped, startPos);
}
seq<S>(acc: S, update: (v: Value<T>, acc: S) => void, ch: string): S {
while (true) {
this.skipws();
if (this.peek() === ch) {
this.index++;
this.advance();
return acc;
}
update(this.next(), acc);
@ -201,7 +220,7 @@ export class Reader<T> {
while (true) {
this.skipws();
if (this.peek() === '"') {
this.index++;
this.advance();
return Bytes.from(acc);
}
acc.push(this.readHex2());
@ -215,12 +234,12 @@ export class Reader<T> {
switch (this.peek()) {
case ':':
if (acc.has(k)) this.error(
`Duplicate key: ${stringify(k)}`);
this.index++;
`Duplicate key: ${stringify(k)}`, this.pos);
this.advance();
acc.set(k, this.next());
break;
default:
this.error('Missing key/value separator');
this.error('Missing key/value separator', this.pos);
}
},
'}');
@ -245,14 +264,14 @@ export class Reader<T> {
readDigit1(kind: IntOrFloat, acc: string, k: IntContinuation, ch?: string): Numeric {
if (ch === void 0) ch = this.nextchar();
if (ch >= '0' && ch <= '9') return this.readDigit0(kind, acc + ch, k);
this.error('Incomplete number');
this.error('Incomplete number', this.pos);
}
readDigit0(kind: IntOrFloat, acc: string, k: IntContinuation): Numeric {
while (true) {
const ch = this.peek();
if (!(ch >= '0' && ch <= '9')) break;
this.index++;
this.advance();
acc = acc + ch;
}
return k(kind, acc);
@ -260,7 +279,7 @@ export class Reader<T> {
readFracexp(kind: IntOrFloat, acc: string): Numeric {
if (this.peek() === '.') {
this.index++;
this.advance();
return this.readDigit1('float', acc + '.', (kind, acc) => this.readExp(kind, acc));
}
return this.readExp(kind, acc);
@ -269,7 +288,7 @@ export class Reader<T> {
readExp(kind: IntOrFloat, acc: string): Numeric {
const ch = this.peek();
if (ch === 'e' || ch === 'E') {
this.index++;
this.advance();
return this.readSignAndExp(acc + ch);
}
return this.finishNumber(kind, acc);
@ -278,7 +297,7 @@ export class Reader<T> {
readSignAndExp(acc: string): Numeric {
const ch = this.peek();
if (ch === '+' || ch === '-') {
this.index++;
this.advance();
return this.readDigit1('float', acc + ch, (kind, acc) => this.finishNumber(kind, acc));
}
return this.readDigit1('float', acc, (kind, acc) => this.finishNumber(kind, acc));
@ -289,7 +308,7 @@ export class Reader<T> {
if (kind === 'int') return i;
const ch = this.peek();
if (ch === 'f' || ch === 'F') {
this.index++;
this.advance();
return Single(i);
} else {
return Double(i);
@ -301,7 +320,7 @@ export class Reader<T> {
if (this.atEnd()) break;
const ch = this.peek();
if (('(){}[]<>";,@#:|'.indexOf(ch) !== -1) || isSpace(ch)) break;
this.index++;
this.advance();
acc = acc + ch;
}
return Symbol.for(acc);
@ -336,7 +355,7 @@ export class Reader<T> {
case 't': acc.push(xform('\x09')); break;
default:
this.error(`Invalid escape code \\${ch}`);
this.error(`Invalid escape code \\${ch}`, this.pos);
}
break;
}
@ -371,7 +390,7 @@ export class Reader<T> {
return String.fromCharCode(n1, n2);
}
}
this.error('Invalid surrogate pair');
this.error('Invalid surrogate pair', this.pos);
}
return String.fromCharCode(n1);
});
@ -381,7 +400,7 @@ export class Reader<T> {
return this.readStringlike(
x => {
const v = x.charCodeAt(0);
if (v >= 256) this.error(`Invalid code point ${v} in literal binary`);
if (v >= 256) this.error(`Invalid code point ${v} in literal binary`, this.pos);
return v;
},
Bytes.from,

View File

@ -24,14 +24,16 @@ export function strip<T = DefaultPointer>(
if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v.item)) {
return Record(step(v.item.label, depth), v.item.map(walk));
} else if (Annotated.isAnnotated(v.item)) {
throw new Error("Improper annotation structure");
} else if (nextDepth === 0) {
return v.item;
} else if (Array.isArray(v.item)) {
return (v.item as Value<T>[]).map(walk);
} else if (Set.isSet<T>(v.item)) {
return v.item.map(walk);
} else if (Dictionary.isDictionary<Value<T>, T>(v.item)) {
return v.item.mapEntries((e) => [walk(e[0]), walk(e[1])]);
} else if (Annotated.isAnnotated(v.item)) {
throw new Error("Improper annotation structure");
} else {
return v.item;
}

View File

@ -28,8 +28,11 @@
"dependencies": {
"@preserves/core": "^0.8.0",
"@types/glob": "^7.1.3",
"@types/minimatch": "^3.0.3",
"@types/yargs": "^16.0.0",
"chalk": "^4.1.0",
"glob": "^7.1.6",
"minimatch": "^3.0.4",
"yargs": "^16.2.0"
}
}

View File

@ -2,14 +2,34 @@ import { compile, readSchema } from '../index';
import fs from 'fs';
import path from 'path';
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';
export type CommandLineArguments = {
input: string;
base: string | undefined;
output: string | undefined;
core: string;
watch: boolean;
};
export type CompilationResult = {
options: CommandLineArguments,
inputFiles: Array<InputFile>,
failures: Array<[string, Error]>,
base: string,
output: string,
};
export type InputFile = {
inputFilePath: string,
outputFilePath: string,
schemaPath: M.ModulePath,
schema: M.Schema,
};
export function computeBase(paths: string[]): string {
@ -24,53 +44,102 @@ export function computeBase(paths: string[]): string {
for (const p of paths) {
if (i >= p.length) return p.slice(0, i);
if (ch === null) ch = p[i];
if (p[i] !== ch) return p.slice(0, i - 1);
if (p[i] !== ch) return p.slice(0, i);
}
i++;
}
}
}
type ToCompile = {
inputFilePath: string,
outputFilePath: string,
schemaPath: M.ModulePath,
schema: M.Schema,
};
export function run(options: CommandLineArguments): void {
if (!options.watch) {
if (runOnce(options).failures.length !== 0) {
process.exit(1);
}
} else {
function runWatch() {
console.clear();
console.log(chalk.yellow(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 watcher = fs.watch(r.base, { recursive: true }, (_event, filename) => {
filename = path.join(r.base, filename);
if (minimatch(filename, options.input)) {
watcher.close();
runWatch();
}
});
}
runWatch();
}
}
function changeExt(p: string, newext: string) {
function changeExt(p: string, newext: string): string {
return p.slice(0, -path.extname(p).length) + newext;
}
export function run(options: CommandLineArguments) {
glob(options.input, (err, matches) => {
if (err) throw err;
export function modulePathTo(file1: string, file2: string): string | null {
if (file1 === file2) return null;
let naive = path.relative(path.dirname(file1), file2);
if (naive[0] !== '.' && naive[0] !== '/') naive = './' + naive;
return changeExt(naive, '');
}
const base = options.base ?? computeBase(matches);
const output = options.output ?? base;
export function runOnce(options: CommandLineArguments): CompilationResult {
const matches = glob.sync(options.input);
const failures: Array<[string, Error]> = [];
const toCompile: Array<ToCompile> = matches.map(inputFilePath => {
if (!inputFilePath.startsWith(base)) {
throw new Error(`Input filename ${inputFilePath} falls outside base ${base}`);
}
const relPath = inputFilePath.slice(base.length);
const outputFilePath = path.join(output, changeExt(relPath, '.ts'));
const base = options.base ?? computeBase(matches);
const output = options.output ?? base;
const inputFiles: Array<InputFile> = matches.flatMap(inputFilePath => {
if (!inputFilePath.startsWith(base)) {
throw new Error(`Input filename ${inputFilePath} falls outside base ${base}`);
}
const relPath = inputFilePath.slice(base.length);
const outputFilePath = path.join(output, changeExt(relPath, '.ts'));
try {
const src = fs.readFileSync(inputFilePath, 'utf-8');
const schema = readSchema(src);
const schemaPath = relPath.split(path.delimiter).map(p => p.split('.')[0]).map(Symbol.for);
return { inputFilePath, outputFilePath, schemaPath, schema };
});
toCompile.forEach(c => {
const env: M.Environment = toCompile.map(cc => ({
schema: cc.schema,
schemaModulePath: cc.schemaPath,
typescriptModulePath: path.relative(c.outputFilePath, cc.outputFilePath) || null,
}));
fs.mkdirSync(path.dirname(c.outputFilePath), { recursive: true });
fs.writeFileSync(c.outputFilePath, compile(env, c.schema, options.core), 'utf-8');
});
const schema = readSchema(src, { name: inputFilePath });
const schemaPath = relPath.split('/').map(p => p.split('.')[0]).map(Symbol.for);
return [{ inputFilePath, outputFilePath, schemaPath, schema }];
} catch (e) {
failures.push([inputFilePath, e]);
return [];
}
});
inputFiles.forEach(c => {
const env: M.Environment = 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');
} catch (e) {
failures.push([c.inputFilePath, 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);
}
}
if (failures.length > 0) {
console.log();
}
return { options, inputFiles, failures, base, output };
}
export function main(argv: Array<string>) {
@ -95,8 +164,14 @@ export function main(argv: Array<string>) {
type: 'string',
description: 'Import path for @preserves/core',
default: '@preserves/core',
})
.option('watch', {
type: 'boolean',
descripion: 'Watch base directory for changes',
default: false,
}),
argv => argv)
.argv;
options.input = path.normalize(options.input);
run(options);
}

View File

@ -2,6 +2,7 @@ import { Pattern, NamedPattern, Schema, Input, Environment, Ref, lookup } from "
import * as M from './meta';
import { Annotated, Bytes, Dictionary, Fold, fold, KeyedSet, 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";
function fnblock(... items: Item[]): Item {
return seq('((() => ', block(... items), ')())');
@ -15,7 +16,7 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr
let temps: Array<string> = [];
function applyPredicate(name: Ref, v: string): Item {
return lookup(name, env,
return lookup(refPosition(name), name, env,
(_p) => `is${Ref._.name(name).description!}(${v})`,
(p) => walk(v, p),
(mod, modPath, _p) => {
@ -57,7 +58,7 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr
case M.$lit:
return `(typeof ${literal(p[0])})`;
case M.$ref:
return lookup(p, env,
return lookup(refPosition(p), p, env,
(_p) => p[1].description!,
(p) => typeFor(p),
(mod, modPath,_p) => {
@ -112,15 +113,7 @@ export function compile(env: Environment, schema: Schema, preservesModule = '@pr
case M.$Symbol: return `typeof ${v} === 'symbol'`;
}
case M.$lit:
switch (typeof p[0]) {
case 'boolean':
case 'number':
case 'string':
case 'symbol':
return `${v} === ${literal(p[0])}`;
default:
return `_.is(${v}, ${literal(p[0])})`;
}
return `_.is(${v}, ${literal(p[0])})`;
case M.$ref:
return applyPredicate(p, v);
case M.$or:
@ -219,13 +212,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("${name.description!}")`),
block(`throw new TypeError("Invalid ${name.description!}")`),
' else ',
block(`return v`)))));
}
const f = new Formatter();
f.write(`import * as _ from ${JSON.stringify(preservesModule)};\n`);
imports.forEach(([identifier, path]) => {
f.write(`import * as ${identifier} from ${JSON.stringify(path)};\n`);
});
f.newline();
const sortedLiterals = Array.from(literals);

View File

@ -0,0 +1,10 @@
import { Position, formatPosition } from '@preserves/core';
export class SchemaSyntaxError extends Error {
readonly pos: Position | null;
constructor(message: string, pos: Position | null) {
super(message);
this.pos = pos;
}
}

View File

@ -1,6 +1,7 @@
import { Environment, Pattern, NamedPattern, lookup } from "./meta";
import * as M from './meta';
import { Value, Float, Bytes, is, isPointer, Record, Dictionary, Set } from "@preserves/core";
import { refPosition } from "./reader";
export function validator(env: Environment, p: Pattern): (v: Value<any>) => boolean {
function walk(p: Pattern, v: Value<any>, recordOkAsTuple = false): boolean {
@ -18,7 +19,7 @@ export function validator(env: Environment, p: Pattern): (v: Value<any>) => bool
case M.$lit:
return is(v, p[0]);
case M.$ref:
return lookup(p, env,
return lookup(refPosition(p), p, env,
(p) => walk(p, v),
(p) => walk(p, v),
(_mod, _modPath, p) => walk(p, v));

View File

@ -1,6 +1,7 @@
import { Value, preserves, is } from '@preserves/core';
import { Value, is, Position } from '@preserves/core';
import { ModulePath, Ref, Schema, Pattern, $thisModule, $definitions } from './gen/schema';
import { BASE } from './base';
import { SchemaSyntaxError } from './error';
export * from './gen/schema';
@ -28,7 +29,8 @@ export type SchemaEnvEntry = {
export type Environment = Array<SchemaEnvEntry>;
export function lookup<R>(name: Ref,
export function lookup<R>(namePos: Position | null,
name: Ref,
env: Environment,
kLocal: (p: Pattern) => R,
kBase: (p: Pattern) => R,
@ -55,5 +57,9 @@ export function lookup<R>(name: Ref,
if (p !== void 0) return kBase(p);
}
throw new Error(preserves`Undefined reference: ${name}`);
throw new SchemaSyntaxError(`Undefined reference: ${formatRef(name)}`, namePos);
}
export function formatRef(r: Ref): string {
return [... r[0] === $thisModule ? [] : r[0], r[1]].map(s => s.description!).join('.');
}

View File

@ -1,7 +1,19 @@
import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Value } from '@preserves/core';
import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Value, Position, position, formatPosition, ReaderOptions } from '@preserves/core';
import { Input, NamedPattern, Pattern, Schema } from './meta';
import * as M from './meta';
const positionTable = new WeakMap<Input & object, Position>();
export function recordPosition<X extends Input & object>(v: X, pos: Position | null): X {
if (pos === null) { console.error('Internal error in Schema reader: null source position for', v); }
if (pos !== null) positionTable.set(v, pos);
return v;
}
export function refPosition(v: Input & object): Position | null {
return positionTable.get(v) ?? null;
}
function splitBy<T>(items: Array<T>, separator: T): Array<Array<T>> {
const groups: Array<Array<T>> = [];
let group: Array<T> = [];
@ -22,16 +34,16 @@ function splitBy<T>(items: Array<T>, separator: T): Array<Array<T>> {
return groups;
}
function invalidClause(clause: Input): never {
throw new Error(preserves`Invalid Schema clause: ${clause}`);
function invalidClause(clause: Array<Input>): never {
throw new Error(formatPosition(position(clause[0] ?? false)) + preserves`: Invalid Schema clause: ${clause}`);
}
function invalidPattern(name: symbol, item: Input): never {
throw new Error(preserves`Invalid pattern in ${name}: ${item}`);
function invalidPattern(name: symbol, item: Input, pos: Position | null): never {
throw new Error(formatPosition(pos) + preserves`: Invalid pattern in ${name}: ${item}`);
}
export function readSchema(source: string): Schema {
const toplevelTokens = new Reader<never>(source, { includeAnnotations: true }).readToEnd();
export function readSchema(source: string, options?: ReaderOptions<never>): Schema {
const toplevelTokens = new Reader<never>(source, { ... options ?? {}, includeAnnotations: true }).readToEnd();
return parseSchema(toplevelTokens);
}
@ -39,22 +51,21 @@ export function parseSchema(toplevelTokens: Array<Input>): Schema {
const toplevelClauses = splitBy(peel(toplevelTokens) as Array<Input>, M.DOT);
let version: M.Version | undefined = void 0;
let definitions = new Dictionary<Pattern, never>();
for (const clause0 of toplevelClauses) {
const clause = clause0.map(peel);
for (const clause of toplevelClauses) {
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];
const name = peel(clause[0]);
if (typeof name !== 'symbol') invalidClause(clause);
if (!M.isValidToken(name.description!)) {
throw new Error(preserves`Invalid definition name: ${name}`);
}
if (definitions.has(name)) {
throw new Error(preserves`Duplicate definition: ${clause}`);
}
definitions.set(name, parseDefinition(name, clause.slice(2).map(peel)));
definitions.set(name, parseDefinition(name, clause.slice(2)));
} else if (clause.length === 2 && is(clause[0], M.$version)) {
version = M.asVersion(clause[1]);
version = M.asVersion(peel(clause[1]));
} else {
invalidClause(clause);
}
@ -93,8 +104,13 @@ function findName(x: Input): symbol | false {
function parseBase(name: symbol, body: Array<Input>): Pattern {
body = peel(body) as Array<Input>;
if (body.length !== 1) invalidPattern(name, body);
if (body.length !== 1) {
invalidPattern(name, body, body.length > 0 ? position(body[0]) : position(body));
}
const pos = position(body[0]);
const item = peel(body[0]);
const walk = (b: Input): Pattern => parseBase(name, [b]);
const namedwalk = (b: Input): NamedPattern => {
const name = findName(b);
@ -108,21 +124,22 @@ function parseBase(name: symbol, body: Array<Input>): Pattern {
};
function complain(): never {
invalidPattern(name, item);
invalidPattern(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 Record(M.$ref, [M.$thisModule, item]);
return recordPosition(Record(M.$ref, [M.$thisModule, item]), pos);
} else {
return Record(M.$ref, [
return recordPosition(Record(M.$ref, [
pieces.slice(0, pieces.length - 1).map(Symbol.for),
Symbol.for(pieces[pieces.length - 1])
]);
]), pos);
}
} else if (Record.isRecord<Input, Tuple<Input>, never>(item)) {
const label = item.label;

View File

@ -575,7 +575,7 @@
jest-diff "^26.0.0"
pretty-format "^26.0.0"
"@types/minimatch@*":
"@types/minimatch@*", "@types/minimatch@^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
@ -1009,7 +1009,7 @@ chalk@^2.0.0:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0.0:
chalk@^4.0.0, chalk@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a"
integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==