package hscript.macros; import haxe.macro.Type.Ref; #if macro import haxe.macro.Type.ClassType; import haxe.macro.Type.ModuleType; import Type.ValueType; import haxe.macro.Expr.Function; import haxe.macro.Expr; import haxe.macro.Type.MetaAccess; import haxe.macro.Type.FieldKind; import haxe.macro.Type.ClassField; import haxe.macro.Type.VarAccess; import haxe.macro.*; using haxe.macro.ComplexTypeTools; using haxe.macro.ExprTools; using haxe.macro.TypeTools; using StringTools; using Lambda; typedef ClassInfo = { var imports:Array; var ?params:Array; } @:nullSafety(Loose) class CustomClassMacro { public static inline final FUNC_PREFIX = "_HX_SUPER__"; public static inline final CLASS_SUFFIX = "_HSX"; public static var unallowedMetas:Array = [":bitmap", ":noCustomClass", ":generic"]; private static var classesToBuild:Map = []; public static function init() { #if !display #if CUSTOM_CLASSES if (Context.defined("display")) return; for (apply in Config.ALLOWED_CUSTOM_CLASSES) { Compiler.addGlobalMetadata(apply, "@:build(hscript.macros.CustomClassMacro.fetchClass())"); } Context.onAfterTyping(build); #end #end } public static function fetchClass():Array { var fields = Context.getBuildFields(); var clRef = Context.getLocalClass(); if (clRef == null) return fields; var cl = clRef.get(); if (cl.isAbstract || cl.isExtern || cl.isFinal || cl.isInterface) return fields; if (!cl.name.endsWith("_Impl_") && !cl.name.endsWith(CLASS_SUFFIX) && !cl.name.endsWith("_HSC")) { var metas = cl.meta.get(); for (m in metas) if (unallowedMetas.contains(m.name)) return fields; // if(cl.params.length > 0) // return fields; var key = cl.module; var fkey = '${cl.module}.${cl.name}'; if (key == "sys.thread.FixedThreadPool") return fields; // Error: Type name sys.thread.Worker_HSX is redefined from module sys.thread.FixedThreadPool if (key == "StdTypes") return fields; // Error: Cant extend basic class if (key == "Xml") return fields; // Error: Cant extend basic class if (key == "Date") return fields; // Error: Cant extend basic class if (key == "away3d.tools.commands.Mirror") return fields; // Error: Unknown identifier if (key == "away3d.tools.commands.SphereMaker") return fields; // Error: Unknown identifier if (key == "away3d.tools.commands.Weld") return fields; // Error: Unknown identifier if (fkey == "hscript.CustomClassHandler.TemplateClass") return fields; // Error: Redefined if (fkey == "hscript.CustomClassHandler.CustomTemplateClass") return fields; // Error: Redefined if (fkey == "hscript.CustomClass") return fields; // Error: Redefined if (key == "sys.thread.EventLoop") return fields; // Error: cant override force inlined if (Config.DISALLOW_CUSTOM_CLASSES.contains(key) || Config.DISALLOW_CUSTOM_CLASSES.contains(fkey)) return fields; if (key.contains("_")) return fields; var imports = Context.getLocalImports().copy(); var clsDecl:ClassInfo = { imports: imports, params: cl.params.length > 0 ? cl.params : null } classesToBuild.set(fkey, clsDecl); } return fields; } // called in an onAfterTyping callback public static function build(modules:Array) { var classesToApply:Array = []; for (m in modules) { switch (m) { case TClassDecl(c): if (c == null) continue; var cl = c.get(); var fkey = '${cl.module}.${cl.name}'; if (classesToBuild.exists(fkey) /*&& classesToBuild.get(fkey).params == null*/) { classesToApply.push(cl); } /* if(cl.isAbstract || cl.isExtern || cl.isFinal || cl.isInterface) continue; if(!cl.name.endsWith("_Impl_") && !cl.name.endsWith(CLASS_SUFFIX) && !cl.name.endsWith("_HSC")) { var fkey = '${cl.module}.${cl.name}'; if(classesToBuild.exists(fkey) && classesToBuild.get(fkey).params == null) { classesToApply.push(cl); } } */ default: } } if (classesToApply.length > 0) buildClasses(classesToApply); } private static function buildClasses(classes:Array) { for (cl in classes) buildClass(cl); } private static function buildClass(cl:ClassType) { var classFields:Array = cl.fields.get(); var classParams:Map = []; if (cl.params.length != 0) { for (i => p in cl.params) { var pName = '${cl.pack.join(".")}.${cl.name}.${p.name}'; var pFinalType = p.defaultType == null ? haxe.macro.Type.TDynamic(null) : p.defaultType; classParams.set(pName, pFinalType); } } var fields:Array = classFields.concat(getSuperFields(cl)); var shadowClass:TypeDefinition = macro class {}; var definedFields:Array = []; var hasNew:Bool = false; if (cl.constructor != null) { hasNew = true; var constField = cl.constructor.get(); switch (constField.type) { case TFun(args, ret): var constArgs:Array = [ for (arg in args) {name: arg.name, opt: arg.opt, type: Context.toComplexType(arg.t)} ]; var constructor:Field = buildConstructor(constArgs, constField.pos); shadowClass.fields.push(constructor); definedFields.push(constField.name); // kinda redundant since is the constructor case TLazy(builder): var v = builder(); switch (v) { case TFun(args, ret): var constArgs:Array = [ for (arg in args) {name: arg.name, opt: arg.opt, type: Context.toComplexType(arg.t)} ]; var constructor:Field = buildConstructor(constArgs, constField.pos); shadowClass.fields.push(constructor); definedFields.push(constField.name); default: } default: } } for (f in fields) { if (f == null) continue; if (f.name.startsWith(FUNC_PREFIX)) continue; if (f.isExtern || f.isFinal) continue; switch (f.kind) { case FMethod(k): switch (k) { case MethInline | MethDynamic | MethMacro: continue; default: } case FVar(_, _): continue; } if (f.name == "hget" || f.name == "hset") continue; if (definedFields.contains(f.name)) continue; for (m in f.meta.get()) if (unallowedMetas.contains(m.name)) continue; switch (f.kind) { case FMethod(k): var newFields = overrideField(f, cl, f.type, classParams); var overField:Field = newFields[0]; var superField:Field = newFields[1]; if (!overField.access.contains(AOverride)) overField.access.push(AOverride); if (superField.access.contains(AOverride)) superField.access.remove(AOverride); shadowClass.fields.push(overField); shadowClass.fields.push(superField); definedFields.push(f.name); default: // No :> } } if (definedFields.length == 0 && !hasNew) return; shadowClass.kind = TDClass({ pack: cl.pack.copy(), name: cl.name, params: cl.params.length == 0 ? null : [ for (s => p in classParams) switch (p) { case TInst(_.get() => {kind: KExpr(e)}, _): TPExpr(e); default: TPType(FixedTypeTools.toComplexType(p)); } ] }, [ {name: "IHScriptCustomClassBehaviour", pack: ["hscript"]} ], false, true, false); shadowClass.name = '${cl.name}$CLASS_SUFFIX'; var fkey = '${cl.module}.${cl.name}'; var imports = classesToBuild.get(fkey).imports; if (imports != null) { setupMetas(shadowClass, imports, cl); processImport(imports, "hscript.utils.UnsafeReflect", "UnsafeReflect", cl.pos); } shadowClass.fields.push({ name: "__cachedFieldSet", pos: cl.pos, kind: FVar(macro :Map), access: [APublic, AStatic] }); shadowClass.fields.push({ name: "__interp", kind: FVar(macro :hscript.Interp), pos: cl.pos, access: [APublic] }); shadowClass.fields.push({ name: "__allowSetGet", pos: cl.pos, kind: FVar(macro :Bool, macro true), access: [APublic] }); shadowClass.fields.push({ name: "__real_fields", pos: cl.pos, kind: FVar(macro :Array), access: [APublic] }); shadowClass.fields.push({ name: "__class__fields", pos: cl.pos, kind: FVar(macro :Array), access: [APublic] }); shadowClass.fields.push({ name: "__callGetter", pos: cl.pos, kind: FFun({ ret: macro :Dynamic, params: [], expr: macro { return null; }, args: [ { name: "name", opt: false, meta: [], type: macro :String } ] }), access: [APublic] }); shadowClass.fields.push({ name: "__callSetter", pos: cl.pos, kind: FFun({ ret: macro :Dynamic, params: [], expr: macro { return null; }, args: [ { name: "name", opt: false, meta: [], type: macro :String }, { name: "val", opt: false, meta: [], type: macro :Dynamic } ] }), access: [APublic] }); if (cl.name == "FunkinShader" || cl.name == "CustomShader" || cl.name == "MultiThreadedScript") { Context.defineModule(cl.module, [shadowClass], imports); return; } var hasHgetInSuper = false; var hasHsetInSuper = false; // TODO: hget & hset for (f in fields) { if (f.name == "new") continue; if (f.name.startsWith(FUNC_PREFIX)) continue; if (f.isExtern || f.isFinal) continue; switch (f.kind) { case FMethod(k): switch (k) { case MethInline | MethDynamic | MethMacro: continue; default: } case FVar(read, write): switch ([read, write]) { case [AccInline, AccInline]: continue; default: } } switch (f.type) { case TFun(args, ret): if (f.params != null && f.params.length > 0) continue; hasHgetInSuper = f.name == "hget"; hasHsetInSuper = f.name == "hset"; if (hasHgetInSuper && hasHsetInSuper) break; default: } } var hgetField = if (hasHgetInSuper) { macro { if (__interp != null) { if (__class__fields.contains(name)) { var v:Dynamic = __interp.variables.get(name); if (v != null && v is hscript.Property) return cast(v, hscript.Property).callGetter(name); return v; } else @:privateAccess { var cls:hscript.CustomClass = cast __interp.__customClass.__upperClass; while (cls != null) { if (cls.hasField(name)) return cls.getField(name); var prev:hscript.CustomClass = cast cls.__upperClass; if (prev == null) break; cls = prev; } } } return super.hget(name); } } else { macro { if (__interp != null) { if (__class__fields.contains(name)) { var v:Dynamic = __interp.variables.get(name); if (v != null && v is hscript.Property) return cast(v, hscript.Property).callGetter(name); return v; } else @:privateAccess { var cls:hscript.CustomClass = cast __interp.__customClass.__upperClass; while (cls != null) { if (cls.hasField(name)) return cls.getField(name); var prev:hscript.CustomClass = cast cls.__upperClass; if (prev == null) break; cls = prev; } } } return UnsafeReflect.getProperty(this, name); } } var hsetField = if (hasHsetInSuper) { macro { if (__interp != null) { if (__class__fields.contains(name)) { var v:Dynamic = __interp.variables.get(name); if (v != null && v is hscript.Property) return cast(v, hscript.Property).callSetter(name, val); __interp.variables.set(name, val); return val; } else @:privateAccess { var cls:hscript.CustomClass = cast __interp.__customClass.__upperClass; while (cls != null) { if (cls.hasField(name)) return cls.setField(name, val); var prev:hscript.CustomClass = cast cls.__upperClass; if (prev == null) break; cls = prev; } } } if (__real_fields.contains(name)) { UnsafeReflect.setProperty(this, name, val); return UnsafeReflect.field(this, name); } return super.hset(name, val); } } else { macro { if (__interp != null) { if (__class__fields.contains(name)) { var v:Dynamic = __interp.variables.get(name); if (v != null && v is hscript.Property) return cast(v, hscript.Property).callSetter(name, val); __interp.variables.set(name, val); return val; } else @:privateAccess { var cls:hscript.CustomClass = cast __interp.__customClass.__upperClass; while (cls != null) { if (cls.hasField(name)) return cls.setField(name, val); var prev:hscript.CustomClass = cast cls.__upperClass; if (prev == null) break; cls = prev; } } } if (__real_fields.contains(name)) { UnsafeReflect.setProperty(this, name, val); return UnsafeReflect.field(this, name); } // __custom__variables.set(name, val); return val; } } shadowClass.fields.push({ name: "hset", pos: cl.pos, access: hasHsetInSuper ? [AOverride, APublic] : [APublic], kind: FFun({ ret: macro :Dynamic, params: [], expr: hsetField, args: [ { name: "name", opt: false, meta: [], type: macro :String }, { name: "val", opt: false, meta: [], type: macro :Dynamic } ] }) }); shadowClass.fields.push({ name: "hget", pos: cl.pos, access: hasHgetInSuper ? [AOverride, APublic] : [APublic], kind: FFun({ ret: macro :Dynamic, params: [], expr: hgetField, args: [ { name: "name", opt: false, meta: [], type: macro :String } ] }) }); /* var p = new Printer(); var aa = p.printTypeDefinition(shadowClass); trace('\n'); trace(aa); */ Context.defineModule(cl.module, [shadowClass], imports); } // Based on Polymod HScriptClass Macro: https://github.com/larsiusprime/polymod/blob/develop/polymod/hscript/_internal/HScriptedClassMacro.hx#L686 private static function overrideField(field:ClassField, cl:ClassType, ?type:haxe.macro.Type, ?params:Map):Array { if (type == null) type = field.type; switch (type) { case TLazy(f): var fv = f(); return overrideField(field, cl, fv, params); case TFun(args, ret): for (a in args) switch (a.t) { case TInst(t, p): var tp = t.get(); if (tp != null && tp.isPrivate) return []; default: } // Inline functions are already skipped // As well for "@:generic" fields var fnInputArgs:Array = []; // We only get limited information about the args from Type, we need to use TypedExprDef. if (field.expr() == null) return []; var fnAccess:Array = [field.isPublic ? APublic : APrivate]; if (field.isFinal) fnAccess.push(AFinal); switch (field.expr().expr) { case TFunction(tfunc): for (arg in tfunc.args) { var opt:Bool = (arg.value != null); var fnMeta:Metadata = arg.v.meta.get(); var fnExpr:Null = !opt ? null : Context.getTypedExpr(arg.value); // The argument type. We have to handle any type parameters, and deparameterizeType does so recursively. var fnType:ComplexType = Context.toComplexType(deparameterizeType(arg.v.t, params)); var fnArg:FunctionArg = { name: arg.v.name, type: fnType, value: fnExpr, meta: fnMeta }; fnInputArgs.push(fnArg); } case TConst(_): // This is actually a VARIABLE storing a function. return []; default: } // TODO var returnsVoid:Bool = ret.toString() == "Void"; var fnCall:Array = [for (a in args) macro $i{a.name}]; var fnParams:Array = [for (fp in field.params) {name: fp.name}]; var fnRet:Null = returnsVoid ? null : Context.toComplexType(deparameterizeType(ret, params)); var fnName:String = field.name; var overField:Field = { name: fnName, access: fnAccess, pos: field.pos, meta: field.meta.get(), kind: FFun({ args: fnInputArgs, params: fnParams, ret: fnRet, expr: macro { var name:String = $v{fnName}; if (__interp != null && __class__fields.contains(name)) { var v:Dynamic = null; if (Reflect.isFunction(v = __interp.variables.get(name))) { ${ !returnsVoid ? (macro return v($a{fnCall})) : macro { v($a{fnCall}); return; } } } } ${ !returnsVoid ? (macro return super.$fnName($a{fnCall})) : (macro super.$fnName($a{fnCall})) } } }) } var superFunField:Field = { name: '$FUNC_PREFIX${fnName}', pos: field.pos, access: [APrivate], meta: field.meta.get(), kind: FFun({ args: fnInputArgs, params: fnParams, ret: fnRet, expr: macro { ${ !returnsVoid ? (macro return super.$fnName($a{fnCall})) : (macro super.$fnName($a{fnCall})) } } }) } return [overField, superFunField]; /* case TInst(_, _): return []; case TEnum(_, _): return []; case TMono(_): return []; case TAnonymous(_): return []; case TDynamic(_): return []; case TAbstract(_, _): return []; */ default: } return []; } static function getBaseParamsOfType(parentType:haxe.macro.Type, paramTypes:Array):Array { var parentParams:Array = []; switch (parentType) { case TMono(t): var ty = t.get(); return getBaseParamsOfType(ty, paramTypes); case TInst(t, params): // Continue parentParams = t.get().params; case TType(t, params): // Recurse var ty:haxe.macro.Type = t.get().type; return getBaseParamsOfType(ty, paramTypes); case TDynamic(t): // Recurse return getBaseParamsOfType(t, paramTypes); case TLazy(f): // Recurse var ty:haxe.macro.Type = f(); return getBaseParamsOfType(ty, paramTypes); case TAbstract(t, _params): // Continue parentParams = t.get().params; // case TEnum(t:Ref, params:Array): // case TFun(args:Array<{name:String, opt:Bool, t:Type}>, ret:Type): // case TAnonymous(a:Ref): default: // Context.error('Unsupported type: ${parentType}', Context.currentPos()); } var result:Array = []; for (i => parentParam in parentParams) { var newParam:haxe.macro.Type.TypeParameter = { name: parentParam.name, t: paramTypes[i], }; result.push(newParam); } return result; } static function scanBaseTypes(type:haxe.macro.Type):Array { switch (type) { case TFun(args, ret): var results:Array = []; for (result in scanBaseTypes(ret)) { results.push(result); } for (arg in args) { for (result in scanBaseTypes(arg.t)) { results.push(result); } } return results; case TAbstract(ty, params): if (params.length == 0) { return [type]; } else { var results:Array = []; for (param in params) { for (result in scanBaseTypes(param)) { results.push(result); } } return results; } default: return [type]; } } static function deparameterizeType(targetType:haxe.macro.Type, targetParams:Map):haxe.macro.Type { var resultType:haxe.macro.Type = targetType; switch (targetType) { case TFun(args, ret): // Function type. // This is not referring to functions of a class, but rather a function taken as a parameter (like a callback). // Deparameterize the return type. var retType:haxe.macro.Type = deparameterizeType(ret, targetParams); // Deparameterize the argument types. var argTypes:Array<{name:String, opt:Bool, t:haxe.macro.Type}> = args.map(function(arg) { return { name: arg.name, opt: arg.opt, t: deparameterizeType(arg.t, targetParams), }; }); // Construct the new type. resultType = TFun(argTypes, retType); case TAbstract(ty, params): // Abstract type. Sometimes used by types like Null. var name = ty.toString(); var typ = ty.get(); // Check if the Abstract type is a parameter we recognize and can replace. if (targetParams.exists(ty.toString())) { // If so, replace it with the real type. resultType = targetParams.get(ty.toString()); // recursive call in case result is a parameter resultType = deparameterizeType(resultType, targetParams); } else if (params.length != 0) { var oldParams:Array = []; var newParams:Array = []; for (param in params) { var baseTypes = scanBaseTypes(param); for (baseType in baseTypes) { var newParam = deparameterizeType(baseType, targetParams); if (newParam.toString() != "Void") { oldParams.push(baseType); newParams.push(newParam); } } } var baseParams = getBaseParamsOfType(resultType, oldParams); newParams = newParams.slice(0, baseParams.length); if (newParams.length > 0) { // Context.info('Building new abstract (${baseParams} + ${newParams})...', Context.currentPos()); resultType = resultType.applyTypeParameters(baseParams, newParams); // Context.info('Deparameterized abstract type: ${resultType.toString()}', Context.currentPos()); } } case TInst(ty, params): // Instance type. Used by most variables. // Check if the Instance type is a parameter we recognize and can replace. if (targetParams.exists(ty.toString())) { // If so, replace it with the real type. resultType = targetParams.get(ty.toString()); // recursive call in case result is a parameter resultType = deparameterizeType(resultType, targetParams); } else if (params.length != 0) { var oldParams:Array = []; var newParams:Array = []; for (param in params) { var baseTypes = scanBaseTypes(param); for (baseType in baseTypes) { var newParam = deparameterizeType(baseType, targetParams); if (newParam.toString() != "Void") { oldParams.push(baseType); newParams.push(newParam); } } } var baseParams = getBaseParamsOfType(resultType, oldParams); newParams = newParams.slice(0, baseParams.length); if (newParams.length > 0) { // Context.info('Building new abstract (${baseParams} + ${newParams})...', Context.currentPos()); resultType = resultType.applyTypeParameters(baseParams, newParams); // Context.info('Deparameterized abstract type: ${resultType.toString()}', Context.currentPos()); } else { // Leave the type as is. } } default: // Do nothing. // Muted because I haven't actually seen any issues caused by this. Maybe investigate in the future. } return resultType; } private static function getSuperFields(cl:ClassType):Array { var fields:Array = []; var superClsRef = cl.superClass; while (superClsRef != null) { var superCls:ClassType = superClsRef.t.get(); fields = fields.concat(superCls.fields.get()); var next = superCls.superClass; if (next == null) break; superClsRef = next; } return fields; } private static function buildConstructor(args:Array, pos:Position):Field { var superCallArgs:Array = [for (arg in args) macro $i{arg.name}]; return { name: 'new', access: [APublic], pos: pos, kind: FFun({ args: args, expr: macro { // Call the super constructor with appropriate args super($a{superCallArgs}); if (__cachedFieldSet != null) { for (k => v in __cachedFieldSet) { Reflect.setProperty(this, k, v); } __cachedFieldSet.clear(); __cachedFieldSet = null; } } }), } } // Grabbed from Utils.hx static function setupMetas(shadowClass:TypeDefinition, imports:Array, cl:ClassType) { shadowClass.meta = [ { name: ":dox", params: [macro hide], pos: cl.pos }, { name: ":noCompletion", params: [], pos: cl.pos } ]; var module = Context.getModule(cl.module); for (m in module) switch (m) { case TInst(t, params): if (t == null) continue; var ty = t.get(); processModule(shadowClass, ty.module, ty.name, cl.pos); processImport(imports, ty.module, ty.name, cl.pos); case TEnum(t, params): if (t == null) continue; var ty = t.get(); processModule(shadowClass, ty.module, ty.name, cl.pos); processImport(imports, ty.module, ty.name, cl.pos); case TType(t, params): if (t == null) continue; var ty = t.get(); processModule(shadowClass, ty.module, ty.name, cl.pos); processImport(imports, ty.module, ty.name, cl.pos); case TAbstract(t, params): if (t == null) continue; var ty = t.get(); processModule(shadowClass, ty.module, ty.name, cl.pos); processImport(imports, ty.module, ty.name, cl.pos); default: } } static function processImport(imports:Array, module:String, n:String, pos:Position) { if (n.endsWith("_Impl_")) n = n.substr(0, n.length - 6); module = fixModuleName(module); if (module.endsWith("_Impl_")) module = module.substr(0, module.length - 6); trace(module); imports.push({ path: [ for (m in module.split(".")) { name: m, pos: pos } ], mode: INormal }); } static function processModule(shadowClass:TypeDefinition, module:String, n:String, pos:Position) { if (n.endsWith("_Impl_")) n = n.substr(0, n.length - 6); if (module.endsWith("_Impl_")) module = module.substr(0, module.length - 6); shadowClass.meta.push({ name: ':access', params: [ Context.parse(fixModuleName(module.endsWith('.${n}') ? module : '${module}.${n}'), pos) ], pos: pos }); } static function fixModuleName(name:String):String { return [for (s in name.split(".")) if (s.charAt(0) == "_") s.substr(1) else s].join("."); } } #else class CustomClassMacro { public var usedClass:Class; public var className:String; } #end