UNPKG

ecmarkup

Version:

Custom element definitions and core utilities for markup that specifies ECMAScript and related technologies.

294 lines (293 loc) 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeParser = exports.ParseError = void 0; class ParseError extends Error { constructor(message, offset) { super(message); this.offset = offset; } } exports.ParseError = ParseError; class TypeParser { constructor(input) { this.offset = 0; this.allowParensInOpaque = true; this.remainder = input; this.input = input; } static parse(input) { return new TypeParser(input).parse(); } parse() { const out = this.parseTypePossiblyWithUnmarkedUnion(); this.eatWs(); if (this.remainder !== '') { if (this.remainder.startsWith('or ')) { throw new ParseError(`could not determine how to associate "or"; try adding "either"`, this.offset); } throw new ParseError(`type was nonempty after parsing`, this.offset); } return out; } parseTypePossiblyWithUnmarkedUnion() { const out = this.parseType(); this.eatWs(); let orOffset = this.offset; const match = this.eat(/^((, )?or\b|,)/); if (match == null) { return out; } let nextFieldIsLast = match[0].includes('or'); const unionTypes = [out]; while (true) { unionTypes.push(this.parseType()); if (nextFieldIsLast) { break; } this.eatWs(); const offset = this.offset; const sep = this.expect(/^((, )?or\b|,)/); if (sep[0].includes('or')) { orOffset = offset; nextFieldIsLast = true; } } if (unionTypes .slice(0, -1) .some(t => t.kind === 'list' || t.kind === 'union' || (t.kind === 'completion' && t.completionType !== 'abrupt' && t.typeOfValueIfNormal !== null))) { throw new ParseError(`type is ambiguous; can't tell where the "or" attaches (add "either" to disambiguate)`, orOffset); } return squashUnionTypes(unionTypes); } parseType() { this.eatWs(); if (this.eat(/^a normal completion containing\b/i) != null) { return { kind: 'completion', typeOfValueIfNormal: this.parseType(), completionType: 'normal', }; } { const match = this.eat(/^an? (Completion Record|(normal|abrupt|throw|break|return|continue) completion)/i); if (match != null) { switch (match[1].toLowerCase()) { case 'normal completion': { return { kind: 'completion', typeOfValueIfNormal: null, completionType: 'normal', }; } case 'completion record': { return { kind: 'completion', typeOfValueIfNormal: null, completionType: 'mixed', }; } default: { return { kind: 'completion', completionType: 'abrupt', }; } } } } if (this.eat(/^(a List|Lists) of\b/i) != null) { return { kind: 'list', elements: this.parseType(), }; } if (this.eat(/^(a List|Lists)\b/i) != null) { return { kind: 'list', elements: null, }; } { const match = this.eat(/^(?:a Record|Records) with field(s?)\b/i); if (match != null) { const parsedFields = { __proto__: null }; let nextFieldIsLast = match[1] === ''; while (true) { this.eatWs(); const offset = this.offset; const name = this.expect(/^[^\s,]+/)[0]; if (name in parsedFields) { throw new ParseError(`duplicate field name ${JSON.stringify(name)}`, offset); } this.eatWs(); if (this.eat(/^\(/) != null) { const oldAllowParensInOpaque = this.allowParensInOpaque; this.allowParensInOpaque = false; const fieldType = this.parseTypePossiblyWithUnmarkedUnion(); this.allowParensInOpaque = oldAllowParensInOpaque; this.expect(/^\)/); this.eatWs(); parsedFields[name] = fieldType; } else { parsedFields[name] = null; } if (nextFieldIsLast) { break; } const sep = this.expect(/^((, )?and\b|,)/); if (sep[0].includes('and')) { nextFieldIsLast = true; } } return { kind: 'record', fields: parsedFields, }; } } if (this.eat(/^(?:either|one of)\b/i)) { let nextFieldIsLast = false; const unionTypes = []; while (true) { unionTypes.push(this.parseType()); if (nextFieldIsLast) { break; } this.eatWs(); const sep = this.expect(/^((, )?or\b|,)/); if (sep[0].includes('or')) { nextFieldIsLast = true; } } return squashUnionTypes(unionTypes); } const opaqueStart = this.offset; const eater = this.allowParensInOpaque ? /,| or\b/ : /\(|\)|,| or\b/; const opaqueThing = { kind: 'opaque', type: this.eatUntil(eater).trim(), }; const start = this.offset; if (this.eat(/^, but not\b/)) { // we don't actually care to represent the type this.parseTypePossiblyWithUnmarkedUnion(); opaqueThing.type += this.input.slice(start, this.offset).trim(); this.eatWs(); if (!(this.remainder === '' || this.remainder[0] === ')' || this.eat(/^,/) != null)) { throw new ParseError(`expecting a parenthesis after a "but not" clause`, this.offset); } } if (opaqueThing.type === '~unused~') { return { kind: 'unused', }; } else if (opaqueThing.type === '') { throw new ParseError(`expected to find a type, got empty string`, opaqueStart); } return opaqueThing; } eatWs() { this.eat(/^\s+/); } eat(regexp) { if (regexp.source[0] !== '^') { throw new Error(`eat expects a regex which binds to the start of the string (got ${regexp})`); } const match = this.remainder.match(regexp); if (match == null) { return match; } this.offset += match[0].length; this.remainder = this.remainder.slice(match[0].length); return match; } eatUntil(regexp) { const match = this.remainder.match(regexp); if (match == null) { const ret = this.remainder; this.offset += ret.length; this.remainder = ''; return ret; } const ret = this.remainder.slice(0, match.index); this.offset += ret.length; this.remainder = this.remainder.slice(ret.length); return ret; } expect(regexp) { const match = this.eat(regexp); if (match == null) { throw new ParseError(`expected ${regexp} at ${JSON.stringify(this.remainder)}`, this.offset); } return match; } } exports.TypeParser = TypeParser; function join(a, b) { if (a == null || b == null) { return null; } if (a.kind === 'union') { if (b.kind === 'union') { return { kind: 'union', types: a.types.concat(b.types), }; } return { kind: 'union', types: a.types.concat([b]), }; } else if (b.kind === 'union') { return { kind: 'union', types: b.types.concat([a]), }; } else { return { kind: 'union', types: [a, b], }; } } function squashUnionTypes(unionTypes) { const out = unionTypes.flatMap(t => (t.kind === 'union' ? t.types : [t])); if (out.every(t => t.kind === 'completion')) { return out.reduce((a, b) => { if (a.completionType !== 'abrupt' && b.completionType !== 'abrupt') { return { kind: 'completion', completionType: a.completionType === 'normal' && b.completionType === 'normal' ? 'normal' : 'mixed', typeOfValueIfNormal: join(a.typeOfValueIfNormal, b.typeOfValueIfNormal), }; } else if (a.completionType === 'abrupt' && b.completionType === 'abrupt') { return { kind: 'completion', completionType: 'abrupt', }; } else { return { kind: 'completion', completionType: 'mixed', typeOfValueIfNormal: a.completionType !== 'abrupt' ? a.typeOfValueIfNormal : b.typeOfValueIfNormal, }; } }); } return { kind: 'union', types: out, }; }