UNPKG

preval

Version:

Wannabe compiler tool for JS

1,216 lines (1,045 loc) 77.9 kB
import * as Tenko from '../lib/tenko.prod.mjs'; // This way it works in browsers and nodejs and github pages ... :/ import { ASSERT, DIM, BOLD, RESET, BLUE, dir, group, groupEnd, log, printNode } from './utils.mjs'; // Functions have been hoisted, scopes have been resolved, time to merge AST nodes and leave as few nodes as possible // without actually interpreting code. // For example, return statement return value tid get merged with the return keyword and the whole statement node. const E = eval; // indirect eval. Because it's safe that way. lol. function linter() { console.log('TOFIX: LINTER'); } linter.check = linter; // OH NO HE DIDNT export function phase2(program, fdata, resolve, req) { const tokenTable = fdata.tokenTable; const tokens = fdata.tenkoOutput.tokens; const lexScopeStack = []; const rootScopeStack = []; const superCallStack = []; // `super()` is validated by the parser so we don't have to worry about scoping rules // Crumb path for walking through the AST. This way you can reach out to parent nodes and manipulate them or whatever. Shoot your own foot. const crumbsNode = []; const crumbsProp = []; const crumbsIndex = []; group('\n\n\n##################################\n## phase2 :: ' + fdata.fname + '\n##################################\n\n\n'); //let actions = []; stmt(null, 'ast', -1, fdata.tenkoOutput.ast); //$(fileState.tenkoOutput.ast, '@log', 'End of Program'); //fileState.globalActions = actions; //log('\nCompiled actions:'); //dir(actions, {depth: null}); //log('\nprint friendly actions:'); //const printFriendly = actions.slice(0,20).map(function r(action, _, __, indent = ' ') { // if (action[0] === '@func') { // return indent + action[0] + ',' + action[3].slice(0, 5) + ', ' + action[3].slice(7).join(', ') + action.slice(4).join(', ') + ':\n' + action[3][6].map(a => r(a, _, __, indent + ' ')).join('\n'); // } // return indent + action.join(', '); //}).join('\n').split('\n'); //log('\n' + printFriendly.slice(0, 100).join('\n'), printFriendly.length >= 100 ? '(... ' + (printFriendly.length - 100) + ' more actions suppressed...)' : ''); groupEnd(); //function $(node, action, ...args) { // const {loc: {start: {column, line}}} = node; // actions.push([action, column, line, args]); //} //function _$(node, action, ...args) { // const {loc: {start: {column, line}}} = node; // actions.unshift([action, column, line, args]); //} function getFirstToken(node) { ASSERT(node, 'getFirstToken wants a node', node); const token = tokenTable.get(node.loc.start.line + ':' + node.loc.start.column); ASSERT(token, 'each ast node should be able to find the first token of that node', node, token); return token; } function getPrevTokenPastGroupAndSpaces(node) { const tokenRhs = getFirstToken(node); let tokenPrev = tokens[tokenRhs.n - 1]; while (tokenPrev && (Tenko.isWhiteToken(tokenPrev.type) || tokenPrev.str === '(')) { tokenPrev = tokens[tokenPrev.n - 1]; } ASSERT(tokenPrev, 'should have found a token', tokenPrev); return tokenPrev; } 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,':', 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; } function crumb(parent, prop, index) { crumbsNode.push(parent); crumbsProp.push(prop); crumbsIndex.push(index); } function uncrumb(parent, prop, index) { const a = crumbsNode.pop(); const b = crumbsProp.pop(); const c = crumbsIndex.pop(); ASSERT(b === true || (a === parent, b === prop, c === index), 'ought to pop the same as we pushed. just a sanity check.'); } function stmt(parent, prop, index, node, isExport) { ASSERT( parent === null || index === 'body' || (Array.isArray(parent[prop]) ? parent[prop][index] === node : parent[prop] === node && index === -1), 'caller should pass on proper parent[prop][index] data', parent, prop, index, node, isExport, ); crumb(parent, prop, index); _stmt(node, isExport); uncrumb(parent, prop, index); } function _stmt(node, isExport = false) { group(DIM + 'stmt(' + RESET + BLUE + node.type + RESET + DIM + ')' + RESET); if (node.$scope || (node.type === 'TryStatement' && node.handler)) { if (node.$scope) lexScopeStack.push(node); else lexScopeStack.push(node.handler); if (['Program', 'FunctionExpression', 'ArrowFunctionExpression', 'FunctionDeclaration'].includes(node.type)) { rootScopeStack.push(node); } } switch (node.type) { case 'BlockStatement': { node.body.forEach((cnode, i) => stmt(node, 'body', i, cnode)); break; } case 'BreakStatement': { // TODO: labels. Can we simplify this? Should we? break; } case 'ClassDeclaration': { processClass(node, false, isExport); break; } case 'ContinueStatement': { // TODO: labels. Can we simplify this? Should we? break; } case 'DebuggerStatement': { break; } case 'DoWhileStatement': { stmt(node, 'body', -1, node.body); expr(node, 'test', -1, node.test); break; } case 'ExportAllDeclaration': { // export * from 'foo' // TODO: replace the star with the actual symbols being exported...? const source = node.source.value; ASSERT(typeof source === 'string'); linter.check('TOFIX', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }, 'todo_export_star'); break; } case 'ExportDefaultDeclaration': { // export default expr // export default function(){} // export default class(){} // Note: imports/exports are not properly tracked. Only do the effort to cover executed code for now. if (node.declaration.type === 'FunctionDeclaration' || node.declaration.type === 'ClassDeclaration') { log('Export defaulting a function or class. Treating it a regular declaration that exports as `default`.'); // It's kind of relevant to parse them as statements because of how their id gets bound stmt(node, 'declaration', -1, node.declaration, 'default'); } else { expr(node, 'declaration', -1, node.declaration); } break; } case 'ExportNamedDeclaration': { // These have a declaration and no specifiers: // - export function f (){} // - export class g {} // - export let a // - export const b = 1 // - export var c = 2 // These have specifiers and no declaration. One specifier per exported symbols. // Type of export may affect type of specifier node // - var d,e; export {d,e} // - export {x} // - export {x as y} // - export x from 'foo' // - export {x} from 'foo' // - export * as y from 'foo' (not same as only *) // - export {x as z} from 'foo' if (node.source) { // export ... from linter.check('TOFIX', locFrom, callerTee.tid); } else if (node.declaration) { ASSERT( node.declaration.type === 'FunctionDeclaration' || node.declaration.type === 'VariableDeclaration' || node.declaration.type === 'ClassDeclaration', 'fixme', node, ); stmt(node, 'declaration', -1, node.declaration, true); } else { node.specifiers.forEach((snode) => { ASSERT(snode.local.type === 'Identifier', 'fixme if different', snode); ASSERT(snode.exported.type === 'Identifier', 'fixme if different', snode); //$(snode.local, '@ident', snode.local.name); //getFirstToken(snode.local).tlog = []; //$(snode, '@export_as', snode.exported.name); }); } break; } case 'ExpressionStatement': { expr(node, 'expression', -1, node.expression); break; } case 'EmptyStatement': { break; } case 'ForStatement': { if (node.init) { if (node.init.type === 'VariableDeclaration') { stmt(node, 'init', -1, node.init); } else { expr(node, 'init', -1, node.init); } } if (node.test) { expr(node, 'test', -1, node.test); } if (node.update) { expr(node, 'update', -1, node.update); } stmt(node, 'body', -1, node.body); break; } case 'ForInStatement': { expr(node, 'right', -1, node.right); if (node.left.type === 'VariableDeclaration') { ASSERT(node.left.declarations.length === 1, 'I think this is syntactically enforced?'); ASSERT(node.left.declarations[0].type === 'VariableDeclarator'); const id = node.left.declarations[0].id; ASSERT(id && id.type === 'Identifier'); ASSERT(!node.left.declarations[0].init, 'for-in lhs should not be allowed to have an init'); //log('For-in lhs:'); //const uniqueName = findUniqueNameForBindingIdent(id); //$(id, '@binding', uniqueName, node.left.kind); } else { expr(node, 'left', -1, node.left); } stmt(node, 'body', -1, node.body); break; } case 'ForOfStatement': { // TODO: This needs proper support for iterable stuff for true support. We could start with superficial support. if (node.await) linter.check('TOFIX', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }, 'todo_for_await'); // Get the kind of the type of the rhs. Initially, that means string for strings and kind for arrays. expr(node, 'right', -1, node.right); if (node.left.type === 'VariableDeclaration') { ASSERT(node.left.declarations.length === 1, 'I think this is syntactically enforced?'); ASSERT(node.left.declarations[0].type === 'VariableDeclarator'); const id = node.left.declarations[0].id; ASSERT(id && id.type === 'Identifier'); ASSERT(!node.left.declarations[0].init, 'for-in lhs should not be allowed to have an init'); //log('For-of lhs:'); //const uniqueName = findUniqueNameForBindingIdent(id); //$(id, '@binding', uniqueName, node.left.kind); } else { expr(node, 'left', -1, node.left); } stmt(node, 'body', -1, node.body); break; } case 'FunctionDeclaration': { //let abak = actions; //actions = [] // //$(node, '@log', 'start of func decl ' + (node.id ? node.id.name : '{anon}')); // //$(node, '@this'); // Top of the stack ought to be the context //$(node, '@arguments'); const { minParamRequired, hasRest, paramBindingNames } = processFuncArgs(node); //$(node, '@body_start'); stmt(node, 'body', -1, node.body); log('The explicitReturns value for its body is:', [node.body.$p.explicitReturns]); if (node.body.$p.explicitReturns !== 'yes') { // Implicitly return `undefined` //$(node, '@push', 'undefined'); //$(node, '@return'); } //let funcActions = actions; //actions = abak; if (node.id) { group('Function decl id:'); const uniqueName = findUniqueNameForBindingIdent(node.id, true); groupEnd(); //_$(node.id, '@binding', uniqueName, 'lex'); } //else linter.check('TOFIX', {filename, line: node.loc.start.line, column: node.loc.start.column}, 'todo_export_default_without_name'); // I mean this'll just crash? :) // For functions, focus on the `function` keyword //let funcToken = getFirstToken(node); //while (funcToken && (funcToken.str === 'export' || funcToken.str === 'default' || Tenko.isWhiteToken(funcToken.type))) { // funcToken = tokens[funcToken.n + 1]; //} //ASSERT(funcToken.str === 'function', 'pretty sure this is an invariant', funcToken); //funcToken.tlog = []; if (isExport) { ASSERT(node.id, 'exported functions (not by default) must have an id as per syntax'); //log('Registering as exported default value'); //_$(node, '@export_as', isExport === true ? node.id.name : 'default'); //_$(node, '@dup', '(exported func value)'); // Note: _$ is reverse order } //const desc = 'Function<' + (node.id ? node.id.name : '<anon>') + ': line ' + node.loc.start.line + ', column ' + node.loc.start.column + '>'; //_$(node, '@func', 'N' + node.$p.tid + '=' + (node.id ? node.id.name : 'anon'), node.id ? node.id.name : '', node.params.map(node => node.name), paramBindingNames, hasRest, minParamRequired, funcActions, 'decl', !!node.$p.thisAccess, node.$p.reachableNames, funcToken.n, filename, node.loc.start.column, node.loc.start.line, desc); if (node.body.length === 0) { // Empty function. Eliminate. node.$p.replaceWith = 'undefined'; } if (node.body.length === 1 && node.body[0].type === 'ReturnStatement') { // TODO: function f(a = sideEffects()) { return 'x' } if (node.body[0].argument) { // function f(){ return 'x'; }f() -> 'x' // TODO: function f(a) { return a } // TODO: let n = 1; function f() { return n } if (node.body[0].argument.type === 'Literal') { // So this is a function with only a return statement and it returns a literal. // TODO: side effects are possible in the param defaults. But barring that... // There are no other side effects so this function can essentially be replaced with the literal for any call of it // If this function now gets resolved as being called then the call will be replaced with this literal. Baby steps. node.$p.replacedBy = node.body[0].argument; } node.$p.replaceWith = node.body[0].argument; } else { // Eh ok. Good test, I guess. node.$p.replaceWith = 'undefined'; } } break; } case 'IfStatement': { expr(node, 'test', -1, node.test); //$(node, '@condition'); stmt(node, 'consequent', -1, node.consequent); if (node.alternate) stmt(node, 'alternative', -1, node.alternate); break; } case 'ImportDeclaration': { const source = node.source; ASSERT(typeof source.value === 'string', 'source should be a string', node); ASSERT(typeof resolve === 'function' || resolve === undefined, 'resolve should be a func or unset', resolve); const from = resolve(source.value, fdata.fname); node.specifiers.forEach((snode) => { const local = snode.local; ASSERT(local && local.type === 'Identifier', 'local should be an ident', snode); const uniqueName = findUniqueNameForBindingIdent(local); // I dont think this is ever different, but w/e switch (snode.type) { case 'ImportDefaultSpecifier': { // import x from 'y' log('Importing the default export from `0' + from + '` as `' + local.name + '` (-> `' + uniqueName + '`)'); //$(snode, '@import_binding', uniqueName, 'default', from); break; } case 'ImportSpecifier': { // import {x} from 'y' // local === imported // import {x,y,z} from 'y' // will be three individual specifier nodes // import {x as y} from 'y' const imported = snode.imported; ASSERT(imported && imported.type === 'Identifier', 'each specifier should have exactly one imported symbol', snode); log( 'Importing the exported symbol `' + imported.name + '` from `' + from + '` as `' + local.name + '` (-> `' + uniqueName + '`)', ); //$(snode, '@import_binding', uniqueName, imported.name, from); break; } case 'ImportNamespaceSpecifier': { // import * as x from 'y' log('Importing all exports from module `' + from + '` as an object in `' + local.name + '` (-> `' + uniqueName + '`)'); //$(snode, '@import_star', uniqueName, from); break; } default: { ASSERT(false, 'fixme'); } } }); break; } case 'Program': { node.body.forEach((cnode, i) => stmt(node, 'body', i, cnode)); break; } case 'ReturnStatement': { if (node.argument) { expr(node, 'argument', -1, node.argument); } //else $(node, '@push', 'undefined'); //$(node, '@return'); // Assumes an (implicit or explicit) arg is pushed break; } case 'TryStatement': { stmt(node, 'block', -1, node.block); if (node.handler) { if (node.handler.param) { log('Processing catch clause'); // Catch clause is present. Register the binding. ASSERT(node.handler.param.type === 'Identifier', 'todo catch clauses that are not idents', node.handler.param); const uniqueName = findUniqueNameForBindingIdent(node.handler.param); //$(node.handler.param, '@push', 'Error.prototype'); //$(node.handler.param, '@obj', ['__proto__']); //// And this will be the init value of the binding //$(node.handler.param, '@binding', uniqueName, 'let'); // TODO: let? I think so :) } // TODO: footgun; setting node as parent but skipping on handler or body in the crumb path? Sorry future me. stmt(node, 'handler', 'body', node.handler.body); } if (node.finalizer) { stmt(node, 'finalizer', -1, node.finalizer); } break; } case 'SwitchStatement': { expr(node, 'discriminant', -1, node.discriminant); node.cases.forEach((cnode, i) => { // Defaults have no test if (cnode.test) { // All cases must have test for same type as discriminant (switch value) expr2(node, 'cases', i, cnode, 'test', -1, cnode.test); //$(cnode.test, '@merge'); // pop the test and the discriminant, merge them, result will be pushed to be the new discriminant. it is dropped later. } cnode.consequent.forEach((dnode, i) => stmt(cnode, 'consequent', i, dnode)); }); break; } case 'ThrowStatement': { expr(node, 'argument', -1, node.argument); //$(node, '@drop'); break; } case 'VariableDeclaration': { const kind = node.kind; node.declarations.forEach((dnode, i) => { ASSERT(dnode.id?.type === 'Identifier', 'todo: implement other kinds of variable declarations'); if (dnode.init) { // Detect whether it was a literal (we don't care to visit those). If not, visit it. If the AST changed, check again. // Phase 1 already checked the first case but if this replacement changed it, we can update the const wasLiteral = dnode.init.type === 'Literal' || dnode.init.type === 'Identifier' && ['undefined', 'null', 'true', 'false'].includes(dnode.init.name); // No need to visit literals if (!wasLiteral) { expr2(node, 'declarations', i, dnode, 'init', -1, dnode.init); const nowLiteral = dnode.init.type === 'Literal' || dnode.init.type === 'Identifier' && ['undefined', 'null', 'true', 'false'].includes(dnode.init.name); if (nowLiteral) { // The visit replaced whatever was there with a literal. This means the binding is now also a side-effect free variable that can be inlined and eliminated. program.globallyUniqueNamingRegistery.get(dnode.id) } } } else { // Ignore? Phase1 should have marked this var as potentially undefined. If it has no actual assignments then it's just `undefined` //const pid = createPlaceholder(store, 'HB', 'binding `' + dnode.id ? dnode.id.name : '<destruct>' + '` without init'); linter.check( 'BINDING_NO_INIT', { filename: fdata.fname, column: dnode.id.loc.start.column, line: dnode.id.loc.start.line }, dnode.id.name, ); //log('Created placeholder', tstr(pid), 'for the binding'); } // The paramNode can be either an Identifier or a pattern of sorts if (dnode.id.type === 'Identifier') { // Simple case. Create the binding and be done. const uniqueName = findUniqueNameForBindingIdent(dnode.id); log('Var decl id:', uniqueName); const meta = fdata.globallyUniqueNamingRegistery.get(uniqueName); const updates = meta.updates; log('This binding has', updates.length, 'updates'); ASSERT(updates, 'should find meta data for each name and should have an updates array', uniqueName, meta); if (updates.length === 1) { ASSERT(updates[0].parent?.[updates[0]?.prop], 'all updates are nodes, right? can this be null?', updates); const update = updates[0].parent[updates[0].prop]; if (update.type === 'Literal') { log('The binding `' + uniqueName + '` has one update and it is a literal:', update); log('This means it can be inlined in all its usages:',meta.usages); } } else { // TODO: cases where all updates are for the same thing } //if (isExport) { // $(dnode, '@dup', '<exported binding decl>'); // $(dnode, '@export_as', isExport === true ? uniqueName : 'default'); //} //$(dnode, '@binding', uniqueName, kind); } else if (dnode.id.type === 'ArrayPattern') { // Complex case. Sort out the final param value vs default, then walk through the destructuring pattern. if (isExport) linter.check( 'TOFIX', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }, 'exported array pattern', ); linter.check('ARRAY_PATTERN_UNSOUND', { filename: fdata.fname, column: dnode.id.loc.start.column, line: dnode.id.loc.start.line, }); // Next we are going to pushpop the top value recursively to process all parts of the pattern dnode.id.elements.forEach((node) => destructBindingArrayElement(node, kind)); } else { // Complex case. Sort out the final param value vs default, then walk through the destructuring pattern. if (isExport) linter.check( 'TOFIX', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }, 'exported object pattern', ); ASSERT(dnode.id.type === 'ObjectPattern', 'fixme if else', dnode.id); // Next we are going to pushpop the top value recursively to process all parts of the pattern dnode.id.properties.forEach((ppnode) => destructBindingObjectProp(ppnode, dnode.id, kind)); } }); break; } case 'WhileStatement': { expr(node, 'test', -1, node.test); stmt(node, 'body', -1, node.body); break; } default: { log('unknown statement node:', node); log('Missing support for stmt ' + node.type); throw new Error('Missing support for stmt ' + node.type); } } if (node.$scope || (node.type === 'TryStatement' && node.handler)) { lexScopeStack.pop(); if (['Program', 'FunctionExpression', 'ArrowFunctionExpression', 'FunctionDeclaration'].includes(node.type)) { rootScopeStack.pop(); } } groupEnd(); } function expr2(parent2, prop2, index2, parent, prop, index, node, isMethod, isCallee, methodName) { // Skip one property crumb(parent2, prop2, index2); expr(parent, prop, index, node, isMethod, isCallee, methodName); uncrumb(parent2, prop2, index2); } function expr(parent, prop, index, node, isMethod, isCallee, methodName) { ASSERT( parent === null || index === 'body' || (Array.isArray(parent[prop]) ? parent[prop][index] === node : parent[prop] === node && index === -1), 'caller should pass on proper parent[prop][index] data', parent, prop, index, node, isMethod, isCallee, methodName, ); crumb(parent, prop, index); _expr(node, isMethod, isCallee, methodName); uncrumb(parent, prop, index); } function _expr(node, isMethod = false, isCallee = false, methodName = '') { group(DIM + 'expr(' + RESET + BLUE + node.type + RESET + DIM + ')' + RESET); if (node.$scope) { lexScopeStack.push(node); if (['Program', 'FunctionExpression', 'ArrowFunctionExpression', 'FunctionDeclaration'].includes(node.type)) { rootScopeStack.push(node); } } switch (node.type) { case 'ArrayExpression': { if (node.elements.length === 0) { //log('Empty array, pushing a placeholder to the stack'); //const pid = 'HEAK' + String(++store.uid); // placeholder id (holder, empty, array, kind) //store.set(pid, {_class: 'placeholder', tid: pid, type: 'H', props: new Map, seen: new Map, placeholder: true, alias: null}); //log('Created a placeholder tee for kind:', tstr(pid)); } else { for (let i = 0; i < node.elements.length; ++i) { const elNode = node.elements[i]; if (elNode.type === 'SpreadElement') { // Special case spread because its behavior differs per parent case expr2(node, 'elements', i, elNode, 'argument', -1, elNode.argument); // Stack should now have an array and the owner array should have its kind merged like all other elements //$(node, '@kind', '<empty array literal>'); // Ignore if the array is empty... (in that case dup the stack) } else { expr(node, 'elements', i, elNode); } } } //$(node, '@arr', Math.max(1, node.elements.length)); break; } case 'AssignmentExpression': { // Get the starting tokens; // - left (either the property name or the binding ident) // - operator (the first non-space token left of node.right // - right (first token of the value being assigned) const tokenOp = getPrevTokenPastGroupAndSpaces(node.right); ASSERT(tokenOp.str.slice(-1) === '=', 'should have found the assign op', tokenOp); tokenOp.tlog = []; expr(node, 'right', -1, node.right); if (node.left.type === 'MemberExpression') { if (node.left.computed) { // No warning yet; this may be // - an indexed array assignment // - a primitve update // - a object-as-map half-supported update // Some warning will be shown by dyn_set expr2(node, 'left', -1, node.left, 'object', -1, node.left.object); expr2(node, 'left', -1, node.left, 'property', -1, node.left.property); //$(node, '@dyn_set', tokenOp.n); } else { expr2(node, 'left', -1, node.left, 'object', -1, node.left.object); //$(node, '@set', node.left.property.name, tokenOp.n); } } else { // TODO: patterns const uniqueName = findUniqueNameForBindingIdent(node.left); //$(node, '@assign', uniqueName, node.operator, tokenOp.n); } break; } case 'ArrowFunctionExpression': { //let abak = actions; //actions = []; //$(node, '@log', 'start of arrow'); //$(node, '@drop'); // Drop the context of the call. Arrows inherit this from their parent function. const { minParamRequired, hasRest, paramBindingNames } = processFuncArgs(node); //$(node, '@body_start'); if (node.expression) { expr(node, 'body', -1, node.body); //$(node, '@return'); } else { stmt(node, 'body', -1, node.body); log('The explicitReturns value for its body is:', [node.body.$p.explicitReturns]); if (node.body.$p.explicitReturns !== 'yes') { // Implicitly return `undefined` //$(node, '@push', 'undefined'); //$(node, '@return'); } } //let funcActions = actions; //actions = abak; // For arrows, focus on the `=>` token. A little annoying since we have to skip an arbitrary header // but we can use the body node as an offset and seek backwards to find the `=>` token. // For method shorthands, we won't have an `=>` token but immediately find the `)` token. Need to figure out // how to deal with that visually. let funcToken = getFirstToken(node.body); while (funcToken && funcToken.str !== '=>') { funcToken = tokens[funcToken.n - 1]; } ASSERT(funcToken && funcToken.str === '=>', 'pretty sure this is an invariant', funcToken); funcToken.tlog = []; //const desc = 'Arrow<line ' + node.loc.start.line + ', column ' + node.loc.start.column + '>'; //$(node, '@func', 'N' + node.$p.tid + '=arrow', '', node.params.map(node => node.name), paramBindingNames, hasRest, minParamRequired, funcActions, 'arrow', false, node.$p.reachableNames, funcToken.n, filename, node.loc.start.column, node.loc.start.line, desc); break; } case 'BinaryExpression': { log('Operator:', node.operator); expr(node, 'left', -1, node.left); expr(node, 'right', -1, node.right); const tokenOp = getPrevTokenPastGroupAndSpaces(node.right); ASSERT(tokenOp.str === node.operator, 'should have found the binary op', tokenOp); //tokenOp.tlog = []; //$(node, '@binop', node.operator, tokenOp.n); // Assumes two values on the stack but won't consume them switch (node.operator) { case '+': { if (node.left.type === 'Literal' && node.right.type === 'Literal') { // TODO: this hack won't work for something like bigint and a few others. I guess. log('Replacing BinaryExpression with a Literal'); const val = E(node.left.raw + ' + ' + node.right.raw); const str = JSON.stringify(val); // eew node.left.value = val; node.left.raw = str; node.left.loc = node.loc; // Keep original loc of this range because who knows source maps amirite const parent = crumbsNode[crumbsNode.length - 1]; const prop = crumbsProp[crumbsProp.length - 1]; const index = crumbsIndex[crumbsIndex.length - 1]; if (index >= 0) parent[prop][index] = node.left; else parent[prop] = node.left; // `node` is a Binary expression that we want to replace in its parent. crumbsNode[crumbsNode.length - 1] = node.left; crumbsProp[crumbsProp.length - 1] = true; // Prevent assertion from blowing up } break; } case '-': { //$(node, '@push', 'number'); //$(node, '@merge'); //$(node, '@merge'); //$(node, '@drop'); // Don't care about the merged value, it always returns a number //$(node, '@push', 'number'); break; } case '==': case '!=': linter.check( 'TOFIX', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }, 'weak comparison is bad and not implemented', ); //$(node, '@merge'); break; case '===': case '!==': //$(node, '@merge'); //$(node, '@drop'); // Don't care about the merged value //$(node, '@push', 'boolean'); break; case '<': case '<=': case '>': case '>=': // Merge left and right with number and return a boolean //$(node, '@push', 'number'); //$(node, '@merge'); // must return a number //$(node, '@merge'); //$(node, '@drop'); // Don't care about the merged value //$(node, '@push', 'boolean'); break; case 'instanceof': { // This model should conclusively determine the truth value of any `instanceof` expression linter.check('INSTANCEOF_OBSOLETE', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }); // TODO: figure out the rules, proper //$(node, '@instanceof'); break; } case 'in': { // This model should conclusively determine the truth value of any `in` expression linter.check('IN_OBSOLETE', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }); // Require the lhs to be a string expr(node, 'left', -1, node.left); //$(node, '@push', 'string'); //$(node, '@merge'); // TODO: figure out valid values of rhs. Probably have to verify that state in the @in callback expr(node, 'right', -1, node.right); //$(node, '@in', node); break; } default: // Merge left and right with number and return a number //$(node, '@push', 'number'); //$(node, '@merge'); // Should return a number //$(node, '@merge'); // Should return a number break; } break; } case 'CallExpression': { log(DIM + 'Setting up call args' + RESET); let spreadAt = -1; // If this call contained a spread, we have to verify it starts on-or-after the rest param, at runtime. node.arguments.forEach((anode, i) => { if (anode.type === 'SpreadElement') { log( 'Spread in a call. Complicated because we can not know the array size in our model. Hence we can only support a handful of cases, mostly concerning this at the end of the arg list', ); // TODO: we can do checks around the spread and see if they are the same type // TODO: can ignore empty arrays (although is that really worth it in the real world?) if (i !== 0) { // note: reversed order, so we expect rest to be index 0 linter.check('SPREAD_NOT_TAIL', { filename: fdata.fname, column: anode.loc.start.column, line: anode.loc.start.line }); } else { spreadAt = node.arguments.length - 1 - i; // Note: list is reversed! } // Either way, get the kind, push it onto the stack for good measure... expr2(node, 'arguments', i, anode, 'argument', -1, anode.argument); //$(node, '@kind', '<call spread arg>'); } else { expr(node, 'arguments', i, anode); } }); log(DIM + 'Setting up callee to call' + RESET); if (node.callee.type === 'Super') { // Note: the extends of the class to which this constructor belongs is syntactically bound to this super() log('This is a `super()` call'); const ownerFunction = superCallStack[superCallStack.length - 1]; ASSERT( ownerFunction && ownerFunction.superClass, 'parser should validate that a `super()` call is contained in an es6 constructor which must mean the owner class is extending a value', ownerFunction, ); expr(ownerFunction, 'superClass', -1, ownerFunction.superClass); // put it on the stack //$(node, '@super_call', node.arguments.length, spreadAt); } else { log(DIM + 'Setting up call context' + RESET); if (node.callee.type === 'Identifier') { // Look up the binding. See if we can statically resolve it to a function value? const uniqueName = findUniqueNameForBindingIdent(node.callee); } if (node.callee.type !== 'MemberExpression') { // in strict mode implicit context is undefined, not global } //log('Putting the callee on the stack'); expr(node, 'callee', -1, node.callee, false, true); //log(DIM + 'Setting up call with ' + node.arguments.length + ' args' + RESET); //$(node, '@call', node.arguments.length, spreadAt); getFirstToken(node).tlog = []; } break; } case 'ClassExpression': { processClass(node, true, false); break; } case 'ConditionalExpression': { expr(node, 'test', -1, node.test); //$(node, '@push', 'boolean'); //$(node, '@merge'); // Merge condition with bool //$(node, '@drop'); // pop the bool expr(node, 'consequent', -1, node.consequent); expr(node, 'alternate', node.alternate); //$(node, '@merge'); // Merge consequent with alternate (not with bool) and return it break; } case 'FunctionExpression': { log('isMethod:', isMethod); //let abak = actions; //actions = []; //$(node, '@log', 'start of func expr ' + (node.id ? node.id.name : '{anon}')); if (node.id) { // Oh fun, function expression id scoping let has = false; let scope = node.$scope; ASSERT(scope.type === Tenko.SCOPE_LAYER_FUNC_BODY, 'please no 1', scope); let names = scope.names; if (names !== Tenko.HAS_NO_BINDINGS && names.has('shadowedFunction')) has = true; scope = scope.parent; ASSERT(scope.type === Tenko.SCOPE_LAYER_FUNC_PARAMS, 'please no 2', scope); names = scope.names; if (names !== Tenko.HAS_NO_BINDINGS && names.has('shadowedFunction')) has = true; scope = scope.parent; ASSERT(scope.type === Tenko.SCOPE_LAYER_FUNC_ROOT, 'please no 3', scope); names = scope.names; if (names !== Tenko.HAS_NO_BINDINGS && names.has('shadowedFunction')) has = true; scope = scope.parent; ASSERT(scope.type === Tenko.SCOPE_LAYER_GLOBAL, 'please no 4', scope); if (has) { log('The func expr name was shadowed by a param name or local var, ignoring it'); linter.check('FUNC_EXPR_NAME_SHADOW', { filename: fdata.fname, column: node.id.loc.start.column, line: node.id.loc.start.line, }); } else { // Ok, the func expr had a name the name was not shadowed, record it log('Making sure the func name gets bound properly'); // Specific hack because we won't have access to the 'current" func instance otherwise log('Function param id:'); const uniqueName = findUniqueNameForBindingIdent(node.id); //$(node, '@func_expr_name', uniqueName); } } else if (isMethod) { log('Function expression is a method with key: `' + methodName + '`'); ASSERT(methodName, 'all methods have a key', methodName); // TODO: What about computed keys? } else { log('Function expression has no name'); } //$(node, '@binding', 'this', 'lex'); // Top of the stack ought to be the context for this call //$(node, '@push', 'number'); //$(node, '@obj', ['length']); // Create the arguments object and put it on the stack //$(node, '@binding', 'arguments', 'lex'); const { minParamRequired, hasRest, paramBindingNames } = processFuncArgs(node); //$(node, '@body_start'); stmt(node, 'body', -1, node.body); log('The explicitReturns value for its body is:', [node.body.$p.explicitReturns]); if (node.body.$p.explicitReturns !== 'yes') { // Implicitly return `undefined` //$(node, '@push', 'undefined'); //$(node, '@return'); } //let funcActions = actions; //actions = abak; // For functions, focus on the `function` keyword let funcToken = getFirstToken(node); funcToken.tlog = []; //const debugName = (isMethod ? methodName : node.id ? node.id.name : '<anon>'); //const desc = (isMethod ? 'Method' : 'Function') + '<' + debugName + ': line ' + node.loc.start.line + ', column ' + node.loc.start.column + '>'; //$(node, '@func', 'N' + node.$p.tid + '=' + debugName, node.id ? node.id.name : '', node.params.map(node => node.name), paramBindingNames, hasRest, minParamRequired, funcActions, isMethod ? 'method' : 'expr', !!node.$p.thisAccess, node.$p.reachableNames, funcToken.n, filename, node.loc.start.column, node.loc.start.line, desc); break; } case 'Identifier': { const uniqueName = node.$p.uniqueName; ASSERT(uniqueName, 'the uniqueName should be resolved in phase1', node, uniqueName); const meta = fdata.globallyUniqueNamingRegistery.get(uniqueName); ASSERT(meta, 'meta data should exist for this name', node.name, uniqueName, meta); //getFirstToken(node).tlog = []; break; } case 'Literal': { if (typeof node.value === 'string') { //$(node, '@push', 'string'); } else if (typeof node.value === 'number') { //$(node, '@push', 'number'); } else if (typeof node.value === 'boolean') { //$(node, '@push', 'boolean'); } else if (node.regex !== undefined) { //$(node, '@push', 'RegExp.prototype'); //$(node, '@obj', ['__proto__']); } else if (node.value === null) { //$(node, '@push', 'null'); } else { log('Missing support for literal', node); linter.check( 'TOFIX', { filename: fdata.fname, line: node.loc.start.line, column: node.loc.start.column }, 'unknown literal type', ); } break; } case 'LogicalExpression': { expr(node, 'left', -1, node.left); expr(node, 'right', -1, node.right); const tokenOp = getPrevTokenPastGroupAndSpaces(node.right); ASSERT(tokenOp.str === node.operator, 'should have found the binary op', tokenOp); tokenOp.tlog = []; //$(node, '@binop', node.operator, tokenOp.n); // Assumes two values on the stack but won't consume them //$(node, '@logical', node.operator); break; } case 'MemberExpression': { // Walk the property structure in such a way that it visits the root object first, then back up to the leaf property // Any dynamic expressions get visited before the next property access // It's kind a messy, useful for the other project, maybe not here, and maybe I'll rewrite it. const nameStack = []; const dynaStack = []; function r(node) { if (node.type === 'MemberExpression') { // Visit the object first, then on the way up potentially walk the computed prop expresison if it one crumb(node, 'object', -1, node.object); r(node.object); uncrumb(node, 'object', -1); if (node.computed) { log('Dynamic property access.'); expr(node, 'computed', -1, node.computed); dynaStack.push(true); } else { dynaStack.push(false); } nameStack.push(node.property); } else if (node.type === 'Super') { log('This is a `super.prop` property'); // The instance that owns the method containing this super prop can be found at the top of store.get('#superStack') //$(node, '@super_prop'); // Root object } else { _expr(node); // Root object } } r(node); //while (nameStack.length > 0) { // const wasDynamic = dynaStack.pop(); // const nameNode = nameStack.pop(); // // if (nameStack.length === 0 && isCallee) { // // The top address is now the callee. We want to get a method from it (consumes top) // // AND use it as context for the call (also consumes top), so dupe it here // //$(node, '@dup', '<top is called and serves as context>'); // } // // if (wasDynamic) { // // Dynamic property access; trying to get the kind of the array as the "get", or kind of object-as-map // expr(nameNode); // //$(nameNode, '@dyn_get', nameStack.length); // } else { // ASSERT(nameNode && nameNode.type === 'Identifier', 'the stack should contain ident nodes or zeroes', nameNode); // //$(nameNode, '@get', nameNode.name); // //getFirstToken(nameNode).tlog = []; // } //} break; } case 'NewExpression': { let spreadAt = -1; // see CallExpression log(DIM + 'Setting up `new` args' + RESET); node.arguments.forEach((anode, i) => { if (anode.type === 'SpreadElement') { log('Spread in a `new`'); spreadAt = i; expr2(node, 'arguments', i, anode, 'argument', -1, anode.argument); //$(node, '@kind', '<spread arg>'); } else { expr(node, 'arguments', i, anode); } }); expr(node, 'callee', -1, node.callee); //$(node, '@new', node.arguments.length, spreadAt); break; } case 'ObjectExpression': { //$(node, '@obj', []); //$(node, '@super_stack_push'); // Make the object reference available to any methods that use super properties const spreads = []; node.properties.forEach((pnode, i) => { if (pnode.type === 'SpreadElement') { expr(node, 'properties', i, pnode, 'argument', -1, pnode.argument); spreads.push(true); } else { ASSERT(pnode.type === 'Property', 'fixmeifnot', pnode); ASSERT(!pnode.computed, 'computed prop wot?', pnode); ASSERT(pnode.key.type === 'Identifier' || pnode.key.type === 'Literal', 'prop key should be ident or num/str', pnode); ASSERT(pnode.value); expr( node, 'properties', i, pnode, 'value', -1, pnode.value, pnode.method ? 'method' : '', false, pnode.method ? (pnode.key.type === 'Identifier' ? pnode.key.name : String(pnode.key.value)) : undefined, ); spreads.push(false); } }); //$(node, '@super_stack_pop'); //$(node, '@obj_init', node.properties.map(node => node.type === 'Property' ? (node.key.type === 'Identifier' ? node.key.name : String(node.key.value)) : '<spread>').reverse(), spreads); break; } case 'RegExpLiteral': { //$(node, '@regex'); break; } case 'SequenceExpression': { node.expressions.forEach((enode, i) => { group('Sequence part', i + 1, '/', node.expressions.length); expr(node, 'expressions', i, enode); if (i < node.expressions.length - 1) { log('This is not the last value in the sequence so dropping it'); } else { log('This is the last value in the sequence so that is what it returns'); } groupEnd(); }); break; } case 'Super': { // Two cases: // - call // - prop // The call is syntactically restricted to es6 constructors of classes that extend _something_