Static Extension support + small fixes

Static extension for real/custom classes. aka. the `using` keyword

Warning: UNTESTED
This commit is contained in:
Jaime Humberto Macias Bustamante
2025-07-11 04:35:03 -07:00
parent fd88d78544
commit 6b54479211
8 changed files with 238 additions and 13 deletions
+1
View File
@@ -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;
+1
View File
@@ -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;
+1 -1
View File
@@ -79,7 +79,7 @@ enum Expr {
EMeta( name : String, args : Array<Expr>, e : Expr );
ECheckType( e : Expr, t : CType );
EImport( c : String, ?asname:String );
EImport( c : String, ?asname:String, ?isUsing:Bool );
EClass( name:String, fields:Array<Expr>, ?extend:String, interfaces:Array<String>, ?isFinal:Bool, ?isPrivate:Bool );
}
+151 -5
View File
@@ -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<String, Dynamic>();
publicVariables = new Map<String, Dynamic>();
staticVariables = new Map<String, Dynamic>();
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> -> Dynamic = null;
var fields:Array<String> = [];
// 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>):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>):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<Dynamic>) {
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> -> Dynamic;
var customClass:CustomClassHandler = cls;
var fields:Array<String> = customClass.__staticFields.copy();
fn = function(o:Dynamic, f:String, args:Array<Dynamic>):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>):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
+5 -2
View File
@@ -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;
+10 -5
View File
@@ -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:
+19
View File
@@ -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
}
}
+50
View File
@@ -0,0 +1,50 @@
package hscript.utils;
@:structInit
class UsingEntry {
public var call:Dynamic->String->Array<Dynamic>->Dynamic;
public var fields:Array<String>;
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<String, UsingEntry> = [];
public function new() {}
public function registerEntry(name:String, entry:Dynamic->String->Array<Dynamic>->Dynamic, fields:Array<String>) {
usingEntries.set(name, {call: entry, fields: fields});
}
public function entryExists(name:String):Bool {
return usingEntries.exists(name);
}
}