From aa6ebaa20561611a67993fdfdab73d7bd25166e9 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Tue, 26 Mar 2019 16:01:31 +0900 Subject: [PATCH 01/17] Publicify Stop enum It's a secret tool we'll use later on. --- hscript/Interp.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hscript/Interp.hx b/hscript/Interp.hx index 44682571..48e3e000 100644 --- a/hscript/Interp.hx +++ b/hscript/Interp.hx @@ -23,7 +23,7 @@ package hscript; import haxe.PosInfos; import hscript.Expr; -private enum Stop { +enum Stop { SBreak; SContinue; SReturn; From 92f61aabb86fc9abd91dccc4c1b9f0369b092c1c Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Tue, 26 Mar 2019 16:04:17 +0900 Subject: [PATCH 02/17] Initial commit Add IterativeInterp, a modified version of Interp that can be stepped through execution, preventing infinite loops. --- hscript/IterativeInterp.hx | 530 +++++++++++++++++++++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 hscript/IterativeInterp.hx diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx new file mode 100644 index 00000000..8dbcc971 --- /dev/null +++ b/hscript/IterativeInterp.hx @@ -0,0 +1,530 @@ +/* + * Copyright (C)2008-2017 Haxe Foundation + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +package; + +import hscript.Expr.*; +import hscript.Tools; +import hscript.Expr; +import hscript.Interp; + + +/** + * Takes a regular hscript EBlock expression and executes it line-by-line + * @author Elliott Smith + */ +class IterativeInterp extends Interp +{ + public var _on_complete:Dynamic->Void; + public var frame_stack:Array; + public var current_frame:StackFrame; + public var script_complete:Bool; + public function prepareScript(e:Expr, ?on_complete:Dynamic->Void):Void{ + depth = 0; + locals = new Map(); + declared = []; + if (on_complete != null){ + _on_complete = on_complete; + } + var me = this; + variables.set("__intern_reset_pc", function(){ + me.current_frame.pc = 0; + trace("Resetting block: " + me.current_frame); + }); + frame_stack = []; + script_complete = false; + switch(e){ + case EBlock(a): + current_frame = new StackFrame(a, CSBlock); + locals = current_frame.locals; + case EWhile(econd, eb): + var a:Array; + switch(eb){ + case EBlock(ea): + a = ea; + default: + a = [eb]; + } + current_frame = new StackFrame(a, CSWhile, econd, declared.length); + locals = current_frame.locals; + case EDoWhile(econd, eb): + var a:Array; + switch(eb){ + case EBlock(ea): + a = ea; + default: + a = [eb]; + } + current_frame = new StackFrame(a, CSDoWhile, econd, declared.length); + locals = current_frame.locals; + default: + current_frame = new StackFrame([e], CSBlock); + locals = current_frame.locals; + } + } + + private function pushFrame(block:StackFrame){ + frame_stack.push(current_frame); + current_frame = block; + } + + private function popFrame(){ + current_frame = frame_stack.pop(); + current_frame.call_count = 0; + if (current_frame.called){ + current_frame.pc --; + } + locals = current_frame.locals; + } + + public function stepScript(steps:Int=1):Void{ + while (!script_complete && steps > 0){ + steps--; + var e:Expr = current_frame.block[current_frame.pc]; + current_frame.pc++; + if (current_frame.called){ + current_frame.called = false; + } + else{ + current_frame.call_results = []; + } + + current_frame.call_count = 0; + + for(b in frame_stack){ + trace(b); + } + trace(current_frame); + trace(e); + try{ + expr(e); + } + catch (s:Stop){ + switch(s){ + case SContinue: + while (true){ + switch(current_frame.control){ + case CSWhile, CSDoWhile: + break; + default: + popFrame(); + if (current_frame == null){ + trace("Invalid continue."); + return; + } + } + } + case SBreak: + while (true){ + switch(current_frame.control){ + case CSWhile, CSDoWhile: + if (frame_stack.length > 0){ + popFrame(); + break; + } + else{ + script_complete = true; + _on_complete(returnValue); + return; + } + default: + popFrame(); + if (current_frame == null){ + trace("Invalid continue."); + return; + } + } + } + case SReturn: + var result = returnValue; + var continuing:Bool = false; + while (frame_stack.length != 0){ + var prev_frame:StackFrame = current_frame; + popFrame(); + switch(prev_frame.control){ + case CSCall: + prev_frame.parent.call_results[prev_frame.call_id] = {result:result, complete:true}; + continuing = true; + break; + default: + } + } + if (!continuing){ + _on_complete(result); + return; + } + } + } + + if (current_frame.pc >= current_frame.block.length){ + var exiting = false; + switch(current_frame.control){ + case CSCall: + current_frame.parent.call_results[current_frame.call_id].complete = true; + current_frame.parent.call_results[current_frame.call_id].result = null; //If we just got to the end of the block without returning, the result is null + exiting = true; + default: + exiting = true; + } + if (exiting){ + trace("Exiting block... "); + if (frame_stack.length != 0){ + popFrame(); + } + else{ + if (_on_complete != null){ + script_complete = true; + _on_complete(returnValue); + return; + } + } + } + } + + } + } + + override public function expr( e : Expr ) : Dynamic { + trace(e); + var entry_frame:StackFrame = current_frame; + var entry_pc:Int = current_frame.pc; + switch(e){ + case ECheckType(e,_): + var val = expr(e); + if (retry_expr(entry_pc, entry_frame)) return null; + return val; + case EMeta(_, _, e): + var val = expr(e); + if (retry_expr(entry_pc, entry_frame)) return null; + return val; + case ESwitch(e, cases, def): + var val : Dynamic = expr(e); + if (retry_expr(entry_pc, entry_frame)) return null; + var match = false; + for( c in cases ) { + for( v in c.values ) + if ( expr(v) == val ) { + if (retry_expr(entry_pc, entry_frame)) return null; + match = true; + break; + } + if( match ) { + val = expr(c.expr); + if (retry_expr(entry_pc, entry_frame)) return null; + break; + } + } + if( !match ){ + val = def == null ? null : expr(def); + if (retry_expr(entry_pc, entry_frame)) return null; + } + return val; + case ETernary(econd, e1, e2): + var cond = expr(econd); + if (retry_expr(entry_pc, entry_frame)) return null; + return if( cond == true ) expr(e1) else expr(e2); + case EObject(fl): + var o = {}; + for( f in fl ) + set(o, f.name, expr(f.e)); + if (retry_expr(entry_pc, entry_frame)) return null; + return o; + case EThrow(e): + var arg = expr(e); + if (retry_expr(entry_pc, entry_frame)) return null; + throw arg; + case ENew(cl,params): + var a = new Array(); + for( e in params ) + a.push(expr(e)); + if (retry_expr(entry_pc, entry_frame)) return null; + return cnew(cl,a); + case EArray(e, index): + var arr:Dynamic = expr(e); + if (retry_expr(entry_pc, entry_frame)) return null; + var index:Dynamic = expr(index); + if (retry_expr(entry_pc, entry_frame)) return null; + if (isMap(arr)) { + return getMapValue(arr, index); + } + else { + return arr[index]; + } + case EReturn(e): + returnValue = e == null ? null : expr(e); + if (retry_expr(entry_pc, entry_frame)) return null; + throw SReturn; + case EIf(econd, e1, e2): + var res = expr(econd); + if (retry_expr(entry_pc, entry_frame)) return null; + return if( res == true ) expr(e1) else if( e2 == null ) null else expr(e2); + case EWhile(econd, eb): + var body:Array; + switch(eb){ + case EBlock(a): + body = a; + default: + body = [eb]; + } + pushFrame(new StackFrame(body, CSWhile, econd, declared.length)); + return null; + case EDoWhile(econd, eb): + var body:Array; + switch(eb){ + case EBlock(a): + body = a; + default: + body = [eb]; + } + pushFrame(new StackFrame(body, CSDoWhile, econd, declared.length)); + return null; + case EBinop(op, e1, e2): + expr(e1); + if(retry_expr(entry_pc, entry_frame)) return null; + expr(e2); + if(retry_expr(entry_pc, entry_frame)) return null; + + reset_calls(); + + var fop = binops.get(op); + if( fop == null ) error(EInvalidOp(op)); + return fop(e1,e2); + case EBlock(eb): + pushFrame(new StackFrame(eb, CSBlock)); + case ECall(e, params): + var args = new Array(); + for ( p in params ){ + args.push(expr(p)); + if(retry_expr(entry_pc, entry_frame)) return null; + } + + switch( Tools.expr(e) ) { + case EField(e,f): + var obj = expr(e); + if (retry_expr(entry_pc, entry_frame)) return null; + if( obj == null ) error(EInvalidAccess(f)); + return fcall(obj,f,args); + default: + var target = expr(e); + if(retry_expr(entry_pc, entry_frame)) return null; + return call(null,target,args); + } + case EFunction(params, fexpr, name, _): + var hasOpt = false, minParams = 0; + for( p in params ) + if( p.opt ) + hasOpt = true; + else + minParams++; + var f = function(args:Array){ + if (current_frame.call_results[current_frame.call_count] != null){ + if (current_frame.call_results[current_frame.call_count].complete){ + trace("Call " + current_frame.call_count + " already resolved (" + current_frame.call_results[current_frame.call_count].result + ")"); + current_frame.call_count++; + return current_frame.call_results[current_frame.call_count-1].result; + } + } + else{ + trace("Making new call sub..."); + } + + var block:Array = switch(fexpr){ + case EBlock(a): + a; + default: + [fexpr]; + } + var frame:StackFrame = new StackFrame(block, CSCall); + + if( args.length != params.length ) { + if( args.length < minParams ) { + var str = "Invalid number of parameters. Got " + args.length + ", required " + minParams; + if( name != null ) str += " for function '" + name+"'"; + throw str; + } + // make sure mandatory args are forced + var args2 = []; + var extraParams = args.length - minParams; + var pos = 0; + for( p in params ) + if( p.opt ) { + if( extraParams > 0 ) { + args2.push(args[pos++]); + extraParams--; + } else + args2.push(null); + } else + args2.push(args[pos++]); + args = args2; + } + + for( i in 0...params.length ){ + frame.locals.set(params[i].name, { r : args[i] }); + } + + current_frame.called = true; + frame.call_id = current_frame.call_count; + current_frame.call_results.push({result:null, complete:false}); + frame.parent = current_frame; + + pushFrame(frame); + return null; + }; + + f = Reflect.makeVarArgs(f); + if (name != null){ + locals.set(name, {r: f}); + if (frame_stack.length == 0){ + variables.set(name, f); + } + } + return f; + case EVar(n, _, e): + var val:Dynamic = null; + if (e != null){ + val = expr(e); + if(retry_expr(entry_pc, entry_frame)) return null; + } + declared.push({ n : n, old : locals.get(n) }); + locals.set(n,{ r : (e == null)?null:val }); + return null; + default: + return super.expr(e); + } + return null; + } + + override function restore(old:Int):Void{ + //do nothing; + } + + //Query: Does this have a bug condition where it won't accept locals declared to null? + override function resolve( id : String ) : Dynamic { + var i:Int = frame_stack.length; + var search_area:Map = current_frame.locals; + var l:Dynamic = null; + while (i >= 0){ + i--; + l = search_area.get(id); + if (l != null){ + return l.r; + } + if(i>=0){ + search_area = frame_stack[i].locals; + } + } + //if ( l != null ) + // return l.r; + var v = variables.get(id); + if( v == null && !variables.exists(id) ) + error(EUnknownVariable(id)); + return v; + } + + override function increment( e : Expr, prefix : Bool, delta : Int ) : Dynamic { + #if hscriptPos + curExpr = e; + var e = e.e; + #end + switch(e) { + case EIdent(id): + var i:Int = frame_stack.length; + var search_area:Map = current_frame.locals; + var l:Dynamic = null; + while (i >= 0){ + i--; + l = search_area.get(id); + if (l != null){ + break; + } + if(i>=0){ + search_area = frame_stack[i].locals; + } + } + + var v : Dynamic = (l == null) ? variables.get(id) : l.r; + if( prefix ) { + v += delta; + if( l == null ) variables.set(id,v) else l.r = v; + } else + if( l == null ) variables.set(id,v + delta) else l.r = v + delta; + return v; + case EField(e,f): + var obj = expr(e); + var v : Dynamic = get(obj,f); + if( prefix ) { + v += delta; + set(obj,f,v); + } else + set(obj,f,v + delta); + return v; + case EArray(e, index): + var arr:Dynamic = expr(e); + var index:Dynamic = expr(index); + if (isMap(arr)) { + var v = getMapValue(arr, index); + if (prefix) { + v += delta; + setMapValue(arr, index, v); + } + else { + setMapValue(arr, index, v + delta); + } + return v; + } + else { + var v = arr[index]; + if( prefix ) { + v += delta; + arr[index] = v; + } else + arr[index] = v + delta; + return v; + } + default: + return error(EInvalidOp((delta > 0)?"++":"--")); + } + } + + + private inline function retry_expr(old_pc:Int, old_frame:StackFrame):Bool{ + if (current_frame != old_frame){ + old_frame.pc = old_pc; + old_frame.call_count = 0; + return true; + } + + return false; + } + + private inline function reset_calls():Void{ + current_frame.call_count = 0; + } +} + + +enum ControlStructure{ + CSBlock; + CSWhile; + CSDoWhile; + CSCall; +} From cc6358c57488c2641f94771537626863e59446ec Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Tue, 26 Mar 2019 16:13:02 +0900 Subject: [PATCH 03/17] Gut readme Just using the readme to explain what this fork is. --- README.md | 96 ++++++++----------------------------------------------- 1 file changed, 13 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 8663a181..53814d9e 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,21 @@ -hscript -======= +IterativeInterp +=============== -[![TravisCI Build Status](https://travis-ci.org/HaxeFoundation/hscript.svg?branch=master)](https://travis-ci.org/HaxeFoundation/hscript) -[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/HaxeFoundation/hscript?branch=master&svg=true)](https://ci.appveyor.com/project/HaxeFoundation/hscript) +An alternative interpreter that has been frankensteined into running somewhat iteratively. -Parse and evalutate Haxe expressions. +Usage: +Make a new instance of IterativeInterp: +`var myInterp = new IterativeInterp();` -In some projects it's sometimes useful to be able to interpret some code dynamically, without recompilation. +Pass `prepareScript` a script (`Expr`) that has been parsed with the regular `hscript.Parser` parser, as well as an optional `Dynamic->Void` callback function. -Haxe script is a complete subset of the Haxe language. +`myInterp.prepareScript(myScript, myCallbackFunction)` -It is dynamically typed but allows all Haxe expressions apart from type (class,enum,typedef) declarations. - -Usage ------ - -```haxe -var expr = "var x = 4; 1 + 2 * x"; -var parser = new hscript.Parser(); -var ast = parser.parseString(expr); -var interp = new hscript.Interp(); -trace(interp.execute(ast)); +Step through the script however you wish. An OpenFL example would be: ``` - -In case of a parsing error an `hscript.Expr.Error` is thrown. You can use `parser.line` to check the line number. - -You can set some globaly accessible identifiers by using `interp.variables.set("name",value)` - -Example -------- - -Here's a small example of Haxe Script usage : -```haxe -var script = " - var sum = 0; - for( a in angles ) - sum += Math.cos(a); - sum; -"; -var parser = new hscript.Parser(); -var program = parser.parseString(script); -var interp = new hscript.Interp(); -interp.variables.set("Math",Math); // share the Math class -interp.variables.set("angles",[0,1,2,3]); // set the angles list -trace( interp.execute(program) ); +//Each frame, run the interpreter for 100 steps or until the script returns (whichever comes first) +addEventListener(Event.ENTER_FRAME, function(e){ + myInterp.step(100); +} ``` - -This will calculate the sum of the cosines of the angles given as input. - -Haxe Script has not been really optimized, and it's not meant to be very fast. But it's entirely crossplatform since it's pure Haxe code (it doesn't use any platform-specific API). - -Advanced Usage --------------- - -When compiled with `-D hscriptPos` you will get fine error reporting at parsing time. - -You can subclass `hscript.Interp` to override behaviors for `get`, `set`, `call`, `fcall` and `cnew`. - -You can add more binary and unary operations to the parser by setting `opPriority`, `opRightAssoc` and `unops` content. - -You can use `parser.allowJSON` to allow JSON data. - -You can use `parser.allowTypes` to parse types for local vars, exceptions, function args and return types. Types are ignored by the interpreter. - -You can use `parser.allowMetadata` to parse metadata before expressions on in anonymous types. Metadata are ignored by the interpreter. - -You can use `new hscript.Macro(pos).convert(ast)` to convert an hscript AST to a Haxe macros one. - -Limitations ------------ - -Compared to Haxe, limitations are : - -- no type declarations (classes, enums, typedefs) : only expressions -- `switch` construct is supported but not pattern matching (no variable capture, we use strict equality to compare `case` values and `switch` value) -- only one variable declaration is allowed in `var` -- the parser supports optional types for `var` and `function` if `allowTypes` is set, but the interpreter ignores them -- you can enable per-expression position tracking by compiling with `-D hscriptPos` - -Install -------- - -In order to install Haxe Script, use `haxelib install hscript` and compile your program with `-lib hscript`. - -There are only three files in hscript : - - - `hscript.Expr` : contains enums declarations - - `hscript.Parser` : a small parser that turns a string into an expression structure (AST) - - `hscript.Interp` : a small interpreter that execute the AST and returns the latest evaluated value From 632556996003cc66562b42ee654ed44c310a44ab Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Tue, 26 Mar 2019 16:13:24 +0900 Subject: [PATCH 04/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 53814d9e..e3ff1157 100644 --- a/README.md +++ b/README.md @@ -17,5 +17,5 @@ Step through the script however you wish. An OpenFL example would be: //Each frame, run the interpreter for 100 steps or until the script returns (whichever comes first) addEventListener(Event.ENTER_FRAME, function(e){ myInterp.step(100); -} +}); ``` From 978d6bdeaba0daa36946d8b0c05a224eb506a3fe Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Tue, 26 Mar 2019 16:25:09 +0900 Subject: [PATCH 05/17] Update README.md --- README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3ff1157..0f56125c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ IterativeInterp An alternative interpreter that has been frankensteined into running somewhat iteratively. -Usage: +Usage +----- Make a new instance of IterativeInterp: `var myInterp = new IterativeInterp();` @@ -19,3 +20,19 @@ addEventListener(Event.ENTER_FRAME, function(e){ myInterp.step(100); }); ``` + +Mechanism +--------- +There are two major changes from the original, fully-recursive interpreter. + +First, the contents of Expr.EBlock(a:Array) expressions are now evaluated one-Expr-per-step, rather than all in one call. This ensures that `while` and other loops cannot lock up your program; even if the script is in an infinite loop, it will yield after the step amount given in `IterativeInterp.step(steps:Int)`. Each nested EBlock runs in its own Haxe-level stack frame, with local variable access attempts bubbling up to higher stack frames. + +Second, functions defined within a script now indicate that they have been called via `ECall`, and all `Expr`s that could return a value will first check whether that `Expr` is an `ECall` waiting on a result before returning. A hscript-defined function being evaluated will force the script to re-attempt the current `Expr` evaluation before continuing. + +Use Case +-------- +IterativeInterp is intended to be used in situations where arbitrary hscript is being executed on non-threaded targets, to ensure that said hscript cannot crash or infinitely hang the program. + +Drawbacks +--------- +As all value-returning expressions are now essentially checking for function calls, and multiple function calls (such as an array declared with multiple members all being the result of a function call), there is considerable overhead. IterativeInterp is likely to be several factors slower than regular Interp. From caa6bb46fc04bb7fdc00d5160adc5bd313f10e92 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Wed, 27 Mar 2019 11:15:26 +0900 Subject: [PATCH 06/17] Fix package --- hscript/IterativeInterp.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index 8dbcc971..f8bc04a7 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -20,7 +20,7 @@ * DEALINGS IN THE SOFTWARE. */ -package; +package hscript; import hscript.Expr.*; import hscript.Tools; From 3a37eba0d08df61effd5c800ffbb7af1d5f2024a Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Wed, 27 Mar 2019 11:20:23 +0900 Subject: [PATCH 07/17] Add StackFrame type Forgot about this little thinger... --- hscript/IterativeInterp.hx | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index f8bc04a7..9197fd44 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -528,3 +528,44 @@ enum ControlStructure{ CSDoWhile; CSCall; } + +class StackFrame +{ + private static var next_id:Int = 0; + public var id:Int; + public var pc:Int; + public var block:Array; + public var control:ControlStructure; + public var condition:Expr; + public var old:Int; + public var locals:Map; + public var call_results:Array<{result:Dynamic, complete:Bool}>; + public var call_count:Int = 0; + public var call_id:Int = -1; + public var called:Bool = false; + public var parent:StackFrame; + + public function new(block:Array, control:ControlStructure, ?condition:Expr, ?old:Int) + { + this.id = next_id; + next_id++; + this.pc = 0; + this.locals = new Map(); + this.call_results = []; + this.block = block; + this.control = control; + this.condition = condition; + this.old = old; + switch(control){ + case CSWhile: + this.block = block.concat([]); + this.block.push(EIf(condition, ECall(EIdent('__intern_reset_pc'), []))); + pc = this.block.length-1; + case CSDoWhile: + this.block = block.concat([]); + this.block.push(EIf(condition, ECall(EIdent('__intern_reset_pc'), []))); + default: + } + } + +} From e08b1edc59bd28c0c2353c782396f887c8b2b2a9 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Wed, 27 Mar 2019 11:50:30 +0900 Subject: [PATCH 08/17] Put debug traces behind hs_verbose haxedef Should quiet things down a bit --- hscript/IterativeInterp.hx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index 9197fd44..b049be59 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -48,7 +48,9 @@ class IterativeInterp extends Interp var me = this; variables.set("__intern_reset_pc", function(){ me.current_frame.pc = 0; + #if hs_verbose trace("Resetting block: " + me.current_frame); + #end }); frame_stack = []; script_complete = false; @@ -110,11 +112,13 @@ class IterativeInterp extends Interp current_frame.call_count = 0; + #if hs_verbose for(b in frame_stack){ trace(b); } trace(current_frame); trace(e); + #end try{ expr(e); } @@ -186,7 +190,9 @@ class IterativeInterp extends Interp exiting = true; } if (exiting){ + #if hs_verbose trace("Exiting block... "); + #end if (frame_stack.length != 0){ popFrame(); } @@ -204,7 +210,9 @@ class IterativeInterp extends Interp } override public function expr( e : Expr ) : Dynamic { + #if hs_verbose trace(e); + #end var entry_frame:StackFrame = current_frame; var entry_pc:Int = current_frame.pc; switch(e){ @@ -338,14 +346,18 @@ class IterativeInterp extends Interp var f = function(args:Array){ if (current_frame.call_results[current_frame.call_count] != null){ if (current_frame.call_results[current_frame.call_count].complete){ + #if hs_verbose trace("Call " + current_frame.call_count + " already resolved (" + current_frame.call_results[current_frame.call_count].result + ")"); + #end current_frame.call_count++; return current_frame.call_results[current_frame.call_count-1].result; } } + #if hs_verbose else{ trace("Making new call sub..."); } + #end var block:Array = switch(fexpr){ case EBlock(a): @@ -356,7 +368,7 @@ class IterativeInterp extends Interp var frame:StackFrame = new StackFrame(block, CSCall); if( args.length != params.length ) { - if( args.length < minParams ) { + if ( args.length < minParams ) { var str = "Invalid number of parameters. Got " + args.length + ", required " + minParams; if( name != null ) str += " for function '" + name+"'"; throw str; From 7501e00465ff2e8dc787412f63b86407af8ec3da Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Wed, 10 Apr 2019 12:56:36 +0900 Subject: [PATCH 09/17] Catch a null-instruction crash case --- hscript/IterativeInterp.hx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index b049be59..c49c4b57 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -22,7 +22,8 @@ package hscript; -import hscript.Expr.*; +import hscript.Expr.Expr; + import hscript.Tools; import hscript.Expr; import hscript.Interp; @@ -38,6 +39,7 @@ class IterativeInterp extends Interp public var frame_stack:Array; public var current_frame:StackFrame; public var script_complete:Bool; + var yield:Bool = false; public function prepareScript(e:Expr, ?on_complete:Dynamic->Void):Void{ depth = 0; locals = new Map(); @@ -52,6 +54,12 @@ class IterativeInterp extends Interp trace("Resetting block: " + me.current_frame); #end }); + variables.set("__intern_yield", function(){ + me.yield = true; + #if hs_verbose + trace("Yield called by user-space script in frame:" +me.current_frame); + #end + }); frame_stack = []; script_complete = false; switch(e){ @@ -98,10 +106,16 @@ class IterativeInterp extends Interp locals = current_frame.locals; } - public function stepScript(steps:Int=1):Void{ - while (!script_complete && steps > 0){ + public function stepScript(steps:Int = 1):Void{ + yield = false; + while (!yield && !script_complete && steps > 0){ steps--; var e:Expr = current_frame.block[current_frame.pc]; + if (e == null){ + script_complete = true; + _on_complete("Script error: Null"); + return; + } current_frame.pc++; if (current_frame.called){ current_frame.called = false; From 68cf69dc86875d774d8436dcfe5ece55ce7a7371 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Wed, 24 Apr 2019 13:06:04 +0900 Subject: [PATCH 10/17] Fix assign() to support stack-based local vars `assign()` (usually triggered by `EBinop("="...`) was not searching through the stack to find local vars outside of the immediate block's scope. It now does so. Also identified a few areas that used the same search method and merged them into a single inlined function, `find_local(id:String):Dynamic`. There are only minor differences between `find_local` and `resolve`, and they could potentially be merged with some adjustments to `assign`, `increment`, etc. --- hscript/IterativeInterp.hx | 104 ++++++++++++++++++++++++++++++------- 1 file changed, 86 insertions(+), 18 deletions(-) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index c49c4b57..dfa26291 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -35,11 +35,17 @@ import hscript.Interp; */ class IterativeInterp extends Interp { - public var _on_complete:Dynamic->Void; - public var frame_stack:Array; - public var current_frame:StackFrame; - public var script_complete:Bool; + var _on_complete:Dynamic->Void; + var frame_stack:Array; + var current_frame:StackFrame; + var script_complete:Bool; var yield:Bool = false; + + /** + Initialize this interp instance with a script to step through. + @param e - an Expr - ideally one containing at least one EBlock. + @param on_complete - a Dynamic->Void which will be called with the Expr's return value should the Expr return. + **/ public function prepareScript(e:Expr, ?on_complete:Dynamic->Void):Void{ depth = 0; locals = new Map(); @@ -62,6 +68,10 @@ class IterativeInterp extends Interp }); frame_stack = []; script_complete = false; + #if hscriptPos + curExpr = e; + var e = e.e; + #end switch(e){ case EBlock(a): current_frame = new StackFrame(a, CSBlock); @@ -106,6 +116,10 @@ class IterativeInterp extends Interp locals = current_frame.locals; } + /** + Iterate through the previously-supplied script. Will stop early if the script completes or a yield is called in user-space + @param steps The number of instructions to step through + **/ public function stepScript(steps:Int = 1):Void{ yield = false; while (!yield && !script_complete && steps > 0){ @@ -221,9 +235,18 @@ class IterativeInterp extends Interp } } + if (script_complete){ + if (_on_complete != null){ + _on_complete(returnValue); + } + } } override public function expr( e : Expr ) : Dynamic { + #if hscriptPos + curExpr = e; + var e = e.e; + #end #if hs_verbose trace(e); #end @@ -473,19 +496,7 @@ class IterativeInterp extends Interp #end switch(e) { case EIdent(id): - var i:Int = frame_stack.length; - var search_area:Map = current_frame.locals; - var l:Dynamic = null; - while (i >= 0){ - i--; - l = search_area.get(id); - if (l != null){ - break; - } - if(i>=0){ - search_area = frame_stack[i].locals; - } - } + var l = find_local(id); var v : Dynamic = (l == null) ? variables.get(id) : l.r; if( prefix ) { @@ -530,8 +541,63 @@ class IterativeInterp extends Interp return error(EInvalidOp((delta > 0)?"++":"--")); } } + + override public function assign(e1:Expr, e2:Expr):Dynamic{ + var v = expr(e2); + switch( Tools.expr(e1) ) { + case EIdent(id): + var l:Dynamic = find_local(id); + if( l == null ){ + variables.set(id, v); + } + else{ + l.r = v; + } + case EField(e,f): + v = set(expr(e),f,v); + case EArray(e, index): + var arr:Dynamic = expr(e); + var index:Dynamic = expr(index); + if (isMap(arr)) { + setMapValue(arr, index, v); + } + else { + arr[index] = v; + } + + default: + error(EInvalidOp("=")); + } + return v; + } + + /** + Searches from the local scope outwards until it finds the variable or returns null + @param id The name of the variable + @return The resolved variable, or null + **/ + private inline function find_local(id:String):Dynamic{ + var i:Int = frame_stack.length; + var search_area:Map = current_frame.locals; + var l:Dynamic = null; + while (i >= 0){ + i--; + l = search_area.get(id); + if (l != null){ + break; + } + if(i>=0){ + search_area = frame_stack[i].locals; + } + } + return l; + } - + /** + If we've changed frame in the middle of a non-sliceable expression, this inline detects that, + and rolls back the PC on the previous frame so as to retry that expression's evaluation once the + new frame (likely spawned by a user-space function call) has returned a value. + **/ private inline function retry_expr(old_pc:Int, old_frame:StackFrame):Bool{ if (current_frame != old_frame){ old_frame.pc = old_pc; @@ -582,6 +648,8 @@ class StackFrame this.control = control; this.condition = condition; this.old = old; + + //Loop structures are modified to just have an if-statement that resets the frame's program counter if necessary switch(control){ case CSWhile: this.block = block.concat([]); From fae43ba08b288dd272e84455c15c402c147a89e1 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Wed, 24 Apr 2019 14:48:08 +0900 Subject: [PATCH 11/17] Move readme elsewhere --- IterativeREADME.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 IterativeREADME.md diff --git a/IterativeREADME.md b/IterativeREADME.md new file mode 100644 index 00000000..96cb8b74 --- /dev/null +++ b/IterativeREADME.md @@ -0,0 +1,38 @@ +IterativeInterp +=============== + +An alternative interpreter that has been frankensteined into running somewhat iteratively. + +Usage +----- + +Make a new instance of IterativeInterp: +`var myInterp = new IterativeInterp();` + +Pass `prepareScript` a script (`Expr`) that has been parsed with the regular `hscript.Parser` parser, as well as an optional `Dynamic->Void` callback function. + +`myInterp.prepareScript(myScript, myCallbackFunction)` + +Step through the script however you wish. An OpenFL example would be: +``` +//Each frame, run the interpreter for 100 steps or until the script returns (whichever comes first) +addEventListener(Event.ENTER_FRAME, function(e){ + myInterp.step(100); +}); +``` + +Mechanism +--------- +There are two major changes from the original, fully-recursive interpreter. + +First, the contents of Expr.EBlock(a:Array) expressions are now evaluated one-Expr-per-step, rather than all in one call. This ensures that `while` and other loops cannot lock up your program; even if the script is in an infinite loop, it will yield after the step amount given in `IterativeInterp.step(steps:Int)`. Each nested EBlock runs in its own Haxe-level stack frame, with local variable access attempts bubbling up to higher stack frames. + +Second, functions defined within a script now indicate that they have been called via `ECall`, and all `Expr`s that could return a value will first check whether that `Expr` is an `ECall` waiting on a result before returning. A hscript-defined function being evaluated will force the script to re-attempt the current `Expr` evaluation before continuing. + +Use Case +-------- +IterativeInterp is intended to be used in situations where arbitrary hscript is being executed on non-threaded targets, to ensure that said hscript cannot crash or infinitely hang the program. + +Drawbacks +--------- +As all value-returning expressions are now essentially checking for function calls, and multiple function calls (such as an array declared with multiple members all being the result of a function call), there is considerable overhead. IterativeInterp is likely to be several factors slower than regular Interp. From 6ffa140fcd44fea962a01a65e5abee15accf66cf Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Wed, 24 Apr 2019 14:49:04 +0900 Subject: [PATCH 12/17] revert readme to make less ugly pull --- README.md | 103 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 0f56125c..8663a181 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,91 @@ -IterativeInterp -=============== +hscript +======= -An alternative interpreter that has been frankensteined into running somewhat iteratively. +[![TravisCI Build Status](https://travis-ci.org/HaxeFoundation/hscript.svg?branch=master)](https://travis-ci.org/HaxeFoundation/hscript) +[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/github/HaxeFoundation/hscript?branch=master&svg=true)](https://ci.appveyor.com/project/HaxeFoundation/hscript) + +Parse and evalutate Haxe expressions. + + +In some projects it's sometimes useful to be able to interpret some code dynamically, without recompilation. + +Haxe script is a complete subset of the Haxe language. + +It is dynamically typed but allows all Haxe expressions apart from type (class,enum,typedef) declarations. Usage ----- -Make a new instance of IterativeInterp: -`var myInterp = new IterativeInterp();` +```haxe +var expr = "var x = 4; 1 + 2 * x"; +var parser = new hscript.Parser(); +var ast = parser.parseString(expr); +var interp = new hscript.Interp(); +trace(interp.execute(ast)); +``` -Pass `prepareScript` a script (`Expr`) that has been parsed with the regular `hscript.Parser` parser, as well as an optional `Dynamic->Void` callback function. +In case of a parsing error an `hscript.Expr.Error` is thrown. You can use `parser.line` to check the line number. -`myInterp.prepareScript(myScript, myCallbackFunction)` +You can set some globaly accessible identifiers by using `interp.variables.set("name",value)` -Step through the script however you wish. An OpenFL example would be: -``` -//Each frame, run the interpreter for 100 steps or until the script returns (whichever comes first) -addEventListener(Event.ENTER_FRAME, function(e){ - myInterp.step(100); -}); +Example +------- + +Here's a small example of Haxe Script usage : +```haxe +var script = " + var sum = 0; + for( a in angles ) + sum += Math.cos(a); + sum; +"; +var parser = new hscript.Parser(); +var program = parser.parseString(script); +var interp = new hscript.Interp(); +interp.variables.set("Math",Math); // share the Math class +interp.variables.set("angles",[0,1,2,3]); // set the angles list +trace( interp.execute(program) ); ``` -Mechanism ---------- -There are two major changes from the original, fully-recursive interpreter. +This will calculate the sum of the cosines of the angles given as input. + +Haxe Script has not been really optimized, and it's not meant to be very fast. But it's entirely crossplatform since it's pure Haxe code (it doesn't use any platform-specific API). + +Advanced Usage +-------------- + +When compiled with `-D hscriptPos` you will get fine error reporting at parsing time. + +You can subclass `hscript.Interp` to override behaviors for `get`, `set`, `call`, `fcall` and `cnew`. + +You can add more binary and unary operations to the parser by setting `opPriority`, `opRightAssoc` and `unops` content. + +You can use `parser.allowJSON` to allow JSON data. + +You can use `parser.allowTypes` to parse types for local vars, exceptions, function args and return types. Types are ignored by the interpreter. + +You can use `parser.allowMetadata` to parse metadata before expressions on in anonymous types. Metadata are ignored by the interpreter. + +You can use `new hscript.Macro(pos).convert(ast)` to convert an hscript AST to a Haxe macros one. + +Limitations +----------- + +Compared to Haxe, limitations are : + +- no type declarations (classes, enums, typedefs) : only expressions +- `switch` construct is supported but not pattern matching (no variable capture, we use strict equality to compare `case` values and `switch` value) +- only one variable declaration is allowed in `var` +- the parser supports optional types for `var` and `function` if `allowTypes` is set, but the interpreter ignores them +- you can enable per-expression position tracking by compiling with `-D hscriptPos` + +Install +------- -First, the contents of Expr.EBlock(a:Array) expressions are now evaluated one-Expr-per-step, rather than all in one call. This ensures that `while` and other loops cannot lock up your program; even if the script is in an infinite loop, it will yield after the step amount given in `IterativeInterp.step(steps:Int)`. Each nested EBlock runs in its own Haxe-level stack frame, with local variable access attempts bubbling up to higher stack frames. - -Second, functions defined within a script now indicate that they have been called via `ECall`, and all `Expr`s that could return a value will first check whether that `Expr` is an `ECall` waiting on a result before returning. A hscript-defined function being evaluated will force the script to re-attempt the current `Expr` evaluation before continuing. +In order to install Haxe Script, use `haxelib install hscript` and compile your program with `-lib hscript`. -Use Case --------- -IterativeInterp is intended to be used in situations where arbitrary hscript is being executed on non-threaded targets, to ensure that said hscript cannot crash or infinitely hang the program. +There are only three files in hscript : -Drawbacks ---------- -As all value-returning expressions are now essentially checking for function calls, and multiple function calls (such as an array declared with multiple members all being the result of a function call), there is considerable overhead. IterativeInterp is likely to be several factors slower than regular Interp. + - `hscript.Expr` : contains enums declarations + - `hscript.Parser` : a small parser that turns a string into an expression structure (AST) + - `hscript.Interp` : a small interpreter that execute the AST and returns the latest evaluated value From 8a9bd900e352054f3a7d909c101f59ca2ee43025 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Fri, 26 Apr 2019 13:22:23 +0900 Subject: [PATCH 13/17] Fix consecutive block ends crashing interp bug Two codeblocks ending consecutively with no expression in between eg ``` if(foo){ if(bar){ trace('hello'); } //no expression here would trigger the bug } ``` Would crash the script. The fetch phase will now, on encountering the end of a block, continue bubbling up through stack frames until it either finds one that will continue or returns a value and ends the script. --- hscript/IterativeInterp.hx | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index dfa26291..9c78b902 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -126,8 +126,9 @@ class IterativeInterp extends Interp steps--; var e:Expr = current_frame.block[current_frame.pc]; if (e == null){ + script_complete = true; - _on_complete("Script error: Null"); + _on_complete("Script error: "+current_frame); return; } current_frame.pc++; @@ -207,29 +208,24 @@ class IterativeInterp extends Interp } } - if (current_frame.pc >= current_frame.block.length){ - var exiting = false; + while (current_frame.pc >= current_frame.block.length){ switch(current_frame.control){ case CSCall: current_frame.parent.call_results[current_frame.call_id].complete = true; current_frame.parent.call_results[current_frame.call_id].result = null; //If we just got to the end of the block without returning, the result is null - exiting = true; default: - exiting = true; } - if (exiting){ - #if hs_verbose - trace("Exiting block... "); - #end - if (frame_stack.length != 0){ - popFrame(); - } - else{ - if (_on_complete != null){ - script_complete = true; - _on_complete(returnValue); - return; - } + #if hs_verbose + trace("Exiting block... "); + #end + if (frame_stack.length != 0){ + popFrame(); + } + else{ + if (_on_complete != null){ + script_complete = true; + _on_complete(returnValue); + return; } } } From e114ebd3902d5f722204a1a835184aaa79ecd14f Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Tue, 7 May 2019 14:26:35 +0900 Subject: [PATCH 14/17] Make compatible with hscriptPos + fix locals bug Now fully compatible with -DhscriptPos. Internally-generated expressions simply show "Internal" source and zero for all line/position telemetry. Fixed a bug where pushing a new frame to the stack wasn't updating locals. --- hscript/IterativeInterp.hx | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index 9c78b902..90037262 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -75,36 +75,34 @@ class IterativeInterp extends Interp switch(e){ case EBlock(a): current_frame = new StackFrame(a, CSBlock); - locals = current_frame.locals; case EWhile(econd, eb): var a:Array; - switch(eb){ + switch(Tools.expr(eb)){ case EBlock(ea): a = ea; default: a = [eb]; } current_frame = new StackFrame(a, CSWhile, econd, declared.length); - locals = current_frame.locals; case EDoWhile(econd, eb): var a:Array; - switch(eb){ + switch(Tools.expr(eb)){ case EBlock(ea): a = ea; default: a = [eb]; } current_frame = new StackFrame(a, CSDoWhile, econd, declared.length); - locals = current_frame.locals; default: - current_frame = new StackFrame([e], CSBlock); - locals = current_frame.locals; + current_frame = new StackFrame([Tools.exprify(e)], CSBlock); } + locals = current_frame.locals; } private function pushFrame(block:StackFrame){ frame_stack.push(current_frame); current_frame = block; + locals = current_frame.locals; } private function popFrame(){ @@ -320,7 +318,7 @@ class IterativeInterp extends Interp return if( res == true ) expr(e1) else if( e2 == null ) null else expr(e2); case EWhile(econd, eb): var body:Array; - switch(eb){ + switch(Tools.expr(eb)){ case EBlock(a): body = a; default: @@ -330,7 +328,7 @@ class IterativeInterp extends Interp return null; case EDoWhile(econd, eb): var body:Array; - switch(eb){ + switch(Tools.expr(eb)){ case EBlock(a): body = a; default: @@ -392,7 +390,7 @@ class IterativeInterp extends Interp } #end - var block:Array = switch(fexpr){ + var block:Array = switch(Tools.expr(fexpr)){ case EBlock(a): a; default: @@ -453,7 +451,7 @@ class IterativeInterp extends Interp locals.set(n,{ r : (e == null)?null:val }); return null; default: - return super.expr(e); + return super.expr(#if hscriptPos curExpr #else e #end); } return null; } @@ -649,13 +647,22 @@ class StackFrame switch(control){ case CSWhile: this.block = block.concat([]); + #if hscriptPos + this.block.push(Tools.exprify(EIf(condition, Tools.exprify(ECall(Tools.exprify(EIdent('__intern_reset_pc')), []))))); + #else this.block.push(EIf(condition, ECall(EIdent('__intern_reset_pc'), []))); + #end pc = this.block.length-1; case CSDoWhile: this.block = block.concat([]); + #if hscriptPos + this.block.push(Tools.exprify(EIf(condition, Tools.exprify(ECall(Tools.exprify(EIdent('__intern_reset_pc')), []))))); + #else this.block.push(EIf(condition, ECall(EIdent('__intern_reset_pc'), []))); + #end default: } } + } From 73d283185bda863dcb0aaeeea28d924edde14009 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Tue, 7 May 2019 14:29:25 +0900 Subject: [PATCH 15/17] Add exprify function (iterative hscriptPos compat) Tools.exprify(e:Expr/ExprDef):Expr is basically the equivalent of Tools.mk for runtime-generated (ie, has no position in source hscript) exprs. It allows hscript.IterativeInterp to be compatible with -DhscriptPos. --- hscript/Tools.hx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/hscript/Tools.hx b/hscript/Tools.hx index e0dc2359..bb761534 100644 --- a/hscript/Tools.hx +++ b/hscript/Tools.hx @@ -105,5 +105,22 @@ class Tools { return e; #end } - -} \ No newline at end of file + + /** + Used for generating contextless Exprs at runtime for hscriptPos compatibility + **/ + public static inline function exprify(#if hscriptPos e:ExprDef #else e:Expr #end):Expr{ + + #if hscriptPos + return { + e : e, + pmin : 0, + pmax : 0, + origin : "Internal", + line : 0 + }; + #else + return e; + #end + } +} From 8bc6104d8830a9422cdf1be0f4afa1f8fa14c3c3 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Thu, 9 May 2019 12:28:03 +0900 Subject: [PATCH 16/17] Support for loops & complex array decs Added support for for loop syntax; for loops exist in their own stack frame and can be interrupted as with other loops. Array declarations now support function calls as members. Improved PosInfo for internal expressions. --- hscript/IterativeInterp.hx | 128 +++++++++++++++++++++++++++++++------ 1 file changed, 107 insertions(+), 21 deletions(-) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index 90037262..ccdf7edc 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -83,7 +83,7 @@ class IterativeInterp extends Interp default: a = [eb]; } - current_frame = new StackFrame(a, CSWhile, econd, declared.length); + current_frame = new StackFrame(a, CSWhile(econd)); case EDoWhile(econd, eb): var a:Array; switch(Tools.expr(eb)){ @@ -92,13 +92,21 @@ class IterativeInterp extends Interp default: a = [eb]; } - current_frame = new StackFrame(a, CSDoWhile, econd, declared.length); + current_frame = new StackFrame(a, CSDoWhile(econd)); default: - current_frame = new StackFrame([Tools.exprify(e)], CSBlock); + current_frame = new StackFrame([curExprOr(e)], CSBlock); } locals = current_frame.locals; } + private inline function curExprOr(e:#if hscriptPos ExprDef #else Expr #end):Expr{ + #if hscriptPos + return curExpr; + #else + return e; + #end + } + private function pushFrame(block:StackFrame){ frame_stack.push(current_frame); current_frame = block; @@ -154,7 +162,9 @@ class IterativeInterp extends Interp case SContinue: while (true){ switch(current_frame.control){ - case CSWhile, CSDoWhile: + case CSWhile(condition), CSDoWhile(condition): + break; + case CSFor(name, iter): break; default: popFrame(); @@ -167,7 +177,17 @@ class IterativeInterp extends Interp case SBreak: while (true){ switch(current_frame.control){ - case CSWhile, CSDoWhile: + case CSWhile(condition), CSDoWhile(condition): + if (frame_stack.length > 0){ + popFrame(); + break; + } + else{ + script_complete = true; + _on_complete(returnValue); + return; + } + case CSFor(name, iter): if (frame_stack.length > 0){ popFrame(); break; @@ -211,6 +231,12 @@ class IterativeInterp extends Interp case CSCall: current_frame.parent.call_results[current_frame.call_id].complete = true; current_frame.parent.call_results[current_frame.call_id].result = null; //If we just got to the end of the block without returning, the result is null + case CSFor(name, iterator): + if (iterator.hasNext()){ + current_frame.locals.set(name, {r:iterator.next()}); + current_frame.pc = 0; + break; + } default: } #if hs_verbose @@ -237,13 +263,13 @@ class IterativeInterp extends Interp } override public function expr( e : Expr ) : Dynamic { + #if hs_verbose + trace(e); + #end #if hscriptPos curExpr = e; var e = e.e; #end - #if hs_verbose - trace(e); - #end var entry_frame:StackFrame = current_frame; var entry_pc:Int = current_frame.pc; switch(e){ @@ -324,7 +350,7 @@ class IterativeInterp extends Interp default: body = [eb]; } - pushFrame(new StackFrame(body, CSWhile, econd, declared.length)); + pushFrame(new StackFrame(body, CSWhile(econd))); return null; case EDoWhile(econd, eb): var body:Array; @@ -334,8 +360,23 @@ class IterativeInterp extends Interp default: body = [eb]; } - pushFrame(new StackFrame(body, CSDoWhile, econd, declared.length)); + pushFrame(new StackFrame(body, CSDoWhile(econd))); return null; + case EFor(v, it, e): + var resolved_it:Dynamic = expr(it); + if (retry_expr(entry_pc, entry_frame)) return null; + var iter:Iterator = makeIterator(resolved_it); + if (iter == null || !iter.hasNext()){ + return null; + } + var body:Array; + switch(Tools.expr(e)){ + case EBlock(a): + body = a; + default: + body = [e]; + } + pushFrame(new StackFrame(body, CSFor(v, iter))); case EBinop(op, e1, e2): expr(e1); if(retry_expr(entry_pc, entry_frame)) return null; @@ -450,6 +491,51 @@ class IterativeInterp extends Interp declared.push({ n : n, old : locals.get(n) }); locals.set(n,{ r : (e == null)?null:val }); return null; + case EArrayDecl(arr): + if (arr.length > 0 && Tools.expr(arr[0]).match(EBinop("=>", _))) { + var isAllString:Bool = true; + var isAllInt:Bool = true; + var isAllObject:Bool = true; + var isAllEnum:Bool = true; + var keys:Array = []; + var values:Array = []; + for (e in arr) { + switch(Tools.expr(e)) { + case EBinop("=>", eKey, eValue): { + var key:Dynamic = expr(eKey); + if (retry_expr(entry_pc, entry_frame)) return null; + var value:Dynamic = expr(eValue); + if (retry_expr(entry_pc, entry_frame)) return null; + isAllString = isAllString && Std.is(key, String); + isAllInt = isAllInt && Std.is(key, Int); + isAllObject = isAllObject && Reflect.isObject(key); + isAllEnum = isAllEnum && Reflect.isEnumValue(key); + keys.push(key); + values.push(value); + } + default: throw("=> expected"); + } + } + var map:Dynamic = { + if (isAllInt) new haxe.ds.IntMap(); + else if (isAllString) new haxe.ds.StringMap(); + else if (isAllEnum) new haxe.ds.EnumValueMap(); + else if (isAllObject) new haxe.ds.ObjectMap(); + else throw 'Inconsistent key types'; + } + for (n in 0...keys.length) { + setMapValue(map, keys[n], values[n]); + } + return map; + } + else { + var a = new Array(); + for ( e in arr ) { + a.push(expr(e)); + if (retry_expr(entry_pc, entry_frame)) return null; + } + return a; + } default: return super.expr(#if hscriptPos curExpr #else e #end); } @@ -610,8 +696,9 @@ class IterativeInterp extends Interp enum ControlStructure{ CSBlock; - CSWhile; - CSDoWhile; + CSFor(name:String, iterator:Iterator); + CSWhile(condition:Expr); + CSDoWhile(condition:Expr); CSCall; } @@ -622,8 +709,6 @@ class StackFrame public var pc:Int; public var block:Array; public var control:ControlStructure; - public var condition:Expr; - public var old:Int; public var locals:Map; public var call_results:Array<{result:Dynamic, complete:Bool}>; public var call_count:Int = 0; @@ -631,7 +716,7 @@ class StackFrame public var called:Bool = false; public var parent:StackFrame; - public function new(block:Array, control:ControlStructure, ?condition:Expr, ?old:Int) + public function new(block:Array, control:ControlStructure) { this.id = next_id; next_id++; @@ -640,26 +725,27 @@ class StackFrame this.call_results = []; this.block = block; this.control = control; - this.condition = condition; - this.old = old; //Loop structures are modified to just have an if-statement that resets the frame's program counter if necessary switch(control){ - case CSWhile: + case CSWhile(condition): this.block = block.concat([]); #if hscriptPos - this.block.push(Tools.exprify(EIf(condition, Tools.exprify(ECall(Tools.exprify(EIdent('__intern_reset_pc')), []))))); + this.block.push(Tools.mk(EIf(condition, Tools.mk(ECall(Tools.mk(EIdent('__intern_reset_pc'), condition), []), condition)), condition)); #else this.block.push(EIf(condition, ECall(EIdent('__intern_reset_pc'), []))); #end pc = this.block.length-1; - case CSDoWhile: + case CSDoWhile(condition): this.block = block.concat([]); #if hscriptPos - this.block.push(Tools.exprify(EIf(condition, Tools.exprify(ECall(Tools.exprify(EIdent('__intern_reset_pc')), []))))); + this.block.push(Tools.mk(EIf(condition, Tools.mk(ECall(Tools.mk(EIdent('__intern_reset_pc'), condition), []), condition)), condition)); #else this.block.push(EIf(condition, ECall(EIdent('__intern_reset_pc'), []))); #end + case CSFor(name, iterator): + //we can guarantee the iterator is valid and hasNext here, because expr(e) won't spawn one of these otherwise :> + locals.set(name, {r:iterator.next()}); default: } } From 3ef1ed9ec21cdf3e952eab43c50cc0cf91eb37a5 Mon Sep 17 00:00:00 2001 From: Skerper <31716517+Skerper@users.noreply.github.com> Date: Fri, 31 May 2019 15:26:34 +0900 Subject: [PATCH 17/17] Fix continue support; `continue` now works as expected in while, dowhile, and for loops. --- hscript/IterativeInterp.hx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hscript/IterativeInterp.hx b/hscript/IterativeInterp.hx index ccdf7edc..f2baa9bb 100644 --- a/hscript/IterativeInterp.hx +++ b/hscript/IterativeInterp.hx @@ -122,6 +122,13 @@ class IterativeInterp extends Interp locals = current_frame.locals; } + + #if hscriptPos + public function currentExpression():Expr{ + return curExpr; + } + #end + /** Iterate through the previously-supplied script. Will stop early if the script completes or a yield is called in user-space @param steps The number of instructions to step through @@ -163,8 +170,10 @@ class IterativeInterp extends Interp while (true){ switch(current_frame.control){ case CSWhile(condition), CSDoWhile(condition): + current_frame.pc = current_frame.block.length - 1; break; case CSFor(name, iter): + current_frame.pc = current_frame.block.length; break; default: popFrame(); @@ -651,6 +660,7 @@ class IterativeInterp extends Interp return v; } + /** Searches from the local scope outwards until it finds the variable or returns null @param id The name of the variable