import { WorkerError } from "../workertypes"; import ErrorStackParser = require("error-stack-parser"); import yufka from 'yufka'; import * as bitmap from "./lib/bitmap"; import * as io from "./lib/io"; import * as output from "./lib/output"; import * as color from "./lib/color"; import * as scriptui from "./lib/scriptui"; export interface Cell { id: string; object?: any; } export interface RunResult { cells: Cell[]; state: {}; } const IMPORTS = { 'bitmap': bitmap, 'io': io, 'output': output, 'color': color, 'ui': scriptui, } const LINE_NUMBER_OFFSET = 3; // TODO: shouldnt need? const GLOBAL_BADLIST = [ 'eval' ] const GLOBAL_GOODLIST = [ 'eval', // 'eval' can't be defined or assigned to in strict mode code 'Math', 'JSON', 'parseFloat', 'parseInt', 'isFinite', 'isNaN', 'String', 'Symbol', 'Number', 'Object', 'Boolean', 'NaN', 'Infinity', 'Date', 'BigInt', 'Set', 'Map', 'RegExp', 'Array', 'ArrayBuffer', 'DataView', 'Float32Array', 'Float64Array', 'Int8Array', 'Int16Array', 'Int32Array', 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Uint8ClampedArray', ] class RuntimeError extends Error { constructor(public loc: acorn.SourceLocation, msg: string) { super(msg); } } export class Environment { preamble: string; postamble: string; obj: {}; seq: number; declvars : {[name : string] : acorn.Node}; constructor( public readonly globalenv: any, public readonly path: string ) { var badlst = Object.getOwnPropertyNames(this.globalenv).filter(name => GLOBAL_GOODLIST.indexOf(name) < 0); this.preamble = `'use strict';var ${badlst.join(',')};`; for (var impname in IMPORTS) { this.preamble += `var ${impname}=$$.${impname};` } this.preamble += '{\n'; this.postamble = '\n}'; } error(varname: string, msg: string) { console.log(varname, this.declvars[varname]); throw new RuntimeError(this.declvars && this.declvars[varname].loc, msg); } preprocess(code: string): string { this.declvars = {}; this.seq = 0; let options = { // https://www.npmjs.com/package/magic-string#sgeneratemap-options- sourceMap: { file: this.path, source: this.path, hires: false, includeContent: false }, // https://github.com/acornjs/acorn/blob/master/acorn/README.md acorn: { ecmaVersion: 6 as any, locations: true, allowAwaitOutsideFunction: true, allowReturnOutsideFunction: true, allowReserved: true, } }; const result = yufka(code, options, (node, { update, source, parent }) => { function isTopLevel() { return parent().type === 'ExpressionStatement' && parent(2) && parent(2).type === 'Program'; } let left = node['left']; switch (node.type) { // error on forbidden keywords case 'Identifier': if (GLOBAL_BADLIST.indexOf(source()) >= 0) { update(`__FORBIDDEN__KEYWORD__${source()}__`) // TODO? how to preserve line number? } break; // x = expr --> var x = expr (first use) case 'AssignmentExpression': if (isTopLevel()) { if (left && left.type === 'Identifier') { if (!this.declvars[left.name]) { update(`var ${left.name}=io.data.get(this.${source()}, ${JSON.stringify(left.name)})`) this.declvars[left.name] = left; } else { update(`${left.name}=this.${source()}`) } } } break; // literal comments case 'Literal': if (isTopLevel() && typeof node['value'] === 'string') { update(`this.$$doc__${this.seq++} = { literaltext: ${source()} };`); } break; } }); return result.toString(); } async run(code: string): Promise { // TODO: split into cells based on "--" linebreaks? code = this.preprocess(code); this.obj = {}; const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor; const fn = new AsyncFunction('$$', this.preamble + code + this.postamble).bind(this.obj, IMPORTS); await fn.call(this); this.checkResult(this.obj, new Set(), []); } // https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm // TODO: return initial location of thingie checkResult(o, checked: Set, fullkey: string[]) { if (o == null) return; if (checked.has(o)) return; if (typeof o === 'object') { if (o.length > 100) return; // big array, don't bother if (o.BYTES_PER_ELEMENT > 0) return; // typed array, don't bother checked.add(o); // so we don't recurse if cycle function prkey() { return fullkey.join('.') } for (var [key, value] of Object.entries(o)) { if (value == null && fullkey.length == 0) { this.error(key, `"${key}" has no value.`) } fullkey.push(key); if (typeof value === 'function') { this.error(fullkey[0], `"${prkey()}" is a function. Did you forget to pass parameters?`); // TODO? did you mean (needs to see entire expr) } if (typeof value === 'symbol') { this.error(fullkey[0], `"${prkey()}" is a Symbol, and can't be used.`) // TODO? } if (value instanceof Promise) { this.error(fullkey[0], `"${prkey()}" is unresolved. Use "await" before expression.`) // TODO? } this.checkResult(value, checked, fullkey); fullkey.pop(); } } } render(): Cell[] { var cells = []; for (var [key, value] of Object.entries(this.obj)) { if (typeof value === 'function') { // TODO: find other values, functions embedded in objects? } else { var cell: Cell = { id: key, object: value }; cells.push(cell); } } return cells; } extractErrors(e: Error): WorkerError[] { let loc = e['loc']; if (loc && loc.start && loc.end) { return [{ path: this.path, msg: e.message, line: loc.start.line, start: loc.start.column, end: loc.end.line, }] } if (loc && loc.line != null) { return [{ path: this.path, msg: e.message, line: loc.line, start: loc.column, }] } // TODO: Cannot parse given Error object let frames = ErrorStackParser.parse(e); let frame = frames.findIndex(f => f.functionName === 'anonymous'); let errors = []; while (frame >= 0) { console.log(frames[frame]); if (frames[frame].fileName.endsWith('Function')) { errors.push( { path: this.path, msg: e.message, line: frames[frame].lineNumber - LINE_NUMBER_OFFSET, start: frames[frame].columnNumber, } ); } --frame; } if (errors.length == 0) { errors.push( { path: this.path, msg: e.message, line: 0 } ); } return errors; } getLoadableState() { let updated = null; for (let [key, value] of Object.entries(this.declvars)) { if (typeof value['$$getstate'] === 'function') { let loadable = value as io.Loadable; if (updated == null) updated = {}; updated[key] = loadable.$$getstate(); } } return updated; } }