Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions lib/lua/compiler/bytecode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ defmodule Lua.Compiler.Bytecode do
@op_numeric_for 36

# B5c-v2: closures, upvalues, varargs, multi-return, loops, self, concat.
# After this block lands, the only opcodes the encoder still rejects are
# the data-shape concerns explicitly out of scope (`:goto` / `:label` for
# label-resolution semantics, `:set_global` for codegen vestige, and the
# bitwise family — none of which are blockers for the closures / OOP /
# string_ops benchmarks).
# The bitwise family and the `:set_list` multi-return tail (below) since
# joined the covered set, so the only opcodes the encoder still rejects
# are `:goto` / `:label` (label-resolution semantics not yet ported to the
# dispatcher's flat tuple model) and `:set_global` (a codegen vestige;
# modern codegen emits `:set_field` on `_ENV` instead).
@op_closure 37
@op_set_upvalue 38
@op_get_open_upvalue 39
Expand All @@ -93,6 +93,18 @@ defmodule Lua.Compiler.Bytecode do
@op_generic_for 51
@op_close_upvalues 52

# Bitwise family (band/bor/bxor/shl/shr/bnot) plus the `:set_list`
# multi-return tail. Each compiles a single op that previously deopted
# the whole enclosing prototype to the interpreter. Tags stay contiguous
# to keep the dispatcher's jump table dense.
@op_bitwise_and 53
@op_bitwise_or 54
@op_bitwise_xor 55
@op_shift_left 56
@op_shift_right 57
@op_bitwise_not 58
@op_set_list_multi 59

@doc """
Compile a prototype, populating its `bytecode` field on success.

Expand Down Expand Up @@ -181,6 +193,19 @@ defmodule Lua.Compiler.Bytecode do
defp encode({:power, dest, a, b, hint_a, hint_b}), do: {:ok, {@op_power, dest, a, b, hint_a, hint_b}}
defp encode({:negate, dest, src, hint}), do: {:ok, {@op_negate, dest, src, hint}}

# Bitwise instructions carry the same per-operand hint tuples as
# arithmetic. The dispatcher inlines a two-integer fast path for
# band/bor/bxor and bridges to `Executor.dispatcher_bitwise/7` /
# `dispatcher_bnot/5` for non-integer operands, metamethods, and all
# shifts — so on-disk bytecode preserves both the int64 wrap and the
# hint suffix on `attempt to perform bitwise operation` errors.
defp encode({:bitwise_and, dest, a, b, hint_a, hint_b}), do: {:ok, {@op_bitwise_and, dest, a, b, hint_a, hint_b}}
defp encode({:bitwise_or, dest, a, b, hint_a, hint_b}), do: {:ok, {@op_bitwise_or, dest, a, b, hint_a, hint_b}}
defp encode({:bitwise_xor, dest, a, b, hint_a, hint_b}), do: {:ok, {@op_bitwise_xor, dest, a, b, hint_a, hint_b}}
defp encode({:shift_left, dest, a, b, hint_a, hint_b}), do: {:ok, {@op_shift_left, dest, a, b, hint_a, hint_b}}
defp encode({:shift_right, dest, a, b, hint_a, hint_b}), do: {:ok, {@op_shift_right, dest, a, b, hint_a, hint_b}}
defp encode({:bitwise_not, dest, src, hint}), do: {:ok, {@op_bitwise_not, dest, src, hint}}

defp encode({:less_than, dest, a, b}), do: {:ok, {@op_less_than, dest, a, b}}
defp encode({:less_equal, dest, a, b}), do: {:ok, {@op_less_equal, dest, a, b}}
defp encode({:greater_than, dest, a, b}), do: {:ok, {@op_greater_than, dest, a, b}}
Expand Down Expand Up @@ -276,19 +301,23 @@ defmodule Lua.Compiler.Bytecode do
do: {:ok, {@op_set_field, table_reg, name, value_reg, name_hint}}

# `:set_list` with a positive integer `count` is the table-constructor
# form (`{1, 2, 3}`). Two adjacent shapes stay on the interpreter:
# form (`{1, 2, 3}`).
#
# - `{:multi, _}` absorbs a multi-return call's results (e.g.
# `{f(), 1}`); it's covered by B5c-v2 alongside the rest of the
# multi-return machinery.
# - `{:multi, init_count}` absorbs a multi-return call's results (e.g.
# `{f(), 1}`): the dispatcher folds `init_count` with
# `state.multi_return_count` and reuses the same `set_list_pairs`
# machinery, mirroring the interpreter's `{:multi, _}` clause.
# - `count == 0` is the interpreter's "consume `multi_return_count`
# trailing values" sentinel. Current codegen never emits it from a
# literal constructor, but encoding it as a no-op would silently
# diverge from the interpreter if codegen ever did. The guard makes
# that contract explicit.
# that contract explicit — it stays on the interpreter.
defp encode({:set_list, table_reg, start, count, offset}) when is_integer(count) and count > 0,
do: {:ok, {@op_set_list, table_reg, start, count, offset}}

defp encode({:set_list, table_reg, start, {:multi, init_count}, offset}),
do: {:ok, {@op_set_list_multi, table_reg, start, init_count, offset}}

defp encode({:length, dest, source}), do: {:ok, {@op_length, dest, source}}

# `:numeric_for`, `:while_loop`, `:repeat_loop`, `:generic_for` each
Expand Down Expand Up @@ -360,10 +389,9 @@ defmodule Lua.Compiler.Bytecode do
defp encode(:break), do: {:ok, {@op_break}}

# Anything else — `:goto` / `:label` (label-resolution semantics not
# ported to the dispatcher), `:set_global` (codegen vestige; modern
# codegen uses `:set_field` on `_ENV` instead), and the bitwise family
# (out of scope for B5c-v2; their own follow-up) — stays on the
# interpreter.
# ported to the dispatcher's flat tuple model) and `:set_global` (codegen
# vestige; modern codegen uses `:set_field` on `_ENV` instead) — stays on
# the interpreter.
#
# `:source_line` is stripped upstream in `encode_list/2`, so it never
# reaches this clause table.
Expand Down Expand Up @@ -428,4 +456,11 @@ defmodule Lua.Compiler.Bytecode do
def op_repeat_loop, do: @op_repeat_loop
def op_generic_for, do: @op_generic_for
def op_close_upvalues, do: @op_close_upvalues
def op_bitwise_and, do: @op_bitwise_and
def op_bitwise_or, do: @op_bitwise_or
def op_bitwise_xor, do: @op_bitwise_xor
def op_shift_left, do: @op_shift_left
def op_shift_right, do: @op_shift_right
def op_bitwise_not, do: @op_bitwise_not
def op_set_list_multi, do: @op_set_list_multi
end
136 changes: 123 additions & 13 deletions lib/lua/vm/dispatcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ defmodule Lua.VM.Dispatcher do
@op_generic_for 51
@op_close_upvalues 52

# Bitwise family plus the `:set_list` multi-return tail. Mirrors the
# `@op_*` block in `Lua.Compiler.Bytecode`; the integers must stay
# identical in both files.
@op_bitwise_and 53
@op_bitwise_or 54
@op_bitwise_xor 55
@op_shift_left 56
@op_shift_right 57
@op_bitwise_not 58
@op_set_list_multi 59

@doc """
Execute a compiled prototype against `args` and `state`.
"""
Expand Down Expand Up @@ -152,13 +163,17 @@ defmodule Lua.VM.Dispatcher do
end
end

# The interpreter sizes register tuples with a +16 slack buffer
# (executor.ex:135) because codegen's `max_registers` undercounts the
# transient scratch slots a few constructs use — notably table
# constructors whose last element is a multi-return call (`{x, f()}`),
# which write through registers past the syntactic peak. The dispatcher
# mirrors that buffer so the same prototypes run safely once they
# compile; multi-return expansion still grows on demand beyond it.
@reg_slack 16

defp init_regs(proto, args) do
# The interpreter sizes register tuples with a +16 buffer for
# multi-return expansion (`ensure_regs_capacity/2`). The
# dispatcher's `:call_one` always wants exactly one result and
# the codegen now honestly reports the peak register, so no
# buffer is needed at all here.
size = max(proto.max_registers, proto.param_count)
size = max(proto.max_registers, proto.param_count) + @reg_slack
regs = Tuple.duplicate(nil, size)
copy_args(regs, 0, args, proto.param_count)
end
Expand Down Expand Up @@ -416,6 +431,81 @@ defmodule Lua.VM.Dispatcher do
regs = :erlang.setelement(dest + 1, regs, value)
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)

# ── Bitwise ─────────────────────────────────────────────────────
#
# band/bor/bxor of two integers take a fast path, but still apply the
# `to_signed_int64` narrow the executor uses: a register can hold an
# integer outside [-2^63, 2^63-1] (e.g. a host int injected via
# `Lua.set!` / `Value.encode/2`, which do not narrow), and skipping
# the narrow would both diverge from the interpreter and leak an
# out-of-range integer back into Lua state. For two already-narrow
# int64s the narrow is a cheap range check that masks nothing. Any
# non-integer operand (incl. float-with-fraction, string-coercible,
# tref with `__band` etc.) bridges to `Executor.dispatcher_bitwise/7`
# so coercion, metamethod dispatch, and hint-suffixed error
# attribution all match the interpreter. Shifts and bnot have no
# profitable number-only fast path (shift amounts and pre-truncation
# values need `lua_shift_*` masking), so they always bridge.

{@op_bitwise_and, dest, a, b, hint_a, hint_b} ->
va = :erlang.element(a + 1, regs)
vb = :erlang.element(b + 1, regs)

if is_integer(va) and is_integer(vb) do
regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.band(va, vb)))
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)
else
{value, state} = Executor.dispatcher_bitwise(:band, va, vb, state, proto, hint_a, hint_b)
regs = :erlang.setelement(dest + 1, regs, value)
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)
end

{@op_bitwise_or, dest, a, b, hint_a, hint_b} ->
va = :erlang.element(a + 1, regs)
vb = :erlang.element(b + 1, regs)

if is_integer(va) and is_integer(vb) do
regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.bor(va, vb)))
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)
else
{value, state} = Executor.dispatcher_bitwise(:bor, va, vb, state, proto, hint_a, hint_b)
regs = :erlang.setelement(dest + 1, regs, value)
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)
end

{@op_bitwise_xor, dest, a, b, hint_a, hint_b} ->
va = :erlang.element(a + 1, regs)
vb = :erlang.element(b + 1, regs)

if is_integer(va) and is_integer(vb) do
regs = :erlang.setelement(dest + 1, regs, Numeric.to_signed_int64(Bitwise.bxor(va, vb)))
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)
else
{value, state} = Executor.dispatcher_bitwise(:bxor, va, vb, state, proto, hint_a, hint_b)
regs = :erlang.setelement(dest + 1, regs, value)
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)
end

{@op_shift_left, dest, a, b, hint_a, hint_b} ->
va = :erlang.element(a + 1, regs)
vb = :erlang.element(b + 1, regs)
{value, state} = Executor.dispatcher_bitwise(:shl, va, vb, state, proto, hint_a, hint_b)
regs = :erlang.setelement(dest + 1, regs, value)
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)

{@op_shift_right, dest, a, b, hint_a, hint_b} ->
va = :erlang.element(a + 1, regs)
vb = :erlang.element(b + 1, regs)
{value, state} = Executor.dispatcher_bitwise(:shr, va, vb, state, proto, hint_a, hint_b)
regs = :erlang.setelement(dest + 1, regs, value)
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)

{@op_bitwise_not, dest, src, hint} ->
val = :erlang.element(src + 1, regs)
{value, state} = Executor.dispatcher_bnot(val, state, proto, hint)
regs = :erlang.setelement(dest + 1, regs, value)
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)

# ── Comparisons ─────────────────────────────────────────────────

{@op_less_than, dest, a, b} ->
Expand Down Expand Up @@ -794,8 +884,8 @@ defmodule Lua.VM.Dispatcher do
state = Executor.dispatcher_set_field(table_val, name, value, state, proto, name_hint)
dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)

# `:set_list` runs only the integer-count form (see encoder). The
# multi-return form (`{:multi, _}`) was filtered upstream and never
# `:set_list` with a positive integer count is the table-constructor
# form. The `count == 0` sentinel was filtered upstream and never
# reaches the dispatcher.
{@op_set_list, table_reg, start, count, offset} ->
{:tref, id} = :erlang.element(table_reg + 1, regs)
Expand All @@ -807,6 +897,21 @@ defmodule Lua.VM.Dispatcher do

dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)

# `:set_list` multi-return tail (`{f(), 1}`): fold the static prefix
# `init_count` with the trailing values count the last multi-return
# call recorded in `state.multi_return_count`. Mirrors the
# interpreter's `{:multi, _}` clause exactly.
{@op_set_list_multi, table_reg, start, init_count, offset} ->
{:tref, id} = :erlang.element(table_reg + 1, regs)
total = init_count + state.multi_return_count

state =
State.update_table(state, {:tref, id}, fn table ->
set_list_into_table(table, regs, start, total, offset, 0)
end)

dispatch(code, pc + 1, regs, upvalues, proto, state, cont, frames)

{@op_length, dest, source} ->
value = :erlang.element(source + 1, regs)

Expand Down Expand Up @@ -1354,11 +1459,17 @@ defmodule Lua.VM.Dispatcher do
return_one(value, state, rest_frames)

{:multi, base, -2} ->
# The expansion dest may sit past the statically reserved register
# range (a constructor tail like `{x, f()}` where the body never
# named that many locals). Grow first, mirroring the interpreter's
# `ensure_regs_capacity/2` at the post-call site.
regs = grow_regs(regs, base + 1)
regs = :erlang.setelement(base + 1, regs, value)
state = %{state | multi_return_count: 1}
dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames)

{:multi, base, n} when is_integer(n) and n > 1 ->
regs = grow_regs(regs, base + n)
regs = :erlang.setelement(base + 1, regs, value)
regs = pad_nils(regs, base + 1, n - 1)
dispatch(code, pc, regs, upvalues, proto, state, cont, rest_frames)
Expand Down Expand Up @@ -1414,11 +1525,10 @@ defmodule Lua.VM.Dispatcher do
end

defp init_callee_regs(callee_proto, src_regs, src_off, arg_count) do
# Same as `init_regs/2`: no buffer needed because the bytecode
# encoder rejects multi-return calls (which are the only thing
# the interpreter's +16 buffer absorbs) and codegen reports the
# honest peak register.
size = max(callee_proto.max_registers, callee_proto.param_count)
# Same +16 slack buffer as `init_regs/2` (executor.ex:1113): the callee
# body may use scratch registers past its reported `max_registers` for
# multi-return constructor tails.
size = max(callee_proto.max_registers, callee_proto.param_count) + @reg_slack
regs = Tuple.duplicate(nil, size)
copy_n = min(arg_count, callee_proto.param_count)
copy_regs(src_regs, src_off, regs, 0, copy_n)
Expand Down
59 changes: 59 additions & 0 deletions lib/lua/vm/executor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,65 @@ defmodule Lua.VM.Executor do
try_unary_metamethod("__unm", val, state, fn -> safe_negate(val, 0, proto.source, hint, state) end)
end

# Bitwise bridges mirror the `:bitwise_*` / `:shift_*` interpreter clauses
# (do_execute) with `line = 0`, matching how `dispatcher_binop/7` drops the
# line. The dispatcher inlines the two-integer fast path for band/bor/bxor
# and bridges here for everything else (non-integers, metamethods) and for
# all of shl/shr (no profitable inline fast path — shift semantics live in
# `lua_shift_left` / `lua_shift_right`).
@doc false
@spec dispatcher_bitwise(atom(), term(), term(), State.t(), term(), term(), term()) ::
{term(), State.t()}
def dispatcher_bitwise(:band, a, b, state, proto, hint_a, hint_b) do
src = proto.source

try_binary_metamethod("__band", a, b, state, fn ->
Numeric.to_signed_int64(Bitwise.band(to_integer!(a, 0, src, hint_a, state), to_integer!(b, 0, src, hint_b, state)))
end)
end

def dispatcher_bitwise(:bor, a, b, state, proto, hint_a, hint_b) do
src = proto.source

try_binary_metamethod("__bor", a, b, state, fn ->
Numeric.to_signed_int64(Bitwise.bor(to_integer!(a, 0, src, hint_a, state), to_integer!(b, 0, src, hint_b, state)))
end)
end

def dispatcher_bitwise(:bxor, a, b, state, proto, hint_a, hint_b) do
src = proto.source

try_binary_metamethod("__bxor", a, b, state, fn ->
Numeric.to_signed_int64(Bitwise.bxor(to_integer!(a, 0, src, hint_a, state), to_integer!(b, 0, src, hint_b, state)))
end)
end

def dispatcher_bitwise(:shl, a, b, state, proto, hint_a, hint_b) do
src = proto.source

try_binary_metamethod("__shl", a, b, state, fn ->
lua_shift_left(to_integer!(a, 0, src, hint_a, state), to_integer!(b, 0, src, hint_b, state))
end)
end

def dispatcher_bitwise(:shr, a, b, state, proto, hint_a, hint_b) do
src = proto.source

try_binary_metamethod("__shr", a, b, state, fn ->
lua_shift_right(to_integer!(a, 0, src, hint_a, state), to_integer!(b, 0, src, hint_b, state))
end)
end

@doc false
@spec dispatcher_bnot(term(), State.t(), term(), term()) :: {term(), State.t()}
def dispatcher_bnot(val, state, proto, hint) do
src = proto.source

try_unary_metamethod("__bnot", val, state, fn ->
Numeric.to_signed_int64(Bitwise.bnot(to_integer!(val, 0, src, hint, state)))
end)
end

@doc false
@spec dispatcher_cmp(atom(), term(), term(), State.t(), term()) :: {term(), State.t()}
def dispatcher_cmp(:less_than, a, b, state, proto) do
Expand Down
Loading
Loading