2021-08-15 15:10:01 +00:00
2021-08-12 23:19:39 +00:00
import { WorkerError } from "../workertypes" ;
import ErrorStackParser = require ( "error-stack-parser" ) ;
import yufka from 'yufka' ;
2021-08-14 16:06:49 +00:00
import * as bitmap from "./lib/bitmap" ;
import * as io from "./lib/io" ;
import * as output from "./lib/output" ;
2021-08-15 15:10:01 +00:00
import * as color from "./lib/color" ;
import * as scriptui from "./lib/scriptui" ;
2021-08-12 23:19:39 +00:00
export interface Cell {
id : string ;
object ? : any ;
}
2021-08-15 15:10:01 +00:00
export interface RunResult {
cells : Cell [ ] ;
state : { } ;
}
2021-08-12 23:19:39 +00:00
const IMPORTS = {
'bitmap' : bitmap ,
'io' : io ,
2021-08-15 15:10:01 +00:00
'output' : output ,
'color' : color ,
'ui' : scriptui ,
2021-08-12 23:19:39 +00:00
}
2021-08-15 15:10:01 +00:00
const LINE_NUMBER_OFFSET = 3 ; // TODO: shouldnt need?
2021-08-12 23:19:39 +00:00
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' ,
]
2021-08-15 15:10:01 +00:00
class RuntimeError extends Error {
constructor ( public loc : acorn.SourceLocation , msg : string ) {
super ( msg ) ;
}
}
2021-08-12 23:19:39 +00:00
export class Environment {
preamble : string ;
postamble : string ;
obj : { } ;
2021-08-15 15:10:01 +00:00
seq : number ;
declvars : { [ name : string ] : acorn . Node } ;
2021-08-18 20:44:13 +00:00
builtins : { }
2021-08-12 23:19:39 +00:00
constructor (
public readonly globalenv : any ,
public readonly path : string
) {
var badlst = Object . getOwnPropertyNames ( this . globalenv ) . filter ( name = > GLOBAL_GOODLIST . indexOf ( name ) < 0 ) ;
2021-08-18 20:44:13 +00:00
this . builtins = {
print : ( . . . args ) = > this . print ( args ) ,
. . . IMPORTS
}
2021-08-12 23:19:39 +00:00
this . preamble = ` 'use strict';var ${ badlst . join ( ',' ) } ; ` ;
2021-08-18 20:44:13 +00:00
for ( var impname in this . builtins ) {
2021-08-12 23:19:39 +00:00
this . preamble += ` var ${ impname } = $ $ . ${ impname } ; `
}
this . preamble += '{\n' ;
this . postamble = '\n}' ;
}
2021-08-15 15:10:01 +00:00
error ( varname : string , msg : string ) {
2021-08-18 16:16:58 +00:00
let obj = this . declvars && this . declvars [ varname ] ;
2021-08-20 23:09:16 +00:00
console . log ( 'ERROR' , varname , obj , this ) ;
2021-08-18 16:16:58 +00:00
throw new RuntimeError ( obj && obj . loc , msg ) ;
2021-08-15 15:10:01 +00:00
}
2021-08-18 20:44:13 +00:00
print ( args : any [ ] ) {
if ( args && args . length > 0 && args [ 0 ] != null ) {
this . obj [ ` $ $ print__ ${ this . seq ++ } ` ] = args . length == 1 ? args [ 0 ] : args ;
}
}
2021-08-12 23:19:39 +00:00
preprocess ( code : string ) : string {
2021-08-15 15:10:01 +00:00
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 } ) = > {
2021-08-18 20:44:13 +00:00
const isTopLevel = ( ) = > {
2021-08-18 16:16:58 +00:00
return parent ( ) && parent ( ) . type === 'ExpressionStatement' && parent ( 2 ) && parent ( 2 ) . type === 'Program' ;
2021-08-15 15:10:01 +00:00
}
2021-08-18 20:44:13 +00:00
const convertTopToPrint = ( ) = > {
2021-08-20 23:09:16 +00:00
if ( isTopLevel ( ) ) {
let printkey = ` $ $ print__ ${ this . seq ++ } ` ;
update ( ` this. ${ printkey } = io.data.load( ${ source ( ) } , ${ JSON . stringify ( printkey ) } ) ` ) ;
//update(`print(${source()});`)
}
2021-08-18 20:44:13 +00:00
}
const left = node [ 'left' ] ;
2021-08-12 23:19:39 +00:00
switch ( node . type ) {
2021-08-20 23:09:16 +00:00
// add preamble, postamble
case 'Program' :
update ( ` ${ this . preamble } ${ source ( ) } ${ this . postamble } ` )
break ;
2021-08-15 15:10:01 +00:00
// error on forbidden keywords
2021-08-12 23:19:39 +00:00
case 'Identifier' :
if ( GLOBAL_BADLIST . indexOf ( source ( ) ) >= 0 ) {
update ( ` __FORBIDDEN__KEYWORD__ ${ source ( ) } __ ` ) // TODO? how to preserve line number?
2021-08-18 20:44:13 +00:00
} else {
convertTopToPrint ( ) ;
2021-08-12 23:19:39 +00:00
}
break ;
2021-08-15 15:10:01 +00:00
// x = expr --> var x = expr (first use)
2021-08-12 23:19:39 +00:00
case 'AssignmentExpression' :
2021-08-15 15:10:01 +00:00
if ( isTopLevel ( ) ) {
2021-08-14 16:06:49 +00:00
if ( left && left . type === 'Identifier' ) {
2021-08-15 15:10:01 +00:00
if ( ! this . declvars [ left . name ] ) {
2021-08-20 23:09:16 +00:00
update ( ` var ${ left . name } =io.data.load(this. ${ source ( ) } , ${ JSON . stringify ( left . name ) } ) ` )
2021-08-15 15:10:01 +00:00
this . declvars [ left . name ] = left ;
2021-08-14 16:06:49 +00:00
} else {
update ( ` ${ left . name } =this. ${ source ( ) } ` )
}
2021-08-12 23:19:39 +00:00
}
}
break ;
2021-08-18 20:44:13 +00:00
// convert lone expressions to print()
case 'UnaryExpression' :
case 'BinaryExpression' :
case 'CallExpression' :
case 'MemberExpression' :
convertTopToPrint ( ) ;
break ;
2021-08-15 15:10:01 +00:00
// literal comments
case 'Literal' :
2021-08-18 20:44:13 +00:00
if ( typeof node [ 'value' ] === 'string' && isTopLevel ( ) ) {
2021-08-15 15:10:01 +00:00
update ( ` this. $ $ doc__ ${ this . seq ++ } = { literaltext: ${ source ( ) } }; ` ) ;
2021-08-18 20:44:13 +00:00
} else {
convertTopToPrint ( ) ;
2021-08-15 15:10:01 +00:00
}
break ;
2021-08-12 23:19:39 +00:00
}
2021-08-15 15:10:01 +00:00
} ) ;
2021-08-12 23:19:39 +00:00
return result . toString ( ) ;
}
async run ( code : string ) : Promise < void > {
2021-08-15 15:10:01 +00:00
// TODO: split into cells based on "--" linebreaks?
2021-08-12 23:19:39 +00:00
code = this . preprocess ( code ) ;
this . obj = { } ;
const AsyncFunction = Object . getPrototypeOf ( async function ( ) { } ) . constructor ;
2021-08-20 23:09:16 +00:00
const fn = new AsyncFunction ( '$$' , code ) . bind ( this . obj , this . builtins ) ;
2021-08-12 23:19:39 +00:00
await fn . call ( this ) ;
2021-08-15 15:10:01 +00:00
this . checkResult ( this . obj , new Set ( ) , [ ] ) ;
2021-08-12 23:19:39 +00:00
}
2021-08-15 15:10:01 +00:00
// 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 < object > , 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 ( '.' ) }
2021-08-20 23:09:16 +00:00
// go through all object properties recursively
2021-08-15 15:10:01 +00:00
for ( var [ key , value ] of Object . entries ( o ) ) {
2021-08-20 23:09:16 +00:00
if ( value == null && fullkey . length == 0 && ! key . startsWith ( "$$" ) ) {
2021-08-15 15:10:01 +00:00
this . error ( key , ` " ${ key } " has no value. ` )
}
fullkey . push ( key ) ;
if ( typeof value === 'function' ) {
2021-08-18 16:16:58 +00:00
if ( fullkey . length == 1 )
this . error ( fullkey [ 0 ] , ` " ${ prkey ( ) } " is a function. Did you forget to pass parameters? ` ) ; // TODO? did you mean (needs to see entire expr)
else
2021-08-20 23:09:16 +00:00
this . error ( fullkey [ 0 ] , ` This expression may be incomplete, or it contains a function object: ${ prkey ( ) } ` ) ; // TODO? did you mean (needs to see entire expr)
2021-08-15 15:10:01 +00:00
}
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 ( ) ;
2021-08-12 23:19:39 +00:00
}
}
}
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 [ ] {
2021-08-15 15:10:01 +00:00
let loc = e [ 'loc' ] ;
if ( loc && loc . start && loc . end ) {
2021-08-12 23:19:39 +00:00
return [ {
path : this.path ,
msg : e.message ,
2021-08-15 15:10:01 +00:00
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 ,
2021-08-12 23:19:39 +00:00
} ]
}
// TODO: Cannot parse given Error object
2021-08-15 15:10:01 +00:00
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' ) ) {
2021-08-20 23:09:16 +00:00
// TODO: use source map
2021-08-15 15:10:01 +00:00
errors . push ( {
path : this.path ,
msg : e.message ,
line : frames [ frame ] . lineNumber - LINE_NUMBER_OFFSET ,
2021-08-20 23:09:16 +00:00
//start: frames[frame].columnNumber,
2021-08-15 15:10:01 +00:00
} ) ;
}
-- 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 ) ) {
2021-08-20 23:09:16 +00:00
// TODO: use Loadable
2021-08-15 15:10:01 +00:00
if ( typeof value [ '$$getstate' ] === 'function' ) {
let loadable = < any > value as io . Loadable ;
if ( updated == null ) updated = { } ;
updated [ key ] = loadable . $ $getstate ( ) ;
}
}
return updated ;
2021-08-12 23:19:39 +00:00
}
}