mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-24 14:46:19 +02:00
A mod with a reskate-plugin.json is a plugin: Luau scripts that run in their own VM, with only
the reskate API ReSkate registers for them. Luau 0.741 is vendored in External/luau (the VM and
compiler only: no native code generation, no loadstring, io or C modules).
- Limits: 64 MB of memory, 50 ms a call, 2 ms a tick on average (over it for 3 s stops it),
more than 5 errors in 10 s stops it, 50 log lines a second. A plugin over a limit cannot catch
it in a pcall and carry on.
- API: reskate.log, on (load, unload, tick, level_loaded, level_unloading), timers, the level,
its own package's files and JSON, require inside the package; console.command and print
("console"); storage get, set, add and keys ("storage"), 1 MB in
%LOCALAPPDATA%\ReSkate\Plugins\<mod>\storage.json.
- Found every 2 s among the mods mods.json leaves on: switching a mod, or changing a plugin's
files, loads, unloads or reloads it without a restart. A package holding a program is refused.
- plugin list, commands, reload, off and on in the console. A plugin cannot take a name of
ReSkate's own commands.
- Tests: DINGOSDK_BUILD_PLUGIN_TESTS, including plugins written to break the sandbox.
994 lines
32 KiB
C++
994 lines
32 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#pragma once
|
|
|
|
#include "Luau/Bytecode.h"
|
|
#include "Luau/BytecodeGraph.h"
|
|
#include "Luau/BytecodeUtils.h"
|
|
#include "Luau/BytecodeValidation.h"
|
|
#include "Luau/VecDeque.h"
|
|
#include "Luau/DenseHash.h"
|
|
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <utility>
|
|
|
|
#include <math.h>
|
|
|
|
namespace Luau
|
|
{
|
|
namespace Bytecode
|
|
{
|
|
|
|
template<typename T>
|
|
bool bcCompare(T a, T b, BcCondition cond)
|
|
{
|
|
switch (cond)
|
|
{
|
|
case BcCondition::Equal:
|
|
return a == b;
|
|
case BcCondition::NotEqual:
|
|
return a != b;
|
|
case BcCondition::Less:
|
|
return a < b;
|
|
case BcCondition::NotLess:
|
|
return !(a < b);
|
|
case BcCondition::LessEqual:
|
|
return a <= b;
|
|
case BcCondition::NotLessEqual:
|
|
return !(a <= b);
|
|
default:
|
|
LUAU_ASSERT(!"Unsupported condition");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
inline bool bcCompare(double a, double b, BcCondition cond)
|
|
{
|
|
// Note: redundant bool() casts work around invalid MSVC optimization that merges cases in this switch, violating IEEE754 comparison semantics
|
|
switch (cond)
|
|
{
|
|
case BcCondition::Equal:
|
|
return a == b;
|
|
case BcCondition::NotEqual:
|
|
return a != b;
|
|
case BcCondition::Less:
|
|
return a < b;
|
|
case BcCondition::NotLess:
|
|
return !bool(a < b);
|
|
case BcCondition::LessEqual:
|
|
return a <= b;
|
|
case BcCondition::NotLessEqual:
|
|
return !bool(a <= b);
|
|
default:
|
|
LUAU_ASSERT(!"Unsupported condition");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
inline std::optional<double> evaluateNumberBinaryOp(double a, double b, LuauOpcode op)
|
|
{
|
|
switch (op)
|
|
{
|
|
case LuauOpcode::LOP_ADD:
|
|
return a + b;
|
|
case LuauOpcode::LOP_SUB:
|
|
return a - b;
|
|
case LuauOpcode::LOP_MUL:
|
|
return a * b;
|
|
case LuauOpcode::LOP_DIV:
|
|
return a / b;
|
|
case LuauOpcode::LOP_MOD:
|
|
return a - floor(a / b) * b;
|
|
case LuauOpcode::LOP_POW:
|
|
return pow(a, b);
|
|
case LuauOpcode::LOP_IDIV:
|
|
return floor(a / b);
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
// SCCP is generic over the constant representation
|
|
// Each instantiation inherits VmConstOps with the operations the pass needs to evaluate constants
|
|
struct VmConstOps
|
|
{
|
|
virtual std::optional<BcOp> evaluate(const BcOp& lhsOp, const BcOp& rhsOp, LuauOpcode op) const = 0;
|
|
virtual bool falsey(const BcOp& falseyOp) const = 0;
|
|
|
|
virtual bool compare(const BcOp& lhsOp, const BcOp& rhsOp, BcCondition cond) const = 0;
|
|
|
|
virtual BcOp makeNil() const = 0;
|
|
virtual BcOp makeImmBool(bool value) const = 0;
|
|
|
|
// true if the VmConst supports ordering comparisons (number, integer, string)
|
|
virtual bool isOrderable(const BcOp& vmConstOp) const = 0;
|
|
virtual bool kindEquals(const BcOp& lhsOp, const BcOp& rhsOp) const = 0;
|
|
|
|
// true if operands represent the same value exactly (NaN == NaN is true)
|
|
virtual bool fullyequal(const BcOp& lhsOp, const BcOp& rhsOp) const = 0;
|
|
|
|
// returns std::nullopt if the comparison is not supported
|
|
virtual std::optional<bool> eq(const BcOp& lhsOp, const BcOp& rhsOp) const = 0;
|
|
|
|
virtual bool isNil(const BcOp& op) const = 0;
|
|
virtual bool isBoolean(const BcOp& op) const = 0;
|
|
virtual bool isNumber(const BcOp& op) const = 0;
|
|
|
|
virtual bool asBoolean(const BcOp& op) const = 0;
|
|
virtual double asNumber(const BcOp& op) const = 0;
|
|
|
|
virtual BcRef<BcImm> asImm(BcOp op) const = 0;
|
|
|
|
VmConstOps() = default;
|
|
virtual ~VmConstOps() = default;
|
|
VmConstOps(const VmConstOps&) = default;
|
|
VmConstOps(VmConstOps&&) = delete;
|
|
VmConstOps& operator=(const VmConstOps&) = default;
|
|
VmConstOps& operator=(VmConstOps&&) = delete;
|
|
};
|
|
|
|
struct BcVmConstImpl : public VmConstOps
|
|
{
|
|
std::optional<BcOp> evaluate(const BcOp& lhsOp, const BcOp& rhsOp, LuauOpcode op) const override;
|
|
bool falsey(const BcOp& falseyOp) const override;
|
|
|
|
bool compare(const BcOp& lhsOp, const BcOp& rhsOp, BcCondition cond) const override;
|
|
|
|
BcOp makeNil() const override;
|
|
BcOp makeImmBool(bool value) const override;
|
|
|
|
bool isOrderable(const BcOp& op) const override;
|
|
bool kindEquals(const BcOp& lhsOp, const BcOp& rhsOp) const override;
|
|
|
|
bool fullyequal(const BcOp& lhsOp, const BcOp& rhsOp) const override;
|
|
|
|
std::optional<bool> eq(const BcOp& lhsOp, const BcOp& rhsOp) const override;
|
|
|
|
bool isNil(const BcOp& op) const override;
|
|
bool isBoolean(const BcOp& op) const override;
|
|
bool isNumber(const BcOp& op) const override;
|
|
|
|
bool asBoolean(const BcOp& op) const override;
|
|
double asNumber(const BcOp& op) const override;
|
|
|
|
BcRef<BcImm> asImm(BcOp op) const override;
|
|
|
|
explicit BcVmConstImpl(BcFunction<BcVmConst>& func)
|
|
: VmConstOps()
|
|
, func(func)
|
|
{
|
|
}
|
|
|
|
BcFunction<BcVmConst>& func;
|
|
};
|
|
|
|
|
|
enum class Constness
|
|
{
|
|
Undetermined, // lattice top
|
|
NotAConstant, // lattice bottom
|
|
Constant,
|
|
};
|
|
|
|
struct ConstnessLattice
|
|
{
|
|
Constness kind = Constness::Undetermined;
|
|
std::optional<BcOp> constant = std::nullopt;
|
|
|
|
ConstnessLattice() = default;
|
|
|
|
ConstnessLattice(Constness kind, BcOp bcOp)
|
|
: kind(kind)
|
|
, constant(bcOp)
|
|
{
|
|
LUAU_ASSERT(kind == Constness::Constant);
|
|
}
|
|
|
|
explicit ConstnessLattice(Constness kind)
|
|
: kind(kind)
|
|
, constant(std::nullopt)
|
|
{
|
|
}
|
|
|
|
ConstnessLattice merge(VmConstOps* impl, const ConstnessLattice& other) const
|
|
{
|
|
// Undetermined is lattice top: meeting with it yields the other operand
|
|
if (kind == Constness::Undetermined)
|
|
return other;
|
|
if (other.kind == Constness::Undetermined)
|
|
return *this;
|
|
// two equal constants meet to themselves; anything else falls to bottom
|
|
if (equal(impl, other))
|
|
return *this;
|
|
return ConstnessLattice(Constness::NotAConstant);
|
|
}
|
|
|
|
bool equal(VmConstOps* impl, const ConstnessLattice& other) const
|
|
{
|
|
if (kind != other.kind)
|
|
return false;
|
|
if (kind == Constness::Constant)
|
|
return impl->fullyequal(constant.value(), other.constant.value());
|
|
return true;
|
|
}
|
|
};
|
|
|
|
enum class ConditionState
|
|
{
|
|
AlwaysFalse,
|
|
AlwaysTrue,
|
|
Unknown,
|
|
};
|
|
|
|
struct JumpTarget
|
|
{
|
|
bool dead = false;
|
|
BcOp blockOp;
|
|
ConditionState condition = ConditionState::Unknown;
|
|
};
|
|
|
|
using OpConstness = DenseHashMap<BcOp, ConstnessLattice, BcOpHash>;
|
|
|
|
struct SccpState
|
|
{
|
|
OpConstness opConstness;
|
|
|
|
ConstnessLattice operandLattice(const BcOp& op)
|
|
{
|
|
if (op.kind == BcOpKind::Proj || op.kind == BcOpKind::VmReg || op.kind == BcOpKind::VmUpvalue)
|
|
return ConstnessLattice(Constness::NotAConstant);
|
|
return opConstness[op];
|
|
}
|
|
|
|
// an unresolved condition is bottom if any operand is bottom, else top
|
|
Constness unknownConditionConstness(std::initializer_list<BcOp> ops)
|
|
{
|
|
for (const BcOp& op : ops)
|
|
{
|
|
ConstnessLattice lat = operandLattice(op);
|
|
if (lat.kind == Constness::NotAConstant)
|
|
return Constness::NotAConstant;
|
|
}
|
|
return Constness::Undetermined;
|
|
}
|
|
};
|
|
|
|
class SccpInterpreter
|
|
{
|
|
public:
|
|
explicit SccpInterpreter(VmConstOps* impl, SccpState* state)
|
|
: impl(impl)
|
|
, state(state)
|
|
{
|
|
}
|
|
|
|
ConditionState evaluateCondition(const BcOp& op);
|
|
ConditionState evaluateComparisonCondition(LuauOpcode op, const BcOp& lhs, const BcOp& rhs);
|
|
ConditionState evaluateXeqkCondition(BcRef<BcInst> inst);
|
|
|
|
ConstnessLattice evaluateArith(LuauOpcode op, BcRef<BcInst> instRepr);
|
|
ConstnessLattice evaluate(LuauOpcode op, BcRef<BcInst> instRepr);
|
|
|
|
|
|
private:
|
|
VmConstOps* impl;
|
|
SccpState* state;
|
|
};
|
|
|
|
template<typename VmConst>
|
|
struct Sccp
|
|
{
|
|
BcFunction<VmConst>& func;
|
|
VmConstOps* impl;
|
|
|
|
SccpState state;
|
|
SccpInterpreter interpreter;
|
|
|
|
// this maps a block (index) to its predecessors that it was reached from
|
|
// if a block is not in this map, it is unreachable
|
|
DenseHashMap<uint32_t, DenseHashSet<BcOp, BcOpHash>> blockUses;
|
|
|
|
VecDeque<BcOp> flowWorklist;
|
|
DenseHashSet<BcOp, BcOpHash> flowWorklistSet;
|
|
|
|
// when a def's lattice value changes, its uses must be re-evaluated
|
|
VecDeque<BcOp> ssaWorklist;
|
|
|
|
explicit Sccp(BcFunction<VmConst>& func, VmConstOps* impl)
|
|
: func(func)
|
|
, impl(impl)
|
|
, interpreter(impl, &state)
|
|
{
|
|
}
|
|
|
|
std::optional<BcOp> getFallthrough(BcRef<BcBlock> block)
|
|
{
|
|
std::optional<BcOp> fallthrough;
|
|
for (auto& succOp : block->successors)
|
|
{
|
|
if (succOp.kind == BcBlockEdgeKind::Fallthrough)
|
|
{
|
|
if (fallthrough)
|
|
{
|
|
LUAU_ASSERT(!"Multiple fallthroughs");
|
|
return std::nullopt;
|
|
}
|
|
|
|
fallthrough = succOp.target;
|
|
}
|
|
}
|
|
|
|
return fallthrough;
|
|
}
|
|
|
|
// builds the target/fallthrough pair for a two-way branch
|
|
// targetTakenOnTrue says which edge the jump takes when the condition holds; the untaken edge is marked dead when the condition resolves
|
|
// a cond of Unknown leaves both edges live, which also covers branches whose condition the pass never folds (loops, CMPPROTO)
|
|
std::vector<JumpTarget> conditionalTargets(BcRef<BcInst> inst, const BcOp& target, ConditionState cond, bool targetTakenOnTrue)
|
|
{
|
|
LUAU_ASSERT(target.kind == BcOpKind::Block);
|
|
std::optional<BcOp> fallthrough = getFallthrough(func.block(inst->block));
|
|
LUAU_ASSERT(fallthrough);
|
|
|
|
bool targetDead = false;
|
|
bool fallthroughDead = false;
|
|
if (cond == ConditionState::AlwaysTrue)
|
|
{
|
|
targetDead = !targetTakenOnTrue;
|
|
fallthroughDead = targetTakenOnTrue;
|
|
}
|
|
else if (cond == ConditionState::AlwaysFalse)
|
|
{
|
|
targetDead = targetTakenOnTrue;
|
|
fallthroughDead = !targetTakenOnTrue;
|
|
}
|
|
|
|
return {{targetDead, target, cond}, {fallthroughDead, *fallthrough, cond}};
|
|
}
|
|
|
|
// when the condition is a known constant, the untaken path is marked dead and each target carries its resolved condition
|
|
std::vector<JumpTarget> jumpTargets(BcRef<BcInst> inst)
|
|
{
|
|
switch (inst->op)
|
|
{
|
|
case LOP_JUMP:
|
|
case LOP_JUMPBACK:
|
|
{
|
|
const BcOp& targetOp = inst->ops[0];
|
|
LUAU_ASSERT(targetOp.kind == BcOpKind::Block);
|
|
return {{false, targetOp, ConditionState::AlwaysTrue}};
|
|
}
|
|
case LOP_JUMPIF:
|
|
case LOP_JUMPIFNOT:
|
|
{
|
|
ConditionState cond = interpreter.evaluateCondition(inst->ops[0]);
|
|
return conditionalTargets(inst, inst->ops[1], cond, inst->op == LOP_JUMPIF);
|
|
}
|
|
case LOP_JUMPIFEQ:
|
|
case LOP_JUMPIFLE:
|
|
case LOP_JUMPIFLT:
|
|
case LOP_JUMPIFNOTEQ:
|
|
case LOP_JUMPIFNOTLE:
|
|
case LOP_JUMPIFNOTLT:
|
|
{
|
|
ConditionState cond = interpreter.evaluateComparisonCondition(inst->op, inst->ops[0], inst->ops[1]);
|
|
return conditionalTargets(inst, inst->ops[2], cond, true);
|
|
}
|
|
case LOP_JUMPXEQKNIL:
|
|
case LOP_JUMPXEQKB:
|
|
case LOP_JUMPXEQKN:
|
|
case LOP_JUMPXEQKS:
|
|
{
|
|
ConditionState cond = interpreter.evaluateXeqkCondition(inst);
|
|
bool negated = func.immOp(inst->ops[1]).valueBoolean;
|
|
return conditionalTargets(inst, inst->ops[2], cond, !negated);
|
|
}
|
|
case LOP_FORNPREP:
|
|
case LOP_FORNLOOP:
|
|
return conditionalTargets(inst, inst->ops[3], ConditionState::Unknown, true);
|
|
case LOP_FORGPREP:
|
|
case LOP_FORGPREP_NEXT:
|
|
case LOP_FORGPREP_INEXT:
|
|
{
|
|
const BcOp& targetOp = inst->ops[3];
|
|
LUAU_ASSERT(targetOp.kind == BcOpKind::Block);
|
|
return {{false, targetOp, ConditionState::AlwaysTrue}};
|
|
}
|
|
case LOP_FORGLOOP:
|
|
// FORGLOOP has two leading imm operands FORGPREP* lack, so its target is ops[5]
|
|
return conditionalTargets(inst, inst->ops[5], ConditionState::Unknown, true);
|
|
case LOP_CMPPROTO:
|
|
return conditionalTargets(inst, inst->ops[2], ConditionState::Unknown, true);
|
|
case LOP_JUMPX:
|
|
LUAU_ASSERT(!"Should have never parsed this");
|
|
[[fallthrough]];
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// for each operand, evaluate and merge the lattice with the phi lattice
|
|
void visitPhi(BcRef<BcPhi> phi)
|
|
{
|
|
LUAU_ASSERT(phi->ops.size() > 0);
|
|
|
|
ConstnessLattice fold;
|
|
|
|
for (size_t i = 0; i < phi->ops.size(); i++)
|
|
{
|
|
BcOp op = phi->ops[i];
|
|
|
|
ConstnessLattice lattice = state.operandLattice(op);
|
|
fold = lattice.merge(impl, fold);
|
|
}
|
|
|
|
const ConstnessLattice& prevLattice = state.opConstness[phi.op];
|
|
if (!fold.equal(impl, prevLattice))
|
|
{
|
|
for (BcOp use : phi->uses)
|
|
ssaWorklist.push_back(use);
|
|
}
|
|
state.opConstness[phi.op] = fold;
|
|
}
|
|
|
|
// evaluate an instruction, comparing against the previous lattice value, and inserting all uses into the SSA worklist if it changed
|
|
void visitInst(BcRef<BcInst> inst)
|
|
{
|
|
// CAPTURE REF can be mutated externally via SETUPVAL
|
|
// the SSA graph doesn't model that alias, so mark the source non-constant to avoid folding a stale value
|
|
if (inst->op == LOP_CAPTURE && inst->ops.size() >= 2)
|
|
{
|
|
const BcOp& captureTypeOp = inst->ops[0];
|
|
LUAU_ASSERT(captureTypeOp.kind == BcOpKind::Imm);
|
|
const BcImm& captureImm = func.immOp(captureTypeOp);
|
|
if (captureImm.kind == BcImmKind::Int && captureImm.valueInt == LCT_REF)
|
|
{
|
|
const BcOp& srcOp = inst->ops[1];
|
|
ConstnessLattice prev = state.opConstness[srcOp];
|
|
if (prev.kind != Constness::NotAConstant && (srcOp.kind == BcOpKind::Inst || srcOp.kind == BcOpKind::Phi))
|
|
{
|
|
state.opConstness[srcOp] = ConstnessLattice(Constness::NotAConstant);
|
|
|
|
|
|
for (BcOp use : usesOf(func, srcOp))
|
|
ssaWorklist.push_back(use);
|
|
}
|
|
}
|
|
}
|
|
|
|
ConstnessLattice lattice = interpreter.evaluate(inst->op, inst);
|
|
const ConstnessLattice& prevLattice = state.opConstness[inst.op];
|
|
|
|
ConstnessLattice newVal = lattice.merge(impl, prevLattice);
|
|
if (!newVal.equal(impl, prevLattice))
|
|
{
|
|
for (BcOp use : inst->uses)
|
|
ssaWorklist.push_back(use);
|
|
}
|
|
|
|
for (const JumpTarget& target : jumpTargets(inst))
|
|
{
|
|
uint32_t blockIdx = func.getBlockIndex(func.blockOp(target.blockOp));
|
|
if (!target.dead)
|
|
{
|
|
blockUses[blockIdx].insert(inst->block);
|
|
if (!flowWorklistSet.find(target.blockOp))
|
|
flowWorklist.push_back(target.blockOp);
|
|
}
|
|
}
|
|
|
|
state.opConstness[inst.op] = newVal;
|
|
}
|
|
|
|
void propagate()
|
|
{
|
|
BcRef<BcBlock> entryBlock = func.block(func.entryBlock);
|
|
BcRef<BcBlock> exitBlock = func.block(func.exitBlock);
|
|
// the entry block is always live
|
|
blockUses[func.getBlockIndex(*entryBlock)].insert(entryBlock.op);
|
|
// the exit block is always live per serialization requirements
|
|
blockUses[func.getBlockIndex(*exitBlock)].insert(exitBlock.op);
|
|
|
|
flowWorklist.push_back(func.entryBlock);
|
|
while (!flowWorklist.empty() || !ssaWorklist.empty())
|
|
{
|
|
while (!flowWorklist.empty())
|
|
{
|
|
BcOp blockOp = flowWorklist.front();
|
|
BcRef<BcBlock> block = func.block(blockOp);
|
|
|
|
flowWorklist.pop_front();
|
|
if (flowWorklistSet.contains(block.op))
|
|
continue;
|
|
|
|
for (BcOp& phiOp : block->phis)
|
|
{
|
|
LUAU_ASSERT(phiOp.kind == BcOpKind::Phi);
|
|
visitPhi(func.phi(phiOp));
|
|
}
|
|
|
|
for (BcOp& op : block->ops)
|
|
{
|
|
LUAU_ASSERT(op.kind == BcOpKind::Inst);
|
|
visitInst(func.inst(op));
|
|
}
|
|
|
|
bool blockEndsWithBranch = false;
|
|
if (!block->ops.empty() && block->ops.back().kind == BcOpKind::Inst)
|
|
blockEndsWithBranch = !jumpTargets(func.inst(block->ops.back())).empty();
|
|
|
|
for (BcBlockEdge succEdge : block->successors)
|
|
{
|
|
if (succEdge.kind == BcBlockEdgeKind::Fallthrough)
|
|
{
|
|
uint32_t succIdx = func.getBlockIndex(func.blockOp(succEdge.target));
|
|
// If this block ends with a branch and the fallthrough wasn't already added by visitInst, skip it
|
|
if (blockEndsWithBranch && !blockUses[succIdx].contains(block.op))
|
|
continue;
|
|
|
|
blockUses[succIdx].insert(block.op);
|
|
if (!flowWorklistSet.contains(succEdge.target))
|
|
flowWorklist.push_back(succEdge.target);
|
|
}
|
|
}
|
|
flowWorklistSet.insert(block.op);
|
|
}
|
|
|
|
while (!ssaWorklist.empty())
|
|
{
|
|
|
|
BcOp op = ssaWorklist.front();
|
|
ssaWorklist.pop_front();
|
|
|
|
if (op.kind == BcOpKind::Inst)
|
|
visitInst(func.inst(op));
|
|
else if (op.kind == BcOpKind::Phi)
|
|
visitPhi(func.phi(op));
|
|
}
|
|
}
|
|
}
|
|
|
|
void replaceOperand(BcRef<BcInst> inst, BcOp oldOp, BcOp newOp)
|
|
{
|
|
for (BcOp& op : inst->ops)
|
|
{
|
|
if (op == oldOp)
|
|
op = newOp;
|
|
}
|
|
}
|
|
|
|
void replacePhiOperand(BcRef<BcPhi> phi, BcOp oldOp, BcOp newOp)
|
|
{
|
|
for (BcOp& op : phi->ops)
|
|
{
|
|
if (op == oldOp)
|
|
op = newOp;
|
|
}
|
|
}
|
|
|
|
bool isLoadInst(BcOp op)
|
|
{
|
|
if (op.kind != BcOpKind::Inst)
|
|
return false;
|
|
LuauOpcode opcode = func.inst(op)->op;
|
|
return opcode == LOP_LOADK || opcode == LOP_LOADKX || opcode == LOP_LOADN || opcode == LOP_LOADB || opcode == LOP_LOADNIL;
|
|
}
|
|
|
|
// rewrite a folded instruction in-place to a load instruction
|
|
// op identity is preserved so all existing uses remain valid
|
|
void rewriteToLoad(BcOp op, const ConstnessLattice& lattice)
|
|
{
|
|
BcRef<BcInst> inst = func.inst(op);
|
|
for (BcOp& usedOp : inst->ops)
|
|
{
|
|
func.eraseUse(op, usedOp);
|
|
}
|
|
|
|
inst->ops.clear();
|
|
|
|
if (lattice.kind == Constness::Constant)
|
|
{
|
|
BcOp op = lattice.constant.value();
|
|
|
|
if (op.kind == BcOpKind::Imm)
|
|
{
|
|
BcImm& imm = func.immOp(op);
|
|
|
|
if (imm.kind == BcImmKind::Boolean)
|
|
inst->op = LOP_LOADB;
|
|
else if (imm.kind == BcImmKind::Int)
|
|
inst->op = LOP_LOADN;
|
|
else
|
|
LUAU_ASSERT(!"unknown constant");
|
|
|
|
inst->ops.push_back(op);
|
|
}
|
|
else if (op.kind == BcOpKind::VmConst)
|
|
{
|
|
inst->op = LOP_LOADK;
|
|
inst->ops.push_back(op);
|
|
}
|
|
else
|
|
{
|
|
LUAU_ASSERT(!"unknown constant");
|
|
}
|
|
}
|
|
}
|
|
|
|
void removeDeadEdges(BcRef<BcInst> inst)
|
|
{
|
|
std::vector<JumpTarget> targets = jumpTargets(inst);
|
|
|
|
BcRef<BcBlock> block = func.block(inst->block);
|
|
|
|
BcOp liveTarget{};
|
|
bool hasLive = false;
|
|
|
|
for (const JumpTarget& target : targets)
|
|
{
|
|
if (target.dead)
|
|
{
|
|
BcEdges& succs = block->successors;
|
|
unsigned writeIdx = 0;
|
|
for (unsigned i = 0; i < succs.size(); i++)
|
|
{
|
|
if (!(succs[i].target == target.blockOp))
|
|
succs[writeIdx++] = succs[i];
|
|
}
|
|
succs.resize(writeIdx);
|
|
}
|
|
else
|
|
{
|
|
liveTarget = target.blockOp;
|
|
hasLive = true;
|
|
}
|
|
}
|
|
|
|
if (!hasLive)
|
|
return;
|
|
|
|
// ensure the live target has a fallthrough edge
|
|
bool hasFallthrough = false;
|
|
for (unsigned i = 0; i < block->successors.size(); i++)
|
|
{
|
|
if (block->successors[i].target == liveTarget)
|
|
{
|
|
block->successors[i].kind = BcBlockEdgeKind::Fallthrough;
|
|
hasFallthrough = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasFallthrough)
|
|
block->successors.push_back({BcBlockEdgeKind::Fallthrough, liveTarget});
|
|
}
|
|
|
|
void replaceUses()
|
|
{
|
|
for (auto& [op, lattice] : state.opConstness)
|
|
{
|
|
if (lattice.kind != Constness::Constant)
|
|
continue;
|
|
if (op.kind != BcOpKind::Inst)
|
|
continue;
|
|
if (isLoadInst(op))
|
|
continue;
|
|
|
|
BcRef<BcInst> inst = func.inst(op);
|
|
LUAU_ASSERT(inst->op != LOP_JUMPX);
|
|
|
|
// isJumpD is safe to use here because JUMPX is not parsed by the GraphParser
|
|
if (isJumpD(inst->op))
|
|
{
|
|
removeDeadEdges(inst);
|
|
func.eraseOp(op);
|
|
}
|
|
else
|
|
rewriteToLoad(op, lattice);
|
|
}
|
|
}
|
|
|
|
void simplifyPhis()
|
|
{
|
|
for (BcOp blockOp : flowWorklistSet)
|
|
{
|
|
BcRef<BcBlock> block = func.block(blockOp);
|
|
|
|
for (auto it = block->phis.begin(); it != block->phis.end();)
|
|
{
|
|
BcOp op = *it;
|
|
LUAU_ASSERT(op.kind == BcOpKind::Phi);
|
|
|
|
BcRef<BcPhi> phi = func.phi(op);
|
|
if (phi->ops.empty())
|
|
{
|
|
++it;
|
|
continue;
|
|
}
|
|
|
|
BcOp unique = phi->ops[0];
|
|
bool allSame = true;
|
|
for (size_t i = 1; i < phi->ops.size(); i++)
|
|
{
|
|
if (phi->ops[i] != unique)
|
|
{
|
|
allSame = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!allSame)
|
|
{
|
|
++it;
|
|
continue;
|
|
}
|
|
|
|
for (BcOp use : usesOf(func, op))
|
|
{
|
|
if (use.kind == BcOpKind::Inst)
|
|
replaceOperand(func.inst(use), op, unique);
|
|
else if (use.kind == BcOpKind::Phi)
|
|
replacePhiOperand(func.phi(use), op, unique);
|
|
|
|
// `unique` can be VmReg(it can be param).
|
|
func.recordUse(unique, use);
|
|
}
|
|
phi->uses.clear();
|
|
|
|
for (BcOp operand : phi->ops)
|
|
func.eraseUse(op, operand);
|
|
phi->ops.clear();
|
|
|
|
it = block->phis.erase(it);
|
|
}
|
|
}
|
|
}
|
|
|
|
void updateBlockUses()
|
|
{
|
|
// mark dead blocks by forward reachability from entry, not by blockUses
|
|
// (which can miss blocks depending on worklist ordering)
|
|
DenseHashSet<uint32_t> reachable;
|
|
std::vector<uint32_t> worklist;
|
|
|
|
uint32_t entryIdx = func.getBlockIndex(*func.block(func.entryBlock));
|
|
uint32_t exitIdx = func.getBlockIndex(*func.block(func.exitBlock));
|
|
reachable.insert(entryIdx);
|
|
reachable.insert(exitIdx);
|
|
worklist.push_back(entryIdx);
|
|
|
|
while (!worklist.empty())
|
|
{
|
|
uint32_t idx = worklist.back();
|
|
worklist.pop_back();
|
|
BcBlock& blk = func.blocks[idx];
|
|
for (const BcBlockEdge& edge : blk.successors)
|
|
{
|
|
uint32_t succIdx = func.getBlockIndex(func.blockOp(edge.target));
|
|
if (!reachable.contains(succIdx))
|
|
{
|
|
reachable.insert(succIdx);
|
|
worklist.push_back(succIdx);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (BcBlock& block : func.blocks)
|
|
{
|
|
uint32_t blockidx = func.getBlockIndex(block);
|
|
block.useCount = static_cast<uint32_t>(blockUses[blockidx].size());
|
|
if (!reachable.contains(blockidx))
|
|
block.flags |= BcBlockFlag::Dead;
|
|
}
|
|
}
|
|
|
|
static std::optional<LuauOpcode> arithToKOpcode(LuauOpcode op)
|
|
{
|
|
switch (op)
|
|
{
|
|
case LOP_ADD:
|
|
return LOP_ADDK;
|
|
case LOP_SUB:
|
|
return LOP_SUBK;
|
|
case LOP_MUL:
|
|
return LOP_MULK;
|
|
case LOP_DIV:
|
|
return LOP_DIVK;
|
|
case LOP_MOD:
|
|
return LOP_MODK;
|
|
case LOP_POW:
|
|
return LOP_POWK;
|
|
default:
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
// pure value producers can be removed once nothing references their result
|
|
bool isPureProducer(LuauOpcode op) const
|
|
{
|
|
switch (op)
|
|
{
|
|
case LOP_LOADK:
|
|
case LOP_LOADKX:
|
|
case LOP_LOADN:
|
|
case LOP_LOADB:
|
|
case LOP_LOADNIL:
|
|
case LOP_GETUPVAL:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
std::optional<Reg> registerOf(BcOp op)
|
|
{
|
|
if (op.kind == BcOpKind::VmReg)
|
|
return static_cast<Reg>(op.index);
|
|
if (auto it = func.regs.find(op); it != func.regs.end())
|
|
return it->second;
|
|
return std::nullopt;
|
|
}
|
|
|
|
void eraseDeadProducer(BcOp op)
|
|
{
|
|
if (op.kind != BcOpKind::Inst)
|
|
return;
|
|
BcRef<BcInst> inst = func.inst(op);
|
|
if (!isPureProducer(inst->op))
|
|
return;
|
|
if (!inst->uses.empty())
|
|
return;
|
|
func.eraseOp(op);
|
|
}
|
|
|
|
// values that are known constant used in arithmetic operations can be turned into their KR or RK variants
|
|
// sub and div have both KR and RK
|
|
void arithToK()
|
|
{
|
|
for (BcBlock& block : func.blocks)
|
|
{
|
|
uint32_t blockidx = func.getBlockIndex(block);
|
|
if (blockUses[blockidx].empty())
|
|
continue;
|
|
|
|
std::vector<BcOp> toErase;
|
|
toErase.reserve(block.ops.size());
|
|
|
|
for (auto bcOpIt = block.ops.begin(); bcOpIt != block.ops.end(); ++bcOpIt)
|
|
{
|
|
BcOp& op = *bcOpIt;
|
|
|
|
BcRef<BcInst> inst = func.inst(op);
|
|
|
|
std::optional<LuauOpcode> kOpcode = arithToKOpcode(inst->op);
|
|
if (!kOpcode || inst->ops.size() != 2)
|
|
continue;
|
|
|
|
BcOp lhs = inst->ops[0];
|
|
BcOp rhs = inst->ops[1];
|
|
// we can safely assume that, at most, one of these can be a Constant
|
|
// if they both were constant, the arith would have been folded
|
|
ConstnessLattice lhsLat = state.operandLattice(lhs);
|
|
ConstnessLattice rhsLat = state.operandLattice(rhs);
|
|
|
|
BcOp nonConstantOp;
|
|
ConstnessLattice constantK;
|
|
bool rk = false;
|
|
|
|
auto isConstNumber = [&](const ConstnessLattice& lat) -> bool
|
|
{
|
|
// Only VmConst are allowed because we don't want to fold a 'compact' LOADN into Tvalue constant
|
|
return lat.kind == Constness::Constant && lat.constant && lat.constant->kind == BcOpKind::VmConst &&
|
|
impl->isNumber(*lat.constant);
|
|
};
|
|
|
|
if (isConstNumber(rhsLat) && lhsLat.kind == Constness::NotAConstant)
|
|
{
|
|
// can fold this to a <ARITH>K variant
|
|
nonConstantOp = lhs;
|
|
constantK = rhsLat;
|
|
}
|
|
else if (isConstNumber(lhsLat) && rhsLat.kind == Constness::NotAConstant)
|
|
{
|
|
if (inst->op != LOP_ADD && inst->op != LOP_MUL && inst->op != LOP_SUB && inst->op != LOP_DIV)
|
|
continue;
|
|
|
|
nonConstantOp = rhs;
|
|
constantK = lhsLat;
|
|
|
|
if (inst->op == LOP_SUB)
|
|
{
|
|
kOpcode = LOP_SUBRK;
|
|
rk = true;
|
|
}
|
|
else if (inst->op == LOP_DIV)
|
|
{
|
|
kOpcode = LOP_DIVRK;
|
|
rk = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
continue;
|
|
}
|
|
|
|
BcOp prevConstOperand = (nonConstantOp == lhs) ? rhs : lhs;
|
|
|
|
const bool constantIsRhs = (nonConstantOp == lhs);
|
|
|
|
// we can do some potential folding here now that we know one operand is constant
|
|
// for instance, adds of zero, muls of zero or 1, pows of zero or 1, etc
|
|
double valueNumber = impl->asNumber(constantK.constant.value());
|
|
if (valueNumber == 0)
|
|
{
|
|
if (inst->op == LOP_ADD || (inst->op == LOP_SUB && constantIsRhs))
|
|
{
|
|
inst->op = LOP_MOVE;
|
|
func.setOps(op, inst, {nonConstantOp});
|
|
}
|
|
else if (inst->op == LOP_MUL)
|
|
{
|
|
inst->op = LOP_LOADN;
|
|
BcImm imm{BcImmKind::Int};
|
|
imm.valueInt = 0;
|
|
func.setOps(op, inst, {func.addImm(imm)});
|
|
}
|
|
else if (inst->op == LOP_POW && constantIsRhs)
|
|
{
|
|
// x ^ 0 == 1 (0 ^ x is not folded: it is 0 for x != 0)
|
|
inst->op = LOP_LOADN;
|
|
BcImm imm{BcImmKind::Int};
|
|
imm.valueInt = 1;
|
|
func.setOps(op, inst, {func.addImm(imm)});
|
|
}
|
|
}
|
|
else if (valueNumber == 1)
|
|
{
|
|
if (inst->op == LOP_MUL || (inst->op == LOP_POW && constantIsRhs) || (inst->op == LOP_DIV && constantIsRhs))
|
|
{
|
|
inst->op = LOP_MOVE;
|
|
func.setOps(op, inst, {nonConstantOp});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
inst->op = *kOpcode;
|
|
if (!rk)
|
|
func.setOps(op, inst, {nonConstantOp, constantK.constant.value()});
|
|
else
|
|
// SUBRK and DIVRK expect B as the constant table index
|
|
func.setOps(op, inst, {constantK.constant.value(), nonConstantOp});
|
|
}
|
|
|
|
toErase.push_back(prevConstOperand);
|
|
}
|
|
|
|
for (BcOp op : toErase)
|
|
{
|
|
eraseDeadProducer(op);
|
|
}
|
|
}
|
|
}
|
|
|
|
void rewrite()
|
|
{
|
|
arithToK();
|
|
replaceUses();
|
|
simplifyPhis();
|
|
updateBlockUses();
|
|
}
|
|
};
|
|
|
|
template<typename VmConst>
|
|
void foldConstants(BcFunction<VmConst>& func, VmConstOps& impl)
|
|
{
|
|
Sccp<VmConst> sccp(func, &impl);
|
|
sccp.propagate();
|
|
sccp.rewrite();
|
|
}
|
|
|
|
} // namespace Bytecode
|
|
} // namespace Luau
|