diff --git a/polymod/Polymod.hx b/polymod/Polymod.hx index de714ae3..f696e7d1 100644 --- a/polymod/Polymod.hx +++ b/polymod/Polymod.hx @@ -893,9 +893,6 @@ class Polymod #if POLYMOD_CPPIA polymod.hscript._internal.PolymodCppiaClassReference.clearCppiaClasses(); #end - #if hscript_typer - polymod.hscript._internal.PolymodTyperEx.clearAllModules(); - #end polymod.hscript.ScriptRunner.clearScripts(); } @@ -1131,18 +1128,22 @@ class Polymod registerAllCppiaClasses(results); - #if hscript_typer - // in the future typed modules might have a use - // but for now we just ignore the typed modules that are returned - var _ = polymod.hscript._internal.PolymodTyperEx.typeAllModules(); - #end + finishScriptRegistration(); + return results; + } + } + + static function finishScriptRegistration():Void + { + @:privateAccess { polymod.hscript._internal.Interp.validateImports(); + #if (POLYMOD_STRICT_SYNTAX && !macro) + polymod.hscript._internal.Typer.run(); + #end Polymod.preheatScriptClasses(); polymod.hscript._internal.PolymodScriptClass.reloadPersistentStaticFields(); polymod.hscript._internal.PolymodScriptClass.scriptsInitialized = true; - if (Polymod.onScriptsLoaded != null) Polymod.onScriptsLoaded(); - return results; } } @@ -1195,14 +1196,7 @@ class Polymod polymod.hscript._internal.PolymodCppiaClassReference.unloadInactiveModules(); #end - // Once all scripts have been registered, THEN validate the imports. - polymod.hscript._internal.Interp.validateImports(); - Polymod.preheatScriptClasses(); - polymod.hscript._internal.PolymodScriptClass.reloadPersistentStaticFields(); - polymod.hscript._internal.PolymodScriptClass.scriptsInitialized = true; - - if (Polymod.onScriptsLoaded != null) Polymod.onScriptsLoaded(); - + finishScriptRegistration(); return lime.app.Future.withValue(results); }); } @@ -2105,6 +2099,8 @@ enum abstract PolymodErrorCode(String) from String to String */ public var SCRIPT_PARSE_FAILED:String = 'script_parse_failed'; + public var SCRIPT_TYPE_ERROR:String = 'script_type_error'; + public var SCRIPT_TYPE_CHECK_DONE:String = 'script_type_check_done'; public var SCRIPT_PREHEAT_WARNING:String = 'script_preheat_warning'; public var SCRIPT_PREHEAT_ERROR:String = 'script_preheat_error'; public var SCRIPT_PREHEAT_DONE:String = 'script_preheat_done'; diff --git a/polymod/hscript/_internal/Checker.hx b/polymod/hscript/_internal/Checker.hx index 4b09bdf9..b44636df 100644 --- a/polymod/hscript/_internal/Checker.hx +++ b/polymod/hscript/_internal/Checker.hx @@ -2,6 +2,9 @@ package polymod.hscript._internal; import polymod.hscript._internal.Expr; import polymod.hscript._internal.Parser; +import polymod.hscript._internal.PolymodScriptClass; +import haxe.rtti.Rtti; +import haxe.rtti.CType as RuntimeCType; /** This is a special type that can be used in API. @@ -94,6 +97,8 @@ typedef CAbstract = var t:TType; var from:Array; var to:Array; + var fields:Map; + var statics:Map; } class Completion @@ -112,9 +117,13 @@ class Completion class CheckerTypes { var types:Map = new Map(); + var aliases:Map = new Map(); var t_string:TType; var localParams:Map; var parser:Parser; + var triedRuntimeTypes:Map = new Map(); + var runtimeAliases:Map = new Map(); + var runtimeAliasesLoaded:Bool = false; public function new() { @@ -125,6 +134,147 @@ class CheckerTypes types.set("Bool", CTAlias(TBool)); types.set("Dynamic", CTAlias(TDynamic)); parser = new Parser(); + + var stringType = defineClass("String"); + stringType.fields.set("length", runtimeField("length", TInt)); + for (name in ["charAt", "charCodeAt", "indexOf", "lastIndexOf", "split", "substr", "substring", "toLowerCase", "toUpperCase", "toString", "copy", "iterator"]) + stringType.fields.set(name, runtimeField(name, TDynamic)); + stringType.statics.set("fromCharCode", runtimeField("fromCharCode", TDynamic)); + + var arrayType = defineClass("Array"); + arrayType.params = [TParam("T")]; + arrayType.constructor = runtimeField("new", TDynamic); + arrayType.fields.set("length", runtimeField("length", TInt)); + for (name in ["push", "pop", "shift", "unshift", "insert", "remove", "contains", "indexOf", "lastIndexOf", "join", "map", "filter", "forEach", "copy", "slice", "splice", "sort", "reverse", "iterator", "keyValueIterator", "toString"]) + arrayType.fields.set(name, runtimeField(name, TDynamic)); + + var mapType = defineAbstract("Map"); + mapType.params = [TParam("K"), TParam("V")]; + mapType.t = TDynamic; + for (name in ["get", "set", "exists", "remove", "keys", "iterator", "keyValueIterator", "copy", "clear", "toString"]) + mapType.fields.set(name, runtimeField(name, TDynamic)); + + var typeCheck = defineTypedef("hscript.TypeCheck"); + typeCheck.params = [TParam("Real"), TParam("Script")]; + typeCheck.t = TParam("Script"); + t_string = TInst(stringType, []); + } + + static function runtimeField(name:String, type:TType):CField + { + return {name: name, isPublic: true, canWrite: true, complete: !StringTools.startsWith(name, "__"), params: [], t: type}; + } + + public function loadRuntimeAliases():Void + { + if (runtimeAliasesLoaded) return; + runtimeAliasesLoaded = true; + // Reuse Polymod's existing typedef cache + var typedefs:Map> = PolymodScriptClass.typedefs; + for (name => targetClass in typedefs) + { + if (name == null || name.length == 0 || targetClass == null || types.exists(name)) continue; + + var target:Null = Type.getClassName(targetClass); + if (target != null && target.length > 0) runtimeAliases.set(name, target); + } + } + + function loadRuntimeType(name:String):Bool + { + if (types.exists(name)) return true; + if (triedRuntimeTypes.exists(name)) return false; + triedRuntimeTypes.set(name, true); + + var cls:Null> = Type.resolveClass(name); + if (cls != null) + { + registerRuntimeClass(name, cls); + return true; + } + + var enm:Null> = Type.resolveEnum(name); + if (enm != null) + { + var enumName = Type.getEnumName(enm); + var result = defineEnum(enumName == null ? name : enumName); + result.constructors = [for (constructor in Type.getEnumConstructs(enm)) {name: constructor}]; + if (enumName != null && enumName != name) aliases.set(name, enumName); + return true; + } + + if (name == "Class" || name == "Enum" || PolymodScriptClass.abstractClassImpls.exists(name)) + { + defineAbstract(name).t = TDynamic; + return true; + } + return false; + } + + function registerRuntimeClass(name:String, cls:Class):Void + { + var canonical = Type.getClassName(cls) ?? name; + var targetName = canonical; + if (types.exists(targetName)) + { + if (name != targetName) aliases.set(name, targetName); + return; + } + + var declaration = defineClass(targetName); + declaration.constructor = runtimeField("new", TDynamic); + if (name != targetName) aliases.set(name, targetName); + if (!Rtti.hasRtti(cls)) + { + for (fieldName in Type.getInstanceFields(cls)) declaration.fields.set(fieldName, runtimeField(fieldName, TDynamic)); + for (fieldName in Type.getClassFields(cls)) declaration.statics.set(fieldName, runtimeField(fieldName, TDynamic)); + return; + } + + var rtti = Rtti.getRtti(cls); + declaration.params = [for (param in rtti.params) TParam(param)]; + for (field in rtti.fields) declaration.fields.set(field.name, runtimeField(field.name, runtimeRttiType(field.type))); + for (field in rtti.statics) declaration.statics.set(field.name, runtimeField(field.name, runtimeRttiType(field.type))); + if (rtti.superClass != null) + declaration.superClass = runtimeRttiPath(rtti.superClass.path, rtti.superClass.params); + if (rtti.interfaces != null) + declaration.interfaces = [for (item in rtti.interfaces) runtimeRttiPath(item.path, item.params)]; + } + + function runtimeRttiPath(name:String, args:Array):TType + { + return resolve(name, [for (arg in args) runtimeRttiType(arg)]) ?? TDynamic; + } + + function runtimeRttiType(type:RuntimeCType):TType + { + return switch (type) + { + case CUnknown: + TDynamic; + case CClass(name, args), CEnum(name, args), CTypedef(name, args): + var resolved = resolve(name, [for (arg in args) runtimeRttiType(arg)]); + resolved ?? TDynamic; + case CAbstract(name, args): + var shortName = name.substr(name.lastIndexOf(".") + 1); + switch (shortName) + { + case "Int": TInt; + case "Float": TFloat; + case "Bool": TBool; + case "Void": TVoid; + case "Null": TNull(args.length == 0 ? TDynamic : runtimeRttiType(args[0])); + default: + var resolved = resolve(name, [for (arg in args) runtimeRttiType(arg)]); + resolved ?? TDynamic; + } + case CFunction(args, ret): + TFun([for (arg in args) {name: arg.name, opt: arg.opt, t: runtimeRttiType(arg.t)}], runtimeRttiType(ret)); + case CAnonymous(fields): + TAnon([for (field in fields) {name: field.name, opt: false, t: runtimeRttiType(field.type)}]); + case CDynamic(_): + TDynamic; + } } public function addXmlApi(api:Xml) @@ -132,10 +282,8 @@ class CheckerTypes var types = new haxe.rtti.XmlParser(); types.process(api, ""); var todo = []; - for (v in types.root) - addXmlType(v, todo); - for (f in todo) - f(); + for (v in types.root) addXmlType(v, todo); + for (f in todo) f(); t_string = getType("String"); } @@ -144,42 +292,87 @@ class CheckerTypes if (ct == null) ct = { name: name, - fields: [], - statics: [], + fields: new Map(), + statics: new Map(), params: [], }; types.set(name, CTClass(ct)); return ct; } + public function defineEnum(name:String, ?en:CEnum):CEnum + { + if (en == null) en = {name: name, params: [], constructors: []}; + types.set(name, CTEnum(en)); + return en; + } + + public function defineTypedef(name:String, ?td:CTypedef):CTypedef + { + if (td == null) td = {name: name, params: [], t: TDynamic}; + types.set(name, CTTypedef(td)); + return td; + } + + public function defineAbstract(name:String, ?abs:CAbstract):CAbstract + { + if (abs == null) abs = { + name: name, + params: [], + t: TDynamic, + from: [], + to: [], + fields: new Map(), + statics: new Map() + }; + types.set(name, CTAbstract(abs)); + return abs; + } + + public function setAlias(alias:String, target:String):Void + { + if (alias == null || alias.length == 0 || target == null || target.length == 0) return; + aliases.set(alias, target); + } + + public function clearAliases():Void + { + aliases.clear(); + } + + public function hasType(name:String):Bool + { + if (name == null || name.length == 0) return false; + if (name == "Null") return true; + if (types.exists(name) || aliases.exists(name) || runtimeAliases.exists(name)) return true; + return loadRuntimeType(name); + } + function addXmlType(x:haxe.rtti.CType.TypeTree, todo:ArrayVoid>) { switch (x) { case TPackage(name, full, subs): - for (s in subs) - addXmlType(s, todo); + for (s in subs) addXmlType(s, todo); case TClassdecl(c): if (types.exists(c.path)) return; - var cl:CClass = - { - name: c.path, - params: [], - fields: new Map(), - statics: new Map(), - }; + var cl:CClass = { + name: c.path, + params: [], + fields: new Map(), + statics: new Map(), + }; addMeta(c, cl); if (c.isInterface) cl.isInterface = true; - for (p in c.params) - cl.params.push(TParam(p)); - todo.push(function() { + for (p in c.params) cl.params.push(TParam(p)); + todo.push(function() + { localParams = [for (t in cl.params) c.path + "." + Checker.typeStr(t) => t]; if (c.superClass != null) cl.superClass = getType(c.superClass.path, [for (t in c.superClass.params) makeXmlType(t)]); if (c.interfaces != null) { cl.interfaces = []; - for (i in c.interfaces) - cl.interfaces.push(getType(i.path, [for (t in i.params) makeXmlType(t)])); + for (i in c.interfaces) cl.interfaces.push(getType(i.path, [for (t in i.params) makeXmlType(t)])); } var pkeys = []; function initField(f:haxe.rtti.CType.ClassField, fields) @@ -191,15 +384,14 @@ class CheckerTypes if (m.name == ":noScript") return; if (m.name == ":noCompletion") complete = false; } - var fl:CField = - { - isPublic: f.isPublic, - canWrite: f.set.match(RNormal | RCall(_) | RDynamic), - complete: complete, - params: [], - name: f.name, - t: null - }; + var fl:CField = { + isPublic: f.isPublic, + canWrite: f.set.match(RNormal | RCall(_) | RDynamic), + complete: complete, + params: [], + name: f.name, + t: null + }; for (p in f.params) { var pt = TParam(p); @@ -212,8 +404,10 @@ class CheckerTypes if (f.meta != null && f.meta.length > 0) { fl.meta = []; - for (m in f.meta) - fl.meta.push({name: m.name, params: [for (p in m.params) try parser.parseString(p) catch (e:hscript.Expr.Error) null]}); + for (m in f.meta) fl.meta.push({ + name: m.name, + params: [for (p in m.params) try parser.parseString(p) catch (e:Expr.Error) null] + }); } while (pkeys.length > 0) localParams.remove(pkeys.pop()); @@ -221,43 +415,45 @@ class CheckerTypes else fields.set(f.name, fl); } - for (f in c.fields) - initField(f, cl.fields); - for (f in c.statics) - initField(f, cl.statics); + for (f in c.fields) initField(f, cl.fields); + for (f in c.statics) initField(f, cl.statics); localParams = null; }); types.set(cl.name, CTClass(cl)); case TEnumdecl(e): if (types.exists(e.path)) return; - var en:CEnum = - { - name: e.path, - params: [], - constructors: [], - }; + var en:CEnum = { + name: e.path, + params: [], + constructors: [], + }; addMeta(e, en); - for (p in e.params) - en.params.push(TParam(p)); - todo.push(function() { + for (p in e.params) en.params.push(TParam(p)); + todo.push(function() + { localParams = [for (t in en.params) e.path + "." + Checker.typeStr(t) => t]; - for (c in e.constructors) - en.constructors.push({name: c.name, args: c.args == null ? null : [for (a in c.args) {name: a.name, opt: a.opt, t: makeXmlType(a.t)}]}); + for (c in e.constructors) en.constructors.push({ + name: c.name, + args: c.args == null ? null : [for (a in c.args) { + name: a.name, + opt: a.opt, + t: makeXmlType(a.t) + }] + }); localParams = null; }); types.set(en.name, CTEnum(en)); case TTypedecl(t): if (types.exists(t.path)) return; - var td:CTypedef = - { - name: t.path, - params: [], - t: null, - }; - for (p in t.params) - td.params.push(TParam(p)); + var td:CTypedef = { + name: t.path, + params: [], + t: null, + }; + for (p in t.params) td.params.push(TParam(p)); if (t.path == "hscript.TypeCheck") td.params.reverse(); - todo.push(function() { + todo.push(function() + { localParams = [for (pt in td.params) t.path + "." + Checker.typeStr(pt) => pt]; td.t = makeXmlType(t.type); localParams = null; @@ -265,24 +461,23 @@ class CheckerTypes types.set(t.path, CTTypedef(td)); case TAbstractdecl(a): if (types.exists(a.path)) return; - var ta:CAbstract = - { - name: a.path, - params: [], - t: null, - from: [], - to: [], - }; + var ta:CAbstract = { + name: a.path, + params: [], + t: null, + from: [], + to: [], + fields: new Map(), + statics: new Map(), + }; addMeta(a, ta); - for (p in a.params) - ta.params.push(TParam(p)); - todo.push(function() { + for (p in a.params) ta.params.push(TParam(p)); + todo.push(function() + { localParams = [for (t in ta.params) a.path + "." + Checker.typeStr(t) => t]; ta.t = makeXmlType(a.athis); - for (f in a.from) - if (f.field == null) ta.from.push(makeXmlType(f.t)); - for (t in a.to) - if (t.field == null) ta.to.push(makeXmlType(t.t)); + for (f in a.from) if (f.field == null) ta.from.push(makeXmlType(f.t)); + for (t in a.to) if (t.field == null) ta.to.push(makeXmlType(t.t)); localParams = null; }); types.set(a.path, CTAbstract(ta)); @@ -293,34 +488,49 @@ class CheckerTypes { if (src.meta == null || src.meta.length == 0) return; to.meta = []; - for (m in src.meta) - to.meta.push({name: m.name, params: [for (p in m.params) try parser.parseString(p) catch (e:hscript.Expr.Error) null]}); + for (m in src.meta) to.meta.push({ + name: m.name, + params: [for (p in m.params) try parser.parseString(p) catch (e:Expr.Error) null] + }); } function makeXmlType(t:haxe.rtti.CType.CType):TType { return switch (t) { - case CUnknown: TUnresolved("Unknown"); - case CEnum(name, params): getType(name, [for (t in params) makeXmlType(t)]); - case CClass(name, params): getType(name, [for (t in params) makeXmlType(t)]); - case CTypedef(name, params): getType(name, [for (t in params) makeXmlType(t)]); - case CFunction(args, ret): TFun([for (a in args) {name: a.name, opt: a.opt, t: makeXmlType(a.t)}], makeXmlType(ret)); + case CUnknown: + TUnresolved("Unknown"); + case CEnum(name, params): + getType(name, [for (t in params) makeXmlType(t)]); + case CClass(name, params): + getType(name, [for (t in params) makeXmlType(t)]); + case CTypedef(name, params): + getType(name, [for (t in params) makeXmlType(t)]); + case CFunction(args, ret): + TFun([for (a in args) { + name: a.name, + opt: a.opt, + t: makeXmlType(a.t) + }], makeXmlType(ret)); case CAnonymous(fields): inline function isOpt(m:haxe.rtti.CType.MetaData) { if (m == null) return false; var b = false; - for (m in m) - if (m.name == ":optional") - { - b = true; - break; - } + for (m in m) if (m.name == ":optional") + { + b = true; + break; + } return b; } - TAnon([for (f in fields) {name: f.name, t: makeXmlType(f.type), opt: isOpt(f.meta)}]); - case CDynamic(t): TDynamic; + TAnon([for (f in fields) { + name: f.name, + t: makeXmlType(f.type), + opt: isOpt(f.meta) + }]); + case CDynamic(t): + TDynamic; case CAbstract(name, params): switch (name) { @@ -358,28 +568,66 @@ class CheckerTypes public function resolve(name:String, ?args:Array):Null { + var alias = aliases.get(name); + if (alias != null) name = alias; + + var runtimeAliasSource:Null = null; + if (!types.exists(name)) + { + var runtimeAlias = runtimeAliases.get(name); + if (runtimeAlias != null) + { + runtimeAliasSource = name; + name = runtimeAlias; + } + } if (name == "Null") { if (args == null || args.length != 1) throw "Missing Null parameter"; return TNull(args[0]); } var t = types.get(name); - if (t == null) return null; + if (t == null) + { + if (!loadRuntimeType(name)) + { + if (runtimeAliasSource != null) + { + types.set(runtimeAliasSource, CTAlias(TDynamic)); + return TDynamic; + } + return null; + } + t = types.get(name); + if (t == null) return null; + } if (args == null) args = []; return switch (t) { - case CTClass(c): TInst(c, args); - case CTEnum(e): TEnum(e, args); - case CTTypedef(t): TType(t, args); - case CTAbstract(a): TAbstract(a, args); - case CTAlias(t): t; + case CTClass(c): + TInst(c, fillDefaultTypeArgs(c.params, args)); + case CTEnum(e): + TEnum(e, fillDefaultTypeArgs(e.params, args)); + case CTTypedef(t): + TType(t, fillDefaultTypeArgs(t.params, args)); + case CTAbstract(a): + TAbstract(a, fillDefaultTypeArgs(a.params, args)); + case CTAlias(t): + t; } } + + function fillDefaultTypeArgs(params:Array, args:Array):Array + { + if (args.length == 0 && params.length > 0) return[for (_ in params) TDynamic]; + return args; + } } class Checker { public var types:CheckerTypes; + public var currentClassName:Null; var locals:Map; var globals:Map = new Map(); @@ -403,21 +651,24 @@ class Checker public function setGlobals(cl:CClass, ?params:Array, allowPrivate = false) { if (params == null) params = [for (p in cl.params) makeMono()]; + var allowPrivateOnCurrentClass = allowPrivate; while (true) { - for (f in cl.fields) - if (f.isPublic || allowPrivate) setGlobal(f.name, f.params.length == 0 ? f.t : TLazy(function() { - var t = apply(f.t, f.params, [for (i in 0...f.params.length) makeMono()]); - return apply(t, cl.params, params); - })); + for (f in cl.fields) if (f.isPublic || allowPrivateOnCurrentClass) setGlobal(f.name, f.params.length == 0 ? f.t : TLazy(function() + { + var t = apply(f.t, f.params, [for (i in 0...f.params.length) makeMono()]); + return apply(t, cl.params, params); + })); if (cl.superClass == null) break; - cl = switch (cl.superClass) + cl = switch (follow(cl.superClass)) { case TInst(csup, pl): params = [for (p in pl) apply(p, cl.params, params)]; csup; - default: throw "assert"; + default: + throw "assert"; } + allowPrivateOnCurrentClass = false; } } @@ -436,6 +687,24 @@ class Checker events.set(name, type); } + public function resolveTypeAnnotation(type:CType, at:Expr):TType + { + return makeType(type, at); + } + + public function checkFunction(args:Array, body:Expr, name:String, ret:CType, expected:TType):TType + { + currentFunType = expected; + return check(mk(EFunction(args, body, null, ret), body), Value); + } + + public function checkExpected(expr:Expr, expected:TType):TType + { + var result = check(expr, WithType(expected)); + unify(result, expected, expr); + return expected; + } + public function getGlobals() { return globals; @@ -448,14 +717,16 @@ class Checker function typeArgs(args:Array, pos:Expr) { - return [ - for (i in 0...args.length) + return[for (i in 0...args.length) + { + var a = args[i]; + var at = a.t == null ? makeMono() : makeType(a.t, pos); { - var a = args[i]; - var at = a.t == null ? makeMono() : makeType(a.t, pos); - {name: a.name, opt: a.opt, t: at}; - } - ]; + name: a.name, + opt: a.opt, + t: at + }; + }]; } public function check(expr:Expr, ?withType:WithType, ?isCompletion = false) @@ -477,8 +748,10 @@ class Checker { switch (edef(e)) { - case EMeta(_, _, e2): e = e2; - default: break; + case EMeta(_, _, e2): + e = e2; + default: + break; } } switch (edef(e)) @@ -487,20 +760,19 @@ class Checker var tret = ret == null ? makeMono() : makeType(ret, e); var ft = TFun(typeArgs(args, e), tret); locals.set(name, ft); - delayed.push(function() { + delayed.push(function() + { currentFunType = ft; typeExpr(e, NoValue); return ft; }); default: - for (f in delayed) - f(); + for (f in delayed) f(); delayed = []; if (el[el.length - 1] == e) last = typeExpr(e, withType); else typeExpr(e, NoValue); } } - for (f in delayed) - last = f(); + for (f in delayed) last = f(); return last; default: } @@ -525,7 +797,7 @@ class Checker function saveLocals() { - return [for (k in locals.keys()) k => locals.get(k)]; + return[for (k in locals.keys()) k => locals.get(k)]; } function makeType(t:CType, e:Expr):Null @@ -542,15 +814,43 @@ class Checker return ct; case CTFun(args, ret): var i = 0; - return TFun([for (a in args) {name: "p" + (i++), opt: false, t: makeType(a, e)}], makeType(ret, e)); + return TFun([for (a in args) switch (a) + { + case CTOpt(inner): + { + name: "p" + (i++), + opt: true, + t: makeType(inner, e) + }; + default: + { + name: "p" + (i++), + opt: false, + t: makeType(a, e) + }; + }], makeType(ret, e)); case CTAnon(fields): - return TAnon([for (f in fields) {name: f.name, opt: false, t: makeType(f.t, e)}]); + return TAnon([for (f in fields) switch (f.t) + { + case CTOpt(inner): + { + name: f.name, + opt: true, + t: makeType(inner, e) + }; + default: + { + name: f.name, + opt: false, + t: makeType(f.t, e) + }; + }]); case CTParent(t): return makeType(t, e); case CTNamed(n, t): return makeType(t, e); case CTOpt(t): - return makeType(t, e); + return TNull(makeType(t, e)); case CTExpr(_): error("Unsupported expr type parameter", e); return null; @@ -559,25 +859,32 @@ class Checker public static function typeStr(t:TType) { - inline function makeArgs(args:Array) - return args.length == 0 ? "" : "<" + [for (t in args) typeStr(t)].join(",") + ">"; + inline function makeArgs(args:Array) return args.length == 0 ? "" : "<" +[for (t in args) typeStr(t)].join(",") + ">"; return switch (t) { - case TMono(r): r.r == null ? "Unknown" : typeStr(r.r); - case TInst(c, args): c.name + makeArgs(args); - case TEnum(e, args): e.name + makeArgs(args); + case TMono(r): + r.r == null ? "Unknown" : typeStr(r.r); + case TInst(c, args): + c.name + makeArgs(args); + case TEnum(e, args): + e.name + makeArgs(args); case TType(t, args): if (t.name == "hscript.TypeCheck") typeStr(args[1]); else t.name + makeArgs(args); - case TAbstract(a, args): a.name + makeArgs(args); - case TFun(args, ret): "(" + [ - for (a in args) - (a.opt ? "?" : "") + (a.name == "" ? "" : a.name + ":") + typeStr(a.t) - ].join(", ") + ") -> " + typeStr(ret); - case TAnon(fields): "{" + [for (f in fields) (f.opt ? "?" : "") + f.name + ":" + typeStr(f.t)].join(", ") + "}"; - case TParam(name): name; - case TNull(t): "Null<" + typeStr(t) + ">"; - case TUnresolved(name): "?" + name; - default: t.getName().substr(1); + case TAbstract(a, args): + a.name + makeArgs(args); + case TFun(args, ret): + "(" +[for (a in args) (a.opt ? "?" : "") + (a.name == "" ? "" : a.name + ":") + typeStr(a.t)].join(", ") + ") -> " + typeStr(ret); + case TAnon(fields): + "{" +[for (f in fields) (f.opt ? "?" : "") + f.name + ":" + typeStr(f.t)].join(", ") + "}"; + case TParam(name): + var separator = name.lastIndexOf("#"); + separator == -1 ? name : name.substr(separator + 1); + case TNull(t): + "Null<" + typeStr(t) + ">"; + case TUnresolved(name): + "?" + name; + default: + t.getName().substr(1); } } @@ -590,15 +897,12 @@ class Checker case TNull(t): callb(t); case TInst(_, tl), TAbstract(_, tl), TEnum(_, tl), TType(_, tl): - for (t in tl) - callb(t); + for (t in tl) callb(t); case TFun(args, ret): - for (t in args) - callb(t.t); + for (t in args) callb(t.t); callb(ret); case TAnon(fl): - for (f in fl) - callb(f.t); + for (f in fl) callb(f.t); case TLazy(f): callb(f()); default: @@ -614,26 +918,24 @@ class Checker if (r.r == null) return false; return linkLoop(a, r.r); case TEnum(_, tl), TInst(_, tl), TType(_, tl), TAbstract(_, tl): - for (t in tl) - if (linkLoop(a, t)) return true; + for (t in tl) if (linkLoop(a, t)) return true; return false; case TFun(args, ret): - for (arg in args) - if (linkLoop(a, arg.t)) return true; + for (arg in args) if (linkLoop(a, arg.t)) return true; return linkLoop(a, ret); case TDynamic: if (t == TDynamic) return false; return linkLoop(a, TDynamic); case TAnon(fl): - for (f in fl) - if (linkLoop(a, f.t)) return true; + for (f in fl) if (linkLoop(a, f.t)) return true; return false; default: return false; } } - function link(a:TType, b:TType, r:{r:TType}) + function link(a:TType, b:TType, r: + {r:TType}) { if (linkLoop(a, b)) return follow(b) == a; if (b == TDynamic) return true; @@ -663,24 +965,20 @@ class Checker } return typeEq(t1, r.r); case [TType(t1, pl1), TType(t2, pl2)] if (t1 == t2): - for (i in 0...pl1.length) - if (!typeEq(pl1[i], pl2[i])) return false; + for (i in 0...pl1.length) if (!typeEq(pl1[i], pl2[i])) return false; return true; case [TType(t1, pl1), _]: return typeEq(apply(t1.t, t1.params, pl1), t2); case [_, TType(t2, pl2)]: return typeEq(t1, apply(t2.t, t2.params, pl2)); case [TInst(cl1, pl1), TInst(cl2, pl2)] if (cl1 == cl2): - for (i in 0...pl1.length) - if (!typeEq(pl1[i], pl2[i])) return false; + for (i in 0...pl1.length) if (!typeEq(pl1[i], pl2[i])) return false; return true; case [TEnum(e1, pl1), TEnum(e2, pl2)] if (e1 == e2): - for (i in 0...pl1.length) - if (!typeEq(pl1[i], pl2[i])) return false; + for (i in 0...pl1.length) if (!typeEq(pl1[i], pl2[i])) return false; return true; case [TAbstract(a1, pl1), TAbstract(a2, pl2)] if (a1 == a2): - for (i in 0...pl1.length) - if (!typeEq(pl1[i], pl2[i])) return false; + for (i in 0...pl1.length) if (!typeEq(pl1[i], pl2[i])) return false; return true; case [TNull(t1), TNull(t2)]: return typeEq(t1, t2); @@ -689,17 +987,15 @@ class Checker case [_, TNull(t2)]: return typeEq(t1, t2); case [TFun(args1, r1), TFun(args2, r2)] if (args1.length == args2.length): - for (i in 0...args1.length) - if (!typeEq(args1[i].t, args2[i].t)) return false; + for (i in 0...args1.length) if (!typeEq(args1[i].t, args2[i].t)) return false; return typeEq(r1, r2); case [TAnon(a1), TAnon(a2)] if (a1.length == a2.length): var m = new Map(); - for (f in a2) - m.set(f.name, f); + for (f in a2) m.set(f.name, f); for (f1 in a1) { var f2 = m.get(f1.name); - if (f2 == null) return false; + if (f2 == null || f1.opt != f2.opt) return false; if (!typeEq(f1.t, f2.t)) return false; } return true; @@ -754,8 +1050,7 @@ class Checker if (a2.length == 0) // always unify with {} return true; var m = new Map(); - for (f in a1) - m.set(f.name, f); + for (f in a1) m.set(f.name, f); for (f2 in a2) { var f1 = m.get(f2.name); @@ -764,6 +1059,7 @@ class Checker if (f2.opt) continue; return false; } + if (!f2.opt && f1.opt) return false; if (!typeEq(f1.t, f2.t)) return false; } return true; @@ -786,15 +1082,16 @@ class Checker } switch (cl1.superClass) { - case null: return false; + case null: + return false; case TInst(c, args): pl1 = [for (a in args) apply(a, cl1.params, pl1)]; cl1 = c; - default: throw "assert"; + default: + throw "assert"; } } - for (i in 0...pl1.length) - if (!typeEq(pl1[i], pl2[i])) return false; + for (i in 0...pl1.length) if (!typeEq(pl1[i], pl2[i])) return false; return true; case [TInst(cl1, pl1), TAnon(fl)]: for (i in 0...fl.length) @@ -809,8 +1106,10 @@ class Checker if (cl.superClass == null) return false; cl = switch (cl.superClass) { - case TInst(c, _): c; - default: throw "assert"; + case TInst(c, _): + c; + default: + throw "assert"; } } if (!typeEq(apply(f1.t, cl1.params, pl1), f2.t)) return false; @@ -818,7 +1117,12 @@ class Checker return true; case [TInt, TFloat]: return true; - case [TFun(_), TAbstract({name: "haxe.Function"}, _)]: + case [ + TFun(_), + TAbstract({ + name: "haxe.Function" + }, _) + ]: return true; case [_, TAbstract(a, args)]: for (ft in a.from) @@ -847,8 +1151,7 @@ class Checker if (args.length != params.length) throw "Invalid number of type parameters"; if (args.length == 0) return t; var subst = new Map(); - for (i in 0...params.length) - subst.set(params[i], args[i]); + for (i in 0...params.length) subst.set(params[i], args[i]); function map(t:TType) { var st = subst.get(t); @@ -880,9 +1183,17 @@ class Checker case TAbstract(a, args): return TAbstract(a, [for (t in args) f(t)]); case TFun(args, ret): - return TFun([for (a in args) {name: a.name, opt: a.opt, t: f(a.t)}], f(ret)); + return TFun([for (a in args) { + name: a.name, + opt: a.opt, + t: f(a.t) + }], f(ret)); case TAnon(fields): - return TAnon([for (af in fields) {name: af.name, opt: af.opt, t: f(af.t)}]); + return TAnon([for (af in fields) { + name: af.name, + opt: af.opt, + t: f(af.t) + }]); case TLazy(l): return f(l()); } @@ -892,15 +1203,21 @@ class Checker { return switch (t) { - case TMono(r): if (r.r != null) follow(r.r) else t; - case TType(t, args): follow(apply(t.t, t.params, args)); - case TNull(t): follow(t); - case TLazy(f): follow(f()); - default: t; + case TMono(r): + if (r.r != null) follow(r.r) else t; + case TType(t, args): + follow(apply(t.t, t.params, args)); + case TNull(t): + follow(t); + case TLazy(f): + follow(f()); + default: + t; } } - public function getFields(t:TType):Array<{name:String, t:TType}> + public function getFields(t:TType):Array< + {name:String, t:TType}> { var fields = []; switch (follow(t)) @@ -919,14 +1236,19 @@ class Checker t = unasync(t); name = name.substr(2); } - fields.push({name: name, t: t}); + fields.push({ + name: name, + t: t + }); } if (c.isInterface && c.interfaces != null) { for (i in c.interfaces) { - for (f in getFields(i)) - fields.push({name: f.name, t: map(f.t)}); + for (f in getFields(i)) fields.push({ + name: f.name, + t: map(f.t) + }); } } if (c.superClass == null) break; @@ -941,10 +1263,26 @@ class Checker } } case TAnon(fl): - for (f in fl) - fields.push({name: f.name, t: f.t}); + for (f in fl) fields.push({ + name: f.name, + t: f.t + }); + case TAbstract(a, args): + for (name in a.fields.keys()) + { + var f = a.fields.get(name); + if (!f.isPublic || !f.complete) continue; + fields.push({ + name: name, + t: apply(f.t, a.params, args) + }); + } + if (fields.length == 0 && a.t != null) fields = getFields(apply(a.t, a.params, args)); case TFun(args, ret): - if (isCompletion) fields.push({name: "bind", t: TFun(args, TVoid)}); + if (isCompletion) fields.push({ + name: "bind", + t: TFun(args, TVoid) + }); default: } return fields; @@ -962,15 +1300,14 @@ class Checker if (cf != null) { var isPublic = true; // consider a_ prefixed as script specific - cf = - { - isPublic: isPublic, - canWrite: false, - params: cf.params, - name: cf.name, - t: unasync(cf.t), - complete: cf.complete - }; + cf = { + isPublic: isPublic, + canWrite: false, + params: cf.params, + name: cf.name, + t: unasync(cf.t), + complete: cf.complete + }; if (cf.t == null) cf = null; } } @@ -989,7 +1326,7 @@ class Checker if (ft != null) ft = apply(ft, c.params, args); return ft; } - if (!cf.isPublic) error("Can't access private field " + f + " on " + c.name, e); + if (!cf.isPublic && currentClassName != c.name) error("Can't access private field " + f + " on " + c.name, e); if (forWrite && !cf.canWrite) error("Can't write readonly field " + f + " on " + c.name, e); var t = cf.t; if (cf.params != null) t = apply(t, cf.params, [for (i in 0...cf.params.length) makeMono()]); @@ -997,8 +1334,24 @@ class Checker case TDynamic: return makeMono(); case TAnon(fields): - for (af in fields) - if (af.name == f) return af.t; + for (af in fields) if (af.name == f) return af.opt ? TNull(af.t) : af.t; + return null; + case TAbstract(a, args): + var cf = a.fields.get(f); + if (cf != null) + { + if (!cf.isPublic) error("Can't access private field " + f + " on " + a.name, e); + if (forWrite && !cf.canWrite) error("Can't write readonly field " + f + " on " + a.name, e); + var ft = cf.t; + if (cf.params != null && cf.params.length > 0) ft = apply(ft, cf.params, [for (_ in cf.params) makeMono()]); + return apply(ft, a.params, args); + } + if (!a.fields.keys().hasNext() && a.t != null) + { + var underlying = apply(a.t, a.params, args); + var ft = getField(underlying, f, e, forWrite); + if (ft != null) return ft; + } return null; default: return null; @@ -1013,7 +1366,8 @@ class Checker var rargs = args.copy(); switch (follow(rargs.shift().t)) { - case TFun([r], _): return TFun(rargs, r.t); + case TFun([r], _): + return TFun(rargs, r.t); default: } default: @@ -1030,14 +1384,24 @@ class Checker function makeMono() { - return TMono({r: null}); + return TMono({ + r: null + }); } function makeIterator(t):TType { return TAnon([ - {name: "next", opt: false, t: TFun([], t)}, - {name: "hasNext", opt: false, t: TFun([], TBool)} + { + name: "next", + opt: false, + t: TFun([], t) + }, + { + name: "hasNext", + opt: false, + t: TFun([], TBool) + } ]); } @@ -1056,10 +1420,19 @@ class Checker #end } - function isString(t:TType) + function isString(t:TType):Bool { - t = follow(t); - return t.match(TInst({name: "String"}, _)); + return switch (follow(t)) + { + case TInst({ + name: "String" + }, _), TAbstract({ + name: "String" + }, _): + true; + default: + false; + } } function onCompletion(expr:Expr, t:TType) @@ -1080,21 +1453,183 @@ class Checker return ft; } + + function mergeComprehensionType(current:Null, next:TType):TType + { + if (current == null) return next; + if (current == TDynamic) return TDynamic; + if (tryUnify(next, current)) return current; + if (tryUnify(current, next)) return next; + return TDynamic; + } + + function typeComprehension(body:Expr, isMap:Bool, withType:WithType):TType + { + var expectedElement:Null = null; + var expectedKey:Null = null; + var expectedValue:Null = null; + + switch (withType) + { + case WithType(expected): + switch (follow(expected)) + { + case TInst(c, args) if (!isMap && c.name == "Array" && args.length > 0): + expectedElement = args[0]; + case TAbstract(a, args) if (isMap && a.name == "Map" && args.length == 2): + expectedKey = args[0]; + expectedValue = args[1]; + default: + } + default: + } + + var elementType:Null = expectedElement; + var keyType:Null = expectedKey; + var valueType:Null = expectedValue; + + function inferElement(value:Expr):Void + { + if (expectedElement != null) + { + var actual = typeExpr(value, WithType(expectedElement)); + unify(actual, expectedElement, value); + } + else + { + elementType = mergeComprehensionType(elementType, typeExpr(value, Value)); + } + } + + function inferKey(key:Expr):Void + { + if (expectedKey != null) + { + var actual = typeExpr(key, WithType(expectedKey)); + unify(actual, expectedKey, key); + } + else + { + keyType = mergeComprehensionType(keyType, typeExpr(key, Value)); + } + } + + function inferValue(value:Expr):Void + { + if (expectedValue != null) + { + var actual = typeExpr(value, WithType(expectedValue)); + unify(actual, expectedValue, value); + } + else + { + valueType = mergeComprehensionType(valueType, typeExpr(value, Value)); + } + } + + function visit(e:Expr):Void + { + switch (edef(e)) + { + case EFor(v, iterator, child): + var savedLocals = saveLocals(); + var iteratorType = typeExpr(iterator, Value); + locals.set(v, getIteratorType(iteratorType, iterator)); + visit(child); + locals = savedLocals; + + case EForGen(iterator, child): + Tools.getKeyIterator(iterator, function(vk, vv, iteratorExpr) + { + if (vk == null) + { + error("Invalid for expression", iteratorExpr); + return; + } + var savedLocals = saveLocals(); + var iteratorType = typeExpr(iteratorExpr, Value); + var iteratorTypes = getKeyIteratorTypes(iteratorType, iteratorExpr); + locals.set(vk, iteratorTypes.key); + locals.set(vv, iteratorTypes.value); + visit(child); + locals = savedLocals; + }); + + case EWhile(cond, child), EDoWhile(cond, child): + typeExprWith(cond, TBool); + visit(child); + + case EIf(cond, e1, e2): + typeExprWith(cond, TBool); + visit(e1); + if (e2 != null) visit(e2); + + case EParent(inner): + visit(inner); + + case EBlock([inner]): + visit(inner); + + case EMeta(_, _, inner): + visit(inner); + + case EBinop("=>", key, value): + if (!isMap) + { + error("Invalid mixed comprehension (got key=>value pair in non-map comprehension)", e); + typeExpr(key, Value); + typeExpr(value, Value); + return; + } + inferKey(key); + inferValue(value); + + default: + if (isMap) + { + error("Invalid mixed comprehension (got non key=>value pair in map comprehension)", e); + typeExpr(e, Value); + } + else + { + inferElement(e); + } + } + } + + visit(body); + + if (isMap) + { + if (keyType == null) keyType = TDynamic; + if (valueType == null) valueType = TDynamic; + return types.resolve("Map", [keyType, valueType]) ?? TDynamic; + } + + if (elementType == null) elementType = TDynamic; + return types.resolve("Array", [elementType]) ?? TDynamic; + } + function typeExpr(expr:Expr, withType:WithType):TType { if (expr == null && isCompletion) return switch (withType) { - case WithType(t): t; - default: TDynamic; + case WithType(t): + t; + default: + TDynamic; } switch (edef(expr)) { case EConst(c): return switch (c) { - case CInt(_): TInt; - case CFloat(_): TFloat; - case CString(_): types.t_string; + case CInt(_): + TInt; + case CFloat(_): + TFloat; + case CString(_): + types.t_string; } case EIdent(v): var l = locals.get(v); @@ -1104,8 +1639,10 @@ class Checker { return switch (g) { - case TLazy(f): f(); - default: g; + case TLazy(f): + f(); + default: + g; } } if (allowAsync) @@ -1126,16 +1663,15 @@ class Checker switch (withType) { case WithType(et = TEnum(e, args)): - for (c in e.constructors) - if (c.name == v) + for (c in e.constructors) if (c.name == v) + { + if (onTopDownEnum(e, v)) { - if (onTopDownEnum(e, v)) - { - var ct = c.args == null ? et : TFun(c.args, et); - return apply(ct, e.params, args); - } - break; + var ct = c.args == null ? et : TFun(c.args, et); + return apply(ct, e.params, args); } + break; + } default: } if (isCompletion) return TDynamic; @@ -1144,8 +1680,7 @@ class Checker case EBlock(el): var t = TVoid; var locals = saveLocals(); - for (e in el) - t = typeExpr(e, e == el[el.length - 1] ? withType : NoValue); + for (e in el) t = typeExpr(e, e == el[el.length - 1] ? withType : NoValue); this.locals = locals; return t; case EVar(n, t, init): @@ -1159,6 +1694,17 @@ class Checker } locals.set(n, vt); return TVoid; + case EFinal(n, t, init): + var vt = t == null ? makeMono() : makeType(t, expr); + if (init != null) + { + var et = typeExpr(init, t == null ? Value : WithType(vt)); + if (t == null) vt = et + else + unify(et, vt, init); + } + locals.set(n, vt); + return TVoid; case EParent(e): return typeExpr(e, withType); case ECall(e, params): @@ -1187,8 +1733,10 @@ class Checker } var ft = typeExpr(e, switch ([edef(e), withType]) { - case [EIdent(_), WithType(TEnum(_))]: withType; - default: Value; + case [EIdent(_), WithType(TEnum(_))]: + withType; + default: + Value; }); switch (follow(ft)) { @@ -1204,12 +1752,10 @@ class Checker var t = typeExpr(params[i], a == null ? Value : WithType(a.t)); unify(t, a.t, params[i]); } - for (i in params.length...args.length) - if (!args[i].opt) error("Missing argument " + args[i].name + ":" + typeStr(args[i].t), expr); + for (i in params.length...args.length) if (!args[i].opt) error("Missing argument " + args[i].name + ":" + typeStr(args[i].t), expr); return ret; case TDynamic: - for (p in params) - typeExpr(p, Value); + for (p in params) typeExpr(p, Value); return makeMono(); default: error(typeStr(ft) + " cannot be called", e); @@ -1217,6 +1763,20 @@ class Checker } case EField(o, f): return typeField(o, f, expr, false); + case ECast(e, t): + if (t == null) + { + typeExpr(e, Value); + return TDynamic; + } + var castType = makeType(t, expr); + var sourceType = typeExpr(e, Value); + if (!tryUnify(sourceType, castType) && !tryUnify(castType, sourceType)) + { + // Haxe casts can intentionally bridge otherwise unrelated types. + // Keep this permissive but still resolve and validate the target type. + } + return castType; case ECheckType(v, t): var ct = makeType(t, expr); var vt = typeExpr(v, WithType(ct)); @@ -1243,21 +1803,27 @@ class Checker { case WithType(follow(_) => TAnon(tfields)) if (tfields.length > 0): var map = [for (f in tfields) f.name => f]; - return TAnon([ - for (f in fl) + return TAnon([for (f in fl) + { + var ft = map.get(f.name); + var ft = if (ft == null) { - var ft = map.get(f.name); - var ft = if (ft == null) - { - error("Extra field " + f.name, f.e); - TDynamic; - } - else ft.t; - {t: typeExprWith(f.e, ft), opt: false, name: f.name} + error("Extra field " + f.name, f.e); + TDynamic; } - ]); + else ft.t; + { + t: typeExprWith(f.e, ft), + opt: false, + name: f.name + } + }]); default: - return TAnon([for (f in fl) {t: typeExpr(f.e, Value), opt: false, name: f.name}]); + return TAnon([for (f in fl) { + t: typeExpr(f.e, Value), + opt: false, + name: f.name + }]); } case EBreak, EContinue: return TVoid; @@ -1269,27 +1835,70 @@ class Checker unify(et, allowReturn, v == null ? expr : v); return makeMono(); case EArrayDecl(el): - var et = null; + var expectedElement:Null = null; + switch (withType) + { + case WithType(expected): + switch (follow(expected)) + { + case TInst(arrayClass, args) if (arrayClass.name == "Array" && args.length > 0): + expectedElement = args[0]; + default: + } + default: + } + if (expectedElement != null) + { + for (v in el) + { + var itemType = typeExpr(v, WithType(expectedElement)); + unify(itemType, expectedElement, v); + } + return types.getType("Array", [expectedElement]); + } + + var et:Null = null; + var heterogeneous = false; for (v in el) { - var t = typeExpr(v, et == null ? Value : WithType(et)); - if (et == null) et = t - else if (!tryUnify(t, et)) + var itemType = typeExpr(v, Value); + if (et == null) { - if (tryUnify(et, t)) et = t - else - unify(t, et, v); + et = itemType; + continue; + } + if (tryUnify(itemType, et)) continue; + if (tryUnify(et, itemType)) + { + et = itemType; + continue; } + heterogeneous = true; } - if (et == null) et = makeMono(); + if (heterogeneous) et = TDynamic; + if (et == null) et = TDynamic; return types.getType("Array", [et]); + + case EComprehension(body, isMap): + return typeComprehension(body, isMap, withType); + case EArray(a, index): typeExprWith(index, TInt); var at = typeExpr(a, Value); switch (follow(at)) { - case TInst({name: "Array"}, [et]): return et; - default: error(typeStr(at) + " is not an Array", a); + case TInst({ + name: "Array" + }, [et]): + return et; + case TInst({ + name: "String" + }, _), TAbstract({ + name: "String" + }, _): + return types.t_string; + default: + error(typeStr(at) + " is not an Array", a); } case EThrow(e): typeExpr(e, Value); @@ -1343,13 +1952,16 @@ class Checker else error("Extra argument " + a.name, expr); } + if (i < args.length && args[i].value != null) typeExprWith(args[i].value, a.t); this.locals.set(a.name, a.t); } - if (withArgs != null && targs.length < withArgs.length) error("Missing " - + (withArgs.length - targs.length) - + " arguments (" - + [for (i in targs.length...withArgs.length) typeStr(withArgs[i].t)].join(",") + ")", - expr); + if ( + withArgs != null + && targs.length < withArgs.length + ) error( + "Missing " + (withArgs.length - targs.length) + " arguments (" +[for (i in targs.length...withArgs.length) typeStr(withArgs[i].t)].join(",") + ")", + expr + ); typeExpr(body, NoValue); if (!hasReturn && !tryUnify(tret, TVoid)) error("Missing return " + typeStr(tret), expr); allowDefine = oldGDef; @@ -1383,7 +1995,8 @@ class Checker this.locals = locals; return TVoid; case EForGen(it, e): - Tools.getKeyIterator(it, function(vk, vv, it) { + Tools.getKeyIterator(it, function(vk, vv, it) + { if (vk == null) { error("Invalid for expression", it); @@ -1419,8 +2032,10 @@ class Checker } var vt = switch (edef(e1)) { - case EField(o, f): typeField(o, f, e1, true); - default: typeExpr(e1, Value); + case EField(o, f): + typeField(o, f, e1, true); + default: + typeExpr(e1, Value); } typeExprWith(e2, vt); return vt; @@ -1475,7 +2090,9 @@ class Checker if (!tryUnify(t1, t2)) unify(t2, t1, e2); switch (follow(t1)) { - case TInt, TFloat, TBool, TInst({name: "String"}, _): + case TInt, TFloat, TBool, TInst({ + name: "String" + }, _): default: error("Cannot compare " + typeStr(t1), expr); } @@ -1487,20 +2104,24 @@ class Checker } error("Unsupported operation " + op, expr); } - case ETry(etry, v, et, ecatch): - var vt = typeExpr(etry, withType); + case ETry(etry, catches): + var tryType = typeExpr(etry, withType); - var old = locals.get(v); - locals.set(v, makeType(et, ecatch)); - var ct = typeExpr(ecatch, withType); - if (old != null) locals.set(v, old) - else - locals.remove(v); + for (catcher in catches) + { + var old = locals.get(catcher.name); + var catchType:TType = catcher.t == null ? TDynamic : makeType(catcher.t, catcher.e); + locals.set(catcher.name, catchType); + var catchExprType = typeExpr(catcher.e, withType); + if (old != null) locals.set(catcher.name, old); + else + locals.remove(catcher.name); + + if (withType != NoValue && !tryUnify(tryType, catchExprType)) unify(catchExprType, tryType, catcher.e); + } if (withType == NoValue) return TVoid; - if (tryUnify(vt, ct)) return ct; - unify(ct, vt, ecatch); - return vt; + return tryType; case ESwitch(value, cases, defaultExpr): var tmin = null; var vt = typeExpr(value, Value); @@ -1529,16 +2150,111 @@ class Checker if (defaultExpr != null) mergeType(typeExpr(defaultExpr, withType), defaultExpr); return withType == NoValue ? TVoid : tmin == null ? makeMono() : tmin; case ENew(cl, params): + var classType = types.resolve(cl); + if (classType == null) + { + error("Unknown type " + cl, expr); + for (param in params) typeExpr(param, Value); + return TDynamic; + } + switch (follow(classType)) + { + case TInst(cls, typeArgs): + if (typeArgs.length == 0 && cls.params.length > 0) typeArgs = [for (_ in cls.params) makeMono()]; + var constructor = cls.constructor; + if (constructor != null) + { + if (!constructor.isPublic && currentClassName != cls.name) error("Can't access private constructor for " + cl, expr); + var constructorFieldType = constructor.t; + if ( + constructor.params != null + && constructor.params.length > 0 + ) constructorFieldType = apply(constructorFieldType, constructor.params, [for (_ in constructor.params) makeMono()]); + var constructorType = apply(constructorFieldType, cls.params, typeArgs); + switch (follow(constructorType)) + { + case TFun(args, _): + checkCallArguments(args, params, expr, "constructor " + cl); + case TDynamic: + for (param in params) typeExpr(param, Value); + default: + if (params.length > 0) error("Type " + cl + " has no callable constructor", expr); + } + } + else if (params.length > 0) + { + error("Type " + cl + " has no constructor accepting arguments", expr); + for (param in params) typeExpr(param, Value); + } + else + { + for (param in params) typeExpr(param, Value); + } + return TInst(cls, typeArgs); + case TAbstract(abs, typeArgs): + if (typeArgs.length == 0 && abs.params.length > 0) typeArgs = [for (_ in abs.params) makeMono()]; + switch (follow(apply(abs.t, abs.params, typeArgs))) + { + case TInst(cls, classArgs) if (cls.constructor != null): + if (!cls.constructor.isPublic && currentClassName != cls.name) error("Can't access private constructor for " + cl, expr); + var constructorFieldType = cls.constructor.t; + if ( + cls.constructor.params != null + && cls.constructor.params.length > 0 + ) constructorFieldType = apply(constructorFieldType, cls.constructor.params, [for (_ in cls.constructor.params) makeMono()]); + var constructorType = apply(constructorFieldType, cls.params, classArgs); + switch (follow(constructorType)) + { + case TFun(args, _): + checkCallArguments(args, params, expr, "constructor " + cl); + default: + for (param in params) typeExpr(param, Value); + } + default: + if (params.length > 0) error("Abstract type " + cl + " cannot be constructed with arguments", expr); + for (param in params) typeExpr(param, Value); + } + return TAbstract(abs, typeArgs); + default: + error("Type " + cl + " cannot be constructed", expr); + for (param in params) typeExpr(param, Value); + return TDynamic; + } } error("Don't know how to type " + edef(expr).getName(), expr); return TDynamic; } + function checkCallArguments(args:Array< + {name:String, opt:Bool, t:TType}>, + params:Array, + at:Expr, + callName:String):Void + { + for (i in 0...params.length) + { + var arg = i < args.length ? args[i] : null; + if (arg == null) + { + error("Too many arguments for " + callName, params[i]); + typeExpr(params[i], Value); + } + else + { + var actual = typeExpr(params[i], WithType(arg.t)); + unify(actual, arg.t, params[i]); + } + } + for (i in params.length...args.length) if (!args[i].opt) error("Missing argument " + args[i].name + ":" + typeStr(args[i].t) + " for " + callName, at); + } + function getIteratorType(itt:TType, it:Expr) { switch (follow(itt)) { - case TInst({name: "Array"}, [t]): + case TInst({ + name: "Array" + }, [t]): return t; default: } @@ -1569,8 +2285,13 @@ class Checker { switch (follow(itt)) { - case TInst({name: "Array"}, [t]): - return {key: TInt, value: t}; + case TInst({ + name: "Array" + }, [t]): + return { + key: TInt, + value: t + }; default: } var ft = getField(itt, "keyValueIterator", it); @@ -1593,8 +2314,21 @@ class Checker var key = makeMono(); var value = makeMono(); var iter = makeIterator(TAnon([ - {name: "key", t: key, opt: false}, {name: "value", t: value, opt: false}])); + { + name: "key", + t: key, + opt: false + }, + { + name: "value", + t: value, + opt: false + } + ])); unify(ft != null ? ft : itt, iter, it); - return {key: key, value: value}; + return { + key: key, + value: value + }; } } diff --git a/polymod/hscript/_internal/Expr.hx b/polymod/hscript/_internal/Expr.hx index 9cc5024d..b4967966 100644 --- a/polymod/hscript/_internal/Expr.hx +++ b/polymod/hscript/_internal/Expr.hx @@ -335,6 +335,8 @@ typedef EnumDecl = > ModuleType, var fields:Array; @:optional var pkg:Array; + @:optional var imports:Map; + @:optional var importsToValidate:Map; } typedef EnumFieldDecl = diff --git a/polymod/hscript/_internal/Interp.hx b/polymod/hscript/_internal/Interp.hx index c46e881a..ddf06ff8 100644 --- a/polymod/hscript/_internal/Interp.hx +++ b/polymod/hscript/_internal/Interp.hx @@ -124,6 +124,9 @@ class Interp var inTry:Bool; var declared:Array<{n:String, old:LocalVar}>; var returnValue:Dynamic; + #if POLYMOD_STRICT_SYNTAX + var currentReturnType:Null = null; + #end #if hscriptPos var curExpr:Expr; #end @@ -592,6 +595,7 @@ class Interp */ public function callScriptClassStaticFunction(clsName:String, fnName:String, args:Array = null):Null { + if (args == null) args = []; // For functions listScriptClasses and scriptInit, we want to return the needed values without much checking. if (fnName == "listScriptClasses") { @@ -689,6 +693,7 @@ class Interp var prefixedName = '$clsName#$fnName'; if (this.functions.exists(prefixedName)) { + validateScriptFunctionArguments(fn, args, fnName); result = Reflect.callMethod(this, this.functions.get(prefixedName), args); } else @@ -704,10 +709,12 @@ class Interp catch (err:Expr.Error) { PolymodScriptClass.reportError(err, clsName, fnName); - // A script error occurred while executing the script function. - // Purge the function from the cache so it is not called again. - // purgeStaticFunction(fnName); - return null; + result = null; + } + catch (err:Dynamic) + { + this._classDeclOverride = previousClassDecl; + throw err; } this._classDeclOverride = previousClassDecl; @@ -861,7 +868,7 @@ class Interp private static function registerScriptEnum(e:EnumDecl) { var name = e.name; - if (e.pkg != null) + if (e.pkg != null && e.pkg.length > 0) { name = e.pkg.join(".") + "." + name; } @@ -918,7 +925,11 @@ class Interp var inf = posInfos(); var v = el.shift(); if (el.length > 0) inf.customParams = el; + #if macro + trace(Std.string(v), inf); + #else haxe.Log.trace(Std.string(v), inf); + #end })); variables.set("Math", #if hl polymod.hscript._internal.HLWrapperMacro.HLMath #else Math #end); @@ -1105,9 +1116,8 @@ class Interp } } - if (_proxy != null && _proxy.fieldExists(id) || variables.exists(id)) + if ((_proxy != null && _proxy.fieldExists(id)) || variables.exists(id)) { - // Fallback to setting in local scope. variables.set(id, v); return v; } @@ -1165,7 +1175,10 @@ class Interp { this.declared.push({ n: a.name, old: this.locals.get(a.name) }); } - this.locals.set(a.name, { r: value }); + #if POLYMOD_STRICT_SYNTAX + if (a.t != null) value = assertRuntimeType(value, a.t, 'argument', false, a.name); + #end + this.locals.set(a.name, {r: value, t: a.t}); i++; } } @@ -1189,6 +1202,12 @@ class Interp if (Std.isOfType(_proxy.superClass, PolymodScriptClass)) { var superClass:PolymodAbstractScriptClass = cast(_proxy.superClass, PolymodScriptClass); + @:privateAccess { + var superDecl = superClass.findVar(id); + #if POLYMOD_STRICT_SYNTAX + if (superDecl != null) v = assertRuntimeType(v, superDecl.type, 'field', false, id); + #end + } return superClass.fieldWrite(id, v); } @@ -1205,6 +1224,9 @@ class Interp if (decl != null) { + #if POLYMOD_STRICT_SYNTAX + v = assertRuntimeType(v, decl.type, 'field', false, id); + #end switch (decl.set) { case "set": @@ -1317,7 +1339,12 @@ class Interp setVar(id, v); } else + { + #if POLYMOD_STRICT_SYNTAX + v = assertRuntimeType(v, l.t, 'local variable', false, id); + #end l.r = v; + } case EField(e0, id): // Make sure setting superclass fields works when using this. // Also ensures property functions are accounted for. @@ -1371,18 +1398,22 @@ class Interp // Do nothing } + var target = fieldTarget(e0); + #if POLYMOD_STRICT_SYNTAX + v = assertRuntimeFieldType(target, id, v); + #end // Fallback to field set - v = set(fieldTarget(e0), id, v); + v = set(target, id, v); case EArray(e, index): var arr:Dynamic = expr(e); - var index:Dynamic = expr(index); + var indexValue:Dynamic = expr(index); if (isMap(arr)) { - setMapValue(arr, index, v); + setMapValue(arr, indexValue, v); } else { - arr[index] = v; + arr[indexValue] = v; } default: @@ -1472,10 +1503,18 @@ class Interp } if (l == null) setVar(id, v) else + { + #if POLYMOD_STRICT_SYNTAX + v = assertRuntimeType(v, l.t, 'local variable', false, id); + #end l.r = v; + } case EField(e, f): var obj = fieldTarget(e); v = fop(get(obj, f), expr(e2)); + #if POLYMOD_STRICT_SYNTAX + v = assertRuntimeFieldType(obj, f, v); + #end v = set(obj, f, v); case EArray(e, index): var arr:Dynamic = expr(e); @@ -1567,11 +1606,22 @@ class Interp v += delta; if (l == null) setVar(id, v) else + { + #if POLYMOD_STRICT_SYNTAX + v = assertRuntimeType(v, l.t, 'local variable', false, id); + #end l.r = v; + } } else if (l == null) setVar(id, v + delta) else - l.r = v + delta; + { + var nextValue:Dynamic = v + delta; + #if POLYMOD_STRICT_SYNTAX + nextValue = assertRuntimeType(nextValue, l.t, 'local variable', false, id); + #end + l.r = nextValue; + } return v; case EField(e, f): var obj = fieldTarget(e); @@ -1579,10 +1629,19 @@ class Interp if (prefix) { v += delta; + #if POLYMOD_STRICT_SYNTAX + v = assertRuntimeFieldType(obj, f, v); + #end set(obj, f, v); } else - set(obj, f, v + delta); + { + var nextValue:Dynamic = v + delta; + #if POLYMOD_STRICT_SYNTAX + nextValue = assertRuntimeFieldType(obj, f, nextValue); + #end + set(obj, f, nextValue); + } return v; case EArray(e, index): var arr:Dynamic = expr(e); @@ -1597,7 +1656,8 @@ class Interp } else { - setMapValue(arr, index, v + delta); + var nextValue:Dynamic = v + delta; + setMapValue(arr, index, nextValue); } return v; } @@ -1610,7 +1670,10 @@ class Interp arr[index] = v; } else - arr[index] = v + delta; + { + var nextValue:Dynamic = v + delta; + arr[index] = nextValue; + } return v; } default: @@ -1651,24 +1714,35 @@ class Interp return exprReturn(expr); } + public function validateScriptFunctionArguments(fn:FunctionDecl, args:Array, fnName:String):Void + { + if (args == null) args = []; + validateArgumentCount(fn.args, args, fnName); + } + public function executeFunction(fn:FunctionDecl, fnName:String, args:Array):Dynamic { var oldDepth:Int = this.depth; var oldLocals = this.locals; var oldDeclared = this.declared; var oldCurrentFunction = this.currentFunction; + #if POLYMOD_STRICT_SYNTAX + var oldReturnType = this.currentReturnType; + #end this.locals = []; this.declared = []; this.depth++; this.currentFunction = fnName; - - setFunctionValues(fn, args, fnName, false); + #if POLYMOD_STRICT_SYNTAX + this.currentReturnType = fn.ret; + #end var result:Null = null; var exception:Null = null; try { + setFunctionValues(fn, args, fnName, false); result = exprReturn(fn.expr); } catch (err:Dynamic) @@ -1680,6 +1754,9 @@ class Interp this.locals = oldLocals; this.declared = oldDeclared; this.currentFunction = oldCurrentFunction; + #if POLYMOD_STRICT_SYNTAX + this.currentReturnType = oldReturnType; + #end // Assuming this error will be handled higher. if (exception != null) @@ -1721,6 +1798,290 @@ class Interp // } } + #if POLYMOD_STRICT_SYNTAX + /** + * Validate a declared HScript type at runtime. + */ + public function assertRuntimeType(value:Dynamic, type:Null, context:String, deep:Bool = false, subject:Null = null, + ?actualTypeHint:String):Dynamic + { + if (type == null || runtimeTypeMatches(value, type, 0, deep) + || (value != null && actualTypeHint != null && runtimeTypeHintMatches(actualTypeHint, type))) return value; + + var expected = new Printer().typeToString(type); + var actual:String; + if (value == null) + actual = 'Null'; + else if (Std.isOfType(value, PolymodEnum.PolymodEnumValue)) + actual = (cast value:PolymodEnum.PolymodEnumValue).parentEnum.getFullName(); + else + { + var scriptProxy = getScriptRuntimeClass(value); + actual = scriptProxy == null ? (actualTypeHint ?? Util.getTypeNameOf(value)) : scriptProxy.fullyQualifiedName; + } + + var location = subject == null ? context : context + ' "' + subject + '"'; + return error(ECustom('Type mismatch for ${location}: expected ${expected}, got ${actual}.')); + } + + function runtimeTypeHintMatches(actualTypeName:String, expectedType:CType):Bool + { + return switch (expectedType) + { + case CTParent(inner) | CTNamed(_, inner) | CTOpt(inner): + runtimeTypeHintMatches(actualTypeName, inner); + case CTPath(path, _): + var expectedName = resolveRuntimeTypePath(path); + if (expectedName == actualTypeName) true; + else + { + var actualRef = PolymodStaticClassReference.tryBuild(actualTypeName); + if (actualRef == null || actualRef.cls == null) false; + else + { + var expectedRef = PolymodStaticClassReference.tryBuild(expectedName); + if (expectedRef != null && expectedRef.cls != null) + { + var expectedFullName = expectedRef.getFullyQualifiedName(); + actualRef.getFullyQualifiedName() == expectedFullName + || PolymodScriptClass.getSuperClasses(actualRef.cls).contains(expectedFullName); + } + else + { + var expectedInterface = PolymodStaticInterfaceReference.tryBuild(expectedName); + if (expectedInterface == null) false; + else + { + var actualFullName = actualRef.getFullyQualifiedName(); + (PolymodScriptClass.classesExtendingInterfaces.get(actualFullName) ?? []).contains(expectedInterface.id); + } + } + } + } + default: + false; + } + } + + function runtimeTypeMatches(value:Dynamic, type:CType, depth:Int, deep:Bool):Bool + { + if (depth > 32) return false; + + return switch (type) + { + case CTParent(inner) | CTNamed(_, inner): + runtimeTypeMatches(value, inner, depth + 1, deep); + case CTOpt(inner): + value == null || runtimeTypeMatches(value, inner, depth + 1, deep); + case CTFun(_, _): + Reflect.isFunction(value); + case CTAnon(fields): + if (value == null || (!Reflect.isObject(value) && !Std.isOfType(value, PolymodScriptClass))) false; + else + { + var matches = true; + for (field in fields) + { + if (!Reflect.hasField(value, field.name) || (deep && !runtimeTypeMatches(Reflect.field(value, field.name), field.t, depth + 1, deep))) + { + matches = false; + break; + } + } + matches; + } + case CTExpr(_): + true; + case CTPath(path, params): + if (path == null || path.length == 0) true; + else + { + var name = path[path.length - 1]; + if (name == 'Dynamic' || name == 'Any' || name == 'Unknown') true; + else if (name == 'Void') true; + else if (name == 'Null') value == null || (params != null && params.length > 0 && runtimeTypeMatches(value, params[0], depth + 1, deep)); + else if (value == null) name != 'Int' && name != 'Float' && name != 'Bool'; + else switch (name) + { + case 'Int': switch (Type.typeof(value)) { case TInt: true; default: false; }; + case 'Float': switch (Type.typeof(value)) { case TInt | TFloat: true; default: false; }; + case 'Bool': switch (Type.typeof(value)) { case TBool: true; default: false; }; + case 'String': Std.isOfType(value, String); + case 'Array': + /*if (!Std.isOfType(value, Array)) false; + else if (!deep || params == null || params.length == 0 || isDynamicRuntimeType(params[0])) true; + else + { + var matches = true; + for (item in (cast value:Array)) + if (!runtimeTypeMatches(item, params[0], depth + 1, deep)) { matches = false; break; } + matches; + }*/ // Sometimes Haxe erases Array element types at runtime. + Std.isOfType(value, Array); + case 'Map': + if (!isMap(value)) false; + else if (!deep || params == null || params.length == 0 || (isDynamicRuntimeType(params[0]) && (params.length < 2 || isDynamicRuntimeType(params[1])))) true; + else + { + var map:haxe.Constraints.IMap = cast value; + var iterator = map.keys(); + var matches = true; + while (iterator.hasNext()) + { + var key = iterator.next(); + if (params != null && params.length > 0 && !runtimeTypeMatches(key, params[0], depth + 1, deep)) { matches = false; break; } + if (params != null && params.length > 1 && !runtimeTypeMatches(map.get(key), params[1], depth + 1, deep)) { matches = false; break; } + } + matches; + } + default: + runtimeTypeMatchesNamed(value, resolveRuntimeTypePath(path)); + } + } + } + } + + inline function isDynamicRuntimeType(type:CType):Bool + { + return switch (type) + { + case CTPath(path, _): path.length > 0 && (path[path.length - 1] == 'Dynamic' || path[path.length - 1] == 'Any'); + case CTParent(inner) | CTNamed(_, inner) | CTOpt(inner): isDynamicRuntimeType(inner); + default: false; + } + } + + function resolveRuntimeTypePath(path:Array):String + { + if (path.length == 0) return ''; + var decl = getClassDecl(); + if (decl != null && decl.imports != null) + { + var imported = decl.imports.get(path[0]); + if (imported != null && !imported.field && imported.fullPath != null) + { + if (path.length == 1) return imported.fullPath; + return imported.fullPath + '.' + path.slice(1).join('.'); + } + } + + var name = path.join('.'); + if (path.length == 1 && decl != null && decl.pkg != null && decl.pkg.length > 0) + { + var packageName = decl.pkg.join('.') + '.' + path[0]; + if (_scriptClassDescriptors.exists(packageName) || _scriptInterfaceDescriptors.exists(packageName) || _scriptEnumDescriptors.exists(packageName)) + return packageName; + } + return name; + } + + function runtimeTypeMatchesNamed(value:Dynamic, typeName:String):Bool + { + var scriptClass = PolymodStaticClassReference.tryBuild(typeName); + if (scriptClass != null) return getScriptRuntimeClass(value) != null && PolymodScriptClass.isOfType(value, scriptClass); + + var scriptInterface = PolymodStaticInterfaceReference.tryBuild(typeName); + if (scriptInterface != null) return getScriptRuntimeClass(value) != null && PolymodScriptClass.isOfType(value, scriptInterface); + + var scriptEnum = PolymodEnum.tryBuild(typeName); + if (scriptEnum != null) + return Std.isOfType(value, PolymodEnum.PolymodEnumValue) && (cast value:PolymodEnum.PolymodEnumValue).parentEnum.getFullName() == typeName; + + var nativeClass:Class = null; + var decl = getClassDecl(); + if (decl != null && decl.imports != null) + { + for (imported in decl.imports) + { + if (imported.fullPath == typeName && imported.cls != null) + { + nativeClass = cast imported.cls; + break; + } + } + } + if (nativeClass == null) nativeClass = Type.resolveClass(typeName); + if (nativeClass != null) return Std.isOfType(value, nativeClass); + + var nativeEnum = Type.resolveEnum(typeName); + if (nativeEnum != null) + return switch (Type.typeof(value)) { case TEnum(actualEnum): actualEnum == nativeEnum; default: false; }; + + return true; + } + + function getScriptRuntimeClass(value:Dynamic):Null + { + if (value == null) return null; + if (Std.isOfType(value, PolymodScriptClass)) return cast value; + + try + { + var asc:Dynamic = value._asc; + if (Std.isOfType(asc, PolymodScriptClass)) return cast asc; + } + catch (_:Dynamic) {} + + return null; + } + + inline function isScriptInstance(value:Dynamic):Bool + { + if (value == null || Std.isOfType(value, String)) return false; + return switch (Type.typeof(value)) + { + case TInt | TFloat | TBool | TFunction: false; + default: getScriptRuntimeClass(value) != null || Std.isOfType(value, HScriptedClass); + } + } + + function assertRuntimeFieldType(obj:Dynamic, fieldName:String, value:Dynamic):Dynamic + { + if (obj == null) return value; + + if (Std.isOfType(obj, PolymodStaticClassReference)) + { + var classRef:PolymodStaticClassReference = cast obj; + if (classRef.cls != null) + { + for (field in classRef.cls.staticFields) + { + if (field.name != fieldName) continue; + switch (field.kind) + { + case KVar(variable): return assertRuntimeType(value, variable.type, 'static field', false, fieldName); + default: + } + } + } + return value; + } + + var proxy:PolymodAbstractScriptClass = null; + if (Std.isOfType(obj, PolymodScriptClass)) + proxy = cast obj; + else + { + try + { + var asc:Dynamic = obj._asc; + if (Std.isOfType(asc, PolymodScriptClass)) proxy = cast asc; + } + catch (_:Dynamic) {} + } + + if (proxy != null) + { + @:privateAccess { + var declaration = proxy.findVar(fieldName); + if (declaration != null) + return assertRuntimeType(value, declaration.type, 'field', false, fieldName); + } + } + return value; + } + #end + function duplicate(h:Map) { var h2 = new Map(); @@ -2158,11 +2519,39 @@ class Interp }); // Evaluate the expression before assigning, applying typing if possible. - var result:Dynamic = (expression != null) ? exprWithType(expression, type) : null; - + var result:Dynamic = null; + #if POLYMOD_STRICT_SYNTAX + var actualTypeHint:Null = null; + switch (name) + { + case _.startsWith('_g') => true: + result = (expression != null) ? this.exprMap([]) : null; + default: + if (expression != null) + { + switch (Tools.expr(expression)) + { + case ENew(className, _): + actualTypeHint = resolveRuntimeTypePath(className.split('.')); + result = expr(expression); + if (result == null) + error(ECustom('Construction of "${className}" returned null while initializing local variable "${name}".')); + default: + result = exprWithType(expression, type); + } + } + } + if (expression != null) result = assertRuntimeType(result, type, 'local variable', true, name, actualTypeHint); + #else + if (name.startsWith('_g')) + result = (expression != null) ? this.exprMap([]) : null; + else if (expression != null) + result = expr(expression); + #end locals.set(name, { r: result, - isfinal: false + isfinal: false, + t: type }); return null; @@ -2172,12 +2561,32 @@ class Interp old: locals.get(name) }); - // Evaluate the expression before assigning, applying typing if possible. - var result = (expression != null) ? exprWithType(expression, type) : null; + // Keep constructor evaluation independent from the declared variable type. + var result:Dynamic = null; + #if POLYMOD_STRICT_SYNTAX + var actualTypeHint:Null = null; + if (expression != null) + { + switch (Tools.expr(expression)) + { + case ENew(className, _): + actualTypeHint = resolveRuntimeTypePath(className.split('.')); + result = expr(expression); + if (result == null) + error(ECustom('Construction of "${className}" returned null while initializing final variable "${name}".')); + default: + result = exprWithType(expression, type); + } + } + if (expression != null) result = assertRuntimeType(result, type, 'local variable', true, name, actualTypeHint); + #else + if (expression != null) result = expr(expression); + #end locals.set(name, { r: result, - isfinal: true + isfinal: true, + t: type }); return null; @@ -2261,10 +2670,10 @@ class Interp if (abs != null && abs.hasInlineFunction(f)) { - var args = new Array(); + var args = new Array(); for (p in params) args.push(expr(p)); - - return abs.callInlineFunction(this, params[0], f, args); + var result = abs.callInlineFunction(this, params[0], f, args); + return result; } default: } @@ -2289,15 +2698,17 @@ class Interp args.push(expr(p)); } + var result:Dynamic; switch (Tools.expr(e)) { case EField(e, f): var obj = fieldTarget(e); if (obj == null) error(ENullObjectReference(f)); - return fcall(obj, f, args); + result = fcall(obj, f, args); default: - return call(null, expr(e), args); + result = call(null, expr(e), args); } + return result; case EIf(econd, e1, e2): return if (expr(econd) == true) expr(e1) else if (e2 == null) null else expr(e2); case EWhile(econd, e): @@ -2330,9 +2741,14 @@ class Interp case ECast(e, t): return expr(e); case EReturn(e): - returnValue = e == null ? null : expr(e); + var result:Dynamic = e == null ? null : expr(e); + #if POLYMOD_STRICT_SYNTAX + if (currentReturnType != null) + result = assertRuntimeType(result, currentReturnType, 'return value', false, currentFunction); + #end + returnValue = result; throw SReturn; - case EFunction(params, fexpr, name, _): + case EFunction(params, fexpr, name, ret): var capturedLocals = duplicate(this.locals); var capturedCallObject = this._nextCallObject; var capturedClassDeclOverride = this._classDeclOverride; @@ -2381,21 +2797,21 @@ class Interp var oldDepth = me.depth; var oldCallObject = me._nextCallObject; var oldClsDeclOverride = me._classDeclOverride; + #if POLYMOD_STRICT_SYNTAX + var oldReturnType = me.currentReturnType; + #end me.depth++; + #if POLYMOD_STRICT_SYNTAX + me.currentReturnType = ret; + #end me.locals = duplicate(capturedLocals); me._nextCallObject = capturedCallObject; me._classDeclOverride = capturedClassDeclOverride; name ??= capturedFunctionName; - for (i in 0...params.length) - { - me.locals.set(params[i].name, { - r: args[i] - }); - } var oldDecl = declared.length; - var r = null; + var r:Dynamic = null; inline function restoreContext() { @@ -2404,42 +2820,41 @@ class Interp me.depth = oldDepth; me._nextCallObject = oldCallObject; me._classDeclOverride = oldClsDeclOverride; + #if POLYMOD_STRICT_SYNTAX + me.currentReturnType = oldReturnType; + #end } - if (inTry) + try { - // True if the SCRIPT wraps the function in a try/catch block. - try - { - r = me.exprReturn(fexpr); - } - catch (e:Dynamic) + for (i in 0...params.length) { - restoreContext(); - #if neko - neko.Lib.rethrow(e); + #if POLYMOD_STRICT_SYNTAX + var argValue = me.assertRuntimeType(args[i], params[i].t, 'argument', false, params[i].name); + me.locals.set(params[i].name, {r: argValue, t: params[i].t}); #else - throw e; + me.locals.set(params[i].name, {r: args[i], t: params[i].t}); #end } + r = me.exprReturn(fexpr); } - else + catch (err:Dynamic) { - // There is no try/catch block. We can add some custom error handling. - try + restoreContext(); + if (inTry) { - r = me.exprReturn(fexpr); - } - catch (err:Expr.Error) - { - PolymodScriptClass.reportError(err, getClassFullyQualifiedName(), name); - r = null; + #if neko + neko.Lib.rethrow(err); + #else + throw err; + #end } - catch (err:Dynamic) + if (Std.isOfType(err, Error)) { - restoreContext(); - throw err; + PolymodScriptClass.reportError(cast err, getClassFullyQualifiedName(), name); + return null; } + throw err; } restoreContext(); @@ -2453,9 +2868,7 @@ class Interp n: name, old: locals.get(name) }); - var ref = { - r: newFun - }; + var ref = {r: newFun}; locals.set(name, ref); capturedLocals.set(name, ref); // allow self-recursion } @@ -2594,8 +3007,12 @@ class Interp default: return expr(e); } - case ECheckType(e, _): + case ECheckType(e, type): + #if POLYMOD_STRICT_SYNTAX + return exprWithType(e, type); + #else return expr(e); + #end } return null; } @@ -2607,6 +3024,11 @@ class Interp * @return The parsed expression. */ public function exprWithType(e:Expr, ?t:CType):Dynamic + { + return exprWithTypeUnchecked(e, t); + } + + function exprWithTypeUnchecked(e:Expr, ?t:CType):Dynamic { if (t == null) { @@ -3341,9 +3763,8 @@ class Interp var it = makeIterator(expr(it)); while (it.hasNext()) { - locals.set(n, { - r: it.next() - }); + var item:Dynamic = it.next(); + locals.set(n, {r: item}); if (!loopRun(bodyFn)) break; } restore(old); @@ -3364,12 +3785,8 @@ class Interp while (it.hasNext()) { var v = it.next(); - locals.set(vk, { - r: v.key - }); - locals.set(vv, { - r: v.value - }); + locals.set(vk, {r: v.key}); + locals.set(vv, {r: v.value}); if (!loopRun(bodyFn)) break; } restore(old); @@ -4249,6 +4666,8 @@ class Interp meta: e.meta, params: e.params, isPrivate: e.isPrivate, + imports: imports, + importsToValidate: importsToValidate, fields: e.fields, }; @@ -5427,6 +5846,9 @@ class Interp if (!this.variables.exists(prefixedName)) { var result = this.exprWithType(v.expr, v.type); + #if POLYMOD_STRICT_SYNTAX + result = assertRuntimeType(result, v.type, 'static field', true, prefixedName); + #end this.variables.set(prefixedName, result); return result; } @@ -5443,6 +5865,9 @@ class Interp else if (v.expr != null) { var result = this.exprWithType(v.expr, v.type); + #if POLYMOD_STRICT_SYNTAX + result = assertRuntimeType(result, v.type, 'static field', true, prefixedName); + #end this.variables.set(prefixedName, result); return result; } @@ -5488,6 +5913,9 @@ class Interp throw 'Cannot override non-dynamic function "${fieldName}"'; } case KVar(v): + #if POLYMOD_STRICT_SYNTAX + value = assertRuntimeType(value, v.type, 'static field', false, prefixedName); + #end if (v.isfinal) { throw 'Cannot override final static field ${prefixedName}'; @@ -5583,6 +6011,7 @@ typedef LocalVar = { var r:Dynamic; var ?isfinal:Bool; + var ?t:CType; } private class ArrayIterator { diff --git a/polymod/hscript/_internal/Parser.hx b/polymod/hscript/_internal/Parser.hx index 1eacee4b..b0cc8077 100644 --- a/polymod/hscript/_internal/Parser.hx +++ b/polymod/hscript/_internal/Parser.hx @@ -58,9 +58,6 @@ private enum InterpState Expr(depth:Int, ?quoteChar:Int); } -#if hscript_typer -@:access(polymod.hscript._internal.PolymodTyperEx) -#end class Parser { // config / variables @@ -1541,15 +1538,6 @@ class Parser decls.push(parseModuleDecl()); } - #if hscript_typer - PolymodTyperEx.allModules.push( - { - decls: decls, - code: content, - origin: origin, - }); - #end - return decls; } diff --git a/polymod/hscript/_internal/PolymodAbstractScriptClass.hx b/polymod/hscript/_internal/PolymodAbstractScriptClass.hx index 92c2509f..f4557f0a 100644 --- a/polymod/hscript/_internal/PolymodAbstractScriptClass.hx +++ b/polymod/hscript/_internal/PolymodAbstractScriptClass.hx @@ -69,7 +69,11 @@ abstract PolymodAbstractScriptClass(PolymodScriptClass) from PolymodScriptClass { if (v.expr != null) { + #if POLYMOD_STRICT_SYNTAX varValue = this._interp.exprWithType(v.expr, v.type); + #else + varValue = this._interp.expr(v.expr); + #end this._interp.variables.set(name, varValue); } } diff --git a/polymod/hscript/_internal/PolymodScriptClass.hx b/polymod/hscript/_internal/PolymodScriptClass.hx index 877dc0e9..d6f778f8 100644 --- a/polymod/hscript/_internal/PolymodScriptClass.hx +++ b/polymod/hscript/_internal/PolymodScriptClass.hx @@ -1189,6 +1189,7 @@ class PolymodScriptClass public function callFunction(fnName:String, ?args:Array):Null { + if (args == null) args = []; var field = findField(fnName); var fn = (field != null) ? findFunction(fnName) : null; @@ -1201,6 +1202,7 @@ class PolymodScriptClass { if(_interp.functions.exists(fnName)) { + _interp.validateScriptFunctionArguments(fn, args, fnName); r = Reflect.callMethod(this, _interp.functions.get(fnName), args); } else @@ -1597,7 +1599,11 @@ class PolymodScriptClass _cachedVarDecls.set(f.name, v); if (v.expr != null) { + #if POLYMOD_STRICT_SYNTAX var varValue = this._interp.exprWithType(v.expr, v.type); + #else + var varValue = this._interp.expr(v.expr); + #end this._interp.variables.set(f.name, varValue); } default: diff --git a/polymod/hscript/_internal/PolymodScriptClassMacro.hx b/polymod/hscript/_internal/PolymodScriptClassMacro.hx index cb32a55c..a326e65c 100644 --- a/polymod/hscript/_internal/PolymodScriptClassMacro.hx +++ b/polymod/hscript/_internal/PolymodScriptClassMacro.hx @@ -5,6 +5,7 @@ import haxe.macro.Context; import haxe.macro.Expr; import haxe.macro.Type; import haxe.macro.Type.ClassType; +import haxe.macro.Type.EnumType; import polymod.util.MacroUtil; #end @@ -56,6 +57,17 @@ class PolymodScriptClassMacro return macro polymod.hscript._internal.PolymodScriptClassMacro.fetchTypedefs(); } + public static macro function listTypedefTargets():ExprOf> + { + if (!onGenerateCallbackRegistered) + { + onGenerateCallbackRegistered = true; + haxe.macro.Context.onGenerate(onGenerate); + } + + return macro polymod.hscript._internal.PolymodScriptClassMacro.fetchTypedefTargets(); + } + /** * @return An expression containing a mapping of each package to what classes are in said package. */ @@ -121,9 +133,13 @@ class PolymodScriptClassMacro switch (type) { case TInst(t, _params): - // Parse classes to check if they are `HScriptedClass` implementations, for processing later. - var classType:ClassType = t.get(); + switch (classType.kind) + { + case KTypeParameter(_), KExpr(_): continue; + default: + } + // Parse classes to check if they are `HScriptedClass` implementations, for processing later. var classPack:String = classType.pack.join('.'); var classPath:String = t.toString(); @@ -252,32 +268,21 @@ class PolymodScriptClassMacro case TAbstract(t, _params): var targetPath:String = t.toString(); - - var entryData = [typedefPath, targetPath]; - - typedefEntries.push(entryData); + typedefEntries.push([typedefPath, targetPath]); addPackageClass(t.get().pack.join('.'), targetPath); case TEnum(t, _params): var targetEnum:EnumType = t.get(); - var targetEnumPath:String = '${targetEnum.pack.concat([targetEnum.name]).join(".")}'; - - var entryData = [typedefPath, targetEnumPath]; - - typedefEntries.push(entryData); + var targetEnumPath = targetEnum.pack.concat([targetEnum.name]).join('.'); + typedefEntries.push([typedefPath, targetEnumPath]); addPackageClass(targetEnum.pack.join('.'), targetEnumPath); case TInst(t, _params): var targetClass:ClassType = t.get(); - var targetClassPath:String = '${targetClass.pack.concat([targetClass.name]).join(".")}'; - - var entryData = [typedefPath, targetClassPath]; - - typedefEntries.push(entryData); + var targetClassPath = targetClass.pack.concat([targetClass.name]).join('.'); + typedefEntries.push([typedefPath, targetClassPath]); addPackageClass(targetClass.pack.join('.'), targetClassPath); default: - // Unknown typedef target type? - trace('TYPEDEF: ${typedefPath} -> ${typedefTarget}'); + continue; } - case TAbstract(t, _params): // Perform additional processing on abstracts classes to ensure they can be accessed at runtime. @@ -285,18 +290,11 @@ class PolymodScriptClassMacro var abstractType = t.get(); var abstractPack:String = abstractType.pack.join('.'); var abstractImpl = abstractType.impl?.get(); - - if (abstractImpl == null) - { - // If the abstract doesn't have an implementation, it's usually an extern or something, so we always want to ignore it. - continue; - } + if (abstractImpl == null) continue; var abstractImplPath:String = abstractType.impl?.toString() ?? ''; - var polymodImplPath:String = 'polymod.hscript._internal._abstract.${abstractPath}_PolymodImpl_'; var hasPolymodImpl:Bool = false; - try { Context.getType(polymodImplPath); @@ -304,14 +302,15 @@ class PolymodScriptClassMacro } catch (e) {} - var entryData = [ + abstractImplEntries.push([ abstractPath, abstractImplPath, hasPolymodImpl ? polymodImplPath : null - ]; - - abstractImplEntries.push(entryData); + ]); addPackageClass(abstractPack, abstractPath); + case TEnum(t, _params): + var enumType:EnumType = t.get(); + addPackageClass(enumType.pack.join('.'), t.toString()); default: continue; } @@ -629,6 +628,7 @@ class PolymodScriptClassMacro return _metadata; } + public static function fetchAbstractImpls():Map { var metaData = fetchMetadata(); @@ -685,6 +685,19 @@ class PolymodScriptClassMacro } } + public static function fetchTypedefTargets():Map + { + var metaData = fetchMetadata(); + var result:Map = []; + var typedefs:Array> = cast metaData.typedefs; + if (typedefs == null) return result; + for (entry in typedefs) + { + if (entry.length == 2) result.set(entry[0], entry[1]); + } + return result; + } + public static function fetchTypedefs():Map> { var metaData = fetchMetadata(); diff --git a/polymod/hscript/_internal/PolymodTyperEx.hx b/polymod/hscript/_internal/PolymodTyperEx.hx deleted file mode 100644 index 19725944..00000000 --- a/polymod/hscript/_internal/PolymodTyperEx.hx +++ /dev/null @@ -1,99 +0,0 @@ -package polymod.hscript._internal; - -#if hscript_typer -import polymod.hscript._internal.Expr; -import hscript.typer.Typer; -import hscript.typer.TypedExpr; - -class PolymodTyperEx extends Typer -{ - static var allModules:Array = []; - - var blacklistImports:Array; - var aliasPaths:Map; - var defaultImports:Map; - - public static function clearAllModules():Void - { - allModules = []; - } - - public static function typeAllModules():Array - { - return new PolymodTyperEx(new Interp(null, null)).typeModules(allModules); - } - - public function new(interp:Interp) - { - super(interp); - - blacklistImports = []; - aliasPaths = new Map(); - for (k => imp in PolymodScriptClass.importOverrides) - { - if (imp == null) blacklistImports.push(k); - else - aliasPaths.set(k, Type.getClassName(imp)); - } - for (k => imp in PolymodScriptClass.abstractClassImpls) - { - aliasPaths.set(k, Type.getClassName(imp)); - } - - defaultImports = new Map(); - for (k => imp in PolymodScriptClass.defaultImports) - { - defaultImports.set(k, CTPath([Type.getClassName(imp)], null)); - } - } - - override function resolveType(path:String):Null - { - if (blacklistImports.contains(path)) moduleError('"${path}" is a blacklisted type'); - path = aliasPaths.get(path) ?? path; - - var pathSplit:Array = path.split('.'); - if (pathSplit.length == 1) - { - var imp:Null = imports.get(path) ?? defaultImports.get(path); - if (imp != null) - { - switch (imp) - { - case CTPath(path, null): - var path:String = path.join('.'); - if (blacklistImports.contains(path)) moduleError('"${path}" is a blacklisted type'); - path = aliasPaths.get(path) ?? path; - var type:Null = null; - type ??= Type.resolveClass(path); - type ??= Type.resolveEnum(path); - type ??= scriptedTypes.get(path); - if (type != null) return type; - - default: - throw 'Should not happen: ${imp}'; - } - } - - for (pack in everythingImports) - { - var path:String = pack.concat([path]).join('.'); - if (blacklistImports.contains(path)) moduleError('"${path}" is a blacklisted type'); - path = aliasPaths.get(path) ?? path; - var type:Null = null; - type ??= Type.resolveClass(path); - type ??= Type.resolveEnum(path); - type ??= scriptedTypes.get(path); - if (type != null) return type; - } - } - - var type:Null = null; - type ??= Type.resolveClass(path); - type ??= Type.resolveEnum(path); - type ??= scriptedTypes.get(path); - - return type; - } -} -#end diff --git a/polymod/hscript/_internal/ScriptPreheater.hx b/polymod/hscript/_internal/ScriptPreheater.hx index 32b758b8..d386ce3b 100644 --- a/polymod/hscript/_internal/ScriptPreheater.hx +++ b/polymod/hscript/_internal/ScriptPreheater.hx @@ -36,8 +36,9 @@ class ScriptPreheater var fields:Map = new Map(); for (field in decl.fields) switch (field.kind) { - case KFunction(fn): + case KFunction(fn) if (fn.expr != null): collectPreheatAssignments(fn.expr, fields); + case KFunction(_): case KVar(v): if (v.expr != null) collectPreheatAssignments(v.expr, fields); } @@ -49,13 +50,16 @@ class ScriptPreheater count++; for (field in decl.staticFields) switch (field.kind) { - case KFunction(fn): + case KFunction(fn) if (fn.expr != null): try PolymodScriptClass.scriptInterp.getScriptClassStaticField(name, field.name) catch (error:Dynamic) + { issues.push( 'Script preheat error in ${name}: could not prepare static method ${field.name} (${preheatPosition(fn.expr, name)}): ${Std.string(error)}' ); + } + case KFunction(_): case KVar(_): } @@ -64,7 +68,7 @@ class ScriptPreheater var isStatic = field.access.contains(AStatic); switch (field.kind) { - case KFunction(fn): + case KFunction(fn) if (fn.expr != null): var locals:Map = new Map(); for (arg in fn.args) locals.set(arg.name, true); var stats = { @@ -72,6 +76,7 @@ class ScriptPreheater }; preheatWalk(fn.expr, decl, name, isStatic, locals, assigned, issues, stats); checked += stats.checked; + case KFunction(_): case KVar(v) if (v.expr != null): var stats = { checked: 0 @@ -86,7 +91,7 @@ class ScriptPreheater if (issues.length > 0) Polymod.error(PolymodErrorCode.SCRIPT_PREHEAT_ERROR, issues.join('\n'), PolymodErrorOrigin.SCRIPT_RUNTIME); Polymod.info( PolymodErrorCode.SCRIPT_PREHEAT_DONE, - 'Script preheat finished: ${count} classes, ${checked} identifier/member references checked, ${issues.length} error(s).', + 'Script preheat finished: ${count} classes, ${checked} identifier/member checks completed, ${issues.length} error(s).', PolymodErrorOrigin.SCRIPT_RUNTIME ); } diff --git a/polymod/hscript/_internal/Typer.hx b/polymod/hscript/_internal/Typer.hx new file mode 100644 index 00000000..54e3d0da --- /dev/null +++ b/polymod/hscript/_internal/Typer.hx @@ -0,0 +1,932 @@ +package polymod.hscript._internal; + +import polymod.Polymod; +import polymod.Polymod.PolymodErrorCode; +import polymod.Polymod.PolymodErrorOrigin; +import polymod.hscript._internal.Checker; +import polymod.hscript._internal.Expr; +import polymod.hscript._internal.Parser; +import polymod.util.Util; + +using Lambda; +using StringTools; + +@:access(polymod.hscript._internal.Interp) +@:access(polymod.hscript._internal.PolymodScriptClass) +class Typer +{ + public static function run():Bool + { + var issues:Array = []; + var checked = analyze(issues); + if (issues.length > 0) + { + Polymod.error(PolymodErrorCode.SCRIPT_TYPE_ERROR, issues.join('\n'), PolymodErrorOrigin.SCRIPT_RUNTIME); + return false; + } + + Polymod.info( + PolymodErrorCode.SCRIPT_TYPE_CHECK_DONE, + 'Script type checking finished: ${checked} declaration/body checks completed with no errors.', + PolymodErrorOrigin.SCRIPT_RUNTIME + ); + return true; + } + + public static function runWithIssues(issues:Array):Int + { + return analyze(issues); + } + + static function analyze(issues:Array):Int + { + var types = new CheckerTypes(); + var checked = 0; + + types.loadRuntimeAliases(); + + // Register every scripted type before filling any signatures. This allows forward references and mutually-recursive classes to be checked + for (name => decl in Interp._scriptClassDescriptors) + types.defineClass(name); + + for (name => decl in Interp._scriptInterfaceDescriptors) + { + var interfaceType = types.defineClass(name); + interfaceType.isInterface = true; + } + + for (name => decl in Interp._scriptEnumDescriptors) + types.defineEnum(name); + + registerScriptEnumSignatures(types, issues); + registerScriptClassSignatures(types, issues); + validateOverrideSignatures(types, issues); + + var methodBodies:Array<{owner:String, decl:ClassDecl, field:FieldDecl}> = []; + for (name => decl in Interp._scriptClassDescriptors) + { + configureAliases(types, decl); + var cls = getClass(types, name); + if (cls == null) continue; + + for (field in decl.fields.concat(decl.staticFields)) + { + switch (field.kind) + { + case KFunction(fn) if (fn.expr != null): + methodBodies.push({owner: name, decl: decl, field: field}); + case KFunction(_): + case KVar(v) if (v.expr != null): + var checker = new Checker(types); + checker.currentClassName = name; + configureCheckerGlobals(checker, types, decl, cls, field.access.contains(AStatic)); + var target = findMemberType(cls, field.name, field.access.contains(AStatic)); + if (target != null) + { + try + { + checker.checkExpected(v.expr, target.t); + if (v.type == null) + { + var inferredType = checker.follow(target.t); + v.type = typeToSourceType(inferredType); + } + checked++; + } + catch (error:Dynamic) + { + issues.push(formatIssue(name, field.name, v.expr, 'initializer type error: ${Std.string(error)}')); + } + } + case KVar(_): + } + } + } + + for (pass in 0...2) + { + for (entry in methodBodies) + { + configureAliases(types, entry.decl); + var cls = getClass(types, entry.owner); + if (cls == null) continue; + var isStatic = entry.field.access.contains(AStatic); + var fn = switch (entry.field.kind) + { + case KFunction(value): value; + default: null; + }; + if (fn == null || fn.expr == null) continue; + + var member = !isStatic && entry.field.name == 'new' ? cls.constructor : findMemberType(cls, entry.field.name, isStatic); + if (member == null) continue; + + var checker = new Checker(types); + checker.currentClassName = entry.owner; + checker.allowAsync = true; + configureCheckerGlobals(checker, types, entry.decl, cls, isStatic); + try + { + checker.checkFunction(fn.args, fn.expr, entry.field.name, fn.ret, member.t); + if (pass == 1) + { + switch (checker.follow(member.t)) + { + case TFun(signatureArgs, signatureReturn): + for (i in 0...fn.args.length) + { + if (fn.args[i].t == null && i < signatureArgs.length) + fn.args[i].t = typeToSourceType(checker.follow(signatureArgs[i].t)); + } + if (fn.ret == null) fn.ret = typeToSourceType(checker.follow(signatureReturn)); + default: + } + } + checked++; + } + catch (error:Dynamic) + { + if (pass == 1) + issues.push(formatIssue(entry.owner, entry.field.name, fn.expr, 'function type error: ${Std.string(error)}')); + } + } + } + + return checked; + } + + static function validateOverrideSignatures(types:CheckerTypes, issues:Array):Void + { + for (name => decl in Interp._scriptClassDescriptors) + { + configureAliases(types, decl); + var cls = getClass(types, name); + if (cls == null) continue; + + for (field in decl.fields) + { + switch (field.kind) + { + case KFunction(fn): + var inherited = findInheritedMemberType(types, cls, field.name, false); + if (inherited == null) continue; + + var own = cls.fields.get(field.name); + if (own == null) continue; + + var anchor = fn.expr == null ? classAnchor(decl) : fn.expr; + var problem = compareOverrideFunctionTypes(types, own.t, inherited.t); + if (problem != null) + issues.push(formatIssue(name, field.name, anchor, 'override signature mismatch: ${problem}')); + case KVar(_): + } + } + } + } + + static function findInheritedMemberType(types:CheckerTypes, cls:CClass, name:String, isStatic:Bool):Null + { + var checker = new Checker(types); + var currentType = cls.superClass; + var depth = 0; + + while (currentType != null && depth++ < 64) + { + var followed = checker.follow(currentType); + switch (followed) + { + case TInst(parent, parentArgs): + var fields = isStatic ? parent.statics : parent.fields; + var member = fields.get(name); + if (member != null && member.isPublic) + { + var instantiatedType = member.t; + if (parent.params != null && parent.params.length == parentArgs.length && parent.params.length > 0) + instantiatedType = checker.apply(instantiatedType, parent.params, parentArgs); + return { + name: member.name, + isPublic: member.isPublic, + canWrite: member.canWrite, + complete: member.complete, + params: member.params == null ? [] : member.params, + t: instantiatedType, + meta: member.meta, + }; + } + currentType = parent.superClass; + case TAbstract(abs, args): + var member = (isStatic ? abs.statics : abs.fields).get(name); + if (member != null) return member; + currentType = null; + default: + currentType = null; + } + } + + return null; + } + + static function compareOverrideFunctionTypes(types:CheckerTypes, actualType:TType, inheritedType:TType):Null + { + var checker = new Checker(types); + var actual = checker.follow(actualType); + var inherited = checker.follow(inheritedType); + + return switch ([actual, inherited]) + { + case [TFun(actualArgs, actualReturn), TFun(inheritedArgs, inheritedReturn)]: + if (actualArgs.length != inheritedArgs.length) + 'expected ${inheritedArgs.length} argument(s), got ${actualArgs.length}'; + else + { + var problem:Null = null; + for (i in 0...actualArgs.length) + { + var actualArg = actualArgs[i]; + var inheritedArg = inheritedArgs[i]; + if (actualArg.opt != inheritedArg.opt) + { + problem = 'argument ${i + 1} optionality differs (expected ${inheritedArg.opt ? "optional" : "required"}, got ${actualArg.opt ? "optional" : "required"})'; + break; + } + if (!overrideTypesCompatible(types, actualArg.t, inheritedArg.t) + || !overrideTypesCompatible(types, inheritedArg.t, actualArg.t)) + { + problem = 'argument "${inheritedArg.name}" has type ${Checker.typeStr(actualArg.t)}, inherited type is ${Checker.typeStr(inheritedArg.t)}'; + break; + } + } + + if (problem != null) problem; + else if (!overrideTypesCompatible(types, actualReturn, inheritedReturn)) + 'return type ${Checker.typeStr(actualReturn)} is incompatible with inherited return type ${Checker.typeStr(inheritedReturn)}'; + else null; + } + case [_, TFun(_, _)]: + 'the overriding member is not a function'; + case [TFun(_, _), _]: + null; + default: + null; + } + } + + static function overrideTypesCompatible(types:CheckerTypes, actual:TType, expected:TType):Bool + { + var checker = new Checker(types); + actual = checker.follow(actual); + expected = checker.follow(expected); + if (isUnknownOverrideType(actual) || isUnknownOverrideType(expected)) return true; + return checker.tryUnify(copyOverrideType(actual), copyOverrideType(expected)); + } + + static function isUnknownOverrideType(type:TType):Bool + { + return switch (type) + { + case TDynamic, TUnresolved(_): true; + case TMono(ref): ref.r == null; + default: false; + } + } + + static function copyOverrideType(type:TType, depth:Int = 0):TType + { + if (depth > 32) return TDynamic; + return switch (type) + { + case TMono(ref): + TMono({ + r: ref.r == null ? null : copyOverrideType(ref.r, depth + 1) + }); + case TNull(inner): + TNull(copyOverrideType(inner, depth + 1)); + case TInst(cls, args): + TInst(cls, [for (arg in args) copyOverrideType(arg, depth + 1)]); + case TEnum(en, args): + TEnum(en, [for (arg in args) copyOverrideType(arg, depth + 1)]); + case TType(td, args): + TType(td, [for (arg in args) copyOverrideType(arg, depth + 1)]); + case TAbstract(abs, args): + TAbstract(abs, [for (arg in args) copyOverrideType(arg, depth + 1)]); + case TFun(args, ret): + TFun([for (arg in args) { + name: arg.name, + opt: arg.opt, + t: copyOverrideType(arg.t, depth + 1) + }], copyOverrideType(ret, depth + 1)); + case TAnon(fields): + TAnon([for (field in fields) { + name: field.name, + opt: field.opt, + t: copyOverrideType(field.t, depth + 1) + }]); + case TLazy(resolve): + copyOverrideType(resolve(), depth + 1); + default: + type; + } + } + + static function registerScriptEnumSignatures(types:CheckerTypes, issues:Array):Void + { + for (name => decl in Interp._scriptEnumDescriptors) + { + configureEnumAliases(types, decl); + var enumType = switch (types.resolve(name)) + { + case TEnum(value, _): + value; + default: + null; + }; + if (enumType == null) continue; + + for (field in decl.fields) + { + var args = []; + for (arg in field.args) + { + var argType:TType = TDynamic; + if (arg.type != null) + { + var resolver = new Checker(types); + try + { + argType = resolver.resolveTypeAnnotation(arg.type, enumAnchor(decl)); + } + catch (error:Dynamic) + { + issues.push(formatIssue(name, field.name, enumAnchor(decl), 'enum argument type error: ${Std.string(error)}')); + } + } + args.push({name: arg.name, opt: false, t: argType}); + } + enumType.constructors.push({name: field.name, args: args.length == 0 ? null : args}); + } + } + } + + static function registerScriptClassSignatures(types:CheckerTypes, issues:Array):Void + { + for (name => decl in Interp._scriptClassDescriptors) + { + configureAliases(types, decl); + var resolver = new Checker(types); + resolver.currentClassName = name; + var cls = getClass(types, name); + if (cls == null) continue; + + if (decl.extend != null) + { + try + { + cls.superClass = resolver.resolveTypeAnnotation(decl.extend, classAnchor(decl)); + } + catch (error:Dynamic) + { + issues.push(formatIssue(name, '', classAnchor(decl), 'superclass type error: ${Std.string(error)}')); + } + } + + cls.interfaces = []; + for (interfaceType in decl.implement) + { + try + { + cls.interfaces.push(resolver.resolveTypeAnnotation(interfaceType, classAnchor(decl))); + } + catch (error:Dynamic) + { + issues.push(formatIssue(name, '', classAnchor(decl), 'interface type error: ${Std.string(error)}')); + } + } + + registerFields(types, resolver, cls, decl.fields, false, name, issues); + registerFields(types, resolver, cls, decl.staticFields, true, name, issues); + } + + for (name => decl in Interp._scriptInterfaceDescriptors) + { + configureAliasesForInterface(types, decl); + var resolver = new Checker(types); + var cls = getClass(types, name); + if (cls == null) continue; + cls.isInterface = true; + cls.interfaces = []; + for (parent in decl.extend) + { + try + { + cls.interfaces.push(resolver.resolveTypeAnnotation(parent, classAnchorFromFields(decl.fields))); + } + catch (error:Dynamic) + { + issues.push(formatIssue(name, '', classAnchorFromFields(decl.fields), 'interface parent type error: ${Std.string(error)}')); + } + } + registerFields(types, resolver, cls, decl.fields, false, name, issues); + } + } + + static function registerFields(types:CheckerTypes, resolver:Checker, cls:CClass, fields:Array, isStatic:Bool, + owner:String, issues:Array):Void + { + var target = isStatic ? cls.statics : cls.fields; + for (field in fields) + { + var declaredPublic = field.access.contains(APublic) || !field.access.contains(APrivate); + switch (field.kind) + { + case KFunction(fn): + var args:Array<{name:String, opt:Bool, t:TType}> = []; + var fnAnchor = fn.expr == null ? classAnchorFromFields(fields) : fn.expr; + for (arg in fn.args) + { + var argType:TType = TMono({r: null}); + if (arg.t != null) + { + try + { + argType = resolver.resolveTypeAnnotation(arg.t, fnAnchor); + } + catch (error:Dynamic) + { + issues.push(formatIssue(owner, field.name, fnAnchor, 'argument "${arg.name}" type error: ${Std.string(error)}')); + } + } + args.push({name: arg.name, opt: arg.opt == true || arg.value != null, t: argType}); + } + + var returnType:TType = TMono({r: null}); + if (fn.ret != null) + { + try + { + returnType = resolver.resolveTypeAnnotation(fn.ret, fnAnchor); + } + catch (error:Dynamic) + { + issues.push(formatIssue(owner, field.name, fnAnchor, 'return type error: ${Std.string(error)}')); + } + } + + var functionField:CField = { + name: field.name, + isPublic: declaredPublic, + canWrite: false, + complete: true, + params: [], + t: TFun(args, returnType), + }; + if (!isStatic && field.name == 'new') cls.constructor = functionField; + else target.set(field.name, functionField); + case KVar(v): + var fieldType:TType = TMono({r: null}); + if (v.type != null) + { + try + { + fieldType = resolver.resolveTypeAnnotation(v.type, v.expr == null ? classAnchorFromFields(fields) : v.expr); + } + catch (error:Dynamic) + { + issues.push(formatIssue(owner, field.name, v.expr == null ? classAnchorFromFields(fields) : v.expr, + 'field type error: ${Std.string(error)}')); + } + } + target.set(field.name, { + name: field.name, + isPublic: declaredPublic, + canWrite: v.isfinal != true && v.set != 'never', + complete: true, + params: [], + t: fieldType, + }); + } + } + } + + static function typeToSourceType(type:TType, depth:Int = 0):CType + { + if (depth > 24) return CTPath(['Dynamic']); + return switch (type) + { + case TMono(ref): + ref.r == null ? CTPath(['Dynamic']) : typeToSourceType(ref.r, depth + 1); + case TVoid: + CTPath(['Void']); + case TInt: + CTPath(['Int']); + case TFloat: + CTPath(['Float']); + case TBool: + CTPath(['Bool']); + case TDynamic, TParam(_), TUnresolved(_): + CTPath(['Dynamic']); + case TNull(inner): + CTPath(['Null'], [typeToSourceType(inner, depth + 1)]); + case TInst(cls, args): + CTPath(cls.name.split('.'), [for (arg in args) typeToSourceType(arg, depth + 1)]); + case TEnum(en, args): + CTPath(en.name.split('.'), [for (arg in args) typeToSourceType(arg, depth + 1)]); + case TAbstract(abs, args): + CTPath(abs.name.split('.'), [for (arg in args) typeToSourceType(arg, depth + 1)]); + case TType(td, args): + CTPath(td.name.split('.'), [for (arg in args) typeToSourceType(arg, depth + 1)]); + case TFun(args, ret): + CTFun([for (arg in args) arg.opt ? CTOpt(typeToSourceType(arg.t, depth + 1)) : typeToSourceType(arg.t, depth + 1)], typeToSourceType(ret, depth + 1)); + case TAnon(fields): + CTAnon([for (field in fields) { + name: field.name, + t: field.opt ? CTOpt(typeToSourceType(field.t, depth + 1)) : typeToSourceType(field.t, depth + 1) + }]); + case TLazy(resolve): + typeToSourceType(resolve(), depth + 1); + } + } + + + static function configureCheckerGlobals(checker:Checker, types:CheckerTypes, owner:ClassDecl, cls:CClass, isStatic:Bool):Void + { + addInterpreterGlobals(checker, types); + + if (!isStatic) + { + checker.setGlobal('this', TInst(cls, [])); + checker.setGlobals(cls, [], true); + if (cls.superClass != null) checker.setGlobal('super', cls.superClass); + for (field in cls.statics) if ( + field.isPublic + || checker.currentClassName == cls.name + ) checker.setGlobal(field.name, instantiatedFieldType(types, field)); + } + else + { + for (field in cls.statics) if ( + field.isPublic + || checker.currentClassName == cls.name + ) checker.setGlobal(field.name, instantiatedFieldType(types, field)); + } + + if (owner.imports != null) for (name in owner.imports.keys()) + { + var imp = owner.imports.get(name); + if (imp == null || imp.wildcard == true) continue; + if (imp.field == true) + { + var fieldName = imp.fullPath.split('.').pop(); + var typePath = imp.fullPath.substr(0, imp.fullPath.length - fieldName.length - 1); + var staticField = findStaticField(types, typePath, fieldName); + if (staticField != null) checker.setGlobal(name, instantiatedFieldType(types, staticField)); + } + else + { + var symbolType = classSymbolType(types, imp.fullPath); + if (symbolType != null) checker.setGlobal(name, symbolType); + } + } + + if (owner.importsToValidate != null) for (name in owner.importsToValidate.keys()) + { + var imp = owner.importsToValidate.get(name); + if (imp == null || imp.wildcard == true || imp.field == true) continue; + var symbolType = classSymbolType(types, imp.fullPath); + if (symbolType != null) checker.setGlobal(name, symbolType); + } + + var packageName = owner.pkg == null ? '' : owner.pkg.join('.'); + if (packageName.length > 0) + { + for (name in Interp._scriptClassDescriptors.keys()) + { + if (!isDirectChild(packageName, name)) continue; + var typeAlias = name.substr(packageName.length + 1); + var symbolType = classSymbolType(types, name); + if (symbolType != null) checker.setGlobal(typeAlias, symbolType); + } + for (name in Interp._scriptEnumDescriptors.keys()) + { + if (!isDirectChild(packageName, name)) continue; + var typeAlias = name.substr(packageName.length + 1); + var symbolType = classSymbolType(types, name); + if (symbolType != null) checker.setGlobal(typeAlias, symbolType); + } + } + + for (name => clsRef in PolymodScriptClass.defaultImports) + { + var path = Type.getClassName(clsRef); + if (path == null) continue; + var symbolType = classSymbolType(types, path); + if (symbolType != null) + { + checker.setGlobal(name.split('.').pop(), symbolType); + checker.setGlobal(name, symbolType); + } + } + + checker.setGlobal(owner.name, classSymbolType(types, Util.getFullClassName(owner)) ?? TDynamic); + checker.setGlobal(Util.getFullClassName(owner), classSymbolType(types, Util.getFullClassName(owner)) ?? TDynamic); + } + + static function addInterpreterGlobals(checker:Checker, types:CheckerTypes):Void + { + if (PolymodScriptClass.scriptInterp.variables != null) + { + for (name in PolymodScriptClass.scriptInterp.variables.keys()) + { + var value = PolymodScriptClass.scriptInterp.variables.get(name); + var symbolType = types.hasType(name) ? classSymbolType(types, name) : null; + checker.setGlobal(name, symbolType == null ? runtimeValueType(types, value, 0) : symbolType); + } + } + if (PolymodScriptClass.scriptInterp.functions != null) + { + for (name in PolymodScriptClass.scriptInterp.functions.keys()) checker.setGlobal(name, TDynamic); + } + } + + static function runtimeValueType(types:CheckerTypes, value:Dynamic, depth:Int):TType + { + if (value == null || depth > 4) return TDynamic; + if (Std.isOfType(value, String)) return types.resolve('String') ?? TDynamic; + + return switch (Type.typeof(value)) + { + case TInt: + TInt; + case TFloat: + TFloat; + case TBool: + TBool; + case TObject, TClass(_): + if (Std.isOfType(value, Array)) + { + types.resolve('Array', [TDynamic]) ?? TDynamic; + } + else + { + var runtimeClass = Type.getClass(value); + var path = runtimeClass == null ? null : Type.getClassName(runtimeClass); + path == null ? TDynamic : types.resolve(path) ?? TDynamic; + } + case TEnum(_), TFunction, TNull, TUnknown: + TDynamic; + } + } + + static function classSymbolType(types:CheckerTypes, path:String):Null + { + if (path == null) return null; + return switch (types.resolve(path)) + { + case TInst(cls, _): + staticMembersType(types, cls.statics); + case TAbstract(abs, _): + staticMembersType(types, abs.statics); + case TEnum(en, args): + enumSymbolType(types, en, args); + case TType(td, args): + switch (new Checker(types).follow(TType(td, args))) + { + case TInst(cls, _): + staticMembersType(types, cls.statics); + case TAbstract(abs, _): + staticMembersType(types, abs.statics); + case TEnum(en, args): + enumSymbolType(types, en, args); + default: + null; + } + default: + null; + } + } + + static function staticMembersType(types:CheckerTypes, fields:Map):TType + { + return TAnon([for (field in fields) if (field.isPublic) { + name: field.name, + opt: false, + t: instantiatedFieldType(types, field) + }]); + } + + static function enumSymbolType(types:CheckerTypes, en:CEnum, args:Array):TType + { + var enumType:TType = TEnum(en, args); + var checker = new Checker(types); + return TAnon([for (constructor in en.constructors) { + name: constructor.name, + opt: false, + t: constructor.args == null ? enumType : TFun([for (arg in constructor.args) { + name: arg.name, + opt: arg.opt, + t: checker.apply(arg.t, en.params, args) + }], enumType), + }]); + } + + static function findStaticField(types:CheckerTypes, path:String, name:String):Null + { + var resolved = types.resolve(path); + if (resolved == null) return null; + return switch (new Checker(types).follow(resolved)) + { + case TInst(cls, _): + findStaticMember(types, cls.statics, name); + case TAbstract(abs, _): + findStaticMember(types, abs.statics, name); + case TEnum(en, args): + var constructor = null; + for (item in en.constructors) if (item.name == name) + { + constructor = item; + break; + } + if (constructor == null) null else { + name: name, + isPublic: true, + canWrite: false, + complete: true, + params: [], + t: constructor.args == null ? TEnum(en, args) : TFun([for (arg in constructor.args) { + name: arg.name, + opt: arg.opt, + t: new Checker(types).apply(arg.t, en.params, args) + }], TEnum(en, args)), + }; + default: + null; + } + } + + static function findStaticMember(types:CheckerTypes, fields:Map, name:String):Null + { + var field = fields.get(name); + return field == null ? null : copyStaticField(types, field); + } + + static function copyStaticField(types:CheckerTypes, field:CField):CField + { + return { + name: field.name, + isPublic: field.isPublic, + canWrite: field.canWrite, + complete: field.complete, + params: [], + t: instantiatedFieldType(types, field), + }; + } + + static function instantiatedFieldType(types:CheckerTypes, field:CField):TType + { + if (field.params == null || field.params.length == 0) return field.t; + return new Checker(types).apply(field.t, field.params, [for (_ in field.params) TMono({ + r: null + })]); + } + + static function findMemberType(cls:CClass, name:String, isStatic:Bool):Null + { + var current:Null = cls; + var depth = 0; + while (current != null && depth++ < 64) + { + var member = (isStatic ? current.statics : current.fields).get(name); + if (member != null) return member; + if (isStatic || current.superClass == null) break; + current = switch (current.superClass) + { + case TInst(parent, _): + parent; + case TType(td, _): + switch (td.t) + { + case TInst(parent, _): + parent; + default: + null; + } + default: + null; + } + } + return null; + } + + static function configureAliases(types:CheckerTypes, owner:ClassDecl):Void + { + configureAliasesCommon(types, owner.pkg, owner.imports, owner.importsToValidate, true); + } + + static function configureAliasesForInterface(types:CheckerTypes, owner:InterfaceDecl):Void + { + configureAliasesCommon(types, owner.pkg, owner.imports, owner.importsToValidate, false); + } + + static function configureEnumAliases(types:CheckerTypes, owner:EnumDecl):Void + { + configureAliasesCommon(types, owner.pkg, owner.imports, owner.importsToValidate, false); + } + + static function configureAliasesCommon(types:CheckerTypes, + packagePath:Null>, + imports:Null>, + importsToValidate:Null>, + includeQualifiedDefaults:Bool):Void + { + types.clearAliases(); + var packageName = packagePath == null ? '' : packagePath.join('.'); + if (packageName.length > 0) + { + for (name in Interp._scriptClassDescriptors.keys()) if (isDirectChild(packageName, name)) types.setAlias(name.substr(packageName.length + 1), name); + for (name in Interp._scriptInterfaceDescriptors.keys()) if (isDirectChild(packageName, name)) types.setAlias(name.substr(packageName.length + 1), name); + for (name in Interp._scriptEnumDescriptors.keys()) if (isDirectChild(packageName, name)) types.setAlias(name.substr(packageName.length + 1), name); + } + + for (name => clsRef in PolymodScriptClass.defaultImports) + { + var path = Type.getClassName(clsRef); + if (path == null) continue; + if (includeQualifiedDefaults) types.setAlias(name, path); + types.setAlias(name.split('.').pop(), path); + } + + if (imports != null) for (name => imp in imports) if (imp != null && imp.field != true && imp.wildcard != true) types.setAlias(name, imp.fullPath); + if (importsToValidate != null) for (name => imp in importsToValidate) if ( + imp != null + && imp.field != true + && imp.wildcard != true + ) types.setAlias(name, imp.fullPath); + // Resolve wildcard imports against the portable metadata index. + if (imports != null) for (imp in imports) if (imp != null && imp.wildcard == true) setWildcardAliases(types, imp.fullPath); + } + + static function setWildcardAliases(types:CheckerTypes, packagePath:String):Void + { + if (packagePath == null || packagePath.length == 0) return; + var packages = PolymodScriptClassMacro.fetchPackagesList(); + var names = packages.get(packagePath); + if (names == null) return; + for (name in names) + { + var prefix = packagePath + '.'; + if (!name.startsWith(prefix)) continue; + var suffix = name.substr(prefix.length); + if (suffix.indexOf('.') == -1) types.setAlias(suffix, name); + } + } + + static function isDirectChild(packageName:String, className:String):Bool + { + var prefix = packageName + '.'; + if (!className.startsWith(prefix)) return false; + return className.substr(prefix.length).indexOf('.') == -1; + } + + static function getClass(types:CheckerTypes, name:String):Null + { + return switch (types.resolve(name)) + { + case TInst(cls, _): + cls; + default: + null; + } + } + + static function classAnchor(decl:ClassDecl):Expr + { + return classAnchorFromFields(decl.fields.concat(decl.staticFields)); + } + + static function classAnchorFromFields(fields:Array):Expr + { + for (field in fields) switch (field.kind) + { + case KFunction(fn) if (fn.expr != null): + return fn.expr; + case KVar(v) if (v.expr != null): + return v.expr; + default: + } + return new Parser().parseString('null', 'script typer'); + } + + static function enumAnchor(decl:EnumDecl):Expr + { + return new Parser().parseString('null', 'script typer (${decl.name})'); + } + + static function formatIssue(owner:String, field:String, expr:Expr, message:String):String + { + #if hscriptPos + return 'Script type error in ${owner}.${field} (${expr.origin}:${expr.line}): ${message}'; + #else + return 'Script type error in ${owner}.${field}: ${message}'; + #end + } + +} diff --git a/polymod/util/Util.hx b/polymod/util/Util.hx index f9cdf702..85b341a3 100644 --- a/polymod/util/Util.hx +++ b/polymod/util/Util.hx @@ -865,6 +865,8 @@ class Util */ public static function getFullEnumClass(enumDecl:EnumDecl):String { - return enumDecl.pkg != null ? '${enumDecl.pkg.join('.')}.${enumDecl}' : enumDecl.name; + if (enumDecl.pkg != null && enumDecl.pkg.length > 0) + return enumDecl.pkg.join('.') + '.' + enumDecl.name; + return enumDecl.name; } }