preserves/implementations/javascript/packages/schema/src/reader.ts

369 lines
15 KiB
TypeScript

import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Position, position, ReaderOptions, stringify, isCompound, KeyedDictionary, annotate, annotations } from '@preserves/core';
import { Input, Pattern, Schema, Definition, CompoundPattern, SimplePattern } from './meta';
import * as M from './meta';
import { SchemaSyntaxError } from './error';
import { checkSchema } from './checker';
const positionTable = new WeakMap<object, Position>();
export function recordPosition<X extends 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: 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> = [];
function finish() {
if (group.length > 0) {
groups.push(group);
group = [];
}
}
for (const item of items) {
if (is(item, separator)) {
finish();
} else {
group.push(item);
}
}
finish();
return groups;
}
function invalidClause(clause: Array<Input>): never {
throw new SchemaSyntaxError(preserves`Invalid Schema clause: ${clause}`,
position(clause[0] ?? false));
}
function invalidPattern(name: string, item: Input, pos: Position | null): never {
throw new SchemaSyntaxError(`Invalid pattern in ${name}: ${stringify(item)}`, pos);
}
export type SchemaReaderOptions = {
readInclude?(includePath: string): string;
};
function _readSchema(source: string, options?: ReaderOptions<never>): Array<Input> {
return new Reader<any>(source, {
... options ?? {},
includeAnnotations: true
}).readToEnd();
}
export function readSchema(source: string,
options?: ReaderOptions<never> & SchemaReaderOptions): Schema
{
return checkSchema(parseSchema(_readSchema(source, options), options ?? {}));
}
export function parseSchema(toplevelTokens: Array<Input>,
options: ReaderOptions<never> & SchemaReaderOptions): Schema
{
let version: M.Version | undefined = void 0;
let embeddedType: M.EmbeddedTypeName = M.EmbeddedTypeName.$false();
let definitions = new KeyedDictionary<symbol, Definition, M._embedded>();
function process(toplevelTokens: Array<Input>): void {
const toplevelClauses = splitBy(peel(toplevelTokens) as Array<Input>, M.DOT);
for (const clause of toplevelClauses) {
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, pos, 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.$embeddedType)) {
const pos = position(clause[1]);
const stx = peel(clause[1]);
if (stx === false) {
embeddedType = M.EmbeddedTypeName.$false();
} else if (typeof stx === 'symbol') {
embeddedType = M.EmbeddedTypeName.Ref(parseRef(stx.description!, pos));
} else {
invalidPattern('embedded type name specification', stx, pos);
}
} 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);
}
}
}
process(toplevelTokens);
if (version === void 0) {
throw new SchemaSyntaxError("Schema: missing version declaration.", null);
}
return M.Schema({ version: M.Version(), embeddedType, definitions });
}
function namedMustBeSimple(p: Position | null): never {
throw new SchemaSyntaxError('Named patterns must be Simple patterns', p);
}
function parseDefinition(name: symbol, pos: Position | null, body: Array<Input>): Definition {
function alternativeName(input: Array<Input>): M.NamedAlternative
{
const n = findName(input) || findName(input[0]);
const p = parsePattern(name, input);
if (n !== false) {
return M.NamedAlternative({ variantLabel: n.description!, pattern: p });
}
if (p._variant === 'CompoundPattern' &&
p.value._variant === 'rec' &&
p.value.label._variant === 'anonymous' &&
p.value.label.value._variant === 'SimplePattern' &&
p.value.label.value.value._variant === 'lit' &&
typeof p.value.label.value.value.value === 'symbol')
{
return M.NamedAlternative({
variantLabel: p.value.label.value.value.value.description!,
pattern: p
});
}
if (p._variant === 'SimplePattern' &&
p.value._variant === 'Ref')
{
return M.NamedAlternative({
variantLabel: p.value.value.name.description!,
pattern: p
});
}
if (p._variant === 'SimplePattern' &&
p.value._variant === 'lit')
{
const s = M.namelike(p.value.value);
if (s !== void 0) return M.NamedAlternative({ variantLabel: s, pattern: p });
}
throw new SchemaSyntaxError(preserves`Name missing for alternative: ${input}`, pos);
}
function patternName(input: Array<Input>): M.NamedPattern {
const n = findName(input) || findName(input[0]);
const p = parsePattern(name, input);
if (n !== false) {
if (p._variant !== 'SimplePattern') namedMustBeSimple(position(input[0]));
return M.NamedPattern.named(M.NamedSimplePattern_({ name: n, pattern: p.value }));
}
return M.NamedPattern.anonymous(p);
}
const andPieces = splitBy(body, M.ANDSYM);
const orPieces = splitBy(body, M.ORSYM);
if (andPieces.length === 0 || orPieces.length === 0) {
throw new SchemaSyntaxError(preserves`Invalid Schema clause: ${body}`, pos);
}
if (andPieces.length > 1 && orPieces.length > 1) {
throw new SchemaSyntaxError(preserves`Mixed "or" and "and" clause: ${body}`, pos);
}
if (andPieces.length > 1) {
return M.Definition.and({
pattern0: patternName(andPieces[0]),
pattern1: patternName(andPieces[1]),
patternN: andPieces.slice(2).map(patternName),
});
}
if (orPieces.length > 1) {
return M.Definition.or({
pattern0: alternativeName(orPieces[0]),
pattern1: alternativeName(orPieces[1]),
patternN: orPieces.slice(2).map(alternativeName),
});
}
return M.Definition.Pattern(parsePattern(name, orPieces[0]));
}
function transferAnnotations(dest: Input, src: Input): Input {
return annotate(dest, ... annotations(src));
}
function parsePattern(name: symbol, body0: Array<Input>): Pattern {
function parseSimple<A>(item0: Input, ks: (p: SimplePattern) => A, kf: () => A): A {
const pos = position(item0);
const item = peel(item0);
function complain(): never { invalidPattern(stringify(name), item, pos); }
if (typeof item === 'symbol') {
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 'embedded': return ks(M.SimplePattern.embedded());
default: return ks((str[0] === '=')
? M.SimplePattern.lit(Symbol.for(str.slice(1)))
: M.SimplePattern.Ref(parseRef(str, pos)));
}
} else if (Record.isRecord<Input, Tuple<Input>, M._embedded>(item)) {
const label = item.label;
if (Record.isRecord<Input, [], M._embedded>(label)) {
if (label.length !== 0) complain();
switch (label.label) {
case M.$lit:
if (item.length !== 1) complain();
return ks(M.SimplePattern.lit(item[0]));
default:
return kf();
}
} else {
return kf();
}
} else if (Array.isArray(item) && item.length === 2 && is(item[1], M.DOTDOTDOT)) {
return ks(M.SimplePattern.seqof(walkSimple(item[0])));
} else if (Set.isSet<M._embedded>(item)) {
if (item.size !== 1) complain();
const [vp] = item.entries();
return ks(M.SimplePattern.setof(walkSimple(vp)));
} else if (Dictionary.isDictionary<M._embedded, Input>(item)
&& item.size === 2
&& item.has(M.DOTDOTDOT))
{
const v = item.clone();
v.delete(M.DOTDOTDOT);
const [[kp, vp]] = v.entries();
return ks(M.SimplePattern.dictof({ key: walkSimple(kp), value: walkSimple(vp) }));
} else if (isCompound(item)) {
return kf();
} else {
return ks(M.SimplePattern.lit(strip(item)));
}
}
function parseCompound(item0: Input): CompoundPattern {
const pos = position(item0);
const item = peel(item0);
function complain(): never { invalidPattern(stringify(name), item, pos); }
if (Record.isRecord<Input, Tuple<Input>, M._embedded>(item)) {
const label = item.label;
if (Record.isRecord<Input, [], M._embedded>(label)) {
if (label.length !== 0) complain();
switch (label.label) {
case M.$rec:
if (item.length !== 2) complain();
return M.CompoundPattern.rec({
label: maybeNamed(item[0]),
fields: maybeNamed(item[1])
});
default:
complain();
}
} else {
return M.CompoundPattern.rec({
label: M.NamedPattern.anonymous(M.Pattern.SimplePattern(M.SimplePattern.lit(label))),
fields: M.NamedPattern.anonymous(parsePattern(name, [transferAnnotations([... item], item0)])),
});
}
} else if (Array.isArray(item) && item.length > 2 && is(item[item.length - 1], M.DOTDOTDOT)) {
const variableTemplateInput = item[item.length - 2];
const variablePart =
transferAnnotations([variableTemplateInput, M.DOTDOTDOT], variableTemplateInput);
return M.CompoundPattern.tuple$STAR$({
fixed: item.slice(0, item.length - 2).map(maybeNamed),
variable: maybeNamedSimple(variablePart),
});
} else if (Array.isArray(item)) {
return M.CompoundPattern.tuple(item.map(maybeNamed));
} else if (Dictionary.isDictionary<M._embedded, Input>(item) && !item.has(M.DOTDOTDOT)) {
return M.CompoundPattern.dict(
M.DictionaryEntries(item.mapEntries<M.NamedSimplePattern, Input, M._embedded>(
([k, vp]) => [
strip(k),
_maybeNamed(
M.NamedSimplePattern.named,
M.NamedSimplePattern.anonymous,
walkSimple,
k)(vp)
])));
} else {
complain();
}
}
const walk = (b: Input): Pattern => parsePattern(name, [b]);
const walkSimple = (b: Input): SimplePattern => parseSimple(b, p => p, () => {
throw new SchemaSyntaxError(`Compound patterns not accepted here`, position(b));
});
function _maybeNamed<R,P>(
named: (p: M.NamedSimplePattern_) => R,
anonymous: (p: P) => R,
recur: (b: Input) => P,
literalName?: Input): (b: Input) => R
{
return (b: Input) => {
let name = findName(b);
if (name === false) {
if (literalName !== void 0 && typeof literalName === 'symbol') {
name = literalName;
}
}
if (name === false) {
return anonymous(recur(b));
}
return named(M.NamedSimplePattern_({ name, pattern: parseSimple(b, p => p, () =>
namedMustBeSimple(position(b))) }));
};
}
const maybeNamed = _maybeNamed(M.NamedPattern.named, M.NamedPattern.anonymous, walk);
const maybeNamedSimple =
_maybeNamed(M.NamedSimplePattern.named, M.NamedSimplePattern.anonymous, walkSimple);
const body = peel(body0) as Array<Input>;
if (body.length !== 1) {
invalidPattern(stringify(name), body, body.length > 0 ? position(body[0]) : position(body));
}
return parseSimple(body[0],
M.Pattern.SimplePattern,
() => M.Pattern.CompoundPattern(parseCompound(body[0])));
}
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;
}
function parseRef(s: string, pos: Position | null): M.Ref {
const pieces = s.split('.');
return recordPosition(M.Ref({
module: M.ModulePath(pieces.slice(0, pieces.length - 1).map(Symbol.for)),
name: Symbol.for(pieces[pieces.length - 1])
}), pos);
}