diff --git a/hscript/CustomClass.hx b/hscript/CustomClass.hx index 3652702..4796a64 100644 --- a/hscript/CustomClass.hx +++ b/hscript/CustomClass.hx @@ -42,6 +42,7 @@ class CustomClass implements IHScriptCustomClassBehaviour { __interp.importFailedCallback = __class.__interp.importFailedCallback; // __interp.variables = __class.staticInterp.variables; + @:privateAccess __interp.usingHandler.usingEntries = __class.ogInterp.usingHandler.usingEntries; __interp.publicVariables = __class.ogInterp.publicVariables; __interp.staticVariables = __class.ogInterp.staticVariables; __interp.customClasses = __class.ogInterp.customClasses; diff --git a/hscript/CustomClassHandler.hx b/hscript/CustomClassHandler.hx index efc3966..65afe6e 100644 --- a/hscript/CustomClassHandler.hx +++ b/hscript/CustomClassHandler.hx @@ -45,6 +45,7 @@ class CustomClassHandler implements IHScriptCustomConstructor implements IHScrip __interp.importFailedCallback = ogInterp.importFailedCallback; //__interp.variables = ogInterp.variables; + __interp.usingHandler.usingEntries = ogInterp.usingHandler.usingEntries; __interp.publicVariables = ogInterp.publicVariables; __interp.staticVariables = ogInterp.staticVariables; __interp.customClasses = ogInterp.customClasses; diff --git a/hscript/Expr.hx b/hscript/Expr.hx index 1d42355..f866f1e 100644 --- a/hscript/Expr.hx +++ b/hscript/Expr.hx @@ -79,7 +79,7 @@ enum Expr { EMeta( name : String, args : Array, e : Expr ); ECheckType( e : Expr, t : CType ); - EImport( c : String, ?asname:String ); + EImport( c : String, ?asname:String, ?isUsing:Bool ); EClass( name:String, fields:Array, ?extend:String, interfaces:Array, ?isFinal:Bool, ?isPrivate:Bool ); } diff --git a/hscript/Interp.hx b/hscript/Interp.hx index b07c206..94929f1 100644 --- a/hscript/Interp.hx +++ b/hscript/Interp.hx @@ -29,6 +29,7 @@ package hscript; import haxe.CallStack; +import hscript.utils.UsingHandler; import hscript.utils.UnsafeReflect; import haxe.PosInfos; import hscript.Expr; @@ -144,6 +145,8 @@ class Interp { // "flixel.FlxG" ]; + var usingHandler:UsingHandler; + #if hscriptPos var curExpr:Expr; #end @@ -160,6 +163,8 @@ class Interp { variables = new Map(); publicVariables = new Map(); staticVariables = new Map(); + + usingHandler = new UsingHandler(); variables.set("null", null); variables.set("true", true); @@ -582,11 +587,11 @@ class Interp { public inline function error(e:#if hscriptPos ErrorDef #else Error #end, rethrow = false):Dynamic { #if hscriptPos var e = new Error(e, curExpr.pmin, curExpr.pmax, curExpr.origin, curExpr.line); #end - if (rethrow) { + if (rethrow) this.rethrow(e); - } else { + else throw e; - } + return null; } @@ -702,7 +707,7 @@ class Interp { return variable == null ? thing : Type.getClassName(variable); } customClasses.set(name, new CustomClassHandler(this, name, fields, importVar(extend), [for (i in interfaces) importVar(i)])); - case EImport(c, n): + case EImport(c, n, isUsing): if (!importEnabled) return null; var splitClassName = [for (e in c.split(".")) e.trim()]; @@ -712,8 +717,20 @@ class Interp { var oldClassName = realClassName; var oldSplitName = splitClassName.copy(); - if (variables.exists(toSetName)) // class is already imported + if (variables.exists(toSetName)) { // class is already imported + if(isUsing && !usingHandler.entryExists(toSetName)) + setUsing(toSetName, variables.get(toSetName)); + return null; + } + + if(isUsing && customClasses.exists(toSetName)) { + // NOTE: you will need to create the class first before + // setting the extension + if(!usingHandler.entryExists(toSetName)) + setCustomClassUsing(toSetName, customClasses.get(toSetName)); + return null; + } var realClassName = getLocalImportRedirect(realClassName); @@ -753,6 +770,7 @@ class Interp { error(EInvalidClass(oldClassName)); } else { if (en != null) { + if(isUsing) error(EInvalidClass(oldClassName)); // ENUM!!!! var enumThingy = {}; for (c in en.getConstructors()) { @@ -768,6 +786,7 @@ class Interp { } variables.set(toSetName, enumThingy); } else { + if(isUsing) setUsing(toSetName, cl); variables.set(toSetName, cl); } } @@ -1315,8 +1334,20 @@ class Interp { } if(v == null) { + #if php + // https://github.com/HaxeFoundation/haxe/issues/4915 + try { + if ((v = UnsafeReflect.getProperty(o, f)) == null) + v = Reflect.getProperty(cls, f); + } + catch(e:Dynamic) { + if ((v = UnsafeReflect.field(o, f)) == null) + v = Reflect.field(cls, f); + } + #else if ((v = UnsafeReflect.getProperty(o, f)) == null) v = Reflect.getProperty(cls, f); + #end } return v; } @@ -1355,6 +1386,110 @@ class Interp { return v; } + // STATIC EXTENSION ("USING") + + // Real class static extension + function setUsing(name:String, obj:Dynamic) { + if (usingHandler.entryExists(name)) return; + + var fn:Dynamic->String->Array -> Dynamic = null; + var fields:Array = []; + + // Predefined static extension classes + switch (name) { + case "StringTools": // https://github.com/pisayesiwsi/hscript-iris/blob/dev/crowplexus/iris/Iris.hx#L45 + fields = Type.getClassFields(StringTools); + fn = function(o:Dynamic, f:String, args:Array):Dynamic { + if (f == "isEof") // has @:noUsing + return null; + return switch (Type.typeof(o)) { + case TInt if (f == 'hex'): + StringTools.hex(o, args[0]); + case TClass(String): + if (UnsafeReflect.hasField(StringTools, f)) { + var field = UnsafeReflect.field(StringTools, f); + if (UnsafeReflect.isFunction(field)) + UnsafeReflect.callMethodUnsafe(StringTools, field, [o].concat(args)); + else + null; + } else null; + default: + null; + } + } + case "Lambda": // https://github.com/pisayesiwsi/hscript-iris/blob/dev/crowplexus/iris/Iris.hx#L62 + fields = Type.getClassFields(Lambda); + fn = function(o:Dynamic, f:String, args:Array):Dynamic { + if (o != null && o.iterator != null) { + if (UnsafeReflect.hasField(Lambda, f)) { + var field = UnsafeReflect.field(Lambda, f); + if (UnsafeReflect.isFunction(field)) { + return UnsafeReflect.callMethodUnsafe(Lambda, field, [o].concat(args)); + } + } + } + return null; + } + default: + if(obj == null) + error(ECustom("Unknown using class " + name)); + + var cls = obj; + switch (Type.typeof(cls)) { + case TClass(c): + fields = Type.getClassFields(c); + case TObject: + fields = Reflect.fields(cls); + default: + error(ECustom('$name is not a class')); + } + fn = function(o:Dynamic, f:String, args:Array) { + if (!Reflect.hasField(cls, f)) + return null; + + var field = Reflect.field(cls, f); + if (!Reflect.isFunction(field)) + return null; + + // invalid if the function has no arguments + var totalArgs = Tools.argCount(field); + if (totalArgs == 0) + return null; + + return UnsafeReflect.callMethodUnsafe(cls, field, [o].concat(args)); + } + } + + if(fn != null) usingHandler.registerEntry(name, fn, fields); + } + + // Custom Class Static Extension + @:access(hscript.CustomClassHandler) + function setCustomClassUsing(name:String, cls:CustomClassHandler) { + if (usingHandler.entryExists(name)) return; + + var fn:Dynamic->String->Array -> Dynamic; + var customClass:CustomClassHandler = cls; + var fields:Array = customClass.__staticFields.copy(); + + fn = function(o:Dynamic, f:String, args:Array):Dynamic { + if (!customClass.hasField(f)) + return null; + + var field:Dynamic = customClass.getField(f); + if (!Reflect.isFunction(field)) + return null; + /* + var totalArgs:Int = Tools.argCount(field); + if (totalArgs == 0) + return null; + */ + return UnsafeReflect.callMethodUnsafe(null, field, args); + } + + usingHandler.registerEntry(name, fn, fields); + } + function fcall(o:Dynamic, f:String, args:Array):Dynamic { // Custom logic to handle super calls to prevent infinite recursion if(inCustomClass) { @@ -1366,6 +1501,17 @@ class Interp { } } + if (usingHandler.usingEntries.iterator().hasNext()) { // If is not empty + var v:Dynamic = null; + for (n => us in usingHandler.usingEntries) { + if(us.hasField(f)) { + v = us.call(o, f, args); + if (v != null) + return v; + } + } + } + var func = get(o, f); // Workaround for an HTML5-specific issue. // https://github.com/HaxeFoundation/haxe/issues/11298 diff --git a/hscript/Parser.hx b/hscript/Parser.hx index 4b1b273..046acc0 100644 --- a/hscript/Parser.hx +++ b/hscript/Parser.hx @@ -956,11 +956,13 @@ class Parser { var tk = token(); push(tk); mk(EFunction(inf.args, inf.body, name, inf.ret, nextIsPublic, nextIsStatic, nextIsOverride, nextIsPrivate, nextIsFinal, nextIsInline),p1,pmax(inf.body)); - case "import": + case "import" | "using": + var isUsing = id == "using"; var oldReadPos = readPos; var tk = token(); switch( tk ) { case TPOpen: + if(isUsing) error(ECustom('Expected identifier'),tokenMin,tokenMax); var tok = token(); switch(tok) { case TConst(c): @@ -986,6 +988,7 @@ class Parser { t = token(); if( t != TDot ) { if(t.match(TId("as"))) { + if(isUsing) error(ECustom('Expected . or ;'),tokenMin,tokenMax); t = token(); switch( t ) { case TId(id): @@ -1010,7 +1013,7 @@ class Parser { ensure(TSemicolon); push(TSemicolon); var p = path.join("."); - mk(EImport(p, asname),p1); + mk(EImport(p, asname, isUsing),p1); default: unexpected(tk); null; diff --git a/hscript/Printer.hx b/hscript/Printer.hx index dadbc10..1189dc2 100644 --- a/hscript/Printer.hx +++ b/hscript/Printer.hx @@ -115,9 +115,9 @@ class Printer { add("??NULL??"); return; } - switch( #if hscriptPos e.e #else e #end ) { - case EImport(c, n): - add("import " + c); + switch(Tools.expr(e)) { + case EImport(c, n, u): + add('${u ? 'using' : 'import'} $c'); if(n != null) add(' as $n'); case EClass(name, fields, extend, interfaces): @@ -127,8 +127,13 @@ class Printer { for(_interface in interfaces) { add(' implements $_interface'); } - add(' {\n'); tabs += "\t"; + add(" {\n"); + for( e in fields ) { + add(tabs); + expr(e); + //add(";\n"); + } //for(field in fields) { // expr(field); //} @@ -211,7 +216,7 @@ class Printer { case ECall(e, args): if( e == null ) expr(e); - else switch( #if hscriptPos e.e #else e #end ) { + else switch( Tools.expr(e)) { case EField(_), EIdent(_), EConst(_): expr(e); default: diff --git a/hscript/Tools.hx b/hscript/Tools.hx index 492f66b..e902d2d 100644 --- a/hscript/Tools.hx +++ b/hscript/Tools.hx @@ -111,4 +111,23 @@ class Tools { #end } + /** + * DO NOT USE INLINE ON THIS FUNCTION + **/ + public static function argCount(func: haxe.Constraints.Function): Int { + // https://github.com/pisayesiwsi/hscript-iris/blob/dev/crowplexus/hscript/Tools.hx#L206 + #if cpp + return untyped __cpp__("{0}->__ArgCount()", func); + #elseif js + return untyped js.Syntax.code("{0}.length", func); + #elseif hl + var ft = hl.Type.getDynamic(func); + if (ft.kind != HFun) + return -1; + return ft.getArgsCount(); + #else + return -1; + #end + } + } \ No newline at end of file diff --git a/hscript/utils/UsingHandler.hx b/hscript/utils/UsingHandler.hx new file mode 100644 index 0000000..0245dfd --- /dev/null +++ b/hscript/utils/UsingHandler.hx @@ -0,0 +1,50 @@ +package hscript.utils; + +@:structInit +class UsingEntry { + public var call:Dynamic->String->Array->Dynamic; + public var fields:Array; + + public function hasField(name:String) { + return fields.contains(name); + } +} + +/** + * Special class that handles static extension function calls. + * + * A static extension allows pseudo-extending + * existing types without modifying their source. + * In Haxe this is achieved by declaring a static method with a first argument + * of the extending type and then bringing the defining class into context through `using`. + * + * Example: + * ```haxe + * class IntExtender { + * static public function triple(i:Int) { + * return i * 3; + * } + * } + * + * using IntExtender; + * + * trace(12.triple()); // 36 + * ``` + * + * @see https://haxe.org/manual/lf-static-extension.html + */ +class UsingHandler { // The ACTUAL UsingHandler.hx >:3 + @:allow(hscript.CustomClass) + @:allow(hscript.CustomClassHandler) + public var usingEntries(default, null):Map = []; + + public function new() {} + + public function registerEntry(name:String, entry:Dynamic->String->Array->Dynamic, fields:Array) { + usingEntries.set(name, {call: entry, fields: fields}); + } + + public function entryExists(name:String):Bool { + return usingEntries.exists(name); + } +}