import { stringify } from '@preserves/core'; import * as M from './meta'; export function checkSchema(schema: M.Schema): ( { ok: true, schema: M.Schema } | { ok: false, problems: Array }) { const checker = new Checker(); schema.definitions.forEach(checker.checkDefinition.bind(checker)); if (checker.problems.length > 0) { return { ok: false, problems: checker.problems }; } else { return { ok: true, schema }; } } enum ValueAvailability { AVAILABLE, NOT_AVAILABLE, }; class Checker { problems: Array = []; recordProblem(context: string, detail: string): void { this.problems.push(`${detail} in ${context}`); } checkBinding(scope: Set, sym: symbol, context: string): void { const name = sym.description!; if (scope.has(name)) { this.recordProblem(context, `duplicate binding named ${JSON.stringify(name)}`); } if (!M.isValidToken(name)) { this.recordProblem(context, `invalid binding name ${JSON.stringify(name)}`); } scope.add(name); } checkDefinition(def: M.Definition, name: symbol): void { switch (def._variant) { case 'or': { const labels = new Set(); [def.pattern0, def.pattern1, ... def.patternN].forEach(({ variantLabel, pattern }) => { const context = `variant ${variantLabel} of ${name.description!}`; if (labels.has(variantLabel)) { this.recordProblem(context, `duplicate variant label`); } if (!M.isValidToken(variantLabel)) { this.recordProblem(context, `invalid variant label`); } labels.add(variantLabel); this.checkPattern(new Set(), pattern, context, ValueAvailability.AVAILABLE); }); break; } case 'and': { const ps = [def.pattern0, def.pattern1, ... def.patternN]; const scope = new Set(); ps.forEach((p) => this.checkNamedPattern(scope, p, name.description!)); break; } case 'Pattern': this.checkPattern( new Set(), def.value, name.description!, ValueAvailability.AVAILABLE); break; } } checkNamedPattern(scope: Set, p: M.NamedPattern, context: string): void { switch (p._variant) { case 'named': { this.checkBinding(scope, p.value.name, context); this.checkPattern(scope, M.Pattern.SimplePattern(p.value.pattern), `${JSON.stringify(p.value.name.description!)} of ${context}`, ValueAvailability.AVAILABLE); break; } case 'anonymous': this.checkPattern(scope, p.value, context, ValueAvailability.NOT_AVAILABLE); break; } } checkPattern(scope: Set, p: M.Pattern, context: string, availability: ValueAvailability): void { switch (p._variant) { case 'SimplePattern': if (p.value._variant !== 'lit' && availability === ValueAvailability.NOT_AVAILABLE) { this.recordProblem(context, 'cannot recover serialization of non-literal pattern'); } if (p.value._variant === 'Ref' && !(M.isValidToken(p.value.value.name.description!) && p.value.value.module.every(n => M.isValidToken(n.description!)))) { this.recordProblem(context, 'invalid reference name'); } break; case 'CompoundPattern': ((p: M.CompoundPattern): void => { switch (p._variant) { case 'rec': this.checkNamedPattern(scope, p.label, `label of ${context}`); this.checkNamedPattern(scope, p.fields, `fields of ${context}`); break; case 'tuple': p.patterns.forEach((pp, i) => this.checkNamedPattern(scope, pp, `item ${i} of ${context}`)); break; case 'tuplePrefix': p.fixed.forEach((pp, i) => this.checkNamedPattern(scope, pp, `item ${i} of ${context}`)); this.checkNamedPattern( scope, M.promoteNamedSimplePattern(p.variable), `tail of ${context}`); break; case 'dict': p.entries.forEach((np, key) => this.checkNamedPattern( scope, M.promoteNamedSimplePattern(np), `entry ${stringify(key)} in dictionary in ${context}`)); break; } })(p.value); } } }