UNPKG

preval

Version:

Wannabe compiler tool for JS

572 lines (506 loc) 26.1 kB
import walk from '../lib/walk.mjs'; import { log, group, groupEnd, ASSERT, BLUE, RESET } from './utils.mjs'; import globals from './globals.mjs'; import * as Tenko from '../lib/tenko.prod.mjs'; // This way it works in browsers and nodejs and github pages ... :/ // This phase is fairly mechanical. Setup scope tracking, imports/exports tracking, return value analysis. That sort of thing. export function phase1(program, fdata, resolve, req) { const ast = fdata.tenkoOutput.ast; const tokenTable = fdata.tokenTable; const funcStack = []; const lexScopeStack = []; let lexScopeCounter = 0; const funcScopeStack = []; const thisStack = []; const globallyUniqueNamingRegistery = new Map(); fdata.globallyUniqueNamingRegistery = globallyUniqueNamingRegistery; const imports = new Map(); fdata.imports = imports; const exports = new Map(); fdata.exports = exports; group('\n\n\n##################################\n## phase1 :: ' + fdata.fname + '\n##################################\n\n\n'); function findUniqueNameForBindingIdent(node, startAtParent = false) { ASSERT(node && node.type === 'Identifier', 'need ident node for this', node); log('Finding unique name for `' + node.name + '`'); let index = lexScopeStack.length; if (startAtParent) --index; // For example: func decl id has to be looked up outside its own inner scope while (--index >= 0) { log('- lex level', index, ': lex id:', lexScopeStack[index].$p.lexScopeId, ':', lexScopeStack[index].$p.nameMapping.has(node.name)); if (lexScopeStack[index].$p.nameMapping.has(node.name)) { break; } } if (index < 0) { log('The ident `' + node.name + '` could not be resolved'); linter.check('IMPLICIT_GLOBAL', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }, node.name); // Register one... log('Creating global binding for `' + node.name + '` now'); lexScopeStack[0].$p.nameMapping.set(node.name, node.name); index = 0; } const uniqueName = lexScopeStack[index].$p.nameMapping.get(node.name); ASSERT(uniqueName !== undefined, 'should exist'); log('Should be bound in scope index', index, 'mapping to `' + uniqueName + '`'); return uniqueName; } let uid = 0; walk(_walker, ast, 'ast'); function _walker(node, before, nodeType, path) { ASSERT(node, 'node should be truthy', node); ASSERT(nodeType === node.type); if (before) { let tid = String(++uid); let needleNode = node; if (needleNode.type === 'TemplateElement') needleNode = path.nodes[path.nodes.length - 2]; const key = needleNode.loc.start.line + ':' + needleNode.loc.start.column; const token = tokenTable.get(key); ASSERT(token, 'each node start should correspond to a token at that position', key, tokenTable, needleNode, path); node.$p = { tid, // Incremental unique id (may have gaps between consecutive nodes but will be unique) explicitReturns: '', // Set if the node is a branching type where it matters tokenIndex: token.n, // Number. Token index on linear list of tokens. First token forward from this node // Add properties here in a comment but not actually (you would do this for perf) because it makes debugging more noisy //scope: undefined, // Only functions get this (not arrows, not global) //nameMapping: undefined, // Map. Scope tracking for nodes that have a scope //scopeBinding: undefined, // Reference to nearest func/global map. Global/func nodes get a fresh Map. //thisAccess: undefined, // boolean. For functions (not arrows), whether it access `this` anywhere in its own scope. Includes whether nested arrows access this in its scope. But not nested functions }; } group(BLUE + nodeType + ':' + (before ? 'before' : 'after'), RESET); const key = nodeType + ':' + (before ? 'before' : 'after'); if (before && node.$scope) { lexScopeStack.push(node); node.$p.lexScopeId = ++lexScopeCounter; node.$scope.$sid = lexScopeCounter; if (['Program', 'FunctionExpression', 'ArrowFunctionExpression', 'FunctionDeclaration'].includes(node.type)) { funcScopeStack.push(node); } } // Assign unique names to bindings to work around lex scope shadowing `let x = 1; { let x = 'x'; }` // This allows us to connect identifier binding references that belong together, indeed together, and distinct a // binding from its shadow by the same name. Otherwise in the previous example, we'd never know "which" x is x. if (before && node.$scope) { // lex binding can look up its unique global name through this (nearest) mapping if (node.type === 'Program') { // global scope globals.forEach((_, key) => globallyUniqueNamingRegistery.set(key, { // ident meta data uid: ++uid, updates: [], // {parent, prop, index} indirect reference ot the node being assigned usages: [], // {parent, prop, index} indirect reference to the node that refers to this binding }), ); node.$p.nameMapping = new Map([...globals.keys(), 'module'].map((k) => [k, k])); } else { // non-global scope node.$p.nameMapping = new Map([ ['this', 'this'], ['arguments', 'arguments'], ]); } const funcNode = funcScopeStack[funcScopeStack.length - 1]; if (funcNode === node) funcNode.$p.scopeBindings = new Map(); const scopeBindings = funcNode.$p.scopeBindings; ASSERT(scopeBindings); let s = node.$scope; ASSERT( ['FunctionExpression', 'FunctionDeclaration'].includes(node.type) ? s.type === Tenko.SCOPE_LAYER_FUNC_BODY : true, 'scope type is body, which we ignore (perhaps not for arrows?)', node.$scope, ); do { group('Checking scope... (sid=', s.$sid, ')'); log('- type:', s.type, ', bindings?', s.names !== Tenko.HAS_NO_BINDINGS); if (node.type === 'BlockStatement' && s.type === Tenko.SCOPE_LAYER_FUNC_PARAMS) { log('Breaking for function header scopes in Block'); groupEnd(); break; } if (s.names === Tenko.HAS_NO_BINDINGS) { log('- no bindings in this scope, parent:', s.parent && s.parent.type); } else if ( ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression'].includes(node.type) && s.type === Tenko.SCOPE_LAYER_FUNC_BODY ) { log('- ignoring scope body in function node'); } else if (node.type === 'CatchClause' && s.type !== Tenko.SCOPE_LAYER_CATCH_HEAD && s.type !== Tenko.SCOPE_LAYER_CATCH_BODY) { log('- in catch clause we only care about the two catch scopes'); break; } else if (node.type === 'BlockStatement' && s.type === Tenko.SCOPE_LAYER_GLOBAL) { log('- do not process global scope in block'); break; } else if ( node.type === 'BlockStatement' && s.type === Tenko.SCOPE_LAYER_FUNC_BODY && !['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression'].includes(path.nodes[path.nodes.length - 2].type) ) { log('- do not process func scope in a block that is not child of a function'); break; } else { s.names.forEach((v, name) => { log('-', name, ':', v); if (v === Tenko.BINDING_TYPE_VAR && funcNode !== node) { // only process `var` bindings in the scope root log(' - skipping var because not scope root'); return; } if (v === Tenko.BINDING_TYPE_FUNC_VAR && s.type === Tenko.SCOPE_LAYER_FUNC_PARAMS) { log(' - skipping func var in param layer or global layer'); return; } // Create a (module) globally unique name. Then use that name for the local scope. let n = 0; if (globallyUniqueNamingRegistery.has(name)) { while (globallyUniqueNamingRegistery.has(name + '_' + ++n)); } log('Adding `' + name + '` to:'); log('- globallyUniqueNamingRegistery -->', n ? name + '_' + n : name); globallyUniqueNamingRegistery.set(name + (n ? '_' + n : ''), { // ident meta data uid: ++uid, // Track all cases where a binding value itself is initialized/mutated (not a property or internal state of its value) // Useful recent thread on binding mutations: https://twitter.com/youyuxi/status/1329221913579827200 // var/let a; // var/const/let a = b; // const a = b; // import a, b as a, * as a, {a, b as a} from 'x'; // export var/let a // export var/const/let a = b // function a(){}; // export function a(){}; // a = b; // a+= b; // var/const/let [a, b: a] = b; // [a, b: a] = b; // var/const/let {a, b: a} = b; // ({a, b: a} = b); // [b: a] = c; // ++a; // a++; // for (a in b); // for ([a] in b); // for ({a} in b); // In a nutshell there are six concrete areas to look for updates; // - [x] binding declarations // - [x] regular // - [ ] destructuring // - [ ] exported // - [x] could be inside `for` header // - [ ] assigning // - [ ] regular // - [ ] compound // - [ ] destructuring array // - [ ] destructuring object // - [ ] imports of any kind // - [ ] function declarations // - [ ] update expressions, pre or postifx, inc or dec // - [ ] for-loop lhs updates: [], // {parent, prop, index} indirect reference ot the node being assigned usages: [], // {parent, prop, index} indirect reference to the node that refers to this binding }); log('- the scope binding for node at linecol:', funcNode.loc.start.line + ':' + funcNode.loc.start.column); scopeBindings.set(name + (n ? '_' + n : ''), uid); log('- the mapping for node at linecol:', node.loc.start.line + ':' + node.loc.start.column); node.$p.nameMapping.set(name, name + (n ? '_' + n : '')); }); } // Only certain nodes have hidden scopes to process. For any other node do not process the parent. if ( !['FunctionExpression', 'ArrowFunctionExpression', 'ArrowFunctionExpression', 'FunctionDeclaration', 'CatchClause'].includes( node.type, ) ) { groupEnd(); break; } groupEnd(); } while (s.type !== Tenko.SCOPE_LAYER_GLOBAL && (s = s.parent)); // Each node should now be able to search through the lexScopeStack, and if any of them .has() the name, it will // be able to .get() the unique name, which can be used in either the root scope or by the compiler in phase2. log( 'node.$p.nameMapping:', new Map( [...node.$p.nameMapping.entries()].filter(([tid]) => node.type === 'Program' ? !globals.has(tid) : !['this', 'arguments'].includes(tid), ), ), ); log('nearest func scope bindings:', [...scopeBindings.entries()].map(([key, value]) => key + ': ' + value).join(', ')); log('node loc;', node.loc.start.line + ':' + node.loc.start.column); } switch (key) { case 'Program:before': { funcStack.push(node); break; } case 'Program:after': { funcStack.pop(); log('-->', node.$p.scopeBindings); break; } case 'FunctionDeclaration:before': case 'FunctionExpression:before': case 'ArrowFunctionExpression:before': { funcStack.push(node); node.$p.returnNodes = []; if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') { node.$p.scope = { type: 'zscope', names: new Map() }; thisStack.push(node); } break; } case 'FunctionDeclaration:after': case 'FunctionExpression:after': case 'ArrowFunctionExpression:after': { funcStack.pop(); if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') { thisStack.pop(); } let lexes = lexScopeStack.slice(1); while (lexes[0] && ['FunctionDeclaration', 'FunctionExpression', 'ArrowFunctionExpression'].includes(lexes[0].type)) lexes.shift(); // Drop global block scopes etc //const map = new Map(); //lexes.forEach((node) => { // node.$p.nameMapping.forEach((newName, bindingName) => { // if (bindingName === 'this' || bindingName === 'arguments') return; // // Binding name may exist. We only care about the inner-most shadow. // map.set(bindingName, newName); // }); //}); //node.$p.reachableNames = map; node.$p.parentScope = funcStack[funcStack.length - 1]; log('-->', node.$p.scopeBindings); break; } case 'CatchClause:before': { // Note: the catch scope is set on node.handler of the try (parent node of the catch clause) break; } case 'ClassExpression:after': case 'ClassDeclaration:after': { break; } case 'Identifier:before': { log('Ident:', node.name); const parentNode = path.nodes[path.nodes.length - 2]; const parentProp = path.props[path.props.length - 1]; log('Parent node: `' + parentNode.type + '`, prop: `' + parentProp + '`'); if ( // We wouldn't want to process the property name of `foo.bar`, but we would want to process `foo`, or a dynamic property `foo[bar]` !(parentNode.type === 'MemberExpression' && parentProp === 'property' && !parentNode.computed) && !( (parentNode.type === 'FunctionDeclaration' || parentNode.type === 'FunctionExpression' || parentNode.type === 'ArrowFunctionExpression') && parentProp === 'params' ) ) { ASSERT(!node.$p.uniqueName, 'dont do this twice'); const uniqueName = findUniqueNameForBindingIdent(node, parentNode.type === 'FunctionDeclaration'); log('- unique name:', uniqueName); node.$p.uniqueName = uniqueName; //// Do not include the declarations as references (although they could be eliminated...?) //if (parentNode.type !== 'VariableDeclarator') { const obj = globallyUniqueNamingRegistery.get(uniqueName); obj.usages.push({parent: parentNode, prop: parentProp, index: -1}); // TODO: index //} } break; } case 'VariableDeclarator:after': { // Find all bindings, resolve their unique name, copy their init (or undefined) to the updates // If we're going to store these then what happens when they're transformed/replaced? // `var a = 1; var b = a; var c = b;`. maybe store the parent node and key instead for an indirect lookup ASSERT(node.id?.type === 'Identifier', 'tofix: var declarations that are not just idents'); const obj = globallyUniqueNamingRegistery.get(node.id.$p.uniqueName); const parent = node; // path.nodes[path.nodes.length - 2]; const prop = 'init'; // path.props[path.props.length - 1]; log('Marking `' + node.id.$p.uniqueName + '` as being updated to', parent.type + '.' + prop + ' (=' + node.init?.type + ')'); obj.updates.push({ parent, prop, index: -1 }); break; } //case 'AssignmentExpression': { // console.log(node); // process.exit() //} case 'ImportDeclaration:before': { ASSERT(node.source && typeof node.source.value === 'string', 'fixme if else', node); const source = node.source.value; ASSERT(typeof resolve === 'function', 'resolve must be a function here', resolve); const resolvedSource = resolve(source, fdata.fname); ASSERT(node.specifiers, 'fixme if different', node); node.specifiers.forEach((snode) => { const id = snode.local; ASSERT(id.type === 'Identifier', 'fixme if local is not an ident', snode); if (snode.type === 'ImportNamespaceSpecifier') { ASSERT(snode.type === 'ImportNamespaceSpecifier'); // This will trigger the file to be processed. It'll be added to the list if new, it will be bumped to the // top of the queue if not finished processing yet. It will resolve before this file. imports.set(id.name, resolvedSource); } else if (snode.type === 'ImportDefaultSpecifier') { // This will trigger the file to be processed. It'll be added to the list if new, it will be bumped to the // top of the queue if not finished processing yet. It will resolve before this file. imports.set(id.name, resolvedSource); } else { ASSERT(snode.imported, 'fixme', snode.type, snode); // This will trigger the file to be processed. It'll be added to the list if new, it will be bumped to the // top of the queue if not finished processing yet. It will resolve before this file. imports.set(id.name, resolvedSource); } }); break; } case 'ImportDefaultSpecifier: after': { ASSERT(node.source && typeof node.source.value === 'string', 'fixme if else', node); const source = node.source.value; const resolvedSource = resolve(source, filename); ASSERT(node.specifiers, 'fixme if different', node); // This will trigger the file to be processed. It'll be added to the list if new, it will be bumped to the // top of the queue if not finished processing yet. It will resolve before this file. imports.set('default', resolvedSource); break; } case 'MemberExpression:after': { break; } case 'Super:after': { break; } case 'ThisExpression:after': { if (thisStack.length) { log('Marking func as having `this` access'); thisStack[thisStack.length - 1].$p.thisAccess = true; } else { store.linter.check('GLOBAL_THIS', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }); } break; } case 'ReturnStatement:before': { node.$p.explicitReturns = 'yes'; // Per definition :) funcStack[funcStack.length - 1].$p.returnNodes.push(node); break; } // block, if, else, try, catch, finally, switch, case, default, [with], label. Not the loops, can't guarantee them case 'BlockStatement:after': // node.body // If there is a node that has explicitReturns=yes and none of the nodes that precede it is break/continue/throw, // then the block returns. Otherwise the block does not return. for (let i = 0; i < node.body.length; ++i) { const snode = node.body[i]; if (snode.$p.explicitReturns === 'yes') { node.$p.explicitReturns = 'yes'; break; } if (node.type === 'BreakStatement' || node.type === 'ContinueStatement' || node.type === 'ThrowStatement') { break; } } if (node.$p.explicitReturns !== 'yes') node.$p.explicitReturns = 'no'; log('- explicitReturns:', node.$p.explicitReturns); break; case 'IfStatement:after': // there is no ElseStatement (!), node.consequent and node.alternate // The problem here is that each branch is not visited explicitly so we can't queue up a return tid for // the `if` and the `else` separately. That's a little annoying. So we have to retroactively check the // last statement, instead. if (node.consequent.$p.explicitReturns !== 'yes') { // The `if` statement must exist. If it is not returning then this doesn't either node.$p.explicitReturns = 'no'; } else if (!node.alternate || node.alternate.$p.explicitReturns !== 'yes') { // The `else` may not exist, in that case the whole thing doesn't return. Otherwise it only returns if the // sub-statement returns. node.$p.explicitReturns = 'no'; } else { node.$p.explicitReturns = 'yes'; } log('- explicitReturns:', node.$p.explicitReturns); break; // case 'TryStatement:before': { // // This is the catch scope (!) // node.handler.$p = {}; // node.handler.$p.scope = {type: 'zscope', names: new Map}; // break; // } case 'TryStatement:after': // node.block, node.handler, node.finalizer // Tricky case. The `try` node returns if; // - there is a finally block; the node returns when the finally returns, and does not when finally does not // - there is only a catch block and both the blocks return // Note that we ignore explicit `throw` statements, but we could later improve that situation. if (node.finalizer) { // If there is a finalizer block, I only care about the explicit return state of that block now. // This is because it is guaranteed to be visited, and its return value trumps that of the try/catch blocks. node.$p.explicitReturns = node.finalizer.$p.explicitReturns === 'yes' ? 'yes' : 'no'; } else if (node.block.$p.explicitReturns !== 'yes') { // There is no finally, so the whole node cannot be explicitReturn if the `try` block is not node.$p.explicitReturns = 'no'; } else if (node.handler.body.$p.explicitReturns !== 'yes') { // There is no finally, so the whole node cannot be explicitReturn if the `catch` block is not node.$p.explicitReturns = 'no'; } else { // No finally and try and catch blocks return, so the whole node returns node.$p.explicitReturns = 'yes'; } log('- explicitReturns:', node.$p.explicitReturns); break; case 'SwitchStatement:after': // node.cases // Only returns if all cases return AND one of the cases is a default // Tricky case is when the case falls through and returns a following case. // No real difference between case and default, except the default is mandatory for the switch to return expl. let hasDefault = false; // For each case check whether it returns explicitly (and before a break/continue) and if it doesn't, check // the next case as well. Mark a default case but don't treat it differently otherwise. When all case blocks // explicitly return then the whole switch node returns explicitly. // In other words, this fails if for any case a `break/continue` is seen before a `return`, or when no `return` // is seen at all since the start of the last `case`. // Note: a node that is marked explicitReturns is similar to a return statement because all its branches return. let seenReturnSinceLastCase = false; let brokeBeforeReturned = false; for (let i = 0; i < node.cases.length && !brokeBeforeReturned; ++i) { seenReturnSinceLastCase = false; const caseNode = node.cases[i]; if (!caseNode.test) hasDefault = true; caseNode.consequent.some((node) => { if (node) { if (node.$p.explicitReturns || node.type === 'ReturnStatement') { // Either this is the return statement, or it's a statement where all branches must lead to a return. seenReturnSinceLastCase = true; return true; } if (node.type === 'BreakStatement' || node.type === 'ContinueStatement') { brokeBeforeReturned = true; return true; } } }); } if (!seenReturnSinceLastCase || brokeBeforeReturned || !hasDefault) { // Either no return statement was seen since, at least, the last case start, or at least one case exists // that had a break/continue before the return statement. Either way, this switch is not an explicit return. node.$p.explicitReturns = 'no'; } else { // For all cases there was at least one return statement before the end of the swtich that was not preceded // by a break or continue. This switch properly returns explicitly for all branches. node.$p.explicitReturns = 'yes'; } log('- explicitReturns:', node.$p.explicitReturns); break; case 'WithStatement:after': // node.body ASSERT(false, 'with is not allowed in a "strict" context. The parser should have rejected this.'); break; case 'LabeledStatement:after': // node.body TODO; // add tests node.$p.explicitReturns = node.body.$p.explicitReturns === 'yes' ? 'yes' : 'no'; log('- explicitReturns:', node.$p.explicitReturns); break; } if (!before && node.$p.scope) { lexScopeStack.pop(); if (['Program', 'FunctionExpression', 'ArrowFunctionExpression', 'FunctionDeclaration'].includes(node.type)) { funcScopeStack.pop(); //} else { // const funcScope = funcScopeStack[funcScopeStack.length - 1]; } } groupEnd(); } log('globallyUniqueNamingRegistery:', globallyUniqueNamingRegistery); log('End of phase 1'); groupEnd(); }