From 926b0a742382e3062082350aab6b693d4fdf0a0d Mon Sep 17 00:00:00 2001 From: Dave Lucia Date: Tue, 26 May 2026 20:42:05 -0400 Subject: [PATCH] fix(stdlib): convert dots to path separators in require() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lua 5.3 §6.3 requires that the module-name dots be replaced with the directory separator before substitution into each `?` slot of a `package.path` template. The new VM was substituting `modname` verbatim, so `require("foo.bar")` looked for `foo.bar.lua` instead of `foo/bar.lua`. This broke downstream consumers that chain dotted requires (e.g. the `luassert` bootstrap: `luassert/init.lua` → `require("luassert.assert")`). The fix hoists a `String.replace(modname, ".", "/")` above the pattern loop. The default `package.path` already hardcodes `/` as the separator, so no new configuration surface is introduced. Adds a regression test exercising the disk-load path with a dotted name; prior tests only used single-segment module names. --- lib/lua/vm/stdlib.ex | 4 +++- test/lua/vm/stdlib/package_test.exs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/lua/vm/stdlib.ex b/lib/lua/vm/stdlib.ex index 597f572..c9e99c5 100644 --- a/lib/lua/vm/stdlib.ex +++ b/lib/lua/vm/stdlib.ex @@ -750,8 +750,10 @@ defmodule Lua.VM.Stdlib do # Find a module file by searching the patterns defp find_module_file(modname, patterns) do + resolved = String.replace(modname, ".", "/") + Enum.find_value(patterns, {:error, :not_found}, fn pattern -> - file_path = String.replace(pattern, "?", modname) + file_path = String.replace(pattern, "?", resolved) case File.read(file_path) do {:ok, content} -> {:ok, file_path, content} diff --git a/test/lua/vm/stdlib/package_test.exs b/test/lua/vm/stdlib/package_test.exs index 3486477..f0c815a 100644 --- a/test/lua/vm/stdlib/package_test.exs +++ b/test/lua/vm/stdlib/package_test.exs @@ -123,4 +123,22 @@ defmodule Lua.VM.Stdlib.PackageTest do assert {[true, 1], _} = eval_with_path(code, tmp_dir) end end + + describe "dotted module names" do + @tag :tmp_dir + test "require('foo.bar') resolves dots to directory separators", %{tmp_dir: tmp_dir} do + File.mkdir_p!(Path.join(tmp_dir, "foo")) + + File.write!(Path.join([tmp_dir, "foo", "bar.lua"]), """ + return { ok = true } + """) + + code = ~S""" + local m = require("foo.bar") + return m.ok + """ + + assert {[true], _} = eval_with_path(code, tmp_dir) + end + end end