Files
hscript-improved/hscript/customclass/CustomClass.hx
T
Jaime Humberto Macias Bustamante 932e643dc9 Better access context (IN PROGRESS)
Also, throws an error if a variable with "(get, set)" doesn't have a type or a default value.
2025-05-15 01:07:29 -07:00

513 lines
15 KiB
Haxe

package hscript.customclass;
import hscript.utils.UnsafeReflect;
import haxe.Constraints.Function;
import hscript.Expr;
import hscript.Expr.FieldPropertyAccess;
import hscript.Expr.VarDecl;
import hscript.Expr.FunctionDecl;
import hscript.Expr.FieldDecl;
using Lambda;
using StringTools;
/**
* The Custom Class core.
*
* Provides handlers for custom classes.
*
* Rest in peace, CustomClassHandler.hx
*
* Based on Polymod Hscript class system
* @see https://github.com/larsiusprime/polymod/tree/master/polymod/hscript/_internal
*/
@:access(hscript.customclass.CustomClassDecl)
class CustomClass implements IHScriptCustomAccessBehaviour {
public var interp:Interp;
public var superClass:Dynamic;
public var superConstructor(default, null):Dynamic;
public var superIsCustomClass(get, never):Bool;
private function get_superIsCustomClass():Bool
return (superClass != null && superClass is CustomClass);
public var className(get, never):String;
private function get_className():String {
return __class.toString();
}
private var __class:CustomClassDecl;
private var __cachedSuperFields:Null<Map<String, Dynamic>> = null;
private var __cachedFieldDecls:Map<String, FieldDecl> = [];
private var __cachedFunctionDecls:Map<String, FunctionDecl> = [];
private var __cachedVarDecls:Map<String, VarDecl> = [];
public var accessContext:AccessContext = COuter;
public var __allowSetGet:Bool = false;
public var __allowPrivateAccess:Bool = false;
private var isInline(default, null):Bool = false;
private var ogVariables(default, null):Map<String, Dynamic>;
private var initializing(default, null):Bool = false; // Allows final variables to be initialized
public function new(__class:CustomClassDecl, args:Array<Dynamic>, ?extendFieldDecl:Map<String, Dynamic>, ?ogInterp:Interp, ?callNew:Bool = true) {
this.__class = __class;
this.interp = new Interp(this);
if (ogInterp != null) {
interp.importFailedCallback = ogInterp.importFailedCallback;
interp.errorHandler = ogInterp.errorHandler;
interp.allowStaticVariables = ogInterp.allowStaticVariables;
interp.staticVariables = ogInterp.staticVariables;
// todo: make it so you can use variables from the same scope as where the class was defined
if(__class.isInline != null && __class.isInline) {
isInline = __class.isInline;
ogVariables = ogInterp.variables;
interp.allowPublicVariables = __class.staticInterp.allowPublicVariables;
interp.publicVariables = __class.staticInterp.publicVariables;
}
}
buildImports();
buildUsings();
if (extendFieldDecl != null)
__cachedSuperFields = extendFieldDecl;
buildClass();
if (hasFunction('new') && callNew) {
buildSuperConstructor();
initializing = true;
callFunction('new', args);
initializing = false;
if (this.superClass == null && this.__class.classDecl.extend != null)
this.interp.error(ECustom("super() not called"));
} else if (__class.classDecl.extend != null) {
createSuperClass(args);
}
}
function buildClass() {
if (__cachedSuperFields == null)
__cachedSuperFields = [];
for (f in __class.classDecl.fields) {
if (f.access.contains(AStatic))
continue; // Skip static field. It's handled by CustomClassDecl.hx
__cachedFieldDecls.set(f.name, f);
switch (f.kind) {
case KFunction(fn):
__cachedFunctionDecls.set(f.name, fn);
#if hscriptPos
var fexpr:Expr = {
e: ExprDef.EFunction(fn.args, fn.body, f.name, fn.ret, false, false),
pmin: fn.body.pmin,
pmax: fn.body.pmax,
line: fn.body.line,
origin: fn.body.origin
};
#else
var fexpr = Expr.EFunction(fn.args, fn.body, f.name, fn.ret, false, false);
#end
var func:Function = this.interp.expr(fexpr);
this.interp.variables.set(f.name, func);
case KVar(v):
if(v.get != ADefault || v.set != ADefault)
__allowSetGet = true;
__cachedVarDecls.set(f.name, v);
if (v.expr != null) {
var varValue = this.interp.expr(v.expr);
this.interp.variables.set(f.name, varValue);
}
}
}
if (!__cachedSuperFields.empty()) {
for (f => v in __cachedSuperFields) {
this.hset(f, v);
}
__cachedSuperFields.clear();
}
}
function buildSuperConstructor() {
superConstructor = Reflect.makeVarArgs(function(args:Array<Dynamic>) {
createSuperClass(args);
});
}
private function createSuperClass(?args:Array<Dynamic>) {
if (args == null)
args = [];
if(__class.superClassDecl is CustomClassDecl)
superClass = new CustomClass(__class.superClassDecl, args, __cachedSuperFields, this.interp);
else {
if (__cachedSuperFields != null) {
Reflect.setField(__class.superClassDecl, "__cachedFields", __cachedSuperFields); // Static field
}
var disallowCopy = Type.getInstanceFields(__class.superClassDecl);
superClass = Type.createInstance(__class.superClassDecl, args);
superClass.__customClass = this;
superClass.__real_fields = disallowCopy;
if(isInline) {
for(s => v in ogVariables)
if(!disallowCopy.contains(s))
interp.variables.set(s, v);
}
}
}
function buildImports() {
var i:Int = 0;
for (_import in __class.imports) {
var importedClass = _import.fullPath;
var importAlias = _import.as;
if (Interp.customClassExist(importedClass) && this.interp.importFailedCallback != null) {
this.interp.importFailedCallback(importedClass.split("."), importAlias);
continue;
}
#if hscriptPos
var e:Expr = {
e: ExprDef.EImport(importedClass, importAlias),
pmin: 0,
pmax: 0,
origin: this.className,
line: i
};
#else
var e = Expr.EImport(importedClass, importAlias);
#end
this.interp.expr(e);
i++;
}
}
inline function buildUsings() {
for (us in __class.usings) {
@:privateAccess this.interp.useUsing(us);
}
}
public function callFunction(name:String, ?args:Array<Dynamic>):Dynamic {
var r:Dynamic = null;
if (hasField(name)) {
var fn = getFunction(name);
try {
if (fn == null)
interp.error(ECustom('${name} is not a function'));
r = UnsafeReflect.callMethodUnsafe(null, fn, args == null ? [] : args);
} catch (e:hscript.Expr.Error) {
// A script error occurred while executing the custom class function.
// Purge the function from the cache so it is not called again.
purgeFunction(name);
}
}
else {
var fixedArgs = [];
for (a in args) {
if ((a is CustomClass)) {
var customClass:CustomClass = cast(a, CustomClass);
fixedArgs.push(customClass.superClass != null ? customClass.getSuperclass() : customClass);
} else {
fixedArgs.push(a);
}
}
var superFn:Function = null;
if(superClass is CustomClass) {
superFn = cast(superClass, CustomClass).hget(name);
}
else {
var fixedName = '_HX_SUPER__${name}';
superFn = Reflect.field(superClass, fixedName);
}
if (superFn == null || !Reflect.isFunction(superFn)) {
this.interp.error(ECustom('Error while calling function super.${name}(): EInvalidAccess'
+ '\n'
+ 'InvalidAccess error: Super function "${name}" does not exist! Define it or call the correct superclass function.'));
}
r = Reflect.callMethod((superClass is CustomClass) ? null : superClass, superFn, fixedArgs);
}
return r;
}
// Field check
private function hasField(name:String):Bool {
return __cachedFieldDecls.exists(name);
}
private function getField(name:String):FieldDecl {
return __cachedFieldDecls != null ? __cachedFieldDecls.get(name) : null;
}
private function hasVar(name:String):Bool {
return __cachedVarDecls.exists(name);
}
private function getVar(name:String):VarDecl {
return __cachedVarDecls.get(name);
}
private function hasFunction(name:String):Bool {
return __cachedFunctionDecls.exists(name);
}
private function getFunction(name:String):Function {
var fn = this.interp.variables.get(name);
return Reflect.isFunction(fn) ? fn : null;
}
// SuperClass field check
private function cacheSuperField(name:String, value:Dynamic) {
if (__cachedSuperFields != null) {
__cachedSuperFields.set(name, value);
}
}
var __superClassFieldList:Array<String> = null;
public function superHasField(name:String):Bool {
if (superClass == null)
return false;
// Reflect.hasField(this, name) is REALLY expensive so we use a cache.
if (__superClassFieldList == null) {
__superClassFieldList = [];
if(superClass is CustomClass) {
var cls:Null<Dynamic> = superClass;
while(cls != null && cls is CustomClass) {
var currentClass = cast(cls, CustomClass);
var fields = [for(f in currentClass.__cachedFieldDecls.keys()) f];
__superClassFieldList.concat(fields);
var next = currentClass.superClass;
if(next == null)
break;
cls = next;
}
// The last fetched class is a real class
if(!(cls is CustomClass))
__superClassFieldList.concat(Reflect.fields(cls).concat(Type.getInstanceFields(Type.getClass(cls))));
}
else {
var realFields = Reflect.fields(superClass).concat(Type.getInstanceFields(Type.getClass(superClass)));
__superClassFieldList.concat(realFields);
}
}
return __superClassFieldList.indexOf(name) != -1;
}
/**
* Remove a function from the cache.
* This is useful when a function is broken and needs to be skipped.
* @param name The name of the function to remove from the cache.
*/
private function purgeFunction(name:String):Void {
if (__cachedFunctionDecls != null) {
__cachedFunctionDecls.remove(name);
}
}
// Access fields
public function hget(name:String):Dynamic {
switch (name) {
case "superClass": return this.superClass;
case "createSuperClass": return this.createSuperClass;
case "hasFunction": return this.hasFunction;
case "callFunction": return this.callFunction;
default:
if (hasFunction(name)) {
var fn:Function = Reflect.makeVarArgs(function(args:Array<Dynamic>) {
return this.callFunction(name, args);
});
return fn;
}
if (hasVar(name)) {
var v = getVar(name);
var getter = v.get;
var isInner = switch (accessContext) {
case CInner(cl): return cl == this.className;
default: false;
}
var value:Dynamic = null;
if(getter == ANever || getter == ANull && (!isInner || !__allowPrivateAccess))
throw 'field $name cannot be accessed for reading';
if (__allowSetGet && getter == AGet) {
if (hasFunction('get_$name'))
value = __callGetter(name);
else
throw 'Method get_$name required by property $name is missing';
}
else if (this.interp.variables.exists(name))
value = this.interp.variables.get(name);
else {
if (v.expr != null) {
value = this.interp.expr(v.expr);
this.interp.variables.set(name, value);
}
}
return value;
}
if (this.superClass != null) {
if (superHasField(name)) {
// Anonymous structure as a super class???
if (Type.getClass(this.superClass) == null) {
// Anonymous structure
if (Reflect.hasField(this.superClass, name))
return Reflect.field(this.superClass, name);
else
throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '[ANONYMOUS STRUCTURE]'";
}
if (this.superClass is CustomClass) {
var superCustomClass:CustomClass = cast(this.superClass, CustomClass);
superCustomClass.__allowSetGet = this.__allowSetGet;
superCustomClass.__allowPrivateAccess = this.__allowPrivateAccess;
superCustomClass.accessContext = switch (accessContext) {
case CInner(_): CInner(superCustomClass.className);
default: COuter;
}
return superCustomClass.hget(name);
}
// Real Class
if (__allowSetGet)
Reflect.getProperty(this.superClass, name);
else
Reflect.field(this.superClass, name);
}
else
throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '" + Type.getClassName(Type.getClass(this.superClass)) + "'";
}
else
throw "field '" + name + "' does not exist in custom class '" + this.className + "'";
}
return null;
}
public function hset(name:String, val:Dynamic):Dynamic {
switch (name) {
default:
if (hasVar(name)) {
var v = getVar(name);
var setter = v.set;
var isInner = switch (accessContext) {
case CInner(cl): return cl == this.className;
default: false;
}
if (setter == ANever || setter == ANull && (!isInner || !__allowPrivateAccess) || (v.isFinal && !initializing))
throw 'field $name cannot be accessed for writing';
if (__allowSetGet && setter == ASet) {
if (hasFunction('set_$name'))
return __callSetter(name, val);
else
throw 'Method set_$name required by property $name is missing';
}
this.interp.variables.set(name, val);
}
else if (this.superClass != null) {
if (superHasField(name)) {
if (Type.getClass(this.superClass) == null) {
// Anonymous structure
if (Reflect.hasField(this.superClass, name)) {
Reflect.setField(this.superClass, name, val);
return val;
}
else
throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '[ANONYMOUS STRUCTURE]'";
}
if (this.superClass is CustomClass) {
var superCustomClass:CustomClass = cast(this.superClass, CustomClass);
superCustomClass.__allowSetGet = this.__allowSetGet;
superCustomClass.__allowPrivateAccess = this.__allowPrivateAccess;
superCustomClass.accessContext = switch (accessContext) {
case CInner(_): CInner(superCustomClass.className);
default: COuter;
}
return superCustomClass.hset(name, val);
}
// Real Class
if (__allowSetGet)
Reflect.setProperty(this.superClass, name, val);
else
Reflect.setField(this.superClass, name, val);
}
else
throw "field '" + name + "' does not exist in custom class '" + this.className + "' or super class '" + Type.getClassName(Type.getClass(this.superClass)) + "'";
}
else
throw "field '" + name + "' does not exist in custom class '" + this.className + "'";
}
return val;
}
public function __callGetter(name:String):Dynamic {
__allowSetGet = false;
var r = callFunction('get_$name');
__allowSetGet = true;
return r;
}
public function __callSetter(name:String, val:Dynamic):Dynamic {
__allowSetGet = false;
var r = callFunction('set_$name', [val]);
__allowSetGet = true;
return r;
}
/**
* Returns the real superClass if the Custom Class
* extends another Custom Class, and so on until
* it reaches a real class, otherwise it will
* return the last fetched Custom Class
* @return Null<Dynamic>
*/
public function getSuperclass():Null<Dynamic> {
var cls:Null<Dynamic> = this.superClass;
// Check if the superClass is another custom class,
// so it will find for a real class, otherwise
// returns the last super CustomClass parent.
while(cls != null && cls is CustomClass) {
var next = cast(cls, CustomClass).superClass;
if(next == null)
break; // Return the Custom Class itself
cls = next;
}
return cls;
}
}