Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9ddbb08
Optimize Lua type assurance boundaries
Frotty Sep 2, 2026
d58a6d2
Preserve generic defaults and boolean values
Frotty Sep 2, 2026
2cf295e
Fix Lua boolean and generic default normalization
Frotty Sep 2, 2026
ea8b239
Normalize generic values used as Lua indices
Frotty Sep 2, 2026
caf7a4e
Normalize erased generic range bounds
Frotty Sep 2, 2026
dcf8da5
Normalize erased generic switch values
Frotty Sep 2, 2026
f09ee16
Preserve stacktraces in Lua assurance calls
Frotty Sep 2, 2026
3eb9de4
Normalize erased generic closure results
Frotty Sep 2, 2026
c99af94
Normalize erased generic statement block results
Frotty Sep 2, 2026
b94d7e6
Propagate range types into expression children
Frotty Sep 2, 2026
e101e63
Use selected overload type for erased arguments
Frotty Sep 2, 2026
5920bf9
Fix erased generic propagation in composite Lua expressions
Frotty Sep 2, 2026
5f21848
Cover unary erased generic closure results
Frotty Sep 2, 2026
24246be
Propagate selected types through composite Lua arguments
Frotty Sep 2, 2026
5c72fae
Propagate concrete types through Lua composite operands
Frotty Sep 2, 2026
510b152
Fix erased operands in Lua composite expressions
Frotty Sep 2, 2026
72489bc
Propagate selected types into statement blocks
Frotty Sep 2, 2026
dc1b299
Preserve normalization for primitive array reads
Frotty Sep 2, 2026
5afe445
Propagate closure types through statement blocks
Frotty Sep 2, 2026
1d54bb4
Finish Lua type assurance boundaries
Frotty Sep 2, 2026
6c7257c
Merge remote-tracking branch 'origin/master' into codex/lua-type-assu…
Frotty Sep 2, 2026
37453da
Handle varargs in constructor assurance
Frotty Sep 2, 2026
053c8e0
Unify constructor vararg assurance
Frotty Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ private ImClass createClass() {


ImExpr translated = e.getImplementation().imTranslateExpr(tr, impl);
translated = ExprTranslation.wrapTranslation(e.getImplementation(), tr, translated,
e.getImplementation().attrTypRaw(), superMethod.attrReturnType());
Comment thread
Frotty marked this conversation as resolved.
Outdated


if (e.getImplementation().attrTyp().isVoid()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@ private static ImExpr wrapTranslation(Expr e, ImTranslator t, ImExpr translated)
}

static ImExpr wrapLua(Element trace, ImTranslator t, ImExpr translated, WurstType actualType) {
// use ensureType functions for lua
// these functions convert nil to the default value for primitive types (int, string, bool, real)
// Erased generic values are the one kind of Wurst value which can lose
// its primitive default when represented in Lua. Keep the
// normalization available to callers which explicitly cross an
// external boundary; ordinary Wurst expressions must not pay for it.
if (t.isLuaTarget() && actualType instanceof WurstTypeBoundTypeParam) {
WurstTypeBoundTypeParam wtb = (WurstTypeBoundTypeParam) actualType;

Expand All @@ -125,13 +127,32 @@ static ImExpr wrapLua(Element trace, ImTranslator t, ImExpr translated, WurstTyp
break;
}
if(ensureType != null) {
// Lua already has the exact cheap operation needed for the
// boolean case. Equality with true preserves false while
// mapping nil (and other non-true values) to false.
if (ensureType == t.ensureBoolFunc) {
return ImOperatorCall(WurstOperator.EQ, ImExprs(
translated, ImBoolVal(true)));
}
return ImFunctionCall(trace, ensureType, ImTypeArguments(), JassIm.ImExprs(translated), false, CallType.NORMAL);
}
}
return translated;
}

static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated, WurstType actualType, WurstType expectedTypRaw) {
static ImExpr wrapTranslation(Expr e, ImTranslator t, ImExpr translated, WurstType actualType, WurstType expectedTypRaw) {
return wrapTranslation(e, t, translated, actualType, expectedTypRaw,
e.getParent() instanceof Indexes);
Comment thread
Frotty marked this conversation as resolved.
}

static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated,
WurstType actualType, WurstType expectedTypRaw) {
return wrapTranslation(trace, t, translated, actualType, expectedTypRaw, false);
}

private static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated,
WurstType actualType, WurstType expectedTypRaw,
boolean indexContext) {
ImFunction toIndex = null;
ImFunction fromIndex = null;
if (actualType instanceof WurstTypeBoundTypeParam) {
Expand Down Expand Up @@ -168,7 +189,17 @@ static ImExpr wrapTranslation(Element trace, ImTranslator t, ImExpr translated,
// System.out.println(" --> toIndex");
return wrapLua(trace, t, ImFunctionCall(trace, toIndex, ImTypeArguments(), JassIm.ImExprs(translated), false, CallType.NORMAL), actualType);
}
return wrapLua(trace, t, translated, actualType);
// Preserve Wurst's primitive defaults when an erased generic value is
// consumed by a concrete primitive expression. Generic-to-generic
// propagation remains raw and is normalized only at its eventual
// concrete/native boundary.
if (actualType instanceof WurstTypeBoundTypeParam
&& !(expectedTypRaw instanceof WurstTypeBoundTypeParam)
&& !(expectedTypRaw instanceof WurstTypeTypeParam)
&& (isPrimitiveType(expectedTypRaw) || indexContext)) {
Comment thread
Frotty marked this conversation as resolved.
Comment thread
Frotty marked this conversation as resolved.
Comment thread
Frotty marked this conversation as resolved.
return wrapLua(trace, t, translated, actualType);
Comment thread
Frotty marked this conversation as resolved.
Comment thread
Frotty marked this conversation as resolved.
Comment thread
Frotty marked this conversation as resolved.
Comment thread
Frotty marked this conversation as resolved.
Comment thread
Frotty marked this conversation as resolved.
Comment thread
Frotty marked this conversation as resolved.
}
return translated;
}

public static ImExpr translateIntern(ExprBinary e, ImTranslator t, ImFunction f) {
Expand Down Expand Up @@ -659,8 +690,14 @@ && isCalledOnDynamicRef(e)
+ " -> dynamicDispatch=" + dynamicDispatch);
}

ImFunction directFunc = null;
if (!dynamicDispatch && !(calledFunc instanceof TupleDef)) {
directFunc = t.getFuncFor(calledFunc);
}

ImExpr receiver = leftExpr == null ? null : leftExpr.imTranslateExpr(t, f);
ImExprs imArgs = translateExprs(arguments, t, f);
boolean normalizeAtBoundary = directFunc != null && isLuaExternalBoundary(directFunc);
ImExprs imArgs = translateExprs(arguments, t, f, normalizeAtBoundary);

if (calledFunc instanceof TupleDef) {
// creating a new tuple...
Expand All @@ -686,7 +723,7 @@ && isCalledOnDynamicRef(e)
t, e.attrFunctionSignature(), e, method.getImplementation().getTypeVariables());
call = ImMethodCall(e, method, typeArguments, receiver, imArgs, false);
} else {
ImFunction calledImFunc = t.getFuncFor(calledFunc);
ImFunction calledImFunc = directFunc;
if (receiver != null) {
imArgs.add(0, receiver);
}
Expand Down Expand Up @@ -784,13 +821,65 @@ private static boolean isCalledOnDynamicRef(FunctionCall e) {
}

private static ImExprs translateExprs(List<Expr> arguments, ImTranslator t, ImFunction f) {
return translateExprs(arguments, t, f, false);
}

private static ImExprs translateExprs(List<Expr> arguments, ImTranslator t, ImFunction f,
boolean externalBoundary) {
ImExprs result = ImExprs();
for (Expr e : arguments) {
result.add(e.imTranslateExpr(t, f));
ImExpr translated = e.imTranslateExpr(t, f);
if (externalBoundary) {
translated = wrapLuaAtExternalBoundary(e, t, translated);
}
result.add(translated);
}
return result;
}

private static boolean isLuaExternalBoundary(ImFunction function) {
return function.isNative() || function.isBj() || function.isExtern();
}

private static ImExpr wrapLuaAtExternalBoundary(Expr source, ImTranslator t, ImExpr translated) {
WurstType actualType = source.attrTypRaw();
// Ordinary Wurst locals and literals already have their normal Lua
// representation. Only values which can lose their primitive default
// in Lua need normalization: raw array reads crossing into untyped
// code. Erased generic values are normalized by wrapTranslation when
// a concrete primitive context consumes them.
if (!(translated instanceof ImVarArrayAccess)) {
return translated;
}
WurstType normalized = actualType.normalize();
ImFunction ensureType = null;
if (normalized instanceof WurstTypeInt) {
ensureType = t.ensureIntFunc;
} else if (normalized instanceof WurstTypeBool) {
ensureType = t.ensureBoolFunc;
} else if (normalized instanceof WurstTypeReal) {
ensureType = t.ensureRealFunc;
} else if (normalized instanceof WurstTypeString) {
ensureType = t.ensureStrFunc;
}
if (ensureType == null) {
return translated;
}
if (ensureType == t.ensureBoolFunc) {
return ImOperatorCall(WurstOperator.EQ, ImExprs(
translated, ImBoolVal(true)));
}
return ImFunctionCall(source, ensureType, ImTypeArguments(), ImExprs(translated), false, CallType.NORMAL);
}

private static boolean isPrimitiveType(WurstType type) {
WurstType normalized = type.normalize();
return normalized instanceof WurstTypeInt
|| normalized instanceof WurstTypeBool
|| normalized instanceof WurstTypeReal
|| normalized instanceof WurstTypeString;
}

public static ImExpr translateIntern(ExprIncomplete e, ImTranslator t, ImFunction f) {
throw new CompileError(e.getSource(), "Incomplete expression.");
}
Expand Down Expand Up @@ -856,8 +945,11 @@ public static ImExpr translate(ExprStatementsBlock e, ImTranslator translator, I

StmtReturn r = e.getReturnStmt();
if (r != null && r.getReturnedObj() instanceof Expr) {
ImExpr expr = ((Expr) r.getReturnedObj()).imTranslateExpr(translator, f);
return JassIm.ImStatementExpr(statements, expr);
Expr returnedExpr = (Expr) r.getReturnedObj();
ImExpr expr = returnedExpr.imTranslateExpr(translator, f);
expr = wrapTranslation(e, translator, expr, returnedExpr.attrTypRaw(), e.attrExpectedTypRaw());
Comment thread
Frotty marked this conversation as resolved.
Outdated
return wrapTranslation(e, translator, JassIm.ImStatementExpr(statements, expr),
e.attrTypRaw(), e.attrExpectedTypRaw());
} else {
return ImHelper.statementExprVoid(statements);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,16 @@ static ImFunction buildEnsureInt(List<ImFunction> out) {
return f;
}

/** local result = false; if x ~= nil then result = x end; return result */
/** return x == true; this preserves false and maps nil to false. */
static ImFunction buildEnsureBool(List<ImFunction> out) {
ImType boolType = TypesHelper.imBool();
ImVar x = JassIm.ImVar(TRACE, boolType.copy(), "x", false);
ImVar result = JassIm.ImVar(TRACE, boolType.copy(), "result", false);

ImStmts body = JassIm.ImStmts(
JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImBoolVal(false)),
JassIm.ImIf(TRACE, notNull(x),
JassIm.ImStmts(JassIm.ImSet(TRACE, JassIm.ImVarAccess(result), JassIm.ImVarAccess(x))),
JassIm.ImStmts()),
JassIm.ImReturn(TRACE, JassIm.ImVarAccess(result))
JassIm.ImReturn(TRACE, isTrue(x))
);
ImFunction f = JassIm.ImFunction(TRACE, "__wurst_ensureBool", JassIm.ImTypeVars(), JassIm.ImVars(x), boolType.copy(),
JassIm.ImVars(result), body, Collections.emptyList());
JassIm.ImVars(), body, Collections.emptyList());
out.add(f);
return f;
}
Expand Down Expand Up @@ -196,6 +191,11 @@ private static ImExpr notNull(ImVar v) {
return JassIm.ImOperatorCall(WurstOperator.NOTEQ, JassIm.ImExprs(JassIm.ImVarAccess(v), JassIm.ImNull(JassIm.ImAnyType())));
}

private static ImExpr isTrue(ImVar v) {
return JassIm.ImOperatorCall(WurstOperator.EQ,
JassIm.ImExprs(JassIm.ImVarAccess(v), JassIm.ImBoolVal(true)));
}

private static ImFunctionCall call(ImFunction f, ImExpr... args) {
return JassIm.ImFunctionCall(TRACE, f, JassIm.ImTypeArguments(), JassIm.ImExprs(args), false, CallType.NORMAL);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public static void transform(ImProg prog, ImTranslator translator) {

lowerStringConcatenation(prog, translator);
lowerDivMod(prog);
lowerPrimitiveArrayEnsure(prog, translator);
lowerPrimitiveArrayBoundaryEnsure(prog, translator);

// Maps original BJ function → replacement (IS_NATIVE stub or nil-safety wrapper).
// Populated lazily during the traversal.
Expand Down Expand Up @@ -334,40 +334,6 @@ private static boolean isIntentionalThreadAbortDivByZero(ImOperatorCall call) {
&& "I2S".equals(parentCall.getFunc().getName());
}

/**
* Rewrites reads (never writes - see {@link LValues#isUsedAsLValue}) of
* primitive-typed ({@code int}/{@code bool}/{@code real}/{@code string})
* array slots into calls against the portable {@code ensureXxx} IM
* functions ({@link ImTranslator#ensureIntFunc} and friends), instead of
* that normalization being applied later as opaque, always-emitted Lua
* source at Lua-emission time. Same treatment as {@link #lowerDivMod}:
* this makes a hot read optimizable (inlinable, foldable) instead of a
* fixed per-read function-call cost, and lets the helper disappear
* entirely from programs whose arrays are never read this way.
*
* <p>The shared per-type array-default metatable (see {@code
* LuaTranslator#newDefaultArray}) already guarantees a typed, non-nil
* default on every miss, so this remains defensive hardening against
* values written from outside typed Wurst code, not a correctness
* requirement for pure Wurst-authored programs.
*/
private static void lowerPrimitiveArrayEnsure(ImProg prog, ImTranslator translator) {
prog.accept(new Element.DefaultVisitor() {
@Override
public void visit(ImVarArrayAccess access) {
super.visit(access);
if (LValues.isUsedAsLValue(access)) {
return;
}
ImFunction ensureFunc = ensureFunctionFor(access.attrTyp(), translator);
if (ensureFunc == null) {
return;
}
access.replaceBy(callWithStacktrace(access.attrTrace(), ensureFunc, JassIm.ImExprs(access.copy())));
}
});
}

private static ImFunctionCall callWithStacktrace(de.peeeq.wurstscript.ast.Element trace, ImFunction f, ImExprs args) {
int stacktraceIndex = stacktraceParamIndex(f);
if (stacktraceIndex >= 0) {
Expand All @@ -386,6 +352,58 @@ private static int stacktraceParamIndex(ImFunction f) {
return -1;
}

/**
* Normalizes primitive array reads only when they enter code outside the
* typed Wurst world. Lua's array metatables already provide Wurst
* defaults for ordinary reads, so doing this at every read is redundant;
* a native/BJ/extern call is the point where an untyped value must be
* made safe for the callee.
*/
private static void lowerPrimitiveArrayBoundaryEnsure(ImProg prog, ImTranslator translator) {
prog.accept(new Element.DefaultVisitor() {
@Override
public void visit(ImFunctionCall call) {
super.visit(call);
ImFunction function = call.getFunc();
if (!isExternalBoundary(function)) {
return;
Comment thread
Frotty marked this conversation as resolved.
}
for (ImExpr argument : new ArrayList<>(call.getArguments())) {
if (!(argument instanceof ImVarArrayAccess)
|| isAlreadyNormalized(argument, translator)) {
continue;
}
ImFunction ensure = ensureFunctionFor(argument.attrTyp(), translator);
if (ensure == null) {
continue;
}
ImExpr normalized;
if (ensure == translator.ensureBoolFunc) {
normalized = JassIm.ImOperatorCall(WurstOperator.EQ,
JassIm.ImExprs(argument.copy(), JassIm.ImBoolVal(true)));
} else {
normalized = callWithStacktrace(call.attrTrace(), ensure,
JassIm.ImExprs(argument.copy()));
}
argument.replaceBy(normalized);
}
}
});
}

private static boolean isExternalBoundary(ImFunction function) {
return !function.getName().startsWith("__wurst_")
&& (function.isNative() || function.isBj() || function.isExtern());
}

private static boolean isAlreadyNormalized(ImExpr argument, ImTranslator translator) {
return argument instanceof ImFunctionCall
&& (((ImFunctionCall) argument).getFunc() == translator.ensureIntFunc
|| ((ImFunctionCall) argument).getFunc() == translator.ensureBoolFunc
|| ((ImFunctionCall) argument).getFunc() == translator.ensureRealFunc
|| ((ImFunctionCall) argument).getFunc() == translator.ensureStrFunc);
}

private static ImFunction ensureFunctionFor(ImType type, ImTranslator translator) {
if (TypesHelper.isIntType(type)) {
return translator.ensureIntFunc;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import de.peeeq.wurstscript.types.TypesHelper;
import de.peeeq.wurstscript.types.WurstType;
import de.peeeq.wurstscript.types.WurstTypeArray;
import de.peeeq.wurstscript.types.WurstTypeInt;
import de.peeeq.wurstscript.types.WurstTypeIntLiteral;
import de.peeeq.wurstscript.types.WurstTypeVararg;
import org.eclipse.jdt.annotation.Nullable;

Expand Down Expand Up @@ -293,8 +295,8 @@ private static ImStmt case_StmtForRange(ImTranslator t, ImFunction f, LocalVarDe
List<ImStmt> result = Lists.newArrayList();
result.add(ImSet(loopVar, ImVarAccess(imLoopVar), fromExpr));

ImExpr toExpr = addCacheVariableSmart(t, f, result, to, TypesHelper.imInt());
ImExpr stepExpr = addCacheVariableSmart(t, f, result, step, TypesHelper.imInt());
ImExpr toExpr = addCacheVariableSmart(t, f, result, to, TypesHelper.imInt(), WurstTypeInt.instance());
Comment thread
Frotty marked this conversation as resolved.
ImExpr stepExpr = addCacheVariableSmart(t, f, result, step, TypesHelper.imInt(), WurstTypeInt.instance());

ImStmts imBody = ImStmts();
// exitwhen imLoopVar > toExpr
Expand All @@ -310,6 +312,18 @@ private static ImStmt case_StmtForRange(ImTranslator t, ImFunction f, LocalVarDe

private static ImExpr addCacheVariableSmart(ImTranslator t, ImFunction f, List<ImStmt> result, Expr toCache, ImType type) {
ImExpr r = toCache.imTranslateExpr(t, f);
return addCacheVariableSmart(t, f, result, toCache, type, r);
}

private static ImExpr addCacheVariableSmart(ImTranslator t, ImFunction f, List<ImStmt> result,
Expr toCache, ImType type, WurstType expectedType) {
ImExpr r = toCache.imTranslateExpr(t, f);
r = ExprTranslation.wrapTranslation(toCache, t, r, toCache.attrTypRaw(), expectedType);
return addCacheVariableSmart(t, f, result, toCache, type, r);
}

private static ImExpr addCacheVariableSmart(ImTranslator t, ImFunction f, List<ImStmt> result,
Expr toCache, ImType type, ImExpr r) {
if (r instanceof ImConst) {
return r;
}
Expand Down Expand Up @@ -471,7 +485,10 @@ public static ImStmt translate(StmtSkip s, ImTranslator translator, ImFunction f
public static ImStmt translate(SwitchStmt switchStmt, ImTranslator t, ImFunction f) {
List<ImStmt> result = Lists.newArrayList();
ImType type = switchStmt.getExpr().attrTyp().imTranslateType(t);
ImExpr tempVar = addCacheVariableSmart(t, f, result, switchStmt.getExpr(), type);
WurstType expectedType = switchExpectedType(switchStmt);
ImExpr tempVar = expectedType == null
? addCacheVariableSmart(t, f, result, switchStmt.getExpr(), type)
: addCacheVariableSmart(t, f, result, switchStmt.getExpr(), type, expectedType);
Comment thread
Frotty marked this conversation as resolved.
// generate ifs
// leerer Block:
//ImStmts();
Expand Down Expand Up @@ -520,6 +537,16 @@ public static ImStmt translate(SwitchStmt switchStmt, ImTranslator t, ImFunction
return ImHelper.statementExprVoid(ImStmts(result));
}

private static @Nullable WurstType switchExpectedType(SwitchStmt switchStmt) {
for (SwitchCase switchCase : switchStmt.getCases()) {
for (Expr expression : switchCase.getExpressions()) {
WurstType type = expression.attrTyp();
return type instanceof WurstTypeIntLiteral ? WurstTypeInt.instance() : type;
}
}
return null;
}

/**
* translate the expressions of a switch case to
* <p>
Expand Down
Loading
Loading