// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/IrBuilder.h" #include "Luau/IrAnalysis.h" #include "Luau/IrDump.h" #include "Luau/IrUtils.h" #include "Luau/OptimizeConstProp.h" #include "Luau/OptimizeDeadStore.h" #include "Luau/OptimizeFinalX64.h" #include "ScopedFlags.h" #include "doctest.h" #include LUAU_FASTFLAG(DebugLuauAbortingChecks) LUAU_FASTFLAG(LuauCodegenInstG) LUAU_FASTFLAG(LuauCodegenFastcall3) LUAU_FASTFLAG(LuauCodegenMathSign) using namespace Luau::CodeGen; class IrBuilderFixture { public: IrBuilderFixture() : build(hooks) { } void constantFold() { for (IrBlock& block : build.function.blocks) { if (block.kind == IrBlockKind::Dead) continue; for (size_t i = block.start; i <= block.finish; i++) { IrInst& inst = build.function.instructions[i]; applySubstitutions(build.function, inst); foldConstants(build, build.function, block, uint32_t(i)); } } } template void withOneBlock(F&& f) { IrOp main = build.block(IrBlockKind::Internal); IrOp a = build.block(IrBlockKind::Internal); build.beginBlock(main); f(a); build.beginBlock(a); build.inst(IrCmd::RETURN, build.constUint(1)); }; template void withTwoBlocks(F&& f) { IrOp main = build.block(IrBlockKind::Internal); IrOp a = build.block(IrBlockKind::Internal); IrOp b = build.block(IrBlockKind::Internal); build.beginBlock(main); f(a, b); build.beginBlock(a); build.inst(IrCmd::RETURN, build.constUint(1)); build.beginBlock(b); build.inst(IrCmd::RETURN, build.constUint(2)); }; void checkEq(IrOp instOp, const IrInst& inst) { const IrInst& target = build.function.instOp(instOp); CHECK(target.cmd == inst.cmd); CHECK(target.a == inst.a); CHECK(target.b == inst.b); CHECK(target.c == inst.c); CHECK(target.d == inst.d); CHECK(target.e == inst.e); CHECK(target.f == inst.f); } void defineCfgTree(const std::vector>& successorSets) { for (const std::vector& successorSet : successorSets) { build.beginBlock(build.block(IrBlockKind::Internal)); build.function.cfg.successorsOffsets.push_back(uint32_t(build.function.cfg.successors.size())); build.function.cfg.successors.insert(build.function.cfg.successors.end(), successorSet.begin(), successorSet.end()); } // Brute-force the predecessor list for (int i = 0; i < int(build.function.blocks.size()); i++) { build.function.cfg.predecessorsOffsets.push_back(uint32_t(build.function.cfg.predecessors.size())); for (int k = 0; k < int(build.function.blocks.size()); k++) { for (uint32_t succIdx : successors(build.function.cfg, k)) { if (succIdx == uint32_t(i)) build.function.cfg.predecessors.push_back(k); } } } computeCfgImmediateDominators(build.function); computeCfgDominanceTreeChildren(build.function); } HostIrHooks hooks; IrBuilder build; // Luau.VM headers are not accessible static const int tnil = 0; static const int tboolean = 1; static const int tnumber = 3; static const int tstring = 5; static const int ttable = 6; static const int tfunction = 7; }; TEST_SUITE_BEGIN("Optimization"); TEST_CASE_FIXTURE(IrBuilderFixture, "FinalX64OptCheckTag") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp tag1 = build.inst(IrCmd::LOAD_TAG, build.vmReg(2)); build.inst(IrCmd::CHECK_TAG, tag1, build.constTag(0), fallback); IrOp tag2 = build.inst(IrCmd::LOAD_TAG, build.vmConst(5)); build.inst(IrCmd::CHECK_TAG, tag2, build.constTag(0), fallback); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); optimizeMemoryOperandsX64(build.function); // Load from memory is 'inlined' into CHECK_TAG CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: CHECK_TAG R2, tnil, bb_fallback_1 CHECK_TAG K5, tnil, bb_fallback_1 RETURN 0u bb_fallback_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FinalX64OptBinaryArith") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp opA = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(1)); IrOp opB = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2)); build.inst(IrCmd::ADD_NUM, opA, opB); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); optimizeMemoryOperandsX64(build.function); // Load from memory is 'inlined' into second argument CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_DOUBLE R1 %2 = ADD_NUM %0, R2 RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FinalX64OptEqTag1") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp opA = build.inst(IrCmd::LOAD_TAG, build.vmReg(1)); IrOp opB = build.inst(IrCmd::LOAD_TAG, build.vmReg(2)); build.inst(IrCmd::JUMP_EQ_TAG, opA, opB, trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); optimizeMemoryOperandsX64(build.function); // Load from memory is 'inlined' into first argument CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %1 = LOAD_TAG R2 JUMP_EQ_TAG R1, %1, bb_1, bb_2 bb_1: RETURN 0u bb_2: RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FinalX64OptEqTag2") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp opA = build.inst(IrCmd::LOAD_TAG, build.vmReg(1)); IrOp opB = build.inst(IrCmd::LOAD_TAG, build.vmReg(2)); build.inst(IrCmd::STORE_TAG, build.vmReg(6), opA); build.inst(IrCmd::JUMP_EQ_TAG, opA, opB, trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); optimizeMemoryOperandsX64(build.function); // Load from memory is 'inlined' into second argument is it can't be done for the first one // We also swap first and second argument to generate memory access on the LHS CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TAG R1 STORE_TAG R6, %0 JUMP_EQ_TAG R2, %0, bb_1, bb_2 bb_1: RETURN 0u bb_2: RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FinalX64OptEqTag3") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp table = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); IrOp arrElem = build.inst(IrCmd::GET_ARR_ADDR, table, build.constInt(0)); IrOp opA = build.inst(IrCmd::LOAD_TAG, arrElem); build.inst(IrCmd::JUMP_EQ_TAG, opA, build.constTag(0), trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); optimizeMemoryOperandsX64(build.function); // Load from memory is 'inlined' into first argument CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 %1 = GET_ARR_ADDR %0, 0i %2 = LOAD_TAG %1 JUMP_EQ_TAG %2, tnil, bb_1, bb_2 bb_1: RETURN 0u bb_2: RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FinalX64OptJumpCmpNum") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp opA = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(1)); IrOp opB = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2)); build.inst(IrCmd::JUMP_CMP_NUM, opA, opB, trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); optimizeMemoryOperandsX64(build.function); // Load from memory is 'inlined' into first argument CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %1 = LOAD_DOUBLE R2 JUMP_CMP_NUM R1, %1, bb_1, bb_2 bb_1: RETURN 0u bb_2: RETURN 0u )"); } TEST_SUITE_END(); TEST_SUITE_BEGIN("ConstantFolding"); TEST_CASE_FIXTURE(IrBuilderFixture, "Numeric") { ScopedFastFlag luauCodegenMathSign{FFlag::LuauCodegenMathSign, true}; IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::ADD_INT, build.constInt(10), build.constInt(20))); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.inst(IrCmd::ADD_INT, build.constInt(INT_MAX), build.constInt(1))); build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::SUB_INT, build.constInt(10), build.constInt(20))); build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::SUB_INT, build.constInt(INT_MIN), build.constInt(1))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(4), build.inst(IrCmd::ADD_NUM, build.constDouble(2), build.constDouble(5))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(5), build.inst(IrCmd::SUB_NUM, build.constDouble(2), build.constDouble(5))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(6), build.inst(IrCmd::MUL_NUM, build.constDouble(2), build.constDouble(5))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(7), build.inst(IrCmd::DIV_NUM, build.constDouble(2), build.constDouble(5))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(8), build.inst(IrCmd::MOD_NUM, build.constDouble(5), build.constDouble(2))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(10), build.inst(IrCmd::MIN_NUM, build.constDouble(5), build.constDouble(2))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(11), build.inst(IrCmd::MAX_NUM, build.constDouble(5), build.constDouble(2))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(12), build.inst(IrCmd::UNM_NUM, build.constDouble(5))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(13), build.inst(IrCmd::FLOOR_NUM, build.constDouble(2.5))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(14), build.inst(IrCmd::CEIL_NUM, build.constDouble(2.5))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(15), build.inst(IrCmd::ROUND_NUM, build.constDouble(2.5))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(16), build.inst(IrCmd::SQRT_NUM, build.constDouble(16))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(17), build.inst(IrCmd::ABS_NUM, build.constDouble(-4))); build.inst(IrCmd::STORE_INT, build.vmReg(18), build.inst(IrCmd::NOT_ANY, build.constTag(tnil), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(1)))); build.inst( IrCmd::STORE_INT, build.vmReg(19), build.inst(IrCmd::NOT_ANY, build.constTag(tnumber), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(1)))); build.inst(IrCmd::STORE_INT, build.vmReg(20), build.inst(IrCmd::NOT_ANY, build.constTag(tboolean), build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(21), build.inst(IrCmd::NOT_ANY, build.constTag(tboolean), build.constInt(1))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(22), build.inst(IrCmd::SIGN_NUM, build.constDouble(-4))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_INT R0, 30i STORE_INT R1, -2147483648i STORE_INT R2, -10i STORE_INT R3, 2147483647i STORE_DOUBLE R4, 7 STORE_DOUBLE R5, -3 STORE_DOUBLE R6, 10 STORE_DOUBLE R7, 0.40000000000000002 STORE_DOUBLE R8, 1 STORE_DOUBLE R10, 2 STORE_DOUBLE R11, 5 STORE_DOUBLE R12, -5 STORE_DOUBLE R13, 2 STORE_DOUBLE R14, 3 STORE_DOUBLE R15, 3 STORE_DOUBLE R16, 4 STORE_DOUBLE R17, 4 STORE_INT R18, 1i STORE_INT R19, 0i STORE_INT R20, 1i STORE_INT R21, 0i STORE_DOUBLE R22, -1 RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "NumericConversions") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.inst(IrCmd::INT_TO_NUM, build.constInt(8))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.inst(IrCmd::UINT_TO_NUM, build.constInt(0xdeee0000u))); build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::NUM_TO_INT, build.constDouble(200.0))); build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::NUM_TO_UINT, build.constDouble(3740139520.0))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_DOUBLE R0, 8 STORE_DOUBLE R1, 3740139520 STORE_INT R2, 200i STORE_INT R3, -554827776i RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "NumericConversionsBlocked") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp nan = build.inst(IrCmd::DIV_NUM, build.constDouble(0.0), build.constDouble(0.0)); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::NUM_TO_INT, build.constDouble(1e20))); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.inst(IrCmd::NUM_TO_UINT, build.constDouble(-10))); build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::NUM_TO_INT, nan)); build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::NUM_TO_UINT, nan)); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %1 = NUM_TO_INT 1e+20 STORE_INT R0, %1 %3 = NUM_TO_UINT -10 STORE_INT R1, %3 %5 = NUM_TO_INT nan STORE_INT R2, %5 %7 = NUM_TO_UINT nan STORE_INT R3, %7 RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "Bit32") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp unk = build.inst(IrCmd::LOAD_INT, build.vmReg(0)); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::BITAND_UINT, build.constInt(0xfe), build.constInt(0xe))); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.inst(IrCmd::BITAND_UINT, unk, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(2), build.inst(IrCmd::BITAND_UINT, build.constInt(0), unk)); build.inst(IrCmd::STORE_INT, build.vmReg(3), build.inst(IrCmd::BITAND_UINT, unk, build.constInt(~0u))); build.inst(IrCmd::STORE_INT, build.vmReg(4), build.inst(IrCmd::BITAND_UINT, build.constInt(~0u), unk)); build.inst(IrCmd::STORE_INT, build.vmReg(5), build.inst(IrCmd::BITXOR_UINT, build.constInt(0xfe), build.constInt(0xe))); build.inst(IrCmd::STORE_INT, build.vmReg(6), build.inst(IrCmd::BITXOR_UINT, unk, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(7), build.inst(IrCmd::BITXOR_UINT, build.constInt(0), unk)); build.inst(IrCmd::STORE_INT, build.vmReg(8), build.inst(IrCmd::BITXOR_UINT, unk, build.constInt(~0u))); build.inst(IrCmd::STORE_INT, build.vmReg(9), build.inst(IrCmd::BITXOR_UINT, build.constInt(~0u), unk)); build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::BITOR_UINT, build.constInt(0xf0), build.constInt(0xe))); build.inst(IrCmd::STORE_INT, build.vmReg(11), build.inst(IrCmd::BITOR_UINT, unk, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(12), build.inst(IrCmd::BITOR_UINT, build.constInt(0), unk)); build.inst(IrCmd::STORE_INT, build.vmReg(13), build.inst(IrCmd::BITOR_UINT, unk, build.constInt(~0u))); build.inst(IrCmd::STORE_INT, build.vmReg(14), build.inst(IrCmd::BITOR_UINT, build.constInt(~0u), unk)); build.inst(IrCmd::STORE_INT, build.vmReg(15), build.inst(IrCmd::BITNOT_UINT, build.constInt(0xe))); build.inst(IrCmd::STORE_INT, build.vmReg(16), build.inst(IrCmd::BITLSHIFT_UINT, build.constInt(0xf0), build.constInt(4))); build.inst(IrCmd::STORE_INT, build.vmReg(17), build.inst(IrCmd::BITLSHIFT_UINT, unk, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(18), build.inst(IrCmd::BITRSHIFT_UINT, build.constInt(0xdeee0000u), build.constInt(8))); build.inst(IrCmd::STORE_INT, build.vmReg(19), build.inst(IrCmd::BITRSHIFT_UINT, unk, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(20), build.inst(IrCmd::BITARSHIFT_UINT, build.constInt(0xdeee0000u), build.constInt(8))); build.inst(IrCmd::STORE_INT, build.vmReg(21), build.inst(IrCmd::BITARSHIFT_UINT, unk, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(22), build.inst(IrCmd::BITLROTATE_UINT, build.constInt(0xdeee0000u), build.constInt(8))); build.inst(IrCmd::STORE_INT, build.vmReg(23), build.inst(IrCmd::BITLROTATE_UINT, unk, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(24), build.inst(IrCmd::BITRROTATE_UINT, build.constInt(0xdeee0000u), build.constInt(8))); build.inst(IrCmd::STORE_INT, build.vmReg(25), build.inst(IrCmd::BITRROTATE_UINT, unk, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(26), build.inst(IrCmd::BITCOUNTLZ_UINT, build.constInt(0xff00))); build.inst(IrCmd::STORE_INT, build.vmReg(27), build.inst(IrCmd::BITCOUNTLZ_UINT, build.constInt(0))); build.inst(IrCmd::STORE_INT, build.vmReg(28), build.inst(IrCmd::BITCOUNTRZ_UINT, build.constInt(0xff00))); build.inst(IrCmd::STORE_INT, build.vmReg(29), build.inst(IrCmd::BITCOUNTRZ_UINT, build.constInt(0))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_INT R0 STORE_INT R0, 14i STORE_INT R1, 0i STORE_INT R2, 0i STORE_INT R3, %0 STORE_INT R4, %0 STORE_INT R5, 240i STORE_INT R6, %0 STORE_INT R7, %0 %17 = BITNOT_UINT %0 STORE_INT R8, %17 %19 = BITNOT_UINT %0 STORE_INT R9, %19 STORE_INT R10, 254i STORE_INT R11, %0 STORE_INT R12, %0 STORE_INT R13, -1i STORE_INT R14, -1i STORE_INT R15, -15i STORE_INT R16, 3840i STORE_INT R17, %0 STORE_INT R18, 14609920i STORE_INT R19, %0 STORE_INT R20, -2167296i STORE_INT R21, %0 STORE_INT R22, -301989666i STORE_INT R23, %0 STORE_INT R24, 14609920i STORE_INT R25, %0 STORE_INT R26, 16i STORE_INT R27, 32i STORE_INT R28, 8i STORE_INT R29, 32i RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "Bit32RangeReduction") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::BITLSHIFT_UINT, build.constInt(0xf), build.constInt(-10))); build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::BITLSHIFT_UINT, build.constInt(0xf), build.constInt(140))); build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::BITRSHIFT_UINT, build.constInt(0xffffff), build.constInt(-10))); build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::BITRSHIFT_UINT, build.constInt(0xffffff), build.constInt(140))); build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::BITARSHIFT_UINT, build.constInt(0xffffff), build.constInt(-10))); build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::BITARSHIFT_UINT, build.constInt(0xffffff), build.constInt(140))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_INT R10, 62914560i STORE_INT R10, 61440i STORE_INT R10, 3i STORE_INT R10, 4095i STORE_INT R10, 3i STORE_INT R10, 4095i RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ReplacementPreservesUses") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp unk = build.inst(IrCmd::LOAD_INT, build.vmReg(0)); build.inst(IrCmd::STORE_INT, build.vmReg(8), build.inst(IrCmd::BITXOR_UINT, unk, build.constInt(~0u))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::Yes) == R"( bb_0: ; useCount: 0 %0 = LOAD_INT R0 ; useCount: 1, lastUse: %0 %1 = BITNOT_UINT %0 ; useCount: 1, lastUse: %0 STORE_INT R8, %1 ; %2 RETURN 0u ; %3 )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "NumericNan") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp nan = build.inst(IrCmd::DIV_NUM, build.constDouble(0.0), build.constDouble(0.0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.inst(IrCmd::MIN_NUM, nan, build.constDouble(2))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.inst(IrCmd::MIN_NUM, build.constDouble(1), nan)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.inst(IrCmd::MAX_NUM, nan, build.constDouble(2))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.inst(IrCmd::MAX_NUM, build.constDouble(1), nan)); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_DOUBLE R0, 2 STORE_DOUBLE R0, nan STORE_DOUBLE R0, 2 STORE_DOUBLE R0, nan RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ControlFlowEq") { withTwoBlocks([this](IrOp a, IrOp b) { build.inst(IrCmd::JUMP_EQ_TAG, build.constTag(tnil), build.constTag(tnil), a, b); }); withTwoBlocks([this](IrOp a, IrOp b) { build.inst(IrCmd::JUMP_EQ_TAG, build.constTag(tnil), build.constTag(tnumber), a, b); }); withTwoBlocks([this](IrOp a, IrOp b) { build.inst(IrCmd::JUMP_CMP_INT, build.constInt(0), build.constInt(0), build.cond(IrCondition::Equal), a, b); }); withTwoBlocks([this](IrOp a, IrOp b) { build.inst(IrCmd::JUMP_CMP_INT, build.constInt(0), build.constInt(1), build.cond(IrCondition::Equal), a, b); }); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: JUMP bb_1 bb_1: RETURN 1u bb_3: JUMP bb_5 bb_5: RETURN 2u bb_6: JUMP bb_7 bb_7: RETURN 1u bb_9: JUMP bb_11 bb_11: RETURN 2u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "NumToIndex") { withOneBlock([this](IrOp a) { build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::TRY_NUM_TO_INDEX, build.constDouble(4), a)); build.inst(IrCmd::RETURN, build.constUint(0)); }); withOneBlock([this](IrOp a) { build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::TRY_NUM_TO_INDEX, build.constDouble(1.2), a)); build.inst(IrCmd::RETURN, build.constUint(0)); }); withOneBlock([this](IrOp a) { IrOp nan = build.inst(IrCmd::DIV_NUM, build.constDouble(0.0), build.constDouble(0.0)); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.inst(IrCmd::TRY_NUM_TO_INDEX, nan, a)); build.inst(IrCmd::RETURN, build.constUint(0)); }); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_INT R0, 4i RETURN 0u bb_2: JUMP bb_3 bb_3: RETURN 1u bb_4: JUMP bb_5 bb_5: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "Guards") { withOneBlock([this](IrOp a) { build.inst(IrCmd::CHECK_TAG, build.constTag(tnumber), build.constTag(tnumber), a); build.inst(IrCmd::RETURN, build.constUint(0)); }); withOneBlock([this](IrOp a) { build.inst(IrCmd::CHECK_TAG, build.constTag(tnil), build.constTag(tnumber), a); build.inst(IrCmd::RETURN, build.constUint(0)); }); updateUseCounts(build.function); constantFold(); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: RETURN 0u bb_2: JUMP bb_3 bb_3: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ControlFlowCmpNum") { auto compareFold = [this](IrOp lhs, IrOp rhs, IrCondition cond, bool result) { IrOp instOp; IrInst instExpected; withTwoBlocks([&](IrOp a, IrOp b) { IrOp nan = build.inst(IrCmd::DIV_NUM, build.constDouble(0.0), build.constDouble(0.0)); instOp = build.inst( IrCmd::JUMP_CMP_NUM, lhs.kind == IrOpKind::None ? nan : lhs, rhs.kind == IrOpKind::None ? nan : rhs, build.cond(cond), a, b); instExpected = IrInst{IrCmd::JUMP, result ? a : b}; }); updateUseCounts(build.function); constantFold(); checkEq(instOp, instExpected); }; IrOp nan; // Empty operand is used to signal a placement of a 'nan' compareFold(build.constDouble(1), build.constDouble(1), IrCondition::Equal, true); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::Equal, false); compareFold(nan, nan, IrCondition::Equal, false); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::NotEqual, false); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::NotEqual, true); compareFold(nan, nan, IrCondition::NotEqual, true); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::Less, false); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::Less, true); compareFold(build.constDouble(2), build.constDouble(1), IrCondition::Less, false); compareFold(build.constDouble(1), nan, IrCondition::Less, false); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::NotLess, true); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::NotLess, false); compareFold(build.constDouble(2), build.constDouble(1), IrCondition::NotLess, true); compareFold(build.constDouble(1), nan, IrCondition::NotLess, true); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::LessEqual, true); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::LessEqual, true); compareFold(build.constDouble(2), build.constDouble(1), IrCondition::LessEqual, false); compareFold(build.constDouble(1), nan, IrCondition::LessEqual, false); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::NotLessEqual, false); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::NotLessEqual, false); compareFold(build.constDouble(2), build.constDouble(1), IrCondition::NotLessEqual, true); compareFold(build.constDouble(1), nan, IrCondition::NotLessEqual, true); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::Greater, false); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::Greater, false); compareFold(build.constDouble(2), build.constDouble(1), IrCondition::Greater, true); compareFold(build.constDouble(1), nan, IrCondition::Greater, false); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::NotGreater, true); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::NotGreater, true); compareFold(build.constDouble(2), build.constDouble(1), IrCondition::NotGreater, false); compareFold(build.constDouble(1), nan, IrCondition::NotGreater, true); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::GreaterEqual, true); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::GreaterEqual, false); compareFold(build.constDouble(2), build.constDouble(1), IrCondition::GreaterEqual, true); compareFold(build.constDouble(1), nan, IrCondition::GreaterEqual, false); compareFold(build.constDouble(1), build.constDouble(1), IrCondition::NotGreaterEqual, false); compareFold(build.constDouble(1), build.constDouble(2), IrCondition::NotGreaterEqual, true); compareFold(build.constDouble(2), build.constDouble(1), IrCondition::NotGreaterEqual, false); compareFold(build.constDouble(1), nan, IrCondition::NotGreaterEqual, true); } TEST_SUITE_END(); TEST_SUITE_BEGIN("ConstantPropagation"); TEST_CASE_FIXTURE(IrBuilderFixture, "RememberTagsAndValues") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.constInt(10)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.constDouble(0.5)); // We know constants from those loads build.inst(IrCmd::STORE_TAG, build.vmReg(3), build.inst(IrCmd::LOAD_TAG, build.vmReg(0))); build.inst(IrCmd::STORE_INT, build.vmReg(4), build.inst(IrCmd::LOAD_INT, build.vmReg(1))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(5), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2))); // We know that these overrides have no effect build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.constInt(10)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.constDouble(0.5)); // But we can invalidate them with unknown values build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.inst(IrCmd::LOAD_TAG, build.vmReg(6))); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.inst(IrCmd::LOAD_INT, build.vmReg(7))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(8))); // So now the constant stores have to be made build.inst(IrCmd::STORE_TAG, build.vmReg(9), build.inst(IrCmd::LOAD_TAG, build.vmReg(0))); build.inst(IrCmd::STORE_INT, build.vmReg(10), build.inst(IrCmd::LOAD_INT, build.vmReg(1))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(11), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber STORE_INT R1, 10i STORE_DOUBLE R2, 0.5 STORE_TAG R3, tnumber STORE_INT R4, 10i STORE_DOUBLE R5, 0.5 %12 = LOAD_TAG R6 STORE_TAG R0, %12 %14 = LOAD_INT R7 STORE_INT R1, %14 %16 = LOAD_DOUBLE R8 STORE_DOUBLE R2, %16 %18 = LOAD_TAG R0 STORE_TAG R9, %18 STORE_INT R10, %14 STORE_DOUBLE R11, %16 RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "PropagateThroughTvalue") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(0.5)); IrOp tv = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), tv); // We know constants from those loads build.inst(IrCmd::STORE_TAG, build.vmReg(3), build.inst(IrCmd::LOAD_TAG, build.vmReg(1))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(3), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(1))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber STORE_DOUBLE R0, 0.5 STORE_SPLIT_TVALUE R1, tnumber, 0.5 STORE_TAG R3, tnumber STORE_DOUBLE R3, 0.5 RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "SkipCheckTag") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), fallback); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "SkipOncePerBlockChecks") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::CHECK_SAFE_ENV); build.inst(IrCmd::CHECK_SAFE_ENV); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::DO_LEN, build.vmReg(1), build.vmReg(2)); // Can make env unsafe build.inst(IrCmd::CHECK_SAFE_ENV); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: CHECK_SAFE_ENV CHECK_GC DO_LEN R1, R2 CHECK_SAFE_ENV RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "RememberTableState") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp table = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::DO_LEN, build.vmReg(1), build.vmReg(2)); // Can access all heap memory build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R0 CHECK_NO_METATABLE %0, bb_fallback_1 CHECK_READONLY %0, bb_fallback_1 DO_LEN R1, R2 CHECK_NO_METATABLE %0, bb_fallback_1 CHECK_READONLY %0, bb_fallback_1 RETURN 0u bb_fallback_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "RememberNewTableState") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp newtable = build.inst(IrCmd::NEW_TABLE, build.constUint(16), build.constUint(32)); build.inst(IrCmd::STORE_POINTER, build.vmReg(0), newtable); IrOp table = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::CHECK_ARRAY_SIZE, table, build.constInt(14), fallback); build.inst(IrCmd::SET_TABLE, build.vmReg(1), build.vmReg(0), build.constUint(13)); // Invalidate table knowledge build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::CHECK_ARRAY_SIZE, table, build.constInt(14), fallback); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = NEW_TABLE 16u, 32u STORE_POINTER R0, %0 SET_TABLE R1, R0, 13u CHECK_NO_METATABLE %0, bb_fallback_1 CHECK_READONLY %0, bb_fallback_1 CHECK_ARRAY_SIZE %0, 14i, bb_fallback_1 RETURN 0u bb_fallback_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "SkipUselessBarriers") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); IrOp table = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); build.inst(IrCmd::BARRIER_TABLE_FORWARD, table, build.vmReg(0), build.undef()); IrOp something = build.inst(IrCmd::LOAD_POINTER, build.vmReg(2)); build.inst(IrCmd::BARRIER_OBJ, something, build.vmReg(0), build.undef()); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ConcatInvalidation") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.constInt(10)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.constDouble(0.5)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(3), build.constDouble(2.0)); build.inst(IrCmd::CONCAT, build.vmReg(0), build.constUint(3)); build.inst(IrCmd::STORE_TAG, build.vmReg(4), build.inst(IrCmd::LOAD_TAG, build.vmReg(0))); build.inst(IrCmd::STORE_INT, build.vmReg(5), build.inst(IrCmd::LOAD_INT, build.vmReg(1))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(6), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(7), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber STORE_INT R1, 10i STORE_DOUBLE R2, 0.5 STORE_DOUBLE R3, 2 CONCAT R0, 3u %5 = LOAD_TAG R0 STORE_TAG R4, %5 %7 = LOAD_INT R1 STORE_INT R5, %7 %9 = LOAD_DOUBLE R2 STORE_DOUBLE R6, %9 STORE_DOUBLE R7, 2 RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "BuiltinFastcallsMayInvalidateMemory") { ScopedFastFlag luauCodegenInstG{FFlag::LuauCodegenInstG, true}; IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(0.5)); IrOp table = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::INVOKE_FASTCALL, build.constUint(LBF_SETMETATABLE), build.vmReg(1), build.vmReg(2), build.vmReg(3), build.undef(), build.constInt(3), build.constInt(1)); build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0))); // At least R0 wasn't touched build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_DOUBLE R0, 0.5 %1 = LOAD_POINTER R0 CHECK_NO_METATABLE %1, bb_fallback_1 CHECK_READONLY %1, bb_fallback_1 %4 = INVOKE_FASTCALL 61u, R1, R2, R3, undef, 3i, 1i CHECK_NO_METATABLE %1, bb_fallback_1 CHECK_READONLY %1, bb_fallback_1 STORE_DOUBLE R1, 0.5 RETURN 0u bb_fallback_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "RedundantStoreCheckConstantType") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(10)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(0.5)); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(10)); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_INT R0, 10i STORE_DOUBLE R0, 0.5 STORE_INT R0, 10i RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TagCheckPropagation") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp unknown = build.inst(IrCmd::LOAD_TAG, build.vmReg(0)); build.inst(IrCmd::CHECK_TAG, unknown, build.constTag(tnumber), fallback); build.inst(IrCmd::CHECK_TAG, unknown, build.constTag(tnumber), fallback); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TAG R0 CHECK_TAG %0, tnumber, bb_fallback_1 RETURN 0u bb_fallback_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TagCheckPropagationConflicting") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp unknown = build.inst(IrCmd::LOAD_TAG, build.vmReg(0)); build.inst(IrCmd::CHECK_TAG, unknown, build.constTag(tnumber), fallback); build.inst(IrCmd::CHECK_TAG, unknown, build.constTag(tnil), fallback); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TAG R0 CHECK_TAG %0, tnumber, bb_fallback_1 JUMP bb_fallback_1 bb_fallback_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TruthyTestRemoval") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp unknown = build.inst(IrCmd::LOAD_TAG, build.vmReg(1)); build.inst(IrCmd::CHECK_TAG, unknown, build.constTag(tnumber), fallback); build.inst(IrCmd::JUMP_IF_TRUTHY, build.vmReg(1), trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(1)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(2)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(3)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TAG R1 CHECK_TAG %0, tnumber, bb_fallback_3 JUMP bb_1 bb_1: RETURN 1u bb_fallback_3: RETURN 3u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FalsyTestRemoval") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp unknown = build.inst(IrCmd::LOAD_TAG, build.vmReg(1)); build.inst(IrCmd::CHECK_TAG, unknown, build.constTag(tnumber), fallback); build.inst(IrCmd::JUMP_IF_FALSY, build.vmReg(1), trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(1)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(2)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(3)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TAG R1 CHECK_TAG %0, tnumber, bb_fallback_3 JUMP bb_2 bb_2: RETURN 2u bb_fallback_3: RETURN 3u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TagEqRemoval") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp tag = build.inst(IrCmd::LOAD_TAG, build.vmReg(1)); build.inst(IrCmd::CHECK_TAG, tag, build.constTag(tboolean)); build.inst(IrCmd::JUMP_EQ_TAG, tag, build.constTag(tnumber), trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(1)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TAG R1 CHECK_TAG %0, tboolean JUMP bb_2 bb_2: RETURN 2u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "IntEqRemoval") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.constInt(5)); IrOp value = build.inst(IrCmd::LOAD_INT, build.vmReg(1)); build.inst(IrCmd::JUMP_CMP_INT, value, build.constInt(5), build.cond(IrCondition::Equal), trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(1)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_INT R1, 5i JUMP bb_1 bb_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "NumCmpRemoval") { IrOp block = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(4.0)); IrOp value = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(1)); build.inst(IrCmd::JUMP_CMP_NUM, value, build.constDouble(8.0), build.cond(IrCondition::Greater), trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(1)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_DOUBLE R1, 4 JUMP bb_2 bb_2: RETURN 2u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DataFlowsThroughDirectJumpToUniqueSuccessor") { IrOp block1 = build.block(IrBlockKind::Internal); IrOp block2 = build.block(IrBlockKind::Internal); build.beginBlock(block1); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::JUMP, block2); build.beginBlock(block2); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.inst(IrCmd::LOAD_TAG, build.vmReg(0))); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber JUMP bb_1 bb_1: STORE_TAG R1, tnumber RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DataDoesNotFlowThroughDirectJumpToNonUniqueSuccessor") { IrOp block1 = build.block(IrBlockKind::Internal); IrOp block2 = build.block(IrBlockKind::Internal); IrOp block3 = build.block(IrBlockKind::Internal); build.beginBlock(block1); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::JUMP, block2); build.beginBlock(block2); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.inst(IrCmd::LOAD_TAG, build.vmReg(0))); build.inst(IrCmd::RETURN, build.constUint(1)); build.beginBlock(block3); build.inst(IrCmd::JUMP, block2); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber JUMP bb_1 bb_1: %2 = LOAD_TAG R0 STORE_TAG R1, %2 RETURN 1u bb_2: JUMP bb_1 )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "EntryBlockUseRemoval") { IrOp entry = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); IrOp repeat = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::JUMP_IF_TRUTHY, build.vmReg(0), exit, repeat); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); build.beginBlock(repeat); build.inst(IrCmd::INTERRUPT, build.constUint(0)); build.inst(IrCmd::JUMP, entry); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber JUMP bb_1 bb_1: RETURN R0, 0i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "RecursiveSccUseRemoval1") { IrOp entry = build.block(IrBlockKind::Internal); IrOp block = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); IrOp repeat = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::JUMP_IF_TRUTHY, build.vmReg(0), exit, repeat); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); build.beginBlock(repeat); build.inst(IrCmd::INTERRUPT, build.constUint(0)); build.inst(IrCmd::JUMP, block); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: RETURN R0, 0i bb_1: STORE_TAG R0, tnumber JUMP bb_2 bb_2: RETURN R0, 0i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "RecursiveSccUseRemoval2") { IrOp entry = build.block(IrBlockKind::Internal); IrOp exit1 = build.block(IrBlockKind::Internal); IrOp block = build.block(IrBlockKind::Internal); IrOp exit2 = build.block(IrBlockKind::Internal); IrOp repeat = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::JUMP_CMP_INT, build.constInt(0), build.constInt(1), build.cond(IrCondition::Equal), block, exit1); build.beginBlock(exit1); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::JUMP_IF_TRUTHY, build.vmReg(0), exit2, repeat); build.beginBlock(exit2); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); build.beginBlock(repeat); build.inst(IrCmd::INTERRUPT, build.constUint(0)); build.inst(IrCmd::JUMP, block); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: JUMP bb_1 bb_1: RETURN R0, 0i bb_2: STORE_TAG R0, tnumber JUMP bb_3 bb_3: RETURN R0, 0i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "IntNumIntPeepholes") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp i1 = build.inst(IrCmd::LOAD_INT, build.vmReg(0)); IrOp u1 = build.inst(IrCmd::LOAD_INT, build.vmReg(1)); IrOp ni1 = build.inst(IrCmd::INT_TO_NUM, i1); IrOp nu1 = build.inst(IrCmd::UINT_TO_NUM, u1); IrOp i2 = build.inst(IrCmd::NUM_TO_INT, ni1); IrOp u2 = build.inst(IrCmd::NUM_TO_UINT, nu1); build.inst(IrCmd::STORE_INT, build.vmReg(0), i2); build.inst(IrCmd::STORE_INT, build.vmReg(1), u2); build.inst(IrCmd::RETURN, build.constUint(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_INT R0 %1 = LOAD_INT R1 STORE_INT R0, %0 STORE_INT R1, %1 RETURN 2u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "InvalidateReglinkVersion") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tstring)); IrOp tv2 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(2)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), tv2); IrOp ft = build.inst(IrCmd::NEW_TABLE, build.constUint(0), build.constUint(0)); build.inst(IrCmd::STORE_POINTER, build.vmReg(2), ft); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(ttable)); IrOp tv1 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(0), tv1); IrOp tag = build.inst(IrCmd::LOAD_TAG, build.vmReg(0)); build.inst(IrCmd::CHECK_TAG, tag, build.constTag(ttable), fallback); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R2, tstring %1 = LOAD_TVALUE R2 STORE_TVALUE R1, %1 %3 = NEW_TABLE 0u, 0u STORE_POINTER R2, %3 STORE_TAG R2, ttable STORE_TVALUE R0, %1 %8 = LOAD_TAG R0 CHECK_TAG %8, ttable, bb_fallback_1 RETURN 0u bb_fallback_1: RETURN 1u )"); } TEST_SUITE_END(); TEST_SUITE_BEGIN("LinearExecutionFlowExtraction"); TEST_CASE_FIXTURE(IrBuilderFixture, "SimplePathExtraction") { IrOp block1 = build.block(IrBlockKind::Internal); IrOp fallback1 = build.block(IrBlockKind::Fallback); IrOp block2 = build.block(IrBlockKind::Internal); IrOp fallback2 = build.block(IrBlockKind::Fallback); IrOp block3 = build.block(IrBlockKind::Internal); IrOp block4 = build.block(IrBlockKind::Internal); build.beginBlock(block1); IrOp tag1 = build.inst(IrCmd::LOAD_TAG, build.vmReg(2)); build.inst(IrCmd::CHECK_TAG, tag1, build.constTag(tnumber), fallback1); build.inst(IrCmd::JUMP, block2); build.beginBlock(fallback1); build.inst(IrCmd::DO_LEN, build.vmReg(1), build.vmReg(2)); build.inst(IrCmd::JUMP, block2); build.beginBlock(block2); IrOp tag2 = build.inst(IrCmd::LOAD_TAG, build.vmReg(2)); build.inst(IrCmd::CHECK_TAG, tag2, build.constTag(tnumber), fallback2); build.inst(IrCmd::JUMP, block3); build.beginBlock(fallback2); build.inst(IrCmd::DO_LEN, build.vmReg(0), build.vmReg(2)); build.inst(IrCmd::JUMP, block3); build.beginBlock(block3); build.inst(IrCmd::JUMP, block4); build.beginBlock(block4); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); createLinearBlocks(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TAG R2 CHECK_TAG %0, tnumber, bb_fallback_1 JUMP bb_linear_6 bb_fallback_1: DO_LEN R1, R2 JUMP bb_2 bb_2: %5 = LOAD_TAG R2 CHECK_TAG %5, tnumber, bb_fallback_3 JUMP bb_4 bb_fallback_3: DO_LEN R0, R2 JUMP bb_4 bb_4: JUMP bb_5 bb_5: RETURN R0, 0i bb_linear_6: RETURN R0, 0i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "NoPathExtractionForBlocksWithLiveOutValues") { IrOp block1 = build.block(IrBlockKind::Internal); IrOp fallback1 = build.block(IrBlockKind::Fallback); IrOp block2 = build.block(IrBlockKind::Internal); IrOp fallback2 = build.block(IrBlockKind::Fallback); IrOp block3 = build.block(IrBlockKind::Internal); IrOp block4a = build.block(IrBlockKind::Internal); IrOp block4b = build.block(IrBlockKind::Internal); build.beginBlock(block1); IrOp tag1 = build.inst(IrCmd::LOAD_TAG, build.vmReg(2)); build.inst(IrCmd::CHECK_TAG, tag1, build.constTag(tnumber), fallback1); build.inst(IrCmd::JUMP, block2); build.beginBlock(fallback1); build.inst(IrCmd::DO_LEN, build.vmReg(1), build.vmReg(2)); build.inst(IrCmd::JUMP, block2); build.beginBlock(block2); IrOp tag2 = build.inst(IrCmd::LOAD_TAG, build.vmReg(2)); build.inst(IrCmd::CHECK_TAG, tag2, build.constTag(tnumber), fallback2); build.inst(IrCmd::JUMP, block3); build.beginBlock(fallback2); build.inst(IrCmd::DO_LEN, build.vmReg(0), build.vmReg(2)); build.inst(IrCmd::JUMP, block3); build.beginBlock(block3); IrOp tag3a = build.inst(IrCmd::LOAD_TAG, build.vmReg(3)); build.inst(IrCmd::JUMP_EQ_TAG, tag3a, build.constTag(tnil), block4a, block4b); build.beginBlock(block4a); build.inst(IrCmd::STORE_TAG, build.vmReg(0), tag3a); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); build.beginBlock(block4b); build.inst(IrCmd::STORE_TAG, build.vmReg(0), tag3a); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); createLinearBlocks(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TAG R2 CHECK_TAG %0, tnumber, bb_fallback_1 JUMP bb_2 bb_fallback_1: DO_LEN R1, R2 JUMP bb_2 bb_2: %5 = LOAD_TAG R2 CHECK_TAG %5, tnumber, bb_fallback_3 JUMP bb_4 bb_fallback_3: DO_LEN R0, R2 JUMP bb_4 bb_4: %10 = LOAD_TAG R3 JUMP_EQ_TAG %10, tnil, bb_5, bb_6 bb_5: STORE_TAG R0, %10 RETURN R0, 0i bb_6: STORE_TAG R0, %10 RETURN R0, 0i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "InfiniteLoopInPathAnalysis") { IrOp block1 = build.block(IrBlockKind::Internal); IrOp block2 = build.block(IrBlockKind::Internal); build.beginBlock(block1); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::JUMP, block2); build.beginBlock(block2); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tboolean)); build.inst(IrCmd::JUMP, block2); updateUseCounts(build.function); constPropInBlockChains(build, true); createLinearBlocks(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber JUMP bb_1 bb_1: STORE_TAG R1, tboolean JUMP bb_1 )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "PartialStoreInvalidation") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(0.5)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); // Should be reloaded build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TVALUE R0 STORE_TVALUE R1, %0 STORE_DOUBLE R0, 0.5 %3 = LOAD_TVALUE R0 STORE_TVALUE R1, %3 STORE_TAG R0, tnumber STORE_SPLIT_TVALUE R1, tnumber, 0.5 RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "VaridicRegisterRangeInvalidation") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tnumber)); build.inst(IrCmd::FALLBACK_GETVARARGS, build.constUint(0), build.vmReg(1), build.constInt(-1)); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tnumber)); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R2, tnumber FALLBACK_GETVARARGS 0u, R1, -1i STORE_TAG R2, tnumber RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "LoadPropagatesOnlyRightType") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(2)); IrOp value1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), value1); IrOp value2 = build.inst(IrCmd::LOAD_INT, build.vmReg(1)); build.inst(IrCmd::STORE_INT, build.vmReg(2), value2); build.inst(IrCmd::RETURN, build.constUint(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_INT R0, 2i %1 = LOAD_DOUBLE R0 STORE_DOUBLE R1, %1 %3 = LOAD_INT R1 STORE_INT R2, %3 RETURN 0u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecks") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); // This roughly corresponds to 'return t.a + t.a' IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); IrOp slot1 = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table1, build.constUint(3), build.vmConst(1)); build.inst(IrCmd::CHECK_SLOT_MATCH, slot1, build.vmConst(1), fallback); IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, slot1, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1); IrOp slot1b = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table1, build.constUint(8), build.vmConst(1)); // This will be removed build.inst(IrCmd::CHECK_SLOT_MATCH, slot1b, build.vmConst(1), fallback); // Key will be replaced with undef here IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, slot1b, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b); IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3)); IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4)); IrOp sum = build.inst(IrCmd::ADD_NUM, a, b); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum); build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); // In the future, we might even see duplicate identical TValue loads go away // In the future, we might even see loads of different VM regs with the same value go away CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 %1 = GET_SLOT_NODE_ADDR %0, 3u, K1 CHECK_SLOT_MATCH %1, K1, bb_fallback_1 %3 = LOAD_TVALUE %1, 0i STORE_TVALUE R3, %3 CHECK_NODE_VALUE %1, bb_fallback_1 %7 = LOAD_TVALUE %1, 0i STORE_TVALUE R4, %7 %9 = LOAD_DOUBLE R3 %10 = LOAD_DOUBLE R4 %11 = ADD_NUM %9, %10 STORE_DOUBLE R2, %11 RETURN R2, 1u bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecksAvoidNil") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); IrOp slot1 = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table1, build.constUint(3), build.vmConst(1)); build.inst(IrCmd::CHECK_SLOT_MATCH, slot1, build.vmConst(1), fallback); IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, slot1, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1); IrOp table2 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(2)); IrOp slot2 = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table2, build.constUint(6), build.vmConst(1)); build.inst(IrCmd::CHECK_SLOT_MATCH, slot2, build.vmConst(1), fallback); build.inst(IrCmd::CHECK_READONLY, table2, fallback); build.inst(IrCmd::STORE_TAG, build.vmReg(4), build.constTag(tnil)); IrOp valueNil = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(4)); build.inst(IrCmd::STORE_TVALUE, slot2, valueNil, build.constInt(0)); // In the future, we might get to track that value became 'nil' and that fallback will be taken IrOp slot1b = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table1, build.constUint(8), build.vmConst(1)); // This will be removed build.inst(IrCmd::CHECK_SLOT_MATCH, slot1b, build.vmConst(1), fallback); // Key will be replaced with undef here IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, slot1b, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1b); IrOp slot2b = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table2, build.constUint(11), build.vmConst(1)); // This will be removed build.inst(IrCmd::CHECK_SLOT_MATCH, slot2b, build.vmConst(1), fallback); // Key will be replaced with undef here build.inst(IrCmd::CHECK_READONLY, table2, fallback); build.inst(IrCmd::STORE_SPLIT_TVALUE, slot2b, build.constTag(tnumber), build.constDouble(1), build.constInt(0)); build.inst(IrCmd::RETURN, build.vmReg(3), build.constUint(2)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(1), build.constUint(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 %1 = GET_SLOT_NODE_ADDR %0, 3u, K1 CHECK_SLOT_MATCH %1, K1, bb_fallback_1 %3 = LOAD_TVALUE %1, 0i STORE_TVALUE R3, %3 %5 = LOAD_POINTER R2 %6 = GET_SLOT_NODE_ADDR %5, 6u, K1 CHECK_SLOT_MATCH %6, K1, bb_fallback_1 CHECK_READONLY %5, bb_fallback_1 STORE_TAG R4, tnil %10 = LOAD_TVALUE R4 STORE_TVALUE %6, %10, 0i CHECK_NODE_VALUE %1, bb_fallback_1 %14 = LOAD_TVALUE %1, 0i STORE_TVALUE R3, %14 CHECK_NODE_VALUE %6, bb_fallback_1 STORE_SPLIT_TVALUE %6, tnumber, 1, 0i RETURN R3, 2u bb_fallback_1: RETURN R1, 2u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecksInvalidation") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); // This roughly corresponds to 'return t.a + t.a' with a stange GC assist in the middle IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); IrOp slot1 = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table1, build.constUint(3), build.vmConst(1)); build.inst(IrCmd::CHECK_SLOT_MATCH, slot1, build.vmConst(1), fallback); IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, slot1, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1); build.inst(IrCmd::CHECK_GC); IrOp slot1b = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table1, build.constUint(8), build.vmConst(1)); build.inst(IrCmd::CHECK_SLOT_MATCH, slot1b, build.vmConst(1), fallback); IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, slot1b, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b); IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3)); IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4)); IrOp sum = build.inst(IrCmd::ADD_NUM, a, b); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum); build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); // In the future, we might even see duplicate identical TValue loads go away // In the future, we might even see loads of different VM regs with the same value go away CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 %1 = GET_SLOT_NODE_ADDR %0, 3u, K1 CHECK_SLOT_MATCH %1, K1, bb_fallback_1 %3 = LOAD_TVALUE %1, 0i STORE_TVALUE R3, %3 CHECK_GC %6 = GET_SLOT_NODE_ADDR %0, 8u, K1 CHECK_SLOT_MATCH %6, K1, bb_fallback_1 %8 = LOAD_TVALUE %6, 0i STORE_TVALUE R4, %8 %10 = LOAD_DOUBLE R3 %11 = LOAD_DOUBLE R4 %12 = ADD_NUM %10, %11 STORE_DOUBLE R2, %12 RETURN R2, 1u bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksSameIndex") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); // This roughly corresponds to 'return t[1] + t[1]' IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); // This will be removed IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); // And this will be substituted IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b); IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3)); IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4)); IrOp sum = build.inst(IrCmd::ADD_NUM, a, b); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum); build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); // In the future, we might even see duplicate identical TValue loads go away // In the future, we might even see loads of different VM regs with the same value go away CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 CHECK_ARRAY_SIZE %0, 0i, bb_fallback_1 %2 = GET_ARR_ADDR %0, 0i %3 = LOAD_TVALUE %2, 0i STORE_TVALUE R3, %3 %7 = LOAD_TVALUE %2, 0i STORE_TVALUE R4, %7 %9 = LOAD_DOUBLE R3 %10 = LOAD_DOUBLE R4 %11 = ADD_NUM %9, %10 STORE_DOUBLE R2, %11 RETURN R2, 1u bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksSameValue") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); // This roughly corresponds to 'return t[i] + t[i]' IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); IrOp index = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2)); IrOp validIndex = build.inst(IrCmd::TRY_NUM_TO_INDEX, index, fallback); IrOp validOffset = build.inst(IrCmd::SUB_INT, validIndex, build.constInt(1)); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, validOffset, fallback); IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1); IrOp validIndex2 = build.inst(IrCmd::TRY_NUM_TO_INDEX, index, fallback); IrOp validOffset2 = build.inst(IrCmd::SUB_INT, validIndex2, build.constInt(1)); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, validOffset2, fallback); // This will be removed IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); // And this will be substituted IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b); IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3)); IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4)); IrOp sum = build.inst(IrCmd::ADD_NUM, a, b); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum); build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); // In the future, we might even see duplicate identical TValue loads go away // In the future, we might even see loads of different VM regs with the same value go away CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 %1 = LOAD_DOUBLE R2 %2 = TRY_NUM_TO_INDEX %1, bb_fallback_1 %3 = SUB_INT %2, 1i CHECK_ARRAY_SIZE %0, %3, bb_fallback_1 %5 = GET_ARR_ADDR %0, 0i %6 = LOAD_TVALUE %5, 0i STORE_TVALUE R3, %6 %12 = LOAD_TVALUE %5, 0i STORE_TVALUE R4, %12 %14 = LOAD_DOUBLE R3 %15 = LOAD_DOUBLE R4 %16 = ADD_NUM %14, %15 STORE_DOUBLE R2, %16 RETURN R2, 1u bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksLowerIndex") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); // This roughly corresponds to 'return t[2] + t[1]' IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(1), fallback); IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(1)); IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); // This will be removed IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b); IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3)); IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4)); IrOp sum = build.inst(IrCmd::ADD_NUM, a, b); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum); build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 CHECK_ARRAY_SIZE %0, 1i, bb_fallback_1 %2 = GET_ARR_ADDR %0, 1i %3 = LOAD_TVALUE %2, 0i STORE_TVALUE R3, %3 %6 = GET_ARR_ADDR %0, 0i %7 = LOAD_TVALUE %6, 0i STORE_TVALUE R4, %7 %9 = LOAD_DOUBLE R3 %10 = LOAD_DOUBLE R4 %11 = ADD_NUM %9, %10 STORE_DOUBLE R2, %11 RETURN R2, 1u bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksInvalidations") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); // This roughly corresponds to 'return t[1] + t[1]' with a strange table.insert in the middle IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1); build.inst(IrCmd::TABLE_SETNUM, table1, build.constInt(2)); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); // This will be removed IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); // And this will be substituted IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b); IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3)); IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4)); IrOp sum = build.inst(IrCmd::ADD_NUM, a, b); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum); build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 CHECK_ARRAY_SIZE %0, 0i, bb_fallback_1 %2 = GET_ARR_ADDR %0, 0i %3 = LOAD_TVALUE %2, 0i STORE_TVALUE R3, %3 %5 = TABLE_SETNUM %0, 2i CHECK_ARRAY_SIZE %0, 0i, bb_fallback_1 %7 = GET_ARR_ADDR %0, 0i %8 = LOAD_TVALUE %7, 0i STORE_TVALUE R4, %8 %10 = LOAD_DOUBLE R3 %11 = LOAD_DOUBLE R4 %12 = ADD_NUM %10, %11 STORE_DOUBLE R2, %12 RETURN R2, 1u bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ArrayElemChecksNegativeIndex") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); // This roughly corresponds to 'return t[1] + t[0]' IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(0), fallback); IrOp elem1 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(0)); IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, elem1, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1); build.inst(IrCmd::CHECK_ARRAY_SIZE, table1, build.constInt(-1), fallback); // This will jump directly to fallback IrOp elem2 = build.inst(IrCmd::GET_ARR_ADDR, table1, build.constInt(-1)); IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, elem2, build.constInt(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b); IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3)); IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4)); IrOp sum = build.inst(IrCmd::ADD_NUM, a, b); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum); build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R1 CHECK_ARRAY_SIZE %0, 0i, bb_fallback_1 %2 = GET_ARR_ADDR %0, 0i %3 = LOAD_TVALUE %2, 0i STORE_TVALUE R3, %3 JUMP bb_fallback_1 bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateBufferLengthChecks") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp sourceBuf = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), sourceBuf); IrOp buffer1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(2)); build.inst(IrCmd::CHECK_BUFFER_LEN, buffer1, build.constInt(12), build.constInt(4), fallback); build.inst(IrCmd::BUFFER_WRITEI32, buffer1, build.constInt(12), build.constInt(32)); // Now with lower index, should be removed build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), sourceBuf); IrOp buffer2 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(2)); build.inst(IrCmd::CHECK_BUFFER_LEN, buffer2, build.constInt(8), build.constInt(4), fallback); build.inst(IrCmd::BUFFER_WRITEI32, buffer2, build.constInt(8), build.constInt(30)); // Now with higher index, should raise the initial check bound build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), sourceBuf); IrOp buffer3 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(2)); build.inst(IrCmd::CHECK_BUFFER_LEN, buffer3, build.constInt(16), build.constInt(4), fallback); build.inst(IrCmd::BUFFER_WRITEI32, buffer3, build.constInt(16), build.constInt(60)); // Now with different access size, should not reuse previous checks (can be improved in the future) build.inst(IrCmd::CHECK_BUFFER_LEN, buffer3, build.constInt(16), build.constInt(2), fallback); build.inst(IrCmd::BUFFER_WRITEI16, buffer3, build.constInt(16), build.constInt(55)); // Now with same, but unknown index value IrOp index = build.inst(IrCmd::LOAD_INT, build.vmReg(1)); build.inst(IrCmd::CHECK_BUFFER_LEN, buffer3, index, build.constInt(2), fallback); build.inst(IrCmd::BUFFER_WRITEI16, buffer3, index, build.constInt(1)); build.inst(IrCmd::CHECK_BUFFER_LEN, buffer3, index, build.constInt(2), fallback); build.inst(IrCmd::BUFFER_WRITEI16, buffer3, index, build.constInt(2)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TVALUE R0 STORE_TVALUE R2, %0 %2 = LOAD_POINTER R2 CHECK_BUFFER_LEN %2, 16i, 4i, bb_fallback_1 BUFFER_WRITEI32 %2, 12i, 32i BUFFER_WRITEI32 %2, 8i, 30i BUFFER_WRITEI32 %2, 16i, 60i CHECK_BUFFER_LEN %2, 16i, 2i, bb_fallback_1 BUFFER_WRITEI16 %2, 16i, 55i %15 = LOAD_INT R1 CHECK_BUFFER_LEN %2, %15, 2i, bb_fallback_1 BUFFER_WRITEI16 %2, %15, 1i BUFFER_WRITEI16 %2, %15, 2i RETURN R1, 1u bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "BufferLenghtChecksNegativeIndex") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp sourceBuf = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), sourceBuf); IrOp buffer1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(2)); build.inst(IrCmd::CHECK_BUFFER_LEN, buffer1, build.constInt(-4), build.constInt(4), fallback); build.inst(IrCmd::BUFFER_WRITEI32, buffer1, build.constInt(-4), build.constInt(32)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constUint(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_TVALUE R0 STORE_TVALUE R2, %0 JUMP bb_fallback_1 bb_fallback_1: RETURN R0, 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TagVectorSkipErrorFix") { IrOp block = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp a = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)); IrOp b = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1)); IrOp mul = build.inst(IrCmd::TAG_VECTOR, build.inst(IrCmd::MUL_VEC, a, b)); IrOp t1 = build.inst(IrCmd::TAG_VECTOR, build.inst(IrCmd::ADD_VEC, mul, mul)); IrOp t2 = build.inst(IrCmd::TAG_VECTOR, build.inst(IrCmd::SUB_VEC, mul, mul)); IrOp t3 = build.inst(IrCmd::TAG_VECTOR, build.inst(IrCmd::DIV_VEC, t1, build.inst(IrCmd::UNM_VEC, t2))); build.inst(IrCmd::STORE_TVALUE, build.vmReg(0), t3); build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::Yes) == R"( bb_0: ; useCount: 0 %0 = LOAD_TVALUE R0 ; useCount: 1, lastUse: %0 %1 = LOAD_TVALUE R1 ; useCount: 1, lastUse: %0 %2 = MUL_VEC %0, %1 ; useCount: 4, lastUse: %0 %4 = ADD_VEC %2, %2 ; useCount: 1, lastUse: %0 %6 = SUB_VEC %2, %2 ; useCount: 1, lastUse: %0 %8 = UNM_VEC %6 ; useCount: 1, lastUse: %0 %9 = DIV_VEC %4, %8 ; useCount: 1, lastUse: %0 %10 = TAG_VECTOR %9 ; useCount: 1, lastUse: %0 STORE_TVALUE R0, %10 ; %11 RETURN R0, 1u ; %12 )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ForgprepInvalidation") { IrOp block = build.block(IrBlockKind::Internal); IrOp followup = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp tbl = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); build.inst(IrCmd::CHECK_READONLY, tbl, build.vmExit(1)); build.inst(IrCmd::FALLBACK_FORGPREP, build.constUint(2), build.vmReg(1), followup); build.beginBlock(followup); build.inst(IrCmd::CHECK_READONLY, tbl, build.vmExit(2)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(3)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1 ; in regs: R0, R1, R2, R3 ; out regs: R1, R2, R3 %0 = LOAD_POINTER R0 CHECK_READONLY %0, exit(1) FALLBACK_FORGPREP 2u, R1, bb_1 bb_1: ; predecessors: bb_0 ; in regs: R1, R2, R3 CHECK_READONLY %0, exit(2) RETURN R1, 3i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FastCallEffects1") { ScopedFastFlag luauCodegenFastcall3{FFlag::LuauCodegenFastcall3, true}; IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::FASTCALL, build.constUint(LBF_MATH_FREXP), build.vmReg(1), build.vmReg(2), build.constInt(2)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(1)), build.constTag(tnumber), build.vmExit(1)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(2)), build.constTag(tnumber), build.vmExit(1)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R2 FASTCALL 14u, R1, R2, 2i RETURN R1, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FastCallEffects2") { ScopedFastFlag luauCodegenFastcall3{FFlag::LuauCodegenFastcall3, true}; IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::FASTCALL, build.constUint(LBF_MATH_MODF), build.vmReg(1), build.vmReg(2), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(1)), build.constTag(tnumber), build.vmExit(1)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(2)), build.constTag(tnumber), build.vmExit(1)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R2 FASTCALL 20u, R1, R2, 1i %3 = LOAD_TAG R2 CHECK_TAG %3, tnumber, exit(1) RETURN R1, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "InferNumberTagFromLimitedContext") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(2.0)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(ttable), build.vmExit(1)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_DOUBLE R0, 2 JUMP exit(1) )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore1") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(ttable), build.vmExit(1)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_INT R0, 1i %1 = LOAD_TAG R0 CHECK_TAG %1, ttable, exit(1) %3 = LOAD_TVALUE R0 STORE_TVALUE R1, %3 RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore2") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(1)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), build.vmExit(1)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_INT R0, 1i %1 = LOAD_TAG R0 CHECK_TAG %1, tnumber, exit(1) %3 = LOAD_TVALUE R0 STORE_TVALUE R1, %3 RETURN R1, 1i )"); } TEST_SUITE_END(); TEST_SUITE_BEGIN("Analysis"); TEST_CASE_FIXTURE(IrBuilderFixture, "SimpleDiamond") { IrOp entry = build.block(IrBlockKind::Internal); IrOp a = build.block(IrBlockKind::Internal); IrOp b = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::JUMP_EQ_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), a, b); build.beginBlock(a); build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); build.inst(IrCmd::JUMP, exit); build.beginBlock(b); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1, bb_2 ; in regs: R0, R1, R2, R3 ; out regs: R1, R2, R3 %0 = LOAD_TAG R0 JUMP_EQ_TAG %0, tnumber, bb_1, bb_2 bb_1: ; predecessors: bb_0 ; successors: bb_3 ; in regs: R1, R3 ; out regs: R2, R3 %2 = LOAD_TVALUE R1 STORE_TVALUE R2, %2 JUMP bb_3 bb_2: ; predecessors: bb_0 ; successors: bb_3 ; in regs: R1, R2 ; out regs: R2, R3 %5 = LOAD_TVALUE R1 STORE_TVALUE R3, %5 JUMP bb_3 bb_3: ; predecessors: bb_1, bb_2 ; in regs: R2, R3 RETURN R2, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ImplicitFixedRegistersInVarargCall") { IrOp entry = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::FALLBACK_GETVARARGS, build.constUint(0), build.vmReg(3), build.constInt(-1)); build.inst(IrCmd::CALL, build.vmReg(0), build.constInt(-1), build.constInt(5)); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(5)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1 ; in regs: R0, R1, R2 ; out regs: R0, R1, R2, R3, R4 FALLBACK_GETVARARGS 0u, R3, -1i CALL R0, -1i, 5i JUMP bb_1 bb_1: ; predecessors: bb_0 ; in regs: R0, R1, R2, R3, R4 RETURN R0, 5i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ExplicitUseOfRegisterInVarargSequence") { ScopedFastFlag luauCodegenInstG{FFlag::LuauCodegenInstG, true}; ScopedFastFlag luauCodegenFastcall3{FFlag::LuauCodegenFastcall3, true}; IrOp entry = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::FALLBACK_GETVARARGS, build.constUint(0), build.vmReg(1), build.constInt(-1)); IrOp results = build.inst(IrCmd::INVOKE_FASTCALL, build.constUint(0), build.vmReg(0), build.vmReg(1), build.vmReg(2), build.undef(), build.constInt(-1), build.constInt(-1)); build.inst(IrCmd::ADJUST_STACK_TO_REG, build.vmReg(0), results); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(-1)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1 ; out regs: R0... FALLBACK_GETVARARGS 0u, R1, -1i %1 = INVOKE_FASTCALL 0u, R0, R1, R2, undef, -1i, -1i ADJUST_STACK_TO_REG R0, %1 JUMP bb_1 bb_1: ; predecessors: bb_0 ; in regs: R0... RETURN R0, -1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "VariadicSequenceRestart") { IrOp entry = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::CALL, build.vmReg(1), build.constInt(0), build.constInt(-1)); build.inst(IrCmd::CALL, build.vmReg(0), build.constInt(-1), build.constInt(-1)); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(-1)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1 ; in regs: R0, R1 ; out regs: R0... CALL R1, 0i, -1i CALL R0, -1i, -1i JUMP bb_1 bb_1: ; predecessors: bb_0 ; in regs: R0... RETURN R0, -1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FallbackDoesNotFlowUp") { IrOp entry = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::FALLBACK_GETVARARGS, build.constUint(0), build.vmReg(1), build.constInt(-1)); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), fallback); build.inst(IrCmd::CALL, build.vmReg(0), build.constInt(-1), build.constInt(-1)); build.inst(IrCmd::JUMP, exit); build.beginBlock(fallback); build.inst(IrCmd::CALL, build.vmReg(0), build.constInt(-1), build.constInt(-1)); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(-1)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_fallback_1, bb_2 ; in regs: R0 ; out regs: R0... FALLBACK_GETVARARGS 0u, R1, -1i %1 = LOAD_TAG R0 CHECK_TAG %1, tnumber, bb_fallback_1 CALL R0, -1i, -1i JUMP bb_2 bb_fallback_1: ; predecessors: bb_0 ; successors: bb_2 ; in regs: R0, R1... ; out regs: R0... CALL R0, -1i, -1i JUMP bb_2 bb_2: ; predecessors: bb_0, bb_fallback_1 ; in regs: R0... RETURN R0, -1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "VariadicSequencePeeling") { IrOp entry = build.block(IrBlockKind::Internal); IrOp a = build.block(IrBlockKind::Internal); IrOp b = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::FALLBACK_GETVARARGS, build.constUint(0), build.vmReg(3), build.constInt(-1)); build.inst(IrCmd::JUMP_EQ_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), a, b); build.beginBlock(a); build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); build.inst(IrCmd::JUMP, exit); build.beginBlock(b); build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(-1)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1, bb_2 ; in regs: R0, R1 ; out regs: R0, R1, R3... FALLBACK_GETVARARGS 0u, R3, -1i %1 = LOAD_TAG R0 JUMP_EQ_TAG %1, tnumber, bb_1, bb_2 bb_1: ; predecessors: bb_0 ; successors: bb_3 ; in regs: R0, R3... ; out regs: R2... %3 = LOAD_TVALUE R0 STORE_TVALUE R2, %3 JUMP bb_3 bb_2: ; predecessors: bb_0 ; successors: bb_3 ; in regs: R1, R3... ; out regs: R2... %6 = LOAD_TVALUE R1 STORE_TVALUE R2, %6 JUMP bb_3 bb_3: ; predecessors: bb_1, bb_2 ; in regs: R2... RETURN R2, -1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "BuiltinVariadicStart") { IrOp entry = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.constDouble(2.0)); build.inst(IrCmd::ADJUST_STACK_TO_REG, build.vmReg(2), build.constInt(1)); build.inst(IrCmd::CALL, build.vmReg(1), build.constInt(-1), build.constInt(1)); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1 ; in regs: R0 ; out regs: R0, R1 STORE_DOUBLE R1, 1 STORE_DOUBLE R2, 2 ADJUST_STACK_TO_REG R2, 1i CALL R1, -1i, 1i JUMP bb_1 bb_1: ; predecessors: bb_0 ; in regs: R0, R1 RETURN R0, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "ForgprepImplicitUse") { IrOp entry = build.block(IrBlockKind::Internal); IrOp direct = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.constDouble(10.0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(3), build.constDouble(1.0)); IrOp tag = build.inst(IrCmd::LOAD_TAG, build.vmReg(0)); build.inst(IrCmd::JUMP_EQ_TAG, tag, build.constTag(tnumber), direct, fallback); build.beginBlock(direct); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); build.beginBlock(fallback); build.inst(IrCmd::FALLBACK_FORGPREP, build.constUint(0), build.vmReg(1), exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(3)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1, bb_2 ; in regs: R0 ; out regs: R0, R1, R2, R3 STORE_DOUBLE R1, 1 STORE_DOUBLE R2, 10 STORE_DOUBLE R3, 1 %3 = LOAD_TAG R0 JUMP_EQ_TAG %3, tnumber, bb_1, bb_2 bb_1: ; predecessors: bb_0 ; in regs: R0 RETURN R0, 1i bb_2: ; predecessors: bb_0 ; successors: bb_3 ; in regs: R1, R2, R3 ; out regs: R1, R2, R3 FALLBACK_FORGPREP 0u, R1, bb_3 bb_3: ; predecessors: bb_2 ; in regs: R1, R2, R3 RETURN R1, 3i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "SetTable") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::SET_TABLE, build.vmReg(0), build.vmReg(1), build.constUint(1)); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0, R1 SET_TABLE R0, R1, 1u RETURN R0, 1i )"); } // 'A Simple, Fast Dominance Algorithm' [Keith D. Cooper, et al]. Figure 2. TEST_CASE_FIXTURE(IrBuilderFixture, "DominanceVerification1") { defineCfgTree({{1, 2}, {3}, {4}, {4}, {3}}); CHECK(build.function.cfg.idoms == std::vector{{~0u, 0, 0, 0, 0}}); } // 'A Linear Time Algorithm for Placing Phi-Nodes' [Vugranam C.Sreedhar]. Figure 1. TEST_CASE_FIXTURE(IrBuilderFixture, "DominanceVerification2") { defineCfgTree({{1, 16}, {2, 3, 4}, {4, 7}, {9}, {5}, {6}, {2, 8}, {8}, {7, 15}, {10, 11}, {12}, {12}, {13}, {3, 14, 15}, {12}, {16}, {}}); CHECK(build.function.cfg.idoms == std::vector{~0u, 0, 1, 1, 1, 4, 5, 1, 1, 3, 9, 9, 9, 12, 13, 1, 0}); } // 'A Linear Time Algorithm for Placing Phi-Nodes' [Vugranam C.Sreedhar]. Figure 4. TEST_CASE_FIXTURE(IrBuilderFixture, "DominanceVerification3") { defineCfgTree({{1, 2}, {3}, {3, 4}, {5}, {5, 6}, {7}, {7}, {}}); CHECK(build.function.cfg.idoms == std::vector{~0u, 0, 0, 0, 2, 0, 4, 0}); } // 'Static Single Assignment Book' Figure 4.1 TEST_CASE_FIXTURE(IrBuilderFixture, "DominanceVerification4") { defineCfgTree({{1}, {2, 10}, {3, 7}, {4}, {5}, {4, 6}, {1}, {8}, {5, 9}, {7}, {}}); IdfContext ctx; computeIteratedDominanceFrontierForDefs(ctx, build.function, {0, 2, 3, 6}, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}); CHECK(ctx.idf == std::vector{1, 4, 5}); } // 'Static Single Assignment Book' Figure 4.5 TEST_CASE_FIXTURE(IrBuilderFixture, "DominanceVerification4") { defineCfgTree({{1}, {2}, {3, 7}, {4, 5}, {6}, {6}, {8}, {8}, {9}, {10, 11}, {11}, {9, 12}, {2}}); IdfContext ctx; computeIteratedDominanceFrontierForDefs(ctx, build.function, {4, 5, 7, 12}, {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}); CHECK(ctx.idf == std::vector{2, 6, 8}); // Pruned form, when variable is only live-in in limited set of blocks computeIteratedDominanceFrontierForDefs(ctx, build.function, {4, 5, 7, 12}, {6, 8, 9}); CHECK(ctx.idf == std::vector{6, 8}); } TEST_SUITE_END(); TEST_SUITE_BEGIN("ValueNumbering"); TEST_CASE_FIXTURE(IrBuilderFixture, "RemoveDuplicateCalculation") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp op2 = build.inst(IrCmd::UNM_NUM, op1); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), op2); IrOp op3 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); // Load propagation is tested here IrOp op4 = build.inst(IrCmd::UNM_NUM, op3); // And allows value numbering to trigger here build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), op4); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_DOUBLE R0 %1 = UNM_NUM %0 STORE_DOUBLE R1, %1 STORE_DOUBLE R2, %1 RETURN R1, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "LateTableStateLink") { IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(block); IrOp tmp = build.inst(IrCmd::DUP_TABLE, build.vmReg(0)); build.inst(IrCmd::STORE_POINTER, build.vmReg(0), tmp); // Late tmp -> R0 link is tested here IrOp table = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); // Store to load propagation test build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::CHECK_NO_METATABLE, table, fallback); build.inst(IrCmd::CHECK_READONLY, table, fallback); build.inst(IrCmd::RETURN, build.constUint(0)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.constUint(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = DUP_TABLE R0 STORE_POINTER R0, %0 CHECK_NO_METATABLE %0, bb_fallback_1 CHECK_READONLY %0, bb_fallback_1 RETURN 0u bb_fallback_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "RegisterVersioning") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp op2 = build.inst(IrCmd::UNM_NUM, op1); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), op2); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); // Doesn't prevent previous store propagation IrOp op3 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); // No longer 'op1' IrOp op4 = build.inst(IrCmd::UNM_NUM, op3); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), op4); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_DOUBLE R0 %1 = UNM_NUM %0 STORE_DOUBLE R0, %1 STORE_TAG R0, tnumber %5 = UNM_NUM %1 STORE_DOUBLE R1, %5 RETURN R0, 2i )"); } // This can be relaxed in the future when SETLIST becomes aware of register allocator TEST_CASE_FIXTURE(IrBuilderFixture, "SetListIsABlocker") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); build.inst(IrCmd::SETLIST); IrOp op2 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp sum = build.inst(IrCmd::ADD_NUM, op1, op2); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), sum); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_DOUBLE R0 SETLIST %2 = LOAD_DOUBLE R0 %3 = ADD_NUM %0, %2 STORE_DOUBLE R0, %3 RETURN R0, 1i )"); } // Luau call will reuse the same stack and spills will be lost // However, in the future we might propagate values that can be rematerialized TEST_CASE_FIXTURE(IrBuilderFixture, "CallIsABlocker") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); build.inst(IrCmd::CALL, build.vmReg(1), build.constInt(1), build.vmReg(2), build.constInt(1)); IrOp op2 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp sum = build.inst(IrCmd::ADD_NUM, op1, op2); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), sum); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_DOUBLE R0 CALL R1, 1i, R2, 1i %2 = LOAD_DOUBLE R0 %3 = ADD_NUM %0, %2 STORE_DOUBLE R1, %3 RETURN R1, 2i )"); } // While constant propagation correctly versions captured registers, IrValueLocationTracking doesn't (yet) TEST_CASE_FIXTURE(IrBuilderFixture, "NoPropagationOfCapturedRegs") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::CAPTURE, build.vmReg(0), build.constUint(1)); IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp op2 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp sum = build.inst(IrCmd::ADD_NUM, op1, op2); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), sum); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( ; captured regs: R0 bb_0: ; in regs: R0 CAPTURE R0, 1u %1 = LOAD_DOUBLE R0 %2 = LOAD_DOUBLE R0 %3 = ADD_NUM %1, %2 STORE_DOUBLE R1, %3 RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "NoDeadLoadReuse") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp op1i = build.inst(IrCmd::NUM_TO_INT, op1); IrOp res = build.inst(IrCmd::BITAND_UINT, op1i, build.constInt(0)); IrOp resd = build.inst(IrCmd::INT_TO_NUM, res); IrOp op2 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp sum = build.inst(IrCmd::ADD_NUM, resd, op2); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), sum); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %4 = LOAD_DOUBLE R0 %5 = ADD_NUM 0, %4 STORE_DOUBLE R1, %5 RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "NoDeadValueReuse") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp op1i = build.inst(IrCmd::NUM_TO_INT, op1); IrOp res = build.inst(IrCmd::BITAND_UINT, op1i, build.constInt(0)); IrOp op2i = build.inst(IrCmd::NUM_TO_INT, op1); IrOp sum = build.inst(IrCmd::ADD_INT, res, op2i); IrOp resd = build.inst(IrCmd::INT_TO_NUM, sum); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), resd); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_DOUBLE R0 %3 = NUM_TO_INT %0 %4 = ADD_INT 0i, %3 %5 = INT_TO_NUM %4 STORE_DOUBLE R1, %5 RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TValueLoadToSplitStore") { IrOp entry = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); build.beginBlock(entry); IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp op1v2 = build.inst(IrCmd::ADD_NUM, op1, build.constDouble(4.0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), op1v2); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); // Check that this TValue store will be replaced by a split store IrOp tv = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), tv); // Check that tag and value can be extracted from R2 now (removing the fallback) IrOp tag2 = build.inst(IrCmd::LOAD_TAG, build.vmReg(2)); build.inst(IrCmd::CHECK_TAG, tag2, build.constTag(tnumber), fallback); IrOp op2 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(3), op2); build.inst(IrCmd::STORE_TAG, build.vmReg(3), build.constTag(tnumber)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); build.beginBlock(fallback); build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_DOUBLE R0 %1 = ADD_NUM %0, 4 STORE_DOUBLE R1, %1 STORE_TAG R1, tnumber STORE_SPLIT_TVALUE R2, tnumber, %1 STORE_DOUBLE R3, %1 STORE_TAG R3, tnumber RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TagStoreUpdatesValueVersion") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp op1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), op1); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tstring)); IrOp str = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); build.inst(IrCmd::STORE_POINTER, build.vmReg(2), str); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tstring)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %0 = LOAD_POINTER R0 STORE_POINTER R1, %0 STORE_TAG R1, tstring STORE_POINTER R2, %0 STORE_TAG R2, tstring RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TagStoreUpdatesSetUpval") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(0.5)); build.inst(IrCmd::SET_UPVALUE, build.vmUpvalue(0), build.vmReg(0), build.undef()); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_TAG R0, tnumber STORE_DOUBLE R0, 0.5 SET_UPVALUE U0, R0, tnumber RETURN R0, 0i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TagSelfEqualityCheckRemoval") { IrOp entry = build.block(IrBlockKind::Internal); IrOp trueBlock = build.block(IrBlockKind::Internal); IrOp falseBlock = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp tag1 = build.inst(IrCmd::LOAD_TAG, build.vmReg(0)); IrOp tag2 = build.inst(IrCmd::LOAD_TAG, build.vmReg(0)); build.inst(IrCmd::JUMP_EQ_TAG, tag1, tag2, trueBlock, falseBlock); build.beginBlock(trueBlock); build.inst(IrCmd::RETURN, build.constUint(1)); build.beginBlock(falseBlock); build.inst(IrCmd::RETURN, build.constUint(2)); updateUseCounts(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: JUMP bb_1 bb_1: RETURN 1u )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "TaggedValuePropagationIntoTvalueChecksRegisterVersion") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp a1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0)); IrOp b1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(1)); IrOp sum1 = build.inst(IrCmd::ADD_NUM, a1, b1); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(7), sum1); build.inst(IrCmd::STORE_TAG, build.vmReg(7), build.constTag(tnumber)); IrOp a2 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(2)); IrOp b2 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3)); IrOp sum2 = build.inst(IrCmd::ADD_NUM, a2, b2); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(8), sum2); build.inst(IrCmd::STORE_TAG, build.vmReg(8), build.constTag(tnumber)); IrOp old7 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(7), build.constInt(0), build.constTag(tnumber)); IrOp old8 = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(8), build.constInt(0), build.constTag(tnumber)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(8), old7); // Invalidate R8 build.inst(IrCmd::STORE_TVALUE, build.vmReg(9), old8); // Old R8 cannot be substituted as it was invalidated build.inst(IrCmd::RETURN, build.vmReg(8), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0, R1, R2, R3 %0 = LOAD_DOUBLE R0 %1 = LOAD_DOUBLE R1 %2 = ADD_NUM %0, %1 STORE_DOUBLE R7, %2 STORE_TAG R7, tnumber %5 = LOAD_DOUBLE R2 %6 = LOAD_DOUBLE R3 %7 = ADD_NUM %5, %6 STORE_DOUBLE R8, %7 STORE_TAG R8, tnumber %11 = LOAD_TVALUE R8, 0i, tnumber STORE_SPLIT_TVALUE R8, tnumber, %2 STORE_TVALUE R9, %11 RETURN R8, 2i )"); } TEST_SUITE_END(); TEST_SUITE_BEGIN("DeadStoreRemoval"); TEST_CASE_FIXTURE(IrBuilderFixture, "SimpleDoubleStore") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(2.0)); // Should remove previous store build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), build.constDouble(1.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tnumber)); build.inst(IrCmd::STORE_INT, build.vmReg(2), build.constInt(4)); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tboolean)); // Should remove previous store of different type build.inst(IrCmd::STORE_TAG, build.vmReg(3), build.constTag(tnil)); build.inst(IrCmd::STORE_TAG, build.vmReg(3), build.constTag(tnumber)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(3), build.constDouble(4.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(4), build.constTag(tnil)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(4), build.constDouble(1.0)); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(4), build.constTag(tnumber), build.constDouble(2.0)); // Should remove two previous stores IrOp someTv = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)); build.inst(IrCmd::STORE_TAG, build.vmReg(5), build.constTag(tnil)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(5), build.constDouble(1.0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(5), someTv); // Should remove two previous stores build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(5)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0 STORE_SPLIT_TVALUE R1, tnumber, 2 STORE_SPLIT_TVALUE R2, tboolean, 4i STORE_TAG R3, tnumber STORE_DOUBLE R3, 4 STORE_SPLIT_TVALUE R4, tnumber, 2 %13 = LOAD_TVALUE R0 STORE_TVALUE R5, %13 RETURN R1, 5i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "UnusedAtReturn") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::STORE_INT, build.vmReg(2), build.constInt(4)); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tboolean)); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(4), build.constTag(tnumber), build.constDouble(2.0)); IrOp someTv = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(5), someTv); build.inst(IrCmd::STORE_TAG, build.vmReg(6), build.constTag(tnil)); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0 RETURN R0, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "UnusedAtReturnPartial") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::STORE_INT, build.vmReg(2), build.constInt(4)); build.inst(IrCmd::STORE_TAG, build.vmReg(3), build.constTag(tnumber)); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // Partial stores cannot be removed, even if unused // Existance of an unpaired partial store means that the other valid part is a block live in (even if not present is this test) CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0 STORE_DOUBLE R1, 1 STORE_INT R2, 4i STORE_TAG R3, tnumber RETURN R0, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse1") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp somePtr = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), somePtr); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); build.inst(IrCmd::CALL, build.vmReg(2), build.constInt(0), build.constInt(1)); build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0, R2 %0 = LOAD_POINTER R0 STORE_POINTER R1, %0 STORE_TAG R1, ttable CALL R2, 0i, 1i RETURN R2, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse2") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp somePtrA = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), somePtrA); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); build.inst(IrCmd::CALL, build.vmReg(2), build.constInt(0), build.constInt(1)); IrOp somePtrB = build.inst(IrCmd::LOAD_POINTER, build.vmReg(2)); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), somePtrB); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // Stores to pointers can be safely removed at 'return' point, but have to preserved for any GC assist trigger (such as a call) CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0, R2 %0 = LOAD_POINTER R0 STORE_POINTER R1, %0 STORE_TAG R1, ttable CALL R2, 0i, 1i RETURN R2, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse3") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); IrOp somePtrA = build.inst(IrCmd::LOAD_POINTER, build.vmReg(0)); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), somePtrA); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); IrOp someTv = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(2)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), someTv); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // Stores to pointers can be safely removed if there are no potential implicit uses by any GC assists CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; in regs: R0, R2 %3 = LOAD_TVALUE R2 STORE_TVALUE R1, %3 RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse4") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(1.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnil)); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // It is important for tag overwrite to TNIL to kill not only the previous tag store, but the value as well // This is important in a following scenario: // - R0 might have been a GCO on entry to bb_0 // - R0 is overwritten by a number // - Stack is visited by GC assist // - R0 is overwritten by nil // If only number tag write would have been killed, there will be a GCO tag with a double value on stack CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: CHECK_GC STORE_TAG R0, tnil RETURN R0, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "PartialVsFullStoresWithRecombination") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::STORE_TVALUE, build.vmReg(0), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: STORE_SPLIT_TVALUE R0, tnumber, 1 RETURN R0, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "IgnoreFastcallAdjustment") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(-1.0)); build.inst(IrCmd::ADJUST_STACK_TO_REG, build.vmReg(1), build.constInt(1)); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ADJUST_STACK_TO_REG R1, 1i STORE_SPLIT_TVALUE R1, tnumber, 1 RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "JumpImplicitLiveOut") { IrOp entry = build.block(IrBlockKind::Internal); IrOp next = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::JUMP, next); build.beginBlock(next); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // Even though bb_0 doesn't have R1 as a live out, chain optimization used the knowledge of those writes happening to optimize duplicate stores CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1 STORE_TAG R1, tnumber STORE_DOUBLE R1, 1 JUMP bb_1 bb_1: ; predecessors: bb_0 RETURN R1, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "KeepCapturedRegisterStores") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::CAPTURE, build.vmReg(1), build.constUint(1)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::DO_ARITH, build.vmReg(0), build.vmReg(2), build.vmReg(3), build.constInt(0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(-1.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::DO_ARITH, build.vmReg(1), build.vmReg(4), build.vmReg(5), build.constInt(0)); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // Captured registers may be modified from called user functions (plain or hidden in metamethods) CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( ; captured regs: R1 bb_0: ; in regs: R1, R2, R3, R4, R5 CAPTURE R1, 1u STORE_DOUBLE R1, 1 STORE_TAG R1, tnumber DO_ARITH R0, R2, R3, 0i STORE_DOUBLE R1, -1 STORE_TAG R1, tnumber DO_ARITH R1, R4, R5, 0i RETURN R0, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "StoreCannotBeReplacedWithCheck") { ScopedFastFlag debugLuauAbortingChecks{FFlag::DebugLuauAbortingChecks, true}; IrOp block = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); IrOp last = build.block(IrBlockKind::Internal); build.beginBlock(block); IrOp ptr = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); build.inst(IrCmd::STORE_POINTER, build.vmReg(2), ptr); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(ttable)); build.inst(IrCmd::CHECK_READONLY, ptr, fallback); build.inst(IrCmd::STORE_POINTER, build.vmReg(2), build.inst(IrCmd::LOAD_POINTER, build.vmReg(0))); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(ttable)); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tnil)); build.inst(IrCmd::JUMP, last); build.beginBlock(fallback); IrOp fallbackPtr = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1)); build.inst(IrCmd::STORE_POINTER, build.vmReg(2), fallbackPtr); build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(ttable)); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::JUMP, last); build.beginBlock(last); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(3)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_fallback_1, bb_2 ; in regs: R0, R1 ; out regs: R0, R1, R2 %0 = LOAD_POINTER R1 CHECK_READONLY %0, bb_fallback_1 STORE_TAG R2, tnil JUMP bb_2 bb_fallback_1: ; predecessors: bb_0 ; successors: bb_2 ; in regs: R0, R1 ; out regs: R0, R1, R2 %9 = LOAD_POINTER R1 STORE_POINTER R2, %9 STORE_TAG R2, ttable CHECK_GC JUMP bb_2 bb_2: ; predecessors: bb_0, bb_fallback_1 ; in regs: R0, R1, R2 RETURN R0, 3i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FullStoreHasToBeObservableFromFallbacks") { IrOp entry = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); IrOp last = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), build.inst(IrCmd::NEW_TABLE, build.constUint(16), build.constUint(32))); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); build.inst(IrCmd::CHECK_SAFE_ENV, fallback); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), build.inst(IrCmd::NEW_TABLE, build.constUint(16), build.constUint(32))); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); build.inst(IrCmd::JUMP, last); build.beginBlock(fallback); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(1), build.constTag(tnumber), build.constDouble(1.0)); build.inst(IrCmd::JUMP, last); build.beginBlock(last); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // Even though R1 is not live in of the fallback, stack state cannot be left in a partial store state // Either tag+pointer store should both remain before the guard, or they both have to be made after CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_fallback_1, bb_2 ; in regs: R0 ; out regs: R0, R1 CHECK_SAFE_ENV bb_fallback_1 %4 = NEW_TABLE 16u, 32u STORE_SPLIT_TVALUE R1, ttable, %4 JUMP bb_2 bb_fallback_1: ; predecessors: bb_0 ; successors: bb_2 ; in regs: R0 ; out regs: R0, R1 CHECK_GC STORE_SPLIT_TVALUE R1, tnumber, 1 JUMP bb_2 bb_2: ; predecessors: bb_0, bb_fallback_1 ; in regs: R0, R1 RETURN R0, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FullStoreHasToBeObservableFromFallbacks2") { IrOp entry = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); IrOp last = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); // Tag store unpaired to a visible value store build.inst(IrCmd::CHECK_SAFE_ENV, fallback); build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(2))); build.inst(IrCmd::JUMP, last); build.beginBlock(fallback); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(1), build.constTag(tnumber), build.constDouble(1.0)); build.inst(IrCmd::JUMP, last); build.beginBlock(last); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // If table tag store at the start is removed, GC assists in the fallback can observe value with a wrong tag CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_fallback_1, bb_2 ; in regs: R0, R2 ; out regs: R0, R1 STORE_TAG R1, tnumber CHECK_SAFE_ENV bb_fallback_1 %2 = LOAD_TVALUE R2 STORE_TVALUE R1, %2 JUMP bb_2 bb_fallback_1: ; predecessors: bb_0 ; successors: bb_2 ; in regs: R0 ; out regs: R0, R1 CHECK_GC STORE_SPLIT_TVALUE R1, tnumber, 1 JUMP bb_2 bb_2: ; predecessors: bb_0, bb_fallback_1 ; in regs: R0, R1 RETURN R0, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "FullStoreHasToBeObservableFromFallbacks3") { IrOp entry = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); IrOp last = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::CHECK_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(1)), build.constTag(tfunction), fallback); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), build.inst(IrCmd::LOAD_POINTER, build.vmConst(10))); build.inst(IrCmd::CHECK_SAFE_ENV, fallback); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1)); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); build.inst(IrCmd::JUMP, last); build.beginBlock(fallback); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(1), build.constTag(tnumber), build.constDouble(1.0)); build.inst(IrCmd::JUMP, last); build.beginBlock(last); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); markDeadStoresInBlockChains(build); // Tag check establishes that at that point, the tag of the value IS a function (as an exit here has to be with well-formed stack) // Later additional function pointer store can be removed, even if it observable from the GC in the fallback CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_fallback_1, bb_fallback_1, bb_2 ; in regs: R0, R1 ; out regs: R0, R1 %0 = LOAD_TAG R1 CHECK_TAG %0, tfunction, bb_fallback_1 CHECK_SAFE_ENV bb_fallback_1 STORE_DOUBLE R1, 1 STORE_TAG R1, tnumber JUMP bb_2 bb_fallback_1: ; predecessors: bb_0, bb_0 ; successors: bb_2 ; in regs: R0 ; out regs: R0, R1 CHECK_GC STORE_SPLIT_TVALUE R1, tnumber, 1 JUMP bb_2 bb_2: ; predecessors: bb_0, bb_fallback_1 ; in regs: R0, R1 RETURN R0, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "SafePartialValueStoresWithPreservedTag") { IrOp entry = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); IrOp last = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(1), build.constTag(tnumber), build.constDouble(1)); build.inst(IrCmd::CHECK_SAFE_ENV, fallback); // While R1 has to be observed in full by the fallback build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(2)); // This partial store is safe to remove because number tag is established build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(3)); // And so is this build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(4)); build.inst(IrCmd::JUMP, last); build.beginBlock(fallback); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::JUMP, last); build.beginBlock(last); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // If table tag store at the start is removed, GC assists in the fallback can observe value with a wrong tag CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_fallback_1, bb_2 ; in regs: R0 ; out regs: R0, R1 STORE_SPLIT_TVALUE R1, tnumber, 1 CHECK_SAFE_ENV bb_fallback_1 STORE_DOUBLE R1, 4 JUMP bb_2 bb_fallback_1: ; predecessors: bb_0 ; successors: bb_2 ; in regs: R0, R1 ; out regs: R0, R1 CHECK_GC JUMP bb_2 bb_2: ; predecessors: bb_0, bb_fallback_1 ; in regs: R0, R1 RETURN R0, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "SafePartialValueStoresWithPreservedTag2") { IrOp entry = build.block(IrBlockKind::Internal); IrOp fallback = build.block(IrBlockKind::Fallback); IrOp last = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(1), build.constTag(tnumber), build.constDouble(1)); build.inst(IrCmd::CHECK_SAFE_ENV, fallback); // While R1 has to be observed in full by the fallback build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(2)); // This partial store is safe to remove because tag is established build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(1), build.constTag(tnumber), build.constDouble(4)); build.inst(IrCmd::JUMP, last); build.beginBlock(fallback); build.inst(IrCmd::CHECK_GC); build.inst(IrCmd::JUMP, last); build.beginBlock(last); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // If table tag store at the start is removed, GC assists in the fallback can observe value with a wrong tag CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_fallback_1, bb_2 ; in regs: R0 ; out regs: R0, R1 STORE_SPLIT_TVALUE R1, tnumber, 1 CHECK_SAFE_ENV bb_fallback_1 STORE_SPLIT_TVALUE R1, tnumber, 4 JUMP bb_2 bb_fallback_1: ; predecessors: bb_0 ; successors: bb_2 ; in regs: R0, R1 ; out regs: R0, R1 CHECK_GC JUMP bb_2 bb_2: ; predecessors: bb_0, bb_fallback_1 ; in regs: R0, R1 RETURN R0, 2i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotReturnWithPartialStores") { IrOp entry = build.block(IrBlockKind::Internal); IrOp success = build.block(IrBlockKind::Internal); IrOp fail = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_POINTER, build.vmReg(1), build.inst(IrCmd::NEW_TABLE, build.constUint(0), build.constUint(0))); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(ttable)); IrOp toUint = build.inst(IrCmd::NUM_TO_UINT, build.constDouble(-1)); IrOp bitAnd = build.inst(IrCmd::BITAND_UINT, toUint, build.constInt(4)); build.inst(IrCmd::JUMP_CMP_INT, bitAnd, build.constInt(0), build.cond(IrCondition::Equal), success, fail); build.beginBlock(success); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.constInt(0)); build.inst(IrCmd::JUMP, exit); build.beginBlock(fail); build.inst(IrCmd::STORE_INT, build.vmReg(1), build.constInt(1)); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tboolean)); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); constPropInBlockChains(build, true); markDeadStoresInBlockChains(build); // Even though R1 is not live out at return, we stored table tag followed by an integer value // Boolean tag store has to remain, even if unused, because all stack slots are visible to GC CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: ; successors: bb_1, bb_2 ; in regs: R0 ; out regs: R0 %0 = NEW_TABLE 0u, 0u STORE_POINTER R1, %0 STORE_TAG R1, ttable %3 = NUM_TO_UINT -1 %4 = BITAND_UINT %3, 4i JUMP_CMP_INT %4, 0i, eq, bb_1, bb_2 bb_1: ; predecessors: bb_0 ; successors: bb_3 ; in regs: R0 ; out regs: R0 STORE_INT R1, 0i JUMP bb_3 bb_2: ; predecessors: bb_0 ; successors: bb_3 ; in regs: R0 ; out regs: R0 STORE_INT R1, 1i JUMP bb_3 bb_3: ; predecessors: bb_1, bb_2 ; in regs: R0 STORE_TAG R1, tboolean RETURN R0, 1i )"); } TEST_CASE_FIXTURE(IrBuilderFixture, "PartialOverFullValue") { IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(0), build.constTag(tnumber), build.constDouble(1.0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(2.0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(4.0)); build.inst( IrCmd::STORE_SPLIT_TVALUE, build.vmReg(0), build.constTag(ttable), build.inst(IrCmd::NEW_TABLE, build.constUint(16), build.constUint(32))); build.inst(IrCmd::STORE_POINTER, build.vmReg(0), build.inst(IrCmd::NEW_TABLE, build.constUint(8), build.constUint(16))); build.inst(IrCmd::STORE_POINTER, build.vmReg(0), build.inst(IrCmd::NEW_TABLE, build.constUint(4), build.constUint(8))); build.inst(IrCmd::STORE_SPLIT_TVALUE, build.vmReg(0), build.constTag(tnumber), build.constDouble(1.0)); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tstring)); IrOp newtable = build.inst(IrCmd::NEW_TABLE, build.constUint(16), build.constUint(32)); build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(ttable)); build.inst(IrCmd::STORE_POINTER, build.vmReg(0), newtable); build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); updateUseCounts(build.function); computeCfgInfo(build.function); markDeadStoresInBlockChains(build); CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( bb_0: %11 = NEW_TABLE 16u, 32u STORE_SPLIT_TVALUE R0, ttable, %11 RETURN R0, 1i )"); } TEST_SUITE_END(); TEST_SUITE_BEGIN("Dump"); TEST_CASE_FIXTURE(IrBuilderFixture, "ToDot") { IrOp entry = build.block(IrBlockKind::Internal); IrOp a = build.block(IrBlockKind::Internal); IrOp b = build.block(IrBlockKind::Internal); IrOp exit = build.block(IrBlockKind::Internal); build.beginBlock(entry); build.inst(IrCmd::JUMP_EQ_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), a, b); build.beginBlock(a); build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); build.inst(IrCmd::JUMP, exit); build.beginBlock(b); build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); build.inst(IrCmd::JUMP, exit); build.beginBlock(exit); build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(2)); updateUseCounts(build.function); computeCfgInfo(build.function); // note: we don't validate the output of these to avoid test churn when formatting changes; we run these to make sure they don't assert/crash toDot(build.function, /* includeInst= */ true); toDotCfg(build.function); toDotDjGraph(build.function); } TEST_SUITE_END();