UNPKG

ecmarkup

Version:

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

585 lines (584 loc) 21.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.dominates = dominates; exports.join = join; exports.meet = meet; exports.serialize = serialize; exports.typeFromExpr = typeFromExpr; exports.typeFromExprType = typeFromExprType; exports.isCompletion = isCompletion; exports.isPossiblyAbruptCompletion = isPossiblyAbruptCompletion; exports.stripWhitespace = stripWhitespace; const simpleKinds = new Set([ 'unknown', 'never', 'record', 'abrupt completion', 'real', 'integer', 'non-negative integer', 'negative integer', 'positive integer', 'ES value', 'string', 'number', 'integral number', 'bigint', 'boolean', 'null', 'undefined', ]); const dominateGraph = { // @ts-expect-error TS does not know about __proto__ __proto__: null, record: ['normal completion', 'abrupt completion'], real: [ 'integer', 'non-negative integer', 'negative integer', 'positive integer', 'concrete real', ], integer: ['non-negative integer', 'negative integer', 'positive integer'], 'non-negative integer': ['positive integer'], 'ES value': [ 'string', 'number', 'integral number', 'bigint', 'boolean', 'null', 'undefined', 'concrete string', 'concrete number', 'concrete bigint', 'concrete boolean', ], string: ['concrete string'], number: ['integral number', 'concrete number'], bigint: ['concrete bigint'], boolean: ['concrete boolean'], }; /* The type lattice used here is very simple (aside from explicit unions). As such we mostly only need to define the `dominates` relationship and apply trivial rules: if `x` dominates `y`, then `join(x,y) = x` and `meet(x,y) = y`; if neither dominates the other than the join is top and the meet is bottom. Unions/lists/completions take a little more work. */ function dominates(a, b) { var _a, _b; if (a.kind === 'unknown' || b.kind === 'never') { return true; } if (b.kind === 'union') { return b.of.every(t => dominates(a, t)); } if (a.kind === 'union') { // not necessarily true for arbitrary lattices, but true for ours return a.of.some(t => dominates(t, b)); } if ((a.kind === 'list' && b.kind === 'list') || (a.kind === 'normal completion' && b.kind === 'normal completion')) { return dominates(a.of, b.of); } if (simpleKinds.has(a.kind) && simpleKinds.has(b.kind) && a.kind === b.kind) { return true; } if ((_b = (_a = dominateGraph[a.kind]) === null || _a === void 0 ? void 0 : _a.includes(b.kind)) !== null && _b !== void 0 ? _b : false) { return true; } if (a.kind === 'integer' && b.kind === 'concrete real') { return !b.value.includes('.'); } if (a.kind === 'integral number' && b.kind === 'concrete number') { return Number.isFinite(b.value) && b.value === Math.round(b.value); } if (a.kind === 'non-negative integer' && b.kind === 'concrete real') { return !b.value.includes('.') && b.value[0] !== '-'; } if (a.kind === 'negative integer' && b.kind === 'concrete real') { return !b.value.includes('.') && b.value[0] === '-'; } if (a.kind === 'positive integer' && b.kind === 'concrete real') { return !b.value.includes('.') && b.value[0] !== '-' && b.value !== '0'; } if (a.kind === b.kind && [ 'concrete string', 'concrete number', 'concrete real', 'concrete bigint', 'concrete boolean', 'enum value', ].includes(a.kind)) { // @ts-expect-error TS is not quite smart enough for this return Object.is(a.value, b.value); } return false; } function addToUnion(types, type) { if (type.kind === 'normal completion') { const existingNormalCompletionIndex = types.findIndex(t => t.kind === 'normal completion'); if (existingNormalCompletionIndex !== -1) { // prettier-ignore const joined = join(types[existingNormalCompletionIndex], type); if (types.length === 1) { return joined; } const typesCopy = [...types]; typesCopy.splice(existingNormalCompletionIndex, 1, joined); return { kind: 'union', of: typesCopy }; } } if (types.some(t => dominates(t, type))) { return { kind: 'union', of: types }; } const live = types.filter(t => !dominates(type, t)); if (live.length === 0) { return type; } return { kind: 'union', of: [...live, type] }; } function join(a, b) { if (dominates(a, b)) { return a; } if (dominates(b, a)) { return b; } if (b.kind === 'union') { [a, b] = [b, a]; } if (a.kind === 'union') { if (b.kind === 'union') { return b.of.reduce((acc, t) => (acc.kind === 'union' ? addToUnion(acc.of, t) : join(acc, t)), a); } return addToUnion(a.of, b); } if ((a.kind === 'list' && b.kind === 'list') || (a.kind === 'normal completion' && b.kind === 'normal completion')) { return { kind: a.kind, of: join(a.of, b.of) }; } return { kind: 'union', of: [a, b] }; } function meet(a, b) { if (dominates(a, b)) { return b; } if (dominates(b, a)) { return a; } if (a.kind !== 'union' && b.kind === 'union') { [a, b] = [b, a]; } if (a.kind === 'union') { // union is join. meet distributes over join. return a.of.map(t => meet(t, b)).reduce(join); } if (a.kind === 'list' && b.kind === 'list') { return { kind: 'list', of: meet(a.of, b.of) }; } if (a.kind === 'normal completion' && b.kind === 'normal completion') { const inner = meet(a.of, b.of); if (inner.kind === 'never') { return { kind: 'never' }; } return { kind: 'normal completion', of: inner }; } return { kind: 'never' }; } function serialize(type) { switch (type.kind) { case 'unknown': { return 'unknown'; } case 'never': { return 'never'; } case 'union': { const parts = type.of.map(serialize); if (parts.length > 2) { return parts.slice(0, -1).join(', ') + ', or ' + parts[parts.length - 1]; } return parts[0] + ' or ' + parts[1]; } case 'list': { if (type.of.kind === 'never') { return 'empty List'; } return 'List of ' + serialize(type.of); } case 'record': { return 'Record'; } case 'normal completion': { if (type.of.kind === 'never') { return 'never'; } else if (type.of.kind === 'unknown') { return 'a normal completion'; } return 'a normal completion containing ' + serialize(type.of); } case 'abrupt completion': { return 'an abrupt completion'; } case 'real': { return 'mathematical value'; } case 'integer': case 'non-negative integer': case 'negative integer': case 'positive integer': case 'null': case 'undefined': { return type.kind; } case 'concrete string': { return `"${type.value}"`; } case 'concrete real': { return type.value; } case 'concrete boolean': { return `${type.value}`; } case 'enum value': { return `~${type.value}~`; } case 'concrete number': { if (Object.is(type.value, 0 / 0)) { return '*NaN*'; } let repr; if (Object.is(type.value, -0)) { repr = '-0'; } else if (type.value === 0) { repr = '+0'; } else if (type.value === 2e308) { repr = '+&infin;'; } else if (type.value === -2e308) { repr = '-&infin;'; } else if (type.value > 4503599627370495.5) { repr = String(BigInt(type.value)); } else { repr = String(type.value); } return `*${repr}*<sub>𝔽</sub>`; } case 'concrete bigint': { return `*${type.value}*<sub>ℤ</sub>`; } case 'ES value': { return 'ECMAScript language value'; } case 'boolean': { return 'Boolean'; } case 'string': { return 'String'; } case 'number': { return 'Number'; } case 'integral number': { return 'integral Number'; } case 'bigint': { return 'BigInt'; } } } function typeFromExpr(expr, biblio, warn) { var _a, _b, _c, _d; seq: if (expr.name === 'seq') { const items = stripWhitespace(expr.items); if (items.length === 1) { expr = items[0]; break seq; } if (items.length === 2 && items[0].name === 'star' && items[0].contents[0].name === 'text' && items[1].name === 'text') { switch (items[1].contents) { case '𝔽': { const text = items[0].contents[0].contents; let value; if (text === '-0') { value = -0; } else if (text === '+&infin;') { value = 2e308; } else if (text === '-&infin;') { value = -2e308; } else { value = parseFloat(text); } return { kind: 'concrete number', value }; } case 'ℤ': { return { kind: 'concrete bigint', value: BigInt(items[0].contents[0].contents) }; } } } if (((_a = items[0]) === null || _a === void 0 ? void 0 : _a.name) === 'text' && ['!', '?'].includes(items[0].contents.trim())) { const remaining = stripWhitespace(items.slice(1)); if (remaining.length === 1 && ['call', 'sdo-call'].includes(remaining[0].name)) { const callType = typeFromExpr(remaining[0], biblio, warn); if (isCompletion(callType)) { const normal = callType.kind === 'normal completion' ? callType.of : callType.kind === 'abrupt completion' ? // the expression `? _abrupt_` strictly speaking has type `never` // however we mostly use `never` to mean user error, so use unknown instead // this should only ever happen after `Return` { kind: 'unknown' } : (_c = (_b = callType.of.find(k => k.kind === 'normal completion')) === null || _b === void 0 ? void 0 : _b.of) !== null && _c !== void 0 ? _c : { kind: 'unknown', }; return normal; } } } } switch (expr.name) { case 'text': { const text = expr.contents.trim(); if (/^-?[0-9]+(\.[0-9]+)?$/.test(text)) { return { kind: 'concrete real', value: text }; } break; } case 'list': { return { kind: 'list', of: expr.elements.map(t => typeFromExpr(t, biblio, warn)).reduce(join, { kind: 'never' }), }; } case 'record': { return { kind: 'record' }; } case 'call': case 'sdo-call': { const { callee } = expr; if (!(callee.length === 1 && callee[0].name === 'text')) { break; } const calleeName = callee[0].contents; // special case: `Completion` is identity on completions if (expr.name === 'call' && calleeName === 'Completion') { if (expr.arguments.length === 1) { const inner = typeFromExpr(expr.arguments[0], biblio, warn); if (!isCompletion(inner)) { // probably unknown, we might as well refine to "some completion" return { kind: 'union', of: [ { kind: 'normal completion', of: { kind: 'unknown' } }, { kind: 'abrupt completion' }, ], }; } } else { warn(expr.location.start.offset, 'expected Completion to be passed exactly one argument'); } } // special case: `NormalCompletion` wraps its input if (expr.name === 'call' && calleeName === 'NormalCompletion') { if (expr.arguments.length === 1) { return { kind: 'normal completion', of: typeFromExpr(expr.arguments[0], biblio, warn) }; } else { warn(expr.location.start.offset, 'expected NormalCompletion to be passed exactly one argument'); } } const biblioEntry = biblio.byAoid(calleeName); if (((_d = biblioEntry === null || biblioEntry === void 0 ? void 0 : biblioEntry.signature) === null || _d === void 0 ? void 0 : _d.return) == null) { break; } return typeFromExprType(biblioEntry.signature.return); } case 'tilde': { if (expr.contents.length === 1 && expr.contents[0].name === 'text') { return { kind: 'enum value', value: expr.contents[0].contents }; } break; } case 'star': { if (expr.contents.length === 1 && expr.contents[0].name === 'text') { const text = expr.contents[0].contents; if (text === 'null') { return { kind: 'null' }; } else if (text === 'undefined') { return { kind: 'undefined' }; } else if (text === 'NaN') { return { kind: 'concrete number', value: 0 / 0 }; } else if (text === 'true') { return { kind: 'concrete boolean', value: true }; } else if (text === 'false') { return { kind: 'concrete boolean', value: false }; } else if (text.startsWith('"') && text.endsWith('"')) { return { kind: 'concrete string', value: text.slice(1, -1) }; } } break; } } return { kind: 'unknown' }; } function typeFromExprType(type) { switch (type.kind) { case 'union': { return type.types.map(typeFromExprType).reduce(join); } case 'list': { return { kind: 'list', of: type.elements == null ? { kind: 'unknown' } : typeFromExprType(type.elements), }; } case 'completion': { if (type.completionType === 'abrupt') { return { kind: 'abrupt completion' }; } else { const normalType = type.typeOfValueIfNormal == null ? { kind: 'unknown' } : typeFromExprType(type.typeOfValueIfNormal); if (type.completionType === 'normal') { return { kind: 'normal completion', of: normalType }; } else if (type.completionType === 'mixed') { return { kind: 'union', of: [{ kind: 'normal completion', of: normalType }, { kind: 'abrupt completion' }], }; } else { throw new Error('unreachable: completion kind ' + type.completionType); } } } case 'opaque': { const text = type.type; if (text.startsWith('"') && text.endsWith('"')) { return { kind: 'concrete string', value: text.slice(1, -1) }; } if (text.startsWith('~') && text.endsWith('~')) { return { kind: 'enum value', value: text.slice(1, -1) }; } if (/^-?[0-9]+(\.[0-9]+)?$/.test(text)) { return { kind: 'concrete real', value: text }; } if (text.startsWith('*') && text.endsWith('*<sub>𝔽</sub>')) { const innerText = text.slice(1, -14); let value; if (innerText === '-0') { value = -0; } else if (innerText === '+&infin;') { value = 2e308; } else if (innerText === '-&infin;') { value = -2e308; } else { value = parseFloat(innerText); } return { kind: 'concrete number', value }; } if (text === '*NaN*') { return { kind: 'concrete number', value: 0 / 0 }; } if (text.startsWith('*') && text.endsWith('*<sub>ℤ</sub>')) { return { kind: 'concrete bigint', value: BigInt(text.slice(1, -14)) }; } if (text === 'an ECMAScript language value' || text === 'ECMAScript language values') { return { kind: 'ES value' }; } if (text === 'a String' || text === 'Strings') { return { kind: 'string' }; } if (text === 'a Number' || text === 'Numbers') { return { kind: 'number' }; } if (text === 'a Boolean' || text === 'Booleans') { return { kind: 'boolean' }; } if (text === 'a BigInt' || text === 'BigInts') { return { kind: 'bigint' }; } if (text === 'an integral Number' || text === 'integral Numbers') { return { kind: 'integral number' }; } if (text === 'a mathematical value' || text === 'mathematical values') { return { kind: 'real' }; } if (text === 'an integer' || text === 'integers') { return { kind: 'integer' }; } if (text === 'a non-negative integer' || text === 'non-negative integers') { return { kind: 'non-negative integer' }; } if (text === 'a negative integer' || text === 'negative integers') { return { kind: 'negative integer' }; } if (text === 'a positive integer' || text === 'positive integers') { return { kind: 'positive integer' }; } if (text === 'a time value' || text === 'time values') { return { kind: 'union', of: [{ kind: 'integral number' }, { kind: 'concrete number', value: 0 / 0 }], }; } if (text === '*null*') { return { kind: 'null' }; } if (text === '*undefined*') { return { kind: 'undefined' }; } break; } case 'unused': { return { kind: 'enum value', value: 'unused' }; } } return { kind: 'unknown' }; } function isCompletion(type) { return (type.kind === 'normal completion' || type.kind === 'abrupt completion' || (type.kind === 'union' && type.of.some(isCompletion))); } function isPossiblyAbruptCompletion(type) { return (type.kind === 'abrupt completion' || (type.kind === 'union' && type.of.some(isPossiblyAbruptCompletion))); } function stripWhitespace(items) { var _a, _b; items = [...items]; while (((_a = items[0]) === null || _a === void 0 ? void 0 : _a.name) === 'text' && /^\s*$/.test(items[0].contents)) { items.shift(); } while (((_b = items[items.length - 1]) === null || _b === void 0 ? void 0 : _b.name) === 'text' && // @ts-expect-error /^\s*$/.test(items[items.length - 1].contents)) { items.pop(); } return items; }