From 5d03aee0f5c5f480f80a45c15f39e8164d5f70a1 Mon Sep 17 00:00:00 2001 From: Vitalii Elenhaupt Date: Sun, 9 Aug 2026 14:47:44 +0200 Subject: [PATCH 1/3] fix: pop nil values instead of leaking them on the stack --- spec/lua/stack_spec.cr | 15 +++++++++++++++ src/lua/stack.cr | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/lua/stack_spec.cr b/spec/lua/stack_spec.cr index a3e57cf..b12ae99 100644 --- a/spec/lua/stack_spec.cr +++ b/spec/lua/stack_spec.cr @@ -141,6 +141,21 @@ module Lua it "returns nil when stack is empty" do Stack.new.pop.should be_nil end + + it "removes a nil element from the top of the stack" do + stack = Stack.new.tap(&.<< nil) + stack.pop.should be_nil + stack.size.should eq 0 + end + + it "does not lose values below a popped nil" do + stack = Stack.new + stack << 5 + stack << nil + stack.pop.should be_nil + stack.pop.should eq 5.0 + stack.size.should eq 0 + end end describe "#remove" do diff --git a/src/lua/stack.cr b/src/lua/stack.cr index 056151f..d767b33 100644 --- a/src/lua/stack.cr +++ b/src/lua/stack.cr @@ -202,7 +202,8 @@ module Lua # stack.size # => 0 # ``` def pop - top.try &.tap { remove } + return if size == 0 + top.tap { remove } end # Removes n elements from the stack. From 7cf4059ed25c3011f3ee9cfbc13a2bd551945dc5 Mon Sep 17 00:00:00 2001 From: Vitalii Elenhaupt Date: Sun, 9 Aug 2026 14:50:24 +0200 Subject: [PATCH 2/3] fix: preserve order of multiple Lua return values --- spec/lua/object/coroutine_spec.cr | 13 +++++++++++++ spec/lua/object/function_spec.cr | 11 +++++++++++ spec/lua/stack/chunk_spec.cr | 15 +++++++++++++++ src/lua/stack/coroutine_support.cr | 3 +-- src/lua/stack/util.cr | 3 ++- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/spec/lua/object/coroutine_spec.cr b/spec/lua/object/coroutine_spec.cr index a380227..209e776 100644 --- a/spec/lua/object/coroutine_spec.cr +++ b/spec/lua/object/coroutine_spec.cr @@ -49,6 +49,19 @@ module Lua lua.close end + it "yields multiple values in order" do + lua = Lua.load + t = lua.run %q( + return coroutine.create(function() + coroutine.yield(1, 2, "three") + end) + ) + + co = t.as(Lua::Coroutine) + co.resume.should eq [1.0, 2.0, "three"] + lua.close + end + it "can return an error" do lua = Lua.load t = lua.run %q( diff --git a/spec/lua/object/function_spec.cr b/spec/lua/object/function_spec.cr index 8e9030c..e8ddf89 100644 --- a/spec/lua/object/function_spec.cr +++ b/spec/lua/object/function_spec.cr @@ -26,6 +26,17 @@ module Lua sum.call(2, 3).should be_nil end + it "returns multiple values in order" do + swap = Lua::Stack.new.run(%q( + function swap(x, y) + return y, x + end + + return swap + )).as(Lua::Function) + swap.call(1, 2).should eq [2.0, 1.0] + end + it "can call a function without arguments" do f = Lua::Stack.new.run(%q( function ff() diff --git a/spec/lua/stack/chunk_spec.cr b/spec/lua/stack/chunk_spec.cr index 0495f67..2751743 100644 --- a/spec/lua/stack/chunk_spec.cr +++ b/spec/lua/stack/chunk_spec.cr @@ -44,6 +44,21 @@ module Lua::StackMixin s.close end + context "with multiple return values" do + it "returns the values in order" do + s = Stack.new + s.run(%q( return 1, 2, "three" )).should eq [1.0, 2.0, "three"] + s.close + end + + it "keeps nil values and leaves the stack clean" do + s = Stack.new + s.run(%q( return 5, nil )).should eq [5.0, nil] + s.size.should eq 0 + s.close + end + end + context "with multibyte UTF-8 characters" do it "keeps the full chunk when a comment contains multibyte characters" do s = Stack.new diff --git a/src/lua/stack/coroutine_support.cr b/src/lua/stack/coroutine_support.cr index c5b08b4..c5dce1e 100644 --- a/src/lua/stack/coroutine_support.cr +++ b/src/lua/stack/coroutine_support.cr @@ -9,14 +9,13 @@ module Lua::StackMixin # Starts and resumes a coroutine in the given thread protected def resume(*args) - thread_pos = size args.each { |arg| self.<< arg } nres = 0 res = CALL.new LibLua.resume(@state, nil, args.size, pointerof(nres)) raise error(res, pop) if res > CALL::YIELD - pick_results thread_pos + pick_results size - nres + 1 end # Returns the status of the current thread. diff --git a/src/lua/stack/util.cr b/src/lua/stack/util.cr index a8f626a..512226b 100644 --- a/src/lua/stack/util.cr +++ b/src/lua/stack/util.cr @@ -35,7 +35,8 @@ module Lua end protected def pick_results(start, finish = size) - elements = (start..finish).map { pop.as(::Lua::Type) } + elements = (start..finish).map { |pos| self[pos].as(::Lua::Type) } + remove elements.size elements.size > 1 ? elements : elements.first? end end From 56c2d37e7bd3661ad3c6caa1f54f0f7491026dd7 Mon Sep 17 00:00:00 2001 From: Vitalii Elenhaupt Date: Sun, 9 Aug 2026 14:51:06 +0200 Subject: [PATCH 3/3] fix: preserve embedded zero bytes in Lua strings --- spec/lua/stack/chunk_spec.cr | 15 +++++++++++++++ spec/lua/stack_spec.cr | 4 ++++ src/lua/callable.cr | 3 ++- src/lua/stack.cr | 17 +++++++++++++---- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/spec/lua/stack/chunk_spec.cr b/spec/lua/stack/chunk_spec.cr index 2751743..cfcf7cb 100644 --- a/spec/lua/stack/chunk_spec.cr +++ b/spec/lua/stack/chunk_spec.cr @@ -75,6 +75,21 @@ module Lua::StackMixin s.close end end + + context "with embedded zero bytes" do + it "returns the full string" do + s = Stack.new + s.run(%q( return "a\0b" )).should eq "a\u{0}b" + s.close + end + + it "passes the full string to Lua" do + s = Stack.new + s.set_global("x", "a\u{0}b") + s.run(%q( return #x )).should eq 3 + s.close + end + end end describe "#run" do diff --git a/spec/lua/stack_spec.cr b/spec/lua/stack_spec.cr index b12ae99..70ac441 100644 --- a/spec/lua/stack_spec.cr +++ b/spec/lua/stack_spec.cr @@ -53,6 +53,10 @@ module Lua Stack.new.tap(&.<< :message)[1].should eq "message" end + it "can push string with embedded zero bytes" do + Stack.new.tap(&.<< "a\u{0}b")[1].should eq "a\u{0}b" + end + it "can push array" do r = Stack.new.tap(&.<< %w[lua is cool])[1].as(Table).map { |_, v| v } r.should eq %w[lua is cool] diff --git a/src/lua/callable.cr b/src/lua/callable.cr index 2113bee..fee9c2d 100644 --- a/src/lua/callable.cr +++ b/src/lua/callable.cr @@ -146,7 +146,8 @@ module LuaCallable def self.__index(state : LibLua::State) : Int32 # __index(t,k) stack = Lua::Stack.new(state, :all) - key = String.new LibLua.tolstring(state, -1, nil) + chars = LibLua.tolstring(state, -1, out len) + key = String.new(chars.as(UInt8*), len) data = LibLua.touserdata(state, -2).as(LuaCallable*) pointer = data.value val = pointer._index(key) diff --git a/src/lua/stack.cr b/src/lua/stack.cr index d767b33..928a26c 100644 --- a/src/lua/stack.cr +++ b/src/lua/stack.cr @@ -82,9 +82,9 @@ module Lua when Int then LibLua.pushinteger(@state, o) when Float then LibLua.pushnumber(@state, o) when Bool then LibLua.pushboolean(@state, o ? 1 : 0) - when Char then LibLua.pushstring(@state, o.to_s) - when String then LibLua.pushstring(@state, o) - when Symbol then LibLua.pushstring(@state, o.to_s) + when Char then push_string o.to_s + when String then push_string o + when Symbol then push_string o.to_s when Array, Tuple then pushtable(o.to_a) when Hash, NamedTuple then pushtable(o.to_h) when Proc(LibLua::State, Int32) then pushclosure(o.as(Proc)) @@ -119,7 +119,7 @@ module Lua when TYPE::TNIL, TYPE::TNONE then nil when TYPE::TBOOLEAN then LibLua.toboolean(@state, pos) == 1 when TYPE::TNUMBER then LibLua.tonumberx(@state, pos, nil) - when TYPE::TSTRING then String.new LibLua.tolstring(@state, pos, nil) + when TYPE::TSTRING then string_at(pos) when TYPE::TTABLE then Table.new self, reference(pos) when TYPE::TFUNCTION then Function.new self, reference(pos) when TYPE::TTHREAD then Coroutine.new Stack.new(LibLua.tothread(@state, pos), libs.to_a) @@ -220,5 +220,14 @@ module Lua n = n < 0 ? 0 : [n, size].min LibLua.settop(@state, -n - 1) end + + private def push_string(s : String) + LibLua.pushlstring(@state, s, s.bytesize) + end + + private def string_at(pos) + chars = LibLua.tolstring(@state, pos, out len) + String.new(chars.as(UInt8*), len) + end end end