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(); export function recordPosition(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(items: Array, separator: T): Array> { const groups: Array> = []; let group: Array = []; 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): 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): Array { return new Reader(source, { ... options ?? {}, includeAnnotations: true }).readToEnd(); } export function readSchema(source: string, options?: ReaderOptions & SchemaReaderOptions): Schema { return checkSchema(parseSchema(_readSchema(source, options), options ?? {})); } export function parseSchema(toplevelTokens: Array, options: ReaderOptions & SchemaReaderOptions): Schema { let version: M.Version | undefined = void 0; let embeddedType: M.EmbeddedTypeName = M.EmbeddedTypeName.$false(); let definitions = new KeyedDictionary(); function process(toplevelTokens: Array): void { const toplevelClauses = splitBy(peel(toplevelTokens) as Array, 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): Definition { function alternativeName(input: Array): 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): 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): Pattern { function parseSimple(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, M._embedded>(item)) { const label = item.label; if (Record.isRecord(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(item)) { if (item.size !== 1) complain(); const [vp] = item.entries(); return ks(M.SimplePattern.setof(walkSimple(vp))); } else if (Dictionary.isDictionary(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, M._embedded>(item)) { const label = item.label; if (Record.isRecord(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(item) && !item.has(M.DOTDOTDOT)) { return M.CompoundPattern.dict( M.DictionaryEntries(item.mapEntries( ([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( 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; 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(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); }