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

119 lines
5.0 KiB
TypeScript
Raw Normal View History

2021-05-21 15:28:18 +00:00
import { typeFor, typeForIntersection } from './gentype';
import { ANY_TYPE, SimpleType } from './type';
import * as M from './meta';
2021-05-24 08:16:00 +00:00
export function checkSchema(schema: M.Schema): (
{ ok: true, schema: M.Schema } | { ok: false, problems: Array<string> })
{
const checker = new Checker();
schema.definitions.forEach(checker.checkDefinition.bind(checker));
if (checker.problems.length > 0) {
2021-05-24 08:16:00 +00:00
return { ok: false, problems: checker.problems };
} else {
return { ok: true, schema };
}
}
class Checker {
problems: Array<string> = [];
recordProblem(context: string, detail: string): void {
this.problems.push(`${detail} in ${context}`);
}
checkBinding(scope: Set<string>, 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': {
const labels = new Set<string>();
[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`);
}
labels.add(variantLabel);
this.checkPattern(new Set(), pattern, context, typeFor(_ref => ANY_TYPE, pattern));
});
break;
}
case 'and': {
2021-05-21 15:28:18 +00:00
const ps = [def.pattern0, def.pattern1, ... def.patternN];
const scope = new Set<string>();
2021-05-21 15:28:18 +00:00
ps.forEach((p) => this.checkNamedPattern(
scope, p, name.description!, typeForIntersection(_ref => ANY_TYPE, ps)));
break;
}
case 'Pattern':
2021-05-21 15:28:18 +00:00
this.checkPattern(
new Set(), def.value, name.description!, typeFor(_ref => ANY_TYPE, def.value));
break;
}
}
2021-05-21 15:28:18 +00:00
checkNamedPattern(scope: Set<string>, p: M.NamedPattern, context: string, t: SimpleType): void {
switch (p._variant) {
case 'named': {
const key = p.value.name.description!;
if (t.kind !== 'record' || !t.fields.has(key)) {
throw new Error(
`Internal error: cannot step ${JSON.stringify(t)} by ${JSON.stringify(key)}`);
}
2021-05-21 14:14:58 +00:00
this.checkBinding(scope, p.value.name, context);
this.checkPattern(scope,
M.Pattern.SimplePattern(p.value.pattern),
`${JSON.stringify(p.value.name.description!)} of ${context}`,
t.fields.get(key)!);
break;
}
case 'anonymous':
2021-05-21 15:28:18 +00:00
this.checkPattern(scope, p.value, context, t);
break;
}
}
2021-05-21 15:28:18 +00:00
checkPattern(scope: Set<string>, 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':
2021-05-21 15:28:18 +00:00
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) =>
2021-05-21 15:28:18 +00:00
this.checkNamedPattern(scope, pp, `item ${i} of ${context}`, t));
break;
case 'tuple*':
p.fixed.forEach((pp, i) =>
2021-05-21 15:28:18 +00:00
this.checkNamedPattern(scope, pp, `item ${i} of ${context}`, t));
this.checkNamedPattern(
scope, M.promoteNamedSimplePattern(p.variable), `tail of ${context}`, t);
break;
case 'dict':
p.entries.forEach((np, key) =>
this.checkNamedPattern(
scope,
M.promoteNamedSimplePattern(M.addNameIfAbsent(np, key)),
2021-05-21 15:28:18 +00:00
`entry ${key.asPreservesText()} in dictionary in ${context}`,
t));
break;
}
})(p.value);
}
}
}