diff --git a/src/kiss/AsyncEmbeddedScript2.hx b/src/kiss/AsyncEmbeddedScript2.hx new file mode 100644 index 0000000..897cc3f --- /dev/null +++ b/src/kiss/AsyncEmbeddedScript2.hx @@ -0,0 +1,387 @@ +package kiss; + +#if macro +import haxe.macro.Expr; +import haxe.macro.Context; +import haxe.macro.PositionTools; +import sys.io.File; +import haxe.io.Path; +using haxe.io.Path; +import kiss.Helpers; +using kiss.Helpers; +using tink.MacroApi; + +#end + +import kiss.Kiss; +using kiss.Kiss; +import kiss.ReaderExp; +import kiss.Prelude; +import kiss.cloner.Cloner; +using StringTools; +import hscript.Parser; +import hscript.Interp; + +typedef Continuation2 = () -> Void; +// (this, skipping, cc) -> Void +typedef AsyncCommand2 = (AsyncEmbeddedScript2, Bool, Continuation2) -> Void; + +class ObjectInterp2 extends Interp { + var obj:T; + var fields:Map = []; + public function new(obj:T) { + this.obj = obj; + + for (field in Type.getInstanceFields(Type.getClass(obj))) { + fields[field] = true; + } + + super(); + } + + override function resolve(id:String):Dynamic { + var fieldVal = Reflect.field(obj, id); + if (fieldVal != null) + return fieldVal; + else + return super.resolve(id); + } + + // TODO every method of setting variables should try to set them on the object, + // but there are a lot of them and I might have missed some. + + override function setVar(name:String, v:Dynamic) { + if (Reflect.field(obj, name) != null) { + Reflect.setField(obj, name, v); + } else { + super.setVar(name, v); + } + } + + public override function expr( e : hscript.Expr ) : Dynamic { + var curExpr = e; + #if hscriptPos + var e = e.e; + #end + switch( e ) { + // Handle fuzzyMaps correctly: + case EArray(e, index): + var arr:Dynamic = expr(e); + var index:Dynamic = expr(index); + if (isMap(arr)) { + if (kiss.FuzzyMapTools.isFuzzy(arr)) + return getMapValue(arr, kiss.FuzzyMapTools.bestMatch(arr, index)); + return getMapValue(arr, index); + } + else { + return arr[index]; + } + case ECall(e,params): + switch( hscript.Tools.expr(e) ) { + case EIdent(name) if (fields.exists(name)): + var args = new Array(); + for( p in params ) + args.push(expr(p)); + return call(obj,expr(e),args); + default: + } + default: + } + return super.expr(curExpr); + } +} + +/** + Utility class for making statically typed, debuggable, ASYNC-BASED embedded Kiss-based DSLs. + Examples are in the hollywoo project. +**/ +class AsyncEmbeddedScript2 { + private var instructions:Array = null; + private var breakPoints:Map Bool> = []; + private var onBreak:AsyncCommand2 = null; + private var lastInstructionPointer = -1; + private var labels:Map = []; + private var noSkipInstructions:Map = []; + + private var parser = new Parser(); + private var interp:ObjectInterp2; + public var interpVariables(get, null):Map; + private function get_interpVariables() { + return interp.variables; + } + + private var hscriptInstructions:Map = []; + private function hscriptInstructionFile() return ""; + + public function setBreakHandler(handler:AsyncCommand2) { + onBreak = handler; + } + + public function addBreakPoint(instruction:Int, ?condition:() -> Bool) { + if (condition == null) { + condition = () -> true; + } + breakPoints[instruction] = condition; + } + + public function removeBreakPoint(instruction:Int) { + breakPoints.remove(instruction); + } + + public function new() { + interp = new ObjectInterp2(this); + kiss.KissInterp.prepare(interp); + if (hscriptInstructionFile().length > 0) { + #if (sys || hxnodejs) + var cacheJson:haxe.DynamicAccess = haxe.Json.parse(sys.io.File.getContent(hscriptInstructionFile())); + for (key => value in cacheJson) { + hscriptInstructions[Std.parseInt(key)] = value; + } + #end + } + } + + private function resetInstructions() {} + + public function instructionCount() { + if (instructions == null) + resetInstructions(); + return instructions.length; + } + + #if test + public var ranHscriptInstruction = false; + #end + private function runHscriptInstruction(instructionPointer:Int, skipping:Bool, cc:Continuation2) { + #if test + ranHscriptInstruction = true; + #end + interp.variables['skipping'] = skipping; + interp.variables['cc'] = cc; + if (printCurrentInstruction) + Prelude.print(hscriptInstructions[instructionPointer]); + interp.execute(parser.parseString(hscriptInstructions[instructionPointer])); + } + + private var skipTarget:Null = null; + + private function runInstruction(instructionPointer:Int, withBreakPoints = true) { + var skipping = false; + if (skipTarget != null) { + if (instructionPointer == skipTarget) { + skipTarget = null; + } + else { + skipping = true; + } + } + + lastInstructionPointer = instructionPointer; + if (instructions == null) + resetInstructions(); + if (withBreakPoints && breakPoints.exists(instructionPointer) && breakPoints[instructionPointer]()) { + if (onBreak != null) { + onBreak(this, false, () -> runInstruction(instructionPointer, false)); + } + } + var continuation = if (instructionPointer < instructions.length - 1) { + () -> { + // runInstruction may be called externally to skip through the script. + // When this happens, make sure other scheduled continuations are canceled + // by verifying that lastInstructionPointer hasn't changed + if (lastInstructionPointer == instructionPointer) { + runInstruction(instructionPointer + 1, withBreakPoints); + } + }; + } else { + () -> {}; + } + if (hscriptInstructions.exists(instructionPointer)) { + runHscriptInstruction(instructionPointer, skipping, continuation); + } else { + instructions[instructionPointer](this, skipping, continuation); + } + } + + public function run(withBreakPoints = true) { + runInstruction(0, withBreakPoints); + } + + public function runFromInstruction(ip:Int, withBreakpoints = true) { + skipTarget = ip; + runInstruction(0, withBreakpoints); + } + + public function runFromNextLabel(newScript:AsyncEmbeddedScript2, withBreakpoints = true) { + var labelPointers = [for (ip in labels) ip]; + labelPointers.sort(Reflect.compare); + for (ip in labelPointers) { + if (ip > lastInstructionPointer) { + newScript.runFromInstruction(ip); + break; + } + } + } + + public function runFromLabel(name:String) { + var ip = labels[name]; + if (lastInstructionPointer > ip) { + throw "Rewinding AsyncEmbeddedScript is not implemented"; + } + runFromInstruction(ip); + } + + public var printCurrentInstruction = true; + + #if macro + public static function build(dslHaxelib:String, dslFile:String, scriptFile:String):Array { + // trace('AsyncEmbeddedScript.build $dslHaxelib $dslFile $scriptFile'); + var k = Kiss.defaultKissState(); + + k.file = scriptFile; + var classPath = Context.getPosInfos(Context.currentPos()).file; + var loadingDirectory = Path.directory(classPath); + var classFields = []; // Kiss.build() will already include Context.getBuildFields() + + var hscriptInstructions:Map = []; + var cache:Map = []; + #if kissCache + var cacheFile = scriptFile.withoutExtension().withoutDirectory() + ".cache.json"; + if (sys.FileSystem.exists(cacheFile)) { + var cacheJson:haxe.DynamicAccess = haxe.Json.parse(sys.io.File.getContent(cacheFile)); + for (key => value in cacheJson) + cache[key] = value; + } + #end + + var hscriptInstructionFile = scriptFile.withoutExtension().withoutDirectory() + ".hscript.json"; + + var commandList:Array = []; + var labelsList:Array = []; + var noSkipList:Array = []; + + var labelNum = 0; + k.macros["label"] = (wholeExp:ReaderExp, args:Array, k:KissState) -> { + k.stateChanged = true; + wholeExp.checkNumArgs(1, 1, '(label