Position tracking in Reader; major driver improvements in schema compiler
This commit is contained in:
parent
94f6f9af9d
commit
c8f564aea4
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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('.');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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==
|
||||
|
|
Loading…
Reference in New Issue