import { typeFor, typeForIntersection } from './gentype'; import { ANY_TYPE, FieldType, SimpleType } from './type'; import * as M from './meta'; export function checkSchema(schema: M.Schema): M.Schema { const checker = new Checker(); schema.definitions.forEach(checker.checkDefinition.bind(checker)); if (checker.problems.length > 0) { throw new Error(`Schema does not specify a bijection:\n` + checker.problems.map(c => ' - ' + c).join('\n')); } return schema; } 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)}`); } else { scope.add(name); } } checkDefinition(def: M.Definition, name: symbol): void { switch (def._variant) { case 'or': [def.pattern0, def.pattern1, ... def.patternN].forEach(({ variantLabel, pattern }) => this.checkPattern(new Set(), pattern, `variant ${variantLabel} of ${name.description!}`, typeFor(_ref => ANY_TYPE, pattern))); break; case 'and': { const ps = [def.pattern0, def.pattern1, ... def.patternN]; const scope = new Set(); ps.forEach((p) => this.checkNamedPattern( scope, p, name.description!, typeForIntersection(_ref => ANY_TYPE, ps))); break; } case 'Pattern': this.checkPattern( new Set(), def.value, name.description!, typeFor(_ref => ANY_TYPE, def.value)); break; } } checkNamedPattern(scope: Set, p: M.NamedPattern, context: string, t: SimpleType): void { switch (p._variant) { case 'named': this.checkBinding(scope, p.value.name, context); this.checkPattern(scope, M.Pattern.SimplePattern(p.value.pattern), `${p.value.name.description!} of ${context}`, stepType(t, p.value.name.description!)); break; case 'anonymous': this.checkPattern(scope, p.value, context, t); break; } } checkPattern(scope: Set, p: M.Pattern, context: string, t: SimpleType): void { switch (p._variant) { case 'SimplePattern': if (p.value._variant !== 'lit' && (t.kind === 'record' || t.kind === 'unit')) { this.recordProblem(context, 'cannot recover serialization of non-literal pattern'); } break; case 'CompoundPattern': ((p: M.CompoundPattern): void => { switch (p._variant) { case 'rec': this.checkNamedPattern(scope, p.label, `label of ${context}`, t); this.checkNamedPattern(scope, p.fields, `fields of ${context}`, t); break; case 'tuple': p.patterns.forEach((pp, i) => this.checkNamedPattern(scope, pp, `item ${i} of ${context}`, t)); break; case 'tuple*': if (p.variable._variant === 'named') { this.checkBinding(scope, p.variable.value.name, context); this.checkPattern(scope, M.Pattern.SimplePattern(p.variable.value.pattern), `${JSON.stringify(p.variable.value.name.description!)} of ${context}`, stepType(t, p.variable.value.name.description!)); } else { if (t.kind !== 'array') { this.recordProblem(context, 'unable to reconstruct tail of tuple* pattern'); } else { this.checkPattern(scope, M.Pattern.SimplePattern(p.variable.value), `variable-length portion of ${context}`, t.type); } } p.fixed.forEach((pp, i) => this.checkNamedPattern(scope, pp, `item ${i} of ${context}`, t)); break; case 'setof': if (t.kind !== 'set') { this.recordProblem(context, 'unable to reconstruct set'); } else { this.checkPattern(scope, M.Pattern.SimplePattern(p.pattern), `set in ${context}`, t.type); } break; case 'dictof': if (t.kind !== 'dictionary') { this.recordProblem(context, 'unable to reconstruct dictionary'); } else { this.checkPattern(scope, M.Pattern.SimplePattern(p.key), `key in dictionary in ${context}`, t.key); this.checkPattern(scope, M.Pattern.SimplePattern(p.value), `value in dictionary in ${context}`, t.value); } break; case 'dict': p.entries.forEach((np, key) => this.checkNamedPattern( scope, M.promoteNamedSimplePattern(M.addNameIfAbsent(np, key)), `entry ${key.asPreservesText()} in dictionary in ${context}`, t)); break; } })(p.value); } } } function stepType(t: SimpleType, key: string): FieldType { if (t.kind !== 'record' || !t.fields.has(key)) { throw new Error( `Internal error: cannot step ${JSON.stringify(t)} by ${JSON.stringify(key)}`); } return t.fields.get(key)!; }