[READ-ONLY] Mirror of https://github.com/CanadaHonk/porffor. An ahead-of-time JavaScript compiler porffor.dev
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731import parse from './parse.js';import { TYPES } from './types.js';
const varId = name => { const lastFunc = scopes[scopes.lastFuncs.at(-1)]; lastFunc._variableIds ??= Object.create(null); lastFunc._variableIds[name] ??= 0; return lastFunc._variableIds[name]++;};
const isFunctionScope = node => node?.type === 'FunctionDeclaration' || node?.type === 'FunctionExpression' || node?.type === 'ArrowFunctionExpression';
const findLexicalThisOwner = currentFunc => { if (currentFunc?.type !== 'ArrowFunctionExpression') return null;
let owner = currentFunc._parentFunc; while (owner) { if (owner.type !== 'ArrowFunctionExpression') return owner; owner = owner._parentFunc; }
return null;};
const markClosurePassThrough = (currentFunc, owner) => { let cursor = currentFunc?._parentFunc; while (cursor && cursor !== owner) { cursor._closurePassThrough = true; cursor = cursor._parentFunc; }};
const isSelfReferenceContext = (currentFunc, owner) => { if (!currentFunc || !owner) return false; if (!isFunctionScope(currentFunc)) return false;
let cursor = currentFunc; while (cursor) { if (cursor === owner) return true; cursor = cursor._parentFunc; }
return false;};
const isLoopScope = scope => scope?.type === 'ForStatement' || scope?.type === 'ForInStatement' || scope?.type === 'ForOfStatement';
const bindingHasLoopScope = variable => { const funcInd = scopes.indexOf(variable.func); const scopeInd = scopes.indexOf(variable.scope); for (let i = funcInd + 1; i <= scopeInd; i++) { if (isLoopScope(scopes[i])) return true; }
return false;};
const renameBindingNode = (node, name) => { if (!node || typeof node !== 'object') return;
if (node.type === 'Identifier') { node.name = name; } else if ((node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration' || node.type === 'ClassExpression') && node.id?.name) { node.id.name = name; }};
const resolveVariable = name => { for (let i = scopes.length - 1; i >= 0; i--) { const variable = scopes[i]._variables?.[name]; if (variable) return variable; }};
const markWrite = node => { if (!node || typeof node !== 'object') return;
if (node.type === 'Identifier') { const variable = node._resolvedVariable ?? node._variable ?? resolveVariable(node.name); if (variable) { variable.node._writes = (variable.node._writes ?? 0) + 1; } return; }
if (node.type === 'ArrayPattern') { for (const x of node.elements) markWrite(x); return; }
if (node.type === 'ObjectPattern') { for (const x of node.properties) markWrite(x.value ?? x.argument); return; }
if (node.type === 'AssignmentPattern') { markWrite(node.left); return; }
if (node.type === 'RestElement') markWrite(node.argument);};
const recordStorageWrite = (target, value) => { if (!target || typeof target !== 'object') return;
if (target.type === 'Identifier') { const variable = target._resolvedVariable ?? target._variable ?? resolveVariable(target.name); if (!variable) return;
if (variable.node._storageType !== null) { variable.node._storageType = storageExpressionType(value) === TYPES.number ? TYPES.number : null; } return; }
if (target.type === 'ArrayPattern') { for (const x of target.elements) recordStorageWrite(x, null); return; }
if (target.type === 'ObjectPattern') { for (const x of target.properties) recordStorageWrite(x.value ?? x.argument, null); return; }
if (target.type === 'AssignmentPattern') { recordStorageWrite(target.left, value); return; }
if (target.type === 'RestElement') recordStorageWrite(target.argument, null);};
const markMaybeUndefinedStorage = node => { if (node?.type !== 'Identifier') return;
const variable = node._resolvedVariable ?? node._variable ?? resolveVariable(node.name); if (variable?.node?._uninitialized) variable.node._storageType = null;};
const undefinedCheckTarget = (left, right) => { if (left?.type === 'Identifier' && right?.type === 'Identifier' && right.name === 'undefined') return left; if (right?.type === 'Identifier' && left?.type === 'Identifier' && left.name === 'undefined') return right;};
const storageExpressionType = node => { if (!node) return null; if (node._type != null) return typeof node._type === 'number' ? node._type : null;
if (node.type === 'Literal') { if (node.bigint != null) return TYPES.bigint; if (node.regex) return TYPES.regexp; if (typeof node.value === 'string') return TYPES.bytestring; return TYPES[typeof node.value] ?? null; }
if (node.type === 'Identifier') { if (node.name === 'undefined') return TYPES.undefined; if (node.name === 'NaN' || node.name === 'Infinity') return TYPES.number; return !node._noStorageInfer && (node._resolvedVariable ?? resolveVariable(node.name))?.node?._storageType === TYPES.number ? TYPES.number : null; }
if (node.type === 'UnaryExpression') { if (node.operator === '!') return TYPES.boolean; if (node.operator === 'void') return TYPES.undefined; if (node.operator === 'delete') return TYPES.boolean; if (node.operator === 'typeof') return TYPES.bytestring; return TYPES.number; }
if (node.type === 'UpdateExpression') return TYPES.number;
if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') { if (['==', '===', '!=', '!==', '>', '>=', '<', '<=', 'instanceof', 'in'].includes(node.operator)) return TYPES.boolean; if (node.type === 'LogicalExpression') return null;
const l = storageExpressionType(node.left), r = storageExpressionType(node.right); if (l === TYPES.bigint || r === TYPES.bigint) return TYPES.bigint; if (node.operator !== '+') return TYPES.number; if (l === TYPES.number && r === TYPES.number) return TYPES.number; if (l === TYPES.string || r === TYPES.string) return TYPES.string; if (l === TYPES.bytestring && r === TYPES.bytestring) return TYPES.bytestring; return null; }
if (node.type === 'AssignmentExpression') { const op = node.operator.slice(0, -1) || '='; return op === '=' ? storageExpressionType(node.right) : storageExpressionType({ type: ['||', '&&', '??'].includes(op) ? 'LogicalExpression' : 'BinaryExpression', left: node.left, right: node.right, operator: op }); }
if (node.type === 'SequenceExpression') return storageExpressionType(node.expressions.at(-1)); if (node.type === 'TemplateLiteral') return TYPES.bytestring; if (node.type === 'ObjectExpression') return TYPES.object; if (node.type === 'ArrayExpression') return TYPES.array;
return null;};
const nearestEvalScope = () => { const lastFunc = scopes.lastFuncs.at(-1); for (let i = scopes.length - 1; i > lastFunc; i--) { const scope = scopes[i]; if (scope._evalScope) return scope; }
return null;};
const nearestStrictEvalScope = () => { const scope = nearestEvalScope(); return scope && (scope._strictEval || scope._strict) ? scope : null;};
const evalLexicalConflict = name => { const evalScope = nearestEvalScope(); if (!evalScope) return false;
const evalInd = scopes.indexOf(evalScope); for (let i = evalInd + 1; i < scopes.length; i++) { const variable = scopes[i]._variables?.[name]; if (variable && variable.kind !== 'var' && variable.kind !== 'function-name') return true; }
return false;};
export const unknownValue = Symbol('Porffor.unknownValue');export const knownValue = (scope, node) => { if (!node) return undefined;
if (node.type === 'Literal' && node.value !== undefined) return node.value;
if (node.type === 'TemplateLiteral') { let out = '';
for (let i = 0; i < node.quasis.length; i++) { out += node.quasis[i].value.cooked;
if (i < node.expressions.length) { const value = knownValue(scope, node.expressions[i]); if (value === unknownValue) return unknownValue;
out += String(value); } }
return out; }
if (node.type === 'BinaryExpression') { const left = knownValue(scope, node.left); if (left === unknownValue) return unknownValue;
const right = knownValue(scope, node.right); if (right === unknownValue) return unknownValue;
switch (node.operator) { case '+': return left + right; case '-': return left - right; case '==': return left == right; case '===': return left === right; case '!=': return left != right; case '!==': return left !== right; } }
return unknownValue;};
const knownEvalSource = node => { const value = knownValue(null, node); return value === unknownValue ? null : String(value);};
const parseEval = node => { const code = knownEvalSource(node._evalSource ?? node.arguments[0]); if (code == null) return;
try { node._evalParsed = { type: 'BlockStatement', body: semantic(semantic.objectHack(parse(code)), node._semanticScopes).body }; } catch (e) { if (e.name !== 'SyntaxError') throw e; node._evalSyntaxError = e.message; }};
const evalCallKind = node => { if (node.callee.name === 'eval') return 'direct'; if (node.callee.type === 'SequenceExpression' && node.callee.expressions.at(-1)?.name === 'eval') return 'indirect'; if ( node.callee.type === 'MemberExpression' && !node.callee.computed && node.callee.object.name === 'eval' && node.callee.property.name === 'call' ) return 'call';};
const declVar = (name, kind, node) => { const func = scopes[scopes.lastFuncs.at(-1)]; // sloppy function decls hoist as var, but each loop iteration still makes a fresh binding if (kind === 'var' && node?.type === 'FunctionDeclaration') { for (let i = scopes.lastFuncs.at(-1) + 1; i < scopes.length; i++) { if (isLoopScope(scopes[i])) { node._loopScopedFuncDecl = true; break; } } } let parent; if (kind === 'var') { parent = nearestStrictEvalScope() ?? func; if (parent._variables?.[name]) { const existing = parent._variables[name]; existing.node = node; if (node && typeof node === 'object') node._variable = existing;
if (existing.internalName) renameBindingNode(node, existing.internalName); return; } } else { parent = scopes.at(-1); }
parent._variables ??= Object.create(null); if (node && typeof node === 'object') node._refs ??= 0;
const id = varId(name); let shadowsOuterFuncBinding = false; if (kind !== 'var') { const currentFuncInd = scopes.lastFuncs.at(-1); for (let i = currentFuncInd - 1; i >= 0; i--) { const variable = scopes[i]._variables?.[name]; if (!variable) continue; shadowsOuterFuncBinding = variable.func !== func && variable.scope?.type !== 'Program'; break; } } const activeEvalScope = nearestEvalScope(); const evalScope = parent._evalScope ? parent : (kind === 'var' ? null : activeEvalScope); const internalName = evalScope ? `${evalScope._evalName}_${name}${id > 0 ? `#${id}` : ''}` : (id > 0 || shadowsOuterFuncBinding ? `${name}#${id}` : null); const variable = { node, id, func, kind, scope: parent, internalName }; parent._variables[name] = variable; if (node && typeof node === 'object') node._variable = variable;
if (internalName) renameBindingNode(node, internalName);};
const analyzePattern = (kind, node) => { if (!node) return; switch (node.type) { case 'Identifier': node._binding = true; declVar(node.name, kind, node); break;
case 'RestElement': analyzePattern(kind, node.argument); break;
case 'AssignmentPattern': analyzePattern(kind, node.left); break;
case 'Property': analyzePattern(kind, node.value); break;
case 'ObjectPattern': for (const x of node.properties) { analyzePattern(kind, x.value); } break;
case 'ArrayPattern': for (const x of node.elements) { analyzePattern(kind, x); } break; }};
let scopes;let evalScopeId = 0;const analyze = (node, strict = false) => { if (!node) return;
const top = scopes.at(-1); if (node.directive === 'use strict') { top._strict = true; }
let openedScope = false; switch (node.type) { case 'ForStatement': case 'ForInStatement': case 'ForOfStatement': case 'SwitchStatement': case 'BlockStatement': scopes.push(node); openedScope = true; break;
case 'CatchClause': scopes.push(node); if (node.param) analyzePattern('let', node.param); openedScope = true; break;
case 'VariableDeclaration': for (const x of node.declarations) { if (x.id?.type === 'Identifier') x.id._declarator = x; analyzePattern(node.kind, x.id); } // only an initializer at the top of the func body dominates later reads if (scopes.length - 1 - scopes.lastFuncs.at(-1) <= 1) { for (const x of node.declarations) { if (x.init && x.id?.type === 'Identifier') x.id._storageTopDecl = true; } } break;
case 'CallExpression': if (evalCallKind(node)) { node._directEval = evalCallKind(node) === 'direct' && !node.optional; node._strictEval = node._directEval && (strict || top._strict); } break;
case 'ClassDeclaration': if (node.id?.name) { node.id._binding = true; declVar(node.id.name, 'let', node); } break;
case 'ClassExpression': scopes.push(node); openedScope = true; if (node.id?.name) { node.id._binding = true; declVar(node.id.name, 'function-name', node); } break;
case 'FunctionDeclaration': if (node.id?.name) { node.id._binding = true; declVar(node.id.name, (strict || evalLexicalConflict(node.id.name)) ? 'let' : 'var', node); } case 'FunctionExpression': case 'ArrowFunctionExpression': node._parentFunc = scopes[scopes.lastFuncs.at(-1)]; scopes.lastFuncs.push(scopes.length); scopes.push(node); openedScope = true;
// named function expressions bind their own name if (node.type === 'FunctionExpression' && node.id?.name) { node.id._binding = true; declVar(node.id.name, 'function-name', node); }
for (const p of node.params) analyzePattern('var', p); break; }
for (const x in node) { if (x[0] === '_') continue; const value = node[x]; if (value != null && typeof value === 'object') { if (Array.isArray(value)) { for (const y of value) analyze(y, strict || top._strict); } else if (value.type) analyze(value, strict || top._strict); } }
if (openedScope) { scopes.pop(); }
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { scopes.lastFuncs.pop(); }};
const annotate = (node, parent = null, key = null) => { if (!node) return;
let openedScope = false; let openedFunc = false; if (node._variables || node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { scopes.push(node); openedScope = true; if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { scopes.lastFuncs.push(scopes.length - 1); openedFunc = true; } }
switch (node.type) { case 'FunctionDeclaration': case 'FunctionExpression': case 'ArrowFunctionExpression': for (const param of node.params) recordStorageWrite(param, null); break;
case 'VariableDeclarator': annotate(node.id, node, 'id'); annotate(node.init, node, 'init'); if (node.init) recordStorageWrite(node.id, node.init); else if (node.id.type === 'Identifier') node.id._uninitialized = true; if (node.init && node.id._storageTopDecl) { const variable = node.id._variable ?? resolveVariable(node.id.name); if (variable) variable.node._storageInitSeen = true; } return;
case 'ForInStatement': case 'ForOfStatement': recordStorageWrite(node.left.type === 'VariableDeclaration' ? node.left.declarations[0]?.id : node.left, null); break;
case 'CatchClause': recordStorageWrite(node.param, null); break;
case 'AssignmentExpression': annotate(node.left, node, 'left'); annotate(node.right, node, 'right'); markWrite(node.left); recordStorageWrite(node.left, node.operator === '=' ? node.right : { type: ['||=', '&&=', '??='].includes(node.operator) ? 'LogicalExpression' : 'BinaryExpression', left: node.left, right: node.right, operator: node.operator.slice(0, -1) }); return;
case 'UpdateExpression': markWrite(node.argument); recordStorageWrite(node.argument, node); break;
case 'BinaryExpression': if (['==', '===', '!=', '!=='].includes(node.operator)) { const target = undefinedCheckTarget(node.left, node.right); if (target) markMaybeUndefinedStorage(target); } break;
case 'Identifier': if (node._binding) break; if (semantic.objectHackers.includes(node.name)) break;
const name = node.name; const currentFunc = scopes[scopes.lastFuncs.at(-1)]; const variable = resolveVariable(name); if (variable) { node._resolvedBinding = true; node._resolvedVariable = variable; variable.node._refs = (variable.node._refs ?? 0) + 1; // reads from other funcs or before the dominating init can see undefined if (variable.func !== currentFunc || !variable.node._storageInitSeen) { node._noStorageInfer = true; variable.node._storageHazardRef = true; } if (parent?.type === 'CallExpression' && key === 'callee' && !parent.optional) { variable.node._directCallRefs = (variable.node._directCallRefs ?? 0) + 1; variable.node._directCallMinStart = variable.node._directCallMinStart == null ? node.start : Math.min(variable.node._directCallMinStart, node.start); } else { variable.node._valueRefs = (variable.node._valueRefs ?? 0) + 1; } if (variable.internalName) node.name = variable.internalName;
if ( (variable.node.type === 'FunctionDeclaration' || variable.node.type === 'FunctionExpression' || variable.node.type === 'ClassExpression') && variable.node.id?.name === node.name && isSelfReferenceContext(currentFunc, variable.node) ) { if (variable.node.type !== 'ClassExpression') variable.node._selfAware = true; node._selfBinding = variable.node; } if (variable.node.type === 'ClassExpression' && variable.scope?.type === 'ClassExpression') { node._selfBinding = variable.node; node._classBinding = true; }
if (variable.func && variable.scope?.type !== 'ClassExpression') { if (variable.func !== currentFunc) { if (variable.scope?.type !== 'Program') { node._closureFunc = variable.func;
currentFunc._captures ??= Object.create(null); currentFunc._captures[node.name] = { func: variable.func, kind: variable.kind, node: variable.node, perIteration: (variable.kind !== 'var' && bindingHasLoopScope(variable)) || !!variable.node?._loopScopedFuncDecl };
variable.func._capturedVars ??= Object.create(null); variable.func._capturedVars[node.name] = { kind: variable.kind, node: variable.node };
markClosurePassThrough(currentFunc, variable.func); } } else if (currentFunc._capturedVars?.[node.name]) { node._closureFunc = currentFunc; } } }
if (!variable && name === 'arguments') { for (let i = scopes.length - 1; i >= 0; i--) { const scope = scopes[i]; if (scope.type === 'FunctionDeclaration' || scope.type === 'FunctionExpression') { scope._usesArguments = true; return; } } } break;
case 'ThisExpression': { const currentFunc = scopes[scopes.lastFuncs.at(-1)]; const owner = findLexicalThisOwner(currentFunc); if (owner) { node._closureThisFunc = owner; currentFunc._capturesThis = owner; owner._capturedThis = true; markClosurePassThrough(currentFunc, owner); } break; }
case 'MemberExpression': if (node.computed) annotate(node.property, node, 'property'); annotate(node.object, node, 'object'); return;
case 'PropertyDefinition': case 'Property': if (node.computed) annotate(node.key, node, 'key'); annotate(node.value, node, 'value'); return;
case 'CallExpression': { const evalKind = evalCallKind(node); if (evalKind) { node._evalScope = true; node._evalName ??= `#eval_${evalScopeId++}`; if (evalKind !== 'direct' || node.optional) { // indirect eval, only top scope node._indirectEval = true; node._semanticScopes = [ scopes[0], node ]; node._semanticScopes.lastFuncs = [ 0 ]; } else { // direct eval, use existing scope node._semanticScopes = Object.assign([], scopes); node._semanticScopes.lastFuncs = scopes.lastFuncs.slice(); node._semanticScopes.push(node); }
if (evalKind === 'call') node._evalSource = node.arguments[1]; parseEval(node); } break; }
case 'NewExpression': if (node.callee.name === 'Function') { // new Function(...) - use global scope and self as scope node._semanticScopes = [ scopes[0], node ]; node._semanticScopes.lastFuncs = [ 0, 1 ]; } break; }
for (const x in node) { if (x[0] === '_') continue; const value = node[x]; if (value != null && typeof value === 'object') { if (Array.isArray(value)) { for (const y of value) annotate(y, node, x); } else if (value.type) annotate(value, node, x); } }
if (openedScope) { scopes.pop(); }
if (openedFunc) { scopes.lastFuncs.pop(); }};
const semantic = (node, _scopes = null) => { const oldScopes = scopes; if (!_scopes) { _scopes = [ node ]; _scopes.lastFuncs = [ 0 ]; } scopes = _scopes;
analyze(node, !!scopes.at(-1)?._strictEval); if (scopes.length !== _scopes.length) throw new Error('Scope mismatch');
annotate(node); scopes = oldScopes; return node;};export default semantic;semantic.objectHack = x => x;