preval
Version:
Wannabe compiler tool for JS
1,216 lines (1,045 loc) • 77.9 kB
JavaScript
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_