From 86d568ddfbce4dbda0749597bfc1eeecea1814d0 Mon Sep 17 00:00:00 2001 From: AutisticLulu Date: Sun, 9 Aug 2026 17:33:38 +0200 Subject: [PATCH 1/2] Fix cppia JIT "Bad move target" on untyped register moves CppiaCompiler::convert moves between two untyped registers in three places. getCommonType(jtAny, jtAny) returns jtAny, which move() rejects. The cppia test suite covers it, run with -jit. --- src/hx/cppia/CppiaCompiler.cpp | 6 +++--- test/cppia/Client.hx | 10 ++++++++++ test/cppia/cases/TestCommon.hx | 10 ++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/hx/cppia/CppiaCompiler.cpp b/src/hx/cppia/CppiaCompiler.cpp index e6f8087e9..3d16cd81f 100644 --- a/src/hx/cppia/CppiaCompiler.cpp +++ b/src/hx/cppia/CppiaCompiler.cpp @@ -898,7 +898,7 @@ class CppiaJitCompiler : public CppiaCompiler { if (inSrc.uses(SLJIT_R1)) { - move(sJitArg0, inSrc); + move(sJitArg0.as(jtInt), inSrc.as(jtInt)); add( sJitTemp1, inTarget.getReg(), inTarget.offset ); callNative( (void *)intToStr, sJitArg0.as(jtInt), sJitTemp1.as(jtPointer)); } @@ -917,7 +917,7 @@ class CppiaJitCompiler : public CppiaCompiler case etObject: if (inSrc.uses(SLJIT_R1)) { - move(sJitArg0, inSrc); + move(sJitArg0.as(jtPointer), inSrc.as(jtPointer)); add( sJitTemp1, inTarget.getReg(), inTarget.offset ); callNative( (void *)objToStr, sJitArg0.as(jtPointer), sJitTemp1.as(jtPointer) ); } @@ -944,7 +944,7 @@ class CppiaJitCompiler : public CppiaCompiler case etObject: if (inSrc==sJitTemp1) { - move(sJitArg0, inSrc); + move(sJitArg0.as(jtPointer), inSrc.as(jtPointer)); makeAddress(sJitTemp1,inTarget); callNative( (void *)objToFloat, sJitArg0.as(jtPointer), sJitTemp1.as(jtPointer)); } diff --git a/test/cppia/Client.hx b/test/cppia/Client.hx index 710373f4b..c9b733f84 100644 --- a/test/cppia/Client.hx +++ b/test/cppia/Client.hx @@ -14,6 +14,16 @@ class ClientFoo implements IFoo { } } +class ClientUntypedMove { + + public static function subtractIndexed():String { + var a:Int = 2; + var ints:Array = [7,8,9]; + + return "" + (ints[a - 1] - ints[0]); + } +} + class Client { public static var clientBool0 = true; diff --git a/test/cppia/cases/TestCommon.hx b/test/cppia/cases/TestCommon.hx index eaacb5a76..7a32b9f36 100644 --- a/test/cppia/cases/TestCommon.hx +++ b/test/cppia/cases/TestCommon.hx @@ -59,6 +59,16 @@ class TestCommon extends Test { Assert.equals(2, Common.callbackSet, 'Bad cppia closure'); } + @:depends(testStatus) + function testUntypedRegisterMove() { + final cls = Type.resolveClass('ClientUntypedMove'); + + if (Assert.notNull(cls, 'Unable to resolve ClientUntypedMove')) { + Assert.equals('1', Std.string(Reflect.callMethod(null, Reflect.field(cls, 'subtractIndexed'), [])), + 'Subtracting two array elements into a string did not answer'); + } + } + @:depends(testStatus) function testInterfaceCalling() { final obj : IFoo = Type.createInstance(Type.resolveClass('ClientFoo'), []); From cb0756043bbee495c9e8de250f770f0cebf2b95c Mon Sep 17 00:00:00 2001 From: AutisticLulu Date: Sun, 16 Aug 2026 12:24:38 +0200 Subject: [PATCH 2/2] Fix the untyped register moves at their source OpSub::genCode handed convert a bare sJitTemp1 where its sibling OpMult::genCode passes sJitTemp1.as(jtInt). That is the whole bug: with OpSub fixed the test passes with CppiaCompiler.cpp untouched. The 2-byte array read in ArrayBuiltin has the same omission against its byte-sized sibling, though nothing instantiates ArrayBuiltin with a 2-byte element, so that block never compiles. The convert hardening stays as defence in depth, since convert is handed inSrcType and every other move in it already types both sides from it. The etObject to etFloat branch guarded on inSrc==sJitTemp1, but JitVal equality includes type, so only a bare untyped sJitTemp1 ever matched. A typed R1 source fell through to a path where makeAddress overwrote R1 before objToFloat read it. Nothing produces that shape today, so this is correctness on an unreachable path rather than a live fix. It now guards on inSrc.uses(SLJIT_R1) like the etObject to etString branch above it, spills once, and shares the memory and non-memory target paths. Tests cover the reported crash, both of the other OpSub destinations, and the three convert paths touched here. They cannot isolate the hardening from the OpSub fix, since with either one present nothing hands convert an untyped register. Emitted sljit code is unchanged: the whole cppia test client jits to identical LIR before and after. --- src/hx/cppia/ArrayBuiltin.cpp | 2 +- src/hx/cppia/Cppia.cpp | 2 +- src/hx/cppia/CppiaCompiler.cpp | 18 +++++++-------- test/cppia/Client.hx | 40 ++++++++++++++++++++++++++++---- test/cppia/cases/TestCommon.hx | 42 +++++++++++++++++++++++++++++----- 5 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/hx/cppia/ArrayBuiltin.cpp b/src/hx/cppia/ArrayBuiltin.cpp index 2d321565e..5a7164e51 100644 --- a/src/hx/cppia/ArrayBuiltin.cpp +++ b/src/hx/cppia/ArrayBuiltin.cpp @@ -1482,7 +1482,7 @@ struct ArrayBuiltin : public ArrayBuiltinBase if (destType!=etInt || isMemoryVal(inDest)) { compiler->move(sJitTemp1.as(jtInt),sJitTemp1.atReg(sJitTemp0,1).as(jtShort)); - compiler->convert(sJitTemp1,etInt, inDest, destType); + compiler->convert(sJitTemp1.as(jtInt),etInt, inDest, destType); } else compiler->move( inDest.as(jtInt), sJitTemp1.atReg(sJitTemp0).as(jtShort) ); diff --git a/src/hx/cppia/Cppia.cpp b/src/hx/cppia/Cppia.cpp index 06375a1ec..05baeb3a4 100644 --- a/src/hx/cppia/Cppia.cpp +++ b/src/hx/cppia/Cppia.cpp @@ -6818,7 +6818,7 @@ struct OpSub : public BinOp else { compiler->sub(sJitTemp1.as(jtInt),lval,sJitTemp0,false); - compiler->convert(sJitTemp1,etInt, inDest, destType); + compiler->convert(sJitTemp1.as(jtInt),etInt, inDest, destType); } } else diff --git a/src/hx/cppia/CppiaCompiler.cpp b/src/hx/cppia/CppiaCompiler.cpp index 3d16cd81f..6cda678b5 100644 --- a/src/hx/cppia/CppiaCompiler.cpp +++ b/src/hx/cppia/CppiaCompiler.cpp @@ -942,24 +942,24 @@ class CppiaJitCompiler : public CppiaCompiler break; case etObject: - if (inSrc==sJitTemp1) - { - move(sJitArg0.as(jtPointer), inSrc.as(jtPointer)); - makeAddress(sJitTemp1,inTarget); - callNative( (void *)objToFloat, sJitArg0.as(jtPointer), sJitTemp1.as(jtPointer)); - } - else { + JitVal src = inSrc.as(jtPointer); + if (inSrc.uses(SLJIT_R1)) + { + move(sJitArg0.as(jtPointer), src); + src = sJitArg0.as(jtPointer); + } + if (isMemoryVal(inTarget)) { makeAddress(sJitTemp1,inTarget); - callNative( (void *)objToFloat, inSrc.as(jtPointer), sJitTemp1.as(jtPointer) ); + callNative( (void *)objToFloat, src, sJitTemp1.as(jtPointer) ); } else { JitTemp temp(this,jtFloat); makeAddress(sJitTemp1,temp); - callNative( (void *)objToFloat, inSrc.as(jtPointer), sJitTemp1.as(jtPointer) ); + callNative( (void *)objToFloat, src, sJitTemp1.as(jtPointer) ); move(inTarget,temp); } } diff --git a/test/cppia/Client.hx b/test/cppia/Client.hx index c9b733f84..a310fadbe 100644 --- a/test/cppia/Client.hx +++ b/test/cppia/Client.hx @@ -14,13 +14,45 @@ class ClientFoo implements IFoo { } } -class ClientUntypedMove { +class ClientJitConvert { - public static function subtractIndexed():String { + public static function subtractToString():String { var a:Int = 2; - var ints:Array = [7,8,9]; - return "" + (ints[a - 1] - ints[0]); + return "" + (a - 1); + } + + public static function subtractToFloat():Float { + var a:Int = 7; + + return (a - 2) / 2; + } + + public static function subtractToDynamic():Dynamic { + var a:Int = 9; + + return a - 4; + } + + public static function dynamicToString():String { + var d:Dynamic = "hello"; + var s:String = d; + + return s + "!"; + } + + public static function dynamicToFloat():Float { + var d:Dynamic = 2.5; + var f:Float = d; + + return f + 1; + } + + public static function dynamicToFloatInRegister():Float { + var d:Dynamic = 1.5; + var m:Float = 2; + + return m * d; } } diff --git a/test/cppia/cases/TestCommon.hx b/test/cppia/cases/TestCommon.hx index 7a32b9f36..1218b3c42 100644 --- a/test/cppia/cases/TestCommon.hx +++ b/test/cppia/cases/TestCommon.hx @@ -59,14 +59,44 @@ class TestCommon extends Test { Assert.equals(2, Common.callbackSet, 'Bad cppia closure'); } - @:depends(testStatus) - function testUntypedRegisterMove() { - final cls = Type.resolveClass('ClientUntypedMove'); + function callConvert(name:String):Dynamic { + final cls = Type.resolveClass('ClientJitConvert'); - if (Assert.notNull(cls, 'Unable to resolve ClientUntypedMove')) { - Assert.equals('1', Std.string(Reflect.callMethod(null, Reflect.field(cls, 'subtractIndexed'), [])), - 'Subtracting two array elements into a string did not answer'); + if (!Assert.notNull(cls, 'Unable to resolve ClientJitConvert')) { + return null; } + + return Reflect.callMethod(null, Reflect.field(cls, name), []); + } + + @:depends(testStatus) + function testSubtractionToString() { + Assert.equals('1', callConvert('subtractToString'), 'Int subtraction into a string did not answer'); + } + + @:depends(testStatus) + function testSubtractionToFloat() { + Assert.floatEquals(2.5, callConvert('subtractToFloat'), 'Int subtraction into a float did not answer'); + } + + @:depends(testStatus) + function testSubtractionToDynamic() { + Assert.equals(5, callConvert('subtractToDynamic'), 'Int subtraction into a dynamic did not answer'); + } + + @:depends(testStatus) + function testDynamicToString() { + Assert.equals('hello!', callConvert('dynamicToString'), 'Dynamic into a string did not answer'); + } + + @:depends(testStatus) + function testDynamicToFloat() { + Assert.floatEquals(3.5, callConvert('dynamicToFloat'), 'Dynamic into a float did not answer'); + } + + @:depends(testStatus) + function testDynamicToFloatInRegister() { + Assert.floatEquals(3.0, callConvert('dynamicToFloatInRegister'), 'Dynamic into a float register did not answer'); } @:depends(testStatus)