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
28 changes: 27 additions & 1 deletion lib/lua/lexer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,21 @@ defmodule Lua.Lexer do
end
end

# \<newline> line continuation: a backslash before a real end-of-line yields a
# single \n byte and advances one line. All four line endings (\n, \r, \r\n,
# \n\r) collapse to one newline, matching PUC-Lua's `read_string`.
defp scan_string(<<?\\, ?\r, ?\n, rest::binary>>, str_acc, acc, pos, start_pos, quote) do
scan_string(rest, str_acc <> "\n", acc, advance_string_line(pos, 3), start_pos, quote)
end

defp scan_string(<<?\\, ?\n, ?\r, rest::binary>>, str_acc, acc, pos, start_pos, quote) do
scan_string(rest, str_acc <> "\n", acc, advance_string_line(pos, 3), start_pos, quote)
end

defp scan_string(<<?\\, nl, rest::binary>>, str_acc, acc, pos, start_pos, quote) when nl in [?\n, ?\r] do
scan_string(rest, str_acc <> "\n", acc, advance_string_line(pos, 2), start_pos, quote)
end

defp scan_string(<<?\\, esc, rest::binary>>, str_acc, acc, pos, start_pos, quote) do
# Escape sequence
case escape_char(esc) do
Expand Down Expand Up @@ -828,12 +843,23 @@ defmodule Lua.Lexer do
end
else
{num, ""} = Integer.parse(num_str)
{:ok, num}
# Lua 5.3.3 §3.1: a decimal integer literal that overflows the signed
# 64-bit range converts to a float (a leading sign is a separate token,
# so `num` here is always the non-negative magnitude). Hex integer
# literals instead wrap via wrap_int64; this branch is decimal only.
# @sign_bit is 2^63, i.e. max_int + 1, so `>= @sign_bit` means overflow.
if num >= @sign_bit, do: {:ok, num * 1.0}, else: {:ok, num}
end
end

# Position tracking helpers
defp advance_column(pos, n) do
%{pos | column: pos.column + n, byte_offset: pos.byte_offset + n}
end

# Advance one source line, consuming `n` raw bytes (the backslash plus the
# one- or two-byte line ending of a \<newline> continuation).
defp advance_string_line(pos, n) do
%{line: pos.line + 1, column: 1, byte_offset: pos.byte_offset + n}
end
end
14 changes: 14 additions & 0 deletions lib/lua/vm/executor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3599,6 +3599,20 @@ defmodule Lua.VM.Executor do

defp to_integer!(v, line, source, hint, state) when is_float(v), do: float_to_integer!(v, line, source, hint, state)

# The `:nan` sentinel stands in for an IEEE NaN float (e.g. `0/0`). Per Lua
# 5.3 §3.4.3 a float converts to integer for bitwise ops only when it has an
# exact integer value; NaN never does, so it raises like any other
# fractional/out-of-range float rather than as a non-number type.
defp to_integer!(:nan, line, source, hint, state) do
raise TypeError,
value: "number has no integer representation" <> format_target_hint(hint),
line: line,
source: source,
error_kind: :bitwise_on_non_integer,
value_type: :number,
state: state
end

defp to_integer!(v, line, source, hint, state) when is_binary(v) do
case Value.parse_number(v) do
nil ->
Expand Down
24 changes: 20 additions & 4 deletions lib/lua/vm/stdlib.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Lua.VM.Stdlib do
alias Lua.VM.AssertionError
alias Lua.VM.Executor
alias Lua.VM.Limits
alias Lua.VM.Numeric
alias Lua.VM.ProtectedCall
alias Lua.VM.RuntimeError
alias Lua.VM.State
Expand Down Expand Up @@ -137,10 +138,7 @@ defmodule Lua.VM.Stdlib do
Value.parse_number(v)

{v, b} when is_binary(v) and is_integer(b) and b >= 2 and b <= 36 ->
case Integer.parse(v, b) do
{n, ""} -> n
_ -> nil
end
parse_in_base(v, b)

_ ->
nil
Expand All @@ -151,6 +149,24 @@ defmodule Lua.VM.Stdlib do

defp lua_tonumber([], state), do: {[nil], state}

# tonumber(s, base) per Lua 5.3 §6.1: surrounding whitespace is ignored, an
# optional sign is allowed, digits a-z/A-Z map to 10-35 (case-insensitive),
# and the result is always an integer (wrapping into the signed 64-bit range
# like an integer literal). The whole trimmed string must be valid digits.
defp parse_in_base(str, base) do
{sign, body} =
case String.trim(str) do
"-" <> rest -> {-1, rest}
"+" <> rest -> {1, rest}
other -> {1, other}
end

case Integer.parse(body, base) do
{n, ""} when body != "" -> Numeric.to_signed_int64(sign * n)
_ -> nil
end
end

# print(...) — prints values separated by tabs, followed by a newline
defp lua_print(args, state) do
{strings, state} =
Expand Down
116 changes: 103 additions & 13 deletions lib/lua/vm/stdlib/math.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ defmodule Lua.VM.Stdlib.Math do
import Bitwise

alias Lua.VM.ArgumentError
alias Lua.VM.Numeric
alias Lua.VM.State
alias Lua.VM.Stdlib.Util
alias Lua.VM.Value

@max_int 9_223_372_036_854_775_807

@impl true
def lib_name, do: "math"
Expand All @@ -52,6 +56,7 @@ defmodule Lua.VM.Stdlib.Math do
"atan" => {:native_func, &math_atan/2},
"ceil" => {:native_func, &math_ceil/2},
"cos" => {:native_func, &math_cos/2},
"deg" => {:native_func, &math_deg/2},
"exp" => {:native_func, &math_exp/2},
"floor" => {:native_func, &math_floor/2},
"fmod" => {:native_func, &math_fmod/2},
Expand All @@ -60,6 +65,7 @@ defmodule Lua.VM.Stdlib.Math do
"min" => {:native_func, &math_min/2},
"modf" => {:native_func, &math_modf/2},
"pi" => :math.pi(),
"rad" => {:native_func, &math_rad/2},
"random" => {:native_func, &math_random/2},
"randomseed" => {:native_func, &math_randomseed/2},
"sin" => {:native_func, &math_sin/2},
Expand All @@ -77,8 +83,14 @@ defmodule Lua.VM.Stdlib.Math do
State.set_global(state, "math", tref)
end

# math.abs(x)
defp math_abs([x], state) when is_number(x) do
# math.abs(x). Integer arithmetic wraps modulo 2^64 (Lua 5.3 §3.4.2), so
# `math.abs(math.mininteger)` is `math.mininteger` itself rather than a value
# that escapes the signed 64-bit range.
defp math_abs([x], state) when is_integer(x) do
{[Numeric.to_signed_int64(abs(x))], state}
end

defp math_abs([x], state) when is_float(x) do
{[abs(x)], state}
end

Expand Down Expand Up @@ -157,9 +169,15 @@ defmodule Lua.VM.Stdlib.Math do
raise ArgumentError.value_expected("math.atan", 1)
end

# math.ceil(x)
defp math_ceil([x], state) when is_number(x) do
{[trunc(Float.ceil(x / 1))], state}
# math.ceil(x). An integer argument is already integral and is returned
# unchanged; routing it through a float would lose precision near the 64-bit
# limits (e.g. `maxint` rounds up to 2^63 as a float).
defp math_ceil([x], state) when is_integer(x) do
{[x], state}
end

defp math_ceil([x], state) when is_float(x) do
{[integral_float_result(Float.ceil(x))], state}
end

defp math_ceil([x | _], _state) do
Expand Down Expand Up @@ -191,6 +209,40 @@ defmodule Lua.VM.Stdlib.Math do
raise ArgumentError.value_expected("math.cos", 1)
end

# math.deg(x) — radians to degrees
defp math_deg([x], state) when is_number(x) do
{[x / 1 * (180.0 / :math.pi())], state}
end

defp math_deg([x | _], _state) do
raise ArgumentError,
function_name: "math.deg",
arg_num: 1,
expected: "number",
got: Util.typeof(x)
end

defp math_deg([], _state) do
raise ArgumentError.value_expected("math.deg", 1)
end

# math.rad(x) — degrees to radians
defp math_rad([x], state) when is_number(x) do
{[x / 1 * (:math.pi() / 180.0)], state}
end

defp math_rad([x | _], _state) do
raise ArgumentError,
function_name: "math.rad",
arg_num: 1,
expected: "number",
got: Util.typeof(x)
end

defp math_rad([], _state) do
raise ArgumentError.value_expected("math.rad", 1)
end

# math.exp(x)
defp math_exp([x], state) when is_number(x) do
{[:math.exp(x / 1)], state}
Expand All @@ -208,9 +260,15 @@ defmodule Lua.VM.Stdlib.Math do
raise ArgumentError.value_expected("math.exp", 1)
end

# math.floor(x)
defp math_floor([x], state) when is_number(x) do
{[trunc(Float.floor(x / 1))], state}
# math.floor(x). An integer argument is already integral and is returned
# unchanged; routing it through a float would lose precision near the 64-bit
# limits (e.g. `maxint` rounds up to 2^63 as a float).
defp math_floor([x], state) when is_integer(x) do
{[x], state}
end

defp math_floor([x], state) when is_float(x) do
{[integral_float_result(Float.floor(x))], state}
end

defp math_floor([x | _], _state) do
Expand All @@ -225,6 +283,13 @@ defmodule Lua.VM.Stdlib.Math do
raise ArgumentError.value_expected("math.floor", 1)
end

# Lua 5.3 §6.7: `math.floor`/`math.ceil` return an integer when the integral
# result fits the signed 64-bit range, otherwise the (already integral) float.
defp integral_float_result(f) do
truncated = trunc(f)
if Numeric.signed?(truncated), do: truncated, else: f
end

# math.fmod(x, y)
#
# Returns the remainder of the division of x by y that rounds the quotient
Expand Down Expand Up @@ -373,6 +438,16 @@ defmodule Lua.VM.Stdlib.Math do
{[:rand.uniform(m)], state}
end

# Lua 5.3 §6.7: the interval `[m, n]` is too large when its span `n - m`
# exceeds the signed 64-bit range (e.g. `math.random(minint, 0)`). The span
# is computed in unbounded integer arithmetic before the range check so the
# overflow is detected rather than silently wrapping.
defp math_random([m, n], _state) when is_integer(m) and is_integer(n) and m <= n and n - m > @max_int do
raise ArgumentError,
function_name: "math.random",
details: "interval too large"
end

defp math_random([m, n], state) when is_integer(m) and is_integer(n) and m <= n do
# Returns an integer in [m, n]
range = n - m + 1
Expand Down Expand Up @@ -483,16 +558,22 @@ defmodule Lua.VM.Stdlib.Math do
raise ArgumentError.value_expected("math.tan", 1)
end

# math.tointeger(x)
# math.tointeger(x). Returns x as an integer when it has an exact integer
# value within the signed 64-bit range, otherwise nil. Strings are coerced
# via the standard numeric-string rules (Lua 5.3 `lua_tointegerx`).
defp math_tointeger([x], state) when is_integer(x) do
{[x], state}
end

defp math_tointeger([x], state) when is_float(x) do
if Float.floor(x) == x do
{[trunc(x)], state}
else
{[nil], state}
{[float_to_integer_or_nil(x)], state}
end

defp math_tointeger([x], state) when is_binary(x) do
case Value.parse_number(x) do
n when is_integer(n) -> {[n], state}
n when is_float(n) -> {[float_to_integer_or_nil(n)], state}
_ -> {[nil], state}
end
end

Expand All @@ -504,6 +585,15 @@ defmodule Lua.VM.Stdlib.Math do
raise ArgumentError.value_expected("math.tointeger", 1)
end

# A float converts to an integer only when it is integral and fits the signed
# 64-bit range (the `1.0e308` `math.huge` stand-in does not).
defp float_to_integer_or_nil(f) do
if Float.floor(f) == f do
truncated = trunc(f)
if Numeric.signed?(truncated), do: truncated
end
end

# math.type(x)
defp math_type([x], state) when is_integer(x) do
{["integer"], state}
Expand Down
13 changes: 10 additions & 3 deletions lib/lua/vm/stdlib/os.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ defmodule Lua.VM.Stdlib.Os do
- `os.difftime(t2, t1)` - Difference in seconds between two times.
- `os.date([format [, time]])` - Formats a time as a string or table.
- `os.getenv(name)` - Value of an environment variable, or nil.
- `os.setlocale([locale [, category]])` - No-op returning "C".
- `os.setlocale([locale [, category]])` - Only the "C" locale is available;
returns "C" for a query or a "C"/"" request and nil for any other locale.
- `os.tmpname()` - A name usable for a temporary file.
- `os.exit([code [, close]])` - Raises to unwind; sandbox cannot exit.
"""
Expand Down Expand Up @@ -168,8 +169,14 @@ defmodule Lua.VM.Stdlib.Os do
raise ArgumentError.value_expected("os.getenv", 1)
end

# os.setlocale([locale [, category]]) — no locale support in the
# sandbox; report the "C" locale as active.
# os.setlocale([locale [, category]]) — the sandbox only carries the default
# "C" locale. A query (nil locale) and a request for "C" or "" all report
# "C"; any other named locale is unavailable, so return nil like a C runtime
# whose locale data is missing.
defp os_setlocale([locale | _], state) when locale not in [nil, "C", ""] do
{[nil], state}
end

defp os_setlocale(_args, state), do: {["C"], state}

# os.tmpname() — a name usable for a temporary file.
Expand Down
Loading
Loading