2021-03-22 11:13:34 +00:00
|
|
|
import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Position, position, ReaderOptions, stringify, isCompound, KeyedDictionary } from '@preserves/core';
|
2021-03-17 19:01:26 +00:00
|
|
|
import { Input, Pattern, Schema, Alternative, Definition, CompoundPattern, SimplePattern } from './meta';
|
2021-03-09 14:59:40 +00:00
|
|
|
import * as M from './meta';
|
2021-03-11 16:59:40 +00:00
|
|
|
import { SchemaSyntaxError } from './error';
|
2021-03-09 14:59:40 +00:00
|
|
|
|
2021-03-22 11:13:34 +00:00
|
|
|
const positionTable = new WeakMap<object, Position>();
|
2021-03-11 13:43:06 +00:00
|
|
|
|
2021-03-22 11:13:34 +00:00
|
|
|
export function recordPosition<X extends object>(v: X, pos: Position | null): X {
|
2021-03-11 13:43:06 +00:00
|
|
|
if (pos === null) { console.error('Internal error in Schema reader: null source position for', v); }
|
|
|
|
if (pos !== null) positionTable.set(v, pos);
|
|
|
|
return v;
|
|
|
|
}
|
|
|
|
|
2021-03-22 11:13:34 +00:00
|
|
|
export function refPosition(v: object): Position | null {
|
2021-03-11 13:43:06 +00:00
|
|
|
return positionTable.get(v) ?? null;
|
|
|
|
}
|
|
|
|
|
2021-03-09 14:59:40 +00:00
|
|
|
function splitBy<T>(items: Array<T>, separator: T): Array<Array<T>> {
|
|
|
|
const groups: Array<Array<T>> = [];
|
|
|
|
let group: Array<T> = [];
|
|
|
|
function finish() {
|
|
|
|
if (group.length > 0) {
|
|
|
|
groups.push(group);
|
|
|
|
group = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const item of items) {
|
|
|
|
if (is(item, separator)) {
|
|
|
|
finish();
|
|
|
|
} else {
|
|
|
|
group.push(item);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
finish();
|
|
|
|
return groups;
|
|
|
|
}
|
|
|
|
|
2021-03-11 13:43:06 +00:00
|
|
|
function invalidClause(clause: Array<Input>): never {
|
2021-03-11 16:59:40 +00:00
|
|
|
throw new SchemaSyntaxError(preserves`Invalid Schema clause: ${clause}`,
|
|
|
|
position(clause[0] ?? false));
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 16:59:40 +00:00
|
|
|
function invalidPattern(name: string, item: Input, pos: Position | null): never {
|
|
|
|
throw new SchemaSyntaxError(`Invalid pattern in ${name}: ${stringify(item)}`, pos);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 22:02:18 +00:00
|
|
|
export type SchemaReaderOptions = {
|
|
|
|
readInclude?(includePath: string): string;
|
|
|
|
};
|
|
|
|
|
|
|
|
function _readSchema(source: string, options?: ReaderOptions<never>): Array<Input> {
|
|
|
|
return new Reader<never>(source, {
|
|
|
|
... options ?? {},
|
|
|
|
includeAnnotations: true
|
|
|
|
}).readToEnd();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function readSchema(source: string,
|
|
|
|
options?: ReaderOptions<never> & SchemaReaderOptions): Schema
|
|
|
|
{
|
|
|
|
return parseSchema(_readSchema(source, options), options ?? {});
|
2021-03-09 15:45:57 +00:00
|
|
|
}
|
|
|
|
|
2021-03-11 22:02:18 +00:00
|
|
|
export function parseSchema(toplevelTokens: Array<Input>,
|
|
|
|
options: ReaderOptions<never> & SchemaReaderOptions): Schema
|
|
|
|
{
|
2021-03-10 22:15:53 +00:00
|
|
|
let version: M.Version | undefined = void 0;
|
2021-03-22 11:13:34 +00:00
|
|
|
let pointer: M.PointerName = M.PointerName.$false();
|
|
|
|
let definitions = new KeyedDictionary<symbol, Definition, M._ptr>();
|
2021-03-11 22:02:18 +00:00
|
|
|
|
|
|
|
function process(toplevelTokens: Array<Input>): void {
|
|
|
|
const toplevelClauses = splitBy(peel(toplevelTokens) as Array<Input>, M.DOT);
|
|
|
|
for (const clause of toplevelClauses) {
|
|
|
|
if (!Array.isArray(clause)) {
|
|
|
|
invalidClause(clause);
|
|
|
|
} else if (clause.length >= 2 && is(clause[1], M.EQUALS)) {
|
|
|
|
const pos = position(clause[0]);
|
|
|
|
const name = peel(clause[0]);
|
|
|
|
if (typeof name !== 'symbol') invalidClause(clause);
|
|
|
|
if (!M.isValidToken(name.description!)) {
|
|
|
|
throw new SchemaSyntaxError(preserves`Invalid definition name: ${name}`, pos);
|
|
|
|
}
|
|
|
|
if (definitions.has(name)) {
|
|
|
|
throw new SchemaSyntaxError(preserves`Duplicate definition: ${clause}`, pos);
|
|
|
|
}
|
|
|
|
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]);
|
2021-03-22 11:13:34 +00:00
|
|
|
if (stx === false) {
|
|
|
|
pointer = M.PointerName.$false();
|
|
|
|
} else if (typeof stx === 'symbol') {
|
|
|
|
pointer = M.PointerName.Ref(parseRef(stx.description!, pos));
|
|
|
|
} else {
|
|
|
|
invalidPattern('pointer name specification', stx, pos);
|
|
|
|
}
|
2021-03-11 22:02:18 +00:00
|
|
|
} else if (clause.length === 2 && is(clause[0], M.INCLUDE)) {
|
|
|
|
const pos = position(clause[1]);
|
|
|
|
const path = peel(clause[1]);
|
|
|
|
if (typeof path !== 'string') {
|
|
|
|
throw new SchemaSyntaxError(preserves`Invalid include: ${clause}`, pos);
|
|
|
|
}
|
|
|
|
if (options.readInclude === void 0) {
|
|
|
|
throw new SchemaSyntaxError(preserves`Cannot include files in schema`, pos);
|
|
|
|
}
|
|
|
|
process(_readSchema(options.readInclude(path), options));
|
|
|
|
} else {
|
|
|
|
invalidClause(clause);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-11 22:02:18 +00:00
|
|
|
|
|
|
|
process(toplevelTokens);
|
|
|
|
|
2021-03-09 14:59:40 +00:00
|
|
|
if (version === void 0) {
|
2021-03-11 16:59:40 +00:00
|
|
|
throw new SchemaSyntaxError("Schema: missing version declaration.", null);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
2021-03-11 22:02:18 +00:00
|
|
|
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.Schema(M.Version(), pointer, definitions);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-03-19 22:42:43 +00:00
|
|
|
function namedMustBeSimple(p: Position | null): never {
|
|
|
|
throw new SchemaSyntaxError('Named patterns must be Simple patterns', p);
|
|
|
|
}
|
|
|
|
|
2021-03-17 11:20:50 +00:00
|
|
|
function parseDefinition(name: symbol, body: Array<Input>): Definition {
|
2021-03-17 13:36:44 +00:00
|
|
|
let nextAnonymousAlternativeNumber = 0;
|
2021-03-17 11:20:50 +00:00
|
|
|
function alternativeName([input, p]: readonly [Array<Input>, Alternative])
|
2021-03-22 11:13:34 +00:00
|
|
|
: M.NamedAlternative
|
2021-03-17 11:20:50 +00:00
|
|
|
{
|
|
|
|
const n = findName(input) || findName(input[0]);
|
|
|
|
if (n !== false) {
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.NamedAlternative(n.description!, p);
|
2021-03-17 11:20:50 +00:00
|
|
|
}
|
2021-03-22 11:13:34 +00:00
|
|
|
if (p._variant === 'Pattern' &&
|
|
|
|
p.value._variant === 'CompoundPattern' &&
|
|
|
|
p.value.value._variant === 'rec' &&
|
|
|
|
p.value.value.label._variant === 'anonymous' &&
|
|
|
|
p.value.value.label.value._variant === 'SimplePattern' &&
|
|
|
|
p.value.value.label.value.value._variant === 'lit' &&
|
|
|
|
typeof p.value.value.label.value.value.value === 'symbol')
|
|
|
|
{
|
|
|
|
return M.NamedAlternative(p.value.value.label.value.value.value.description!, p);
|
2021-03-17 11:20:50 +00:00
|
|
|
}
|
2021-03-22 11:13:34 +00:00
|
|
|
if (p._variant === 'Pattern' &&
|
|
|
|
p.value._variant === 'SimplePattern' &&
|
|
|
|
p.value.value._variant === 'Ref')
|
|
|
|
{
|
|
|
|
return M.NamedAlternative(p.value.value.value.name.description!, p);
|
2021-03-17 11:20:50 +00:00
|
|
|
}
|
2021-03-22 11:13:34 +00:00
|
|
|
if (p._variant === 'Pattern' &&
|
|
|
|
p.value._variant === 'SimplePattern' &&
|
|
|
|
p.value.value._variant === 'lit')
|
|
|
|
{
|
|
|
|
const s = M.namelike(p.value.value.value);
|
|
|
|
if (s !== void 0) return M.NamedAlternative(s, p);
|
2021-03-17 11:20:50 +00:00
|
|
|
}
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.NamedAlternative('_anonymous' + nextAnonymousAlternativeNumber++, p);
|
2021-03-17 11:20:50 +00:00
|
|
|
}
|
|
|
|
|
2021-03-19 22:42:43 +00:00
|
|
|
function patternName([input, p]: readonly [Array<Input>, Pattern]) : M.NamedPattern {
|
|
|
|
const n = findName(input) || findName(input[0]);
|
|
|
|
if (n !== false) {
|
2021-03-22 11:13:34 +00:00
|
|
|
if (p._variant !== 'SimplePattern') namedMustBeSimple(position(input[0]));
|
|
|
|
return M.NamedPattern.named(M.NamedSimplePattern__(n, p.value));
|
2021-03-19 22:42:43 +00:00
|
|
|
}
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.NamedPattern.anonymous(p);
|
2021-03-19 22:42:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: deal with situation where there's an or of ands, where
|
|
|
|
// the branches of the and arenamed. The parsing is ambiguous, and
|
|
|
|
// with the current code I think (?) you end up with the same name
|
|
|
|
// attached to the or-branch as to the leftmost and-branch.
|
|
|
|
|
2021-03-17 11:20:50 +00:00
|
|
|
return parseOp(body,
|
|
|
|
M.ORSYM,
|
|
|
|
p => [p, parseOp(p,
|
|
|
|
M.ANDSYM,
|
2021-03-19 22:42:43 +00:00
|
|
|
p => [p, parsePattern(name, p)] as const,
|
2021-03-22 11:13:34 +00:00
|
|
|
ps => M.Alternative.and(ps.map(patternName)),
|
|
|
|
p => M.Alternative.Pattern(p[1]))] as const,
|
|
|
|
ps => M.Definition.or(ps.map(alternativeName)),
|
|
|
|
p => M.Definition.Alternative(p[1]));
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
2021-03-17 13:36:44 +00:00
|
|
|
function parsePattern(name: symbol, body0: Array<Input>): Pattern {
|
2021-03-22 11:13:34 +00:00
|
|
|
function parseSimple<A>(item0: Input, ks: (p: SimplePattern) => A, kf: () => A): A {
|
2021-03-17 13:36:44 +00:00
|
|
|
const pos = position(item0);
|
|
|
|
const item = peel(item0);
|
|
|
|
function complain(): never { invalidPattern(stringify(name), item, pos); }
|
|
|
|
if (typeof item === 'symbol') {
|
2021-03-22 11:13:34 +00:00
|
|
|
const str = item.description!;
|
|
|
|
switch (str) {
|
|
|
|
case 'any': return ks(M.SimplePattern.any());
|
|
|
|
case 'bool': return ks(M.SimplePattern.atom(M.AtomKind.Boolean()));
|
|
|
|
case 'float': return ks(M.SimplePattern.atom(M.AtomKind.Float()));
|
|
|
|
case 'double': return ks(M.SimplePattern.atom(M.AtomKind.Double()));
|
|
|
|
case 'int': return ks(M.SimplePattern.atom(M.AtomKind.SignedInteger()));
|
|
|
|
case 'string': return ks(M.SimplePattern.atom(M.AtomKind.String()));
|
|
|
|
case 'bytes': return ks(M.SimplePattern.atom(M.AtomKind.ByteString()));
|
|
|
|
case 'symbol': return ks(M.SimplePattern.atom(M.AtomKind.Symbol()));
|
|
|
|
case 'ref': return ks(M.SimplePattern.pointer());
|
|
|
|
default: return ks((str[0] === '=')
|
|
|
|
? M.SimplePattern.lit(Symbol.for(str.slice(1)))
|
|
|
|
: M.SimplePattern.Ref(parseRef(str, pos)));
|
2021-03-19 22:42:43 +00:00
|
|
|
}
|
2021-03-17 13:36:44 +00:00
|
|
|
} else if (Record.isRecord<Input, Tuple<Input>, never>(item)) {
|
|
|
|
const label = item.label;
|
|
|
|
if (Record.isRecord<Input, [], never>(label)) {
|
|
|
|
if (label.length !== 0) complain();
|
|
|
|
switch (label.label) {
|
|
|
|
case M.$lit:
|
|
|
|
if (item.length !== 1) complain();
|
2021-03-22 11:13:34 +00:00
|
|
|
return ks(M.SimplePattern.lit(item[0]));
|
2021-03-17 13:36:44 +00:00
|
|
|
default:
|
|
|
|
return kf();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return kf();
|
|
|
|
}
|
|
|
|
} else if (isCompound(item)) {
|
|
|
|
return kf();
|
|
|
|
} else {
|
2021-03-22 11:13:34 +00:00
|
|
|
return ks(M.SimplePattern.lit(strip(item)));
|
2021-03-17 13:36:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseCompound(item0: Input): CompoundPattern {
|
|
|
|
const pos = position(item0);
|
|
|
|
const item = peel(item0);
|
|
|
|
function complain(): never { invalidPattern(stringify(name), item, pos); }
|
|
|
|
|
2021-03-22 11:13:34 +00:00
|
|
|
const walkSimple = (b: Input): SimplePattern => parseSimple(b, p => p, () => {
|
2021-03-17 13:36:44 +00:00
|
|
|
throw new SchemaSyntaxError(`Compound patterns not accepted here`, position(b));
|
|
|
|
});
|
|
|
|
const walk = (b: Input): Pattern => parsePattern(name, [b]);
|
|
|
|
|
2021-03-22 11:13:34 +00:00
|
|
|
function _maybeNamed<R,P>(
|
|
|
|
named: (p: M.NamedSimplePattern_) => R,
|
|
|
|
anonymous: (p: P) => R,
|
|
|
|
recur: (b: Input) => P,
|
|
|
|
literalName?: Input): (b: Input) => R
|
2021-03-18 10:15:10 +00:00
|
|
|
{
|
2021-03-17 18:17:19 +00:00
|
|
|
return (b: Input) => {
|
2021-03-18 10:15:10 +00:00
|
|
|
let name = findName(b);
|
|
|
|
if (name === false) {
|
|
|
|
if (literalName !== void 0 && typeof literalName === 'symbol') {
|
|
|
|
name = literalName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (name === false) {
|
2021-03-22 11:13:34 +00:00
|
|
|
return anonymous(recur(b));
|
2021-03-18 10:15:10 +00:00
|
|
|
}
|
2021-03-22 11:13:34 +00:00
|
|
|
return named(M.NamedSimplePattern__(name, parseSimple(b, p => p, () =>
|
|
|
|
namedMustBeSimple(position(b)))));
|
2021-03-17 18:17:19 +00:00
|
|
|
};
|
2021-03-17 13:36:44 +00:00
|
|
|
}
|
2021-03-22 11:13:34 +00:00
|
|
|
const maybeNamed = _maybeNamed(M.NamedPattern.named, M.NamedPattern.anonymous, walk);
|
|
|
|
const maybeNamedSimple =
|
|
|
|
_maybeNamed(M.NamedSimplePattern.named, M.NamedSimplePattern.anonymous, walkSimple);
|
2021-03-17 13:36:44 +00:00
|
|
|
|
|
|
|
if (Record.isRecord<Input, Tuple<Input>, never>(item)) {
|
|
|
|
const label = item.label;
|
|
|
|
if (Record.isRecord<Input, [], never>(label)) {
|
|
|
|
if (label.length !== 0) complain();
|
|
|
|
switch (label.label) {
|
|
|
|
case M.$rec:
|
|
|
|
if (item.length !== 2) complain();
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.CompoundPattern.rec(maybeNamed(item[0]), maybeNamed(item[1]));
|
2021-03-17 13:36:44 +00:00
|
|
|
default:
|
|
|
|
complain();
|
|
|
|
}
|
|
|
|
} else {
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.CompoundPattern.rec(
|
|
|
|
M.NamedPattern.anonymous(M.Pattern.SimplePattern(M.SimplePattern.lit(label))),
|
|
|
|
M.NamedPattern.anonymous(M.Pattern.CompoundPattern(
|
|
|
|
M.CompoundPattern.tuple(item.map(maybeNamed)))));
|
2021-03-17 13:36:44 +00:00
|
|
|
}
|
|
|
|
} else if (Array.isArray(item)) {
|
|
|
|
if (is(item[item.length - 1], M.DOTDOTDOT)) {
|
|
|
|
if (item.length < 2) complain();
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.CompoundPattern.tuple_STAR_(
|
2021-03-17 13:36:44 +00:00
|
|
|
item.slice(0, item.length - 2).map(maybeNamed),
|
2021-03-22 11:13:34 +00:00
|
|
|
maybeNamedSimple(item[item.length - 2]));
|
2021-03-17 13:36:44 +00:00
|
|
|
} else {
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.CompoundPattern.tuple(item.map(maybeNamed));
|
2021-03-17 13:36:44 +00:00
|
|
|
}
|
|
|
|
} else if (Dictionary.isDictionary<never, Input>(item)) {
|
|
|
|
if (item.size === 2 && item.has(M.DOTDOTDOT)) {
|
|
|
|
const v = item.clone();
|
|
|
|
v.delete(M.DOTDOTDOT);
|
|
|
|
const [[kp, vp]] = v.entries();
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.CompoundPattern.dictof(walkSimple(kp), walkSimple(vp));
|
2021-03-17 13:36:44 +00:00
|
|
|
} else {
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.CompoundPattern.dict(
|
|
|
|
M.DictionaryEntries(item.mapEntries<M.NamedSimplePattern, Input, M._ptr>(
|
|
|
|
([k, vp]) => [
|
|
|
|
strip(k),
|
|
|
|
_maybeNamed(
|
|
|
|
M.NamedSimplePattern.named,
|
|
|
|
M.NamedSimplePattern.anonymous,
|
|
|
|
walkSimple,
|
|
|
|
k)(vp)
|
|
|
|
])));
|
2021-03-17 13:36:44 +00:00
|
|
|
}
|
|
|
|
} else if (Set.isSet<never>(item)) {
|
|
|
|
if (item.size !== 1) complain();
|
|
|
|
const [vp] = item.entries();
|
2021-03-22 11:13:34 +00:00
|
|
|
return M.CompoundPattern.setof(walkSimple(vp));
|
2021-03-17 13:36:44 +00:00
|
|
|
} else {
|
|
|
|
complain();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const body = peel(body0) as Array<Input>;
|
|
|
|
if (body.length !== 1) {
|
|
|
|
invalidPattern(stringify(name), body, body.length > 0 ? position(body[0]) : position(body));
|
|
|
|
}
|
2021-03-22 11:13:34 +00:00
|
|
|
return parseSimple(body[0],
|
|
|
|
M.Pattern.SimplePattern,
|
|
|
|
() => M.Pattern.CompoundPattern(parseCompound(body[0])));
|
2021-03-17 13:36:44 +00:00
|
|
|
}
|
|
|
|
|
2021-03-17 11:20:50 +00:00
|
|
|
function parseOp<Each, Combined>(body: Array<Input>,
|
|
|
|
op: Input,
|
|
|
|
each: (p: Array<Input>) => Each,
|
|
|
|
combineN: (ps: Array<Each>) => Combined,
|
|
|
|
finish1: (p: Each) => Combined): Combined
|
|
|
|
{
|
|
|
|
const pieces = splitBy(body, op).map(each);
|
|
|
|
return (pieces.length === 1) ? finish1(pieces[0]) : combineN(pieces);
|
2021-03-09 14:59:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function findName(x: Input): symbol | false {
|
|
|
|
if (!Annotated.isAnnotated<never>(x)) return false;
|
|
|
|
for (const a0 of x.annotations) {
|
|
|
|
const a = peel(a0);
|
|
|
|
if (typeof a === 'symbol') return a;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-03-22 11:13:34 +00:00
|
|
|
function parseRef(s: string, pos: Position | null): M.Ref {
|
2021-03-11 16:59:40 +00:00
|
|
|
const pieces = s.split('.');
|
2021-03-22 11:13:34 +00:00
|
|
|
return recordPosition(M.Ref(
|
|
|
|
M.ModulePath(pieces.slice(0, pieces.length - 1).map(Symbol.for)),
|
|
|
|
Symbol.for(pieces[pieces.length - 1])), pos);
|
2021-03-11 16:59:40 +00:00
|
|
|
}
|