Repository: yuin/gopher-lua (2025-01-10T12:14:55Z)

Issue:
LOADNIL merging optimisation (still) produces invalid code
You **must** post issues only here. Questions, ideas **must** be posted in [discussions](https://github.com/yuin/gopher-lua/discussions). 

- [X] GopherLua is a Lua5.1 implementation. You should be familiar with Lua programming language. Have you read [Lua 5.1 reference manual](http://www.lua.org/manual/5.1/) carefully?
- [X] GopherLua is a Lua5.1 implementation.  In Lua, to keep it simple, **it is more important to remove functionalities rather than to add functionalities unlike other languages** . If you are going to introduce some new cool functionalities into the GopherLua code base and the functionalities can be implemented by existing APIs, It should be implemented as a library. 

Please answer the following before submitting your issue:

1. What version of GopherLua are you using? : `ccacf662c9d24821ea6cb1abaa515cc3b6aad7c6`
2. What version of Go are you using? : 1.22.3
3. What operating system and processor architecture are you using? : Mac M1 ARM 64
4. What did you do? : See below
5. What did you expect to see? : See below
6. What did you see instead? : See below

There is a bug in gopher lua master lastest `ccacf662c9d24821ea6cb1abaa515cc3b6aad7c6`.

Under certain conditions, the compiler can produce invalid code, resulting in a nil pointer deference panic.

The following code will reproduce the bug:

````lua
id = "foo"

function get_def()
  return {}
end

function test()
  local def = id ~= nil and get_def() or nil
  if def ~= nil then
    print("def is not nil")
  end
end

test()
````

Invoke with:

    > ./glua  test3.lua
    runtime error: invalid memory address or nil pointer dereference
    stack traceback:
    test3.lua:9: in function 'test'
    test3.lua:14: in main chunk
    [G]: ?

The issue is caused by incorrect byte code generation, due to another issue with the `LOADNIL` merging optimisation (see previously resolved issue #495).

The micro code generated by the above should generate:

    [001] GETGLOBAL      |  0, 0; R(0) := Gbl[Kst(0)] (line:8)
    [002] LOADNIL      |  1, 1, 0; R(1) := ... := R(1) := nil (line:8)
    [003] EQ      |  1, 0, 1; if ((RK(0) == RK(1)) ~= 1) then pc++ (line:8)
    [004] JMP      |  0, 4; pc+=4 (line:8)
    [005] GETGLOBAL      |  0, 1; R(0) := Gbl[Kst(1)] (line:8)
    [006] CALL      |  0, 1, 2; R(0) ... R(0+2-2) := R(0)(R(0+1) ... R(0+1-1)) (line:8)
    [007] TESTSET      |  0, 0, 1; if (R(0) <=> 1) then R(0) := R(0) else pc++ (line:8)
    [008] JMP      |  0, 1; pc+=1 (line:8)                                                  >>>>v   CORRECT JMP TARGET
    [009] LOADNIL      |  0, 0, 0; R(0) := ... := R(0) := nil (line:8)                          v
    [010] LOADNIL      |  1, 1, 0; R(1) := ... := R(1) := nil (line:9)                      <<<<<
    [011] EQ      |  1, 0, 1; if ((RK(0) == RK(1)) ~= 1) then pc++ (line:9)
    [012] JMP      |  0, 3; pc+=3 (line:9)
    [013] GETGLOBAL      |  1, 2; R(1) := Gbl[Kst(2)] (line:10)
    [014] LOADK      |  2, 3; R(2) := Kst(3) (line:10)
    [015] CALL      |  1, 2, 1; R(1) ... R(1+1-2) := R(1)(R(1+1) ... R(1+2-1)) (line:10)
    [016] RETURN      |  0, 1, 0; return R(0) ... R(0+1-2) (line:12)
    ; end of function

But instead, it generates:

    [001] GETGLOBAL      |  0, 0; R(0) := Gbl[Kst(0)] (line:8)
    [002] LOADNIL      |  1, 1, 0; R(1) := ... := R(1) := nil (line:8)
    [003] EQ      |  1, 0, 1; if ((RK(0) == RK(1)) ~= 1) then pc++ (line:8)
    [004] JMP      |  0, 4; pc+=4 (line:8)
    [005] GETGLOBAL      |  0, 1; R(0) := Gbl[Kst(1)] (line:8)
    [006] CALL      |  0, 1, 2; R(0) ... R(0+2-2) := R(0)(R(0+1) ... R(0+1-1)) (line:8)
    [007] TESTSET      |  0, 0, 1; if (R(0) <=> 1) then R(0) := R(0) else pc++ (line:8)
    [008] JMP      |  0, 1; pc+=1 (line:8)                                              >>>>v   INCORRECT JMP TARGET
    [009] LOADNIL      |  0, 1, 0; R(0) := ... := R(1) := nil (line:8)                      v
    [010] EQ      |  1, 0, 1; if ((RK(0) == RK(1)) ~= 1) then pc++ (line:9)             <<<<<
    [011] JMP      |  0, 3; pc+=3 (line:9)
    [012] GETGLOBAL      |  1, 2; R(1) := Gbl[Kst(2)] (line:10)
    [013] LOADK      |  2, 3; R(2) := Kst(3) (line:10)
    [014] CALL      |  1, 2, 1; R(1) ... R(1+1-2) := R(1)(R(1+1) ... R(1+2-1)) (line:10)
    [015] RETURN      |  0, 1, 0; return R(0) ... R(0+1-2) (line:12)
    ; end of function

Note, the `LOADNIL` instructions on lines 9 and 10 have been merged by the optimiser, but this is incorrect. The
2nd `LOADNIL` instruction was a jump target for the `JMP` on line 8, and merging it into the prior instruction meant
the `JMP` missed its target, and that the the `LOADNIL` for reg 1 was skipped. This resulted in an uninitialised
register value being used in subsequent operations, causing the panic.

Summary
---------
The `LOADNIL` merging optimisation merges multiple `LOADNIL` instructions of consecutive registers into a single
instruction, however it does not take into account if any of the `LOADNIL` instructions are jump targets, and so breaks
the logic.


Discussion:

Patch:
diff --git a/compile.go b/compile.go
index e46e0d75..f9fbf576 100644
--- a/compile.go
+++ b/compile.go
@@ -236,12 +236,13 @@ func (cd *codeStore) PropagateMV(top int, save *int, reg *int, inc int) {
 }
 
 func (cd *codeStore) AddLoadNil(a, b, line int) {
-	last := cd.Last()
-	if opGetOpCode(last) == OP_LOADNIL && (opGetArgB(last)+1) == a {
-		cd.SetB(cd.LastPC(), b)
-	} else {
-		cd.AddABC(OP_LOADNIL, a, b, 0, line)
-	}
+	// this method used to merge multiple consecutive LOADNIL instructions
+	// of consecutive registers into a single LOADNIL instruction, but it
+	// caused issues when the merged instructions were JMP targets, and so
+	// generated invalid code; so the merging functionality has been removed.
+	// It is safe to merge the LOADNIL instructions under certain conditions,
+	// but additional logic / complexity would be needed here.
+	cd.AddABC(OP_LOADNIL, a, b, 0, line)
 }
 
 func (cd *codeStore) SetOpCode(pc int, v int) {


Test patch:
diff --git a/script_test.go b/script_test.go
index 48f48b3c..3358b362 100644
--- a/script_test.go
+++ b/script_test.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"os"
 	"runtime"
-	"strings"
 	"sync/atomic"
 	"testing"
 	"time"
@@ -148,6 +147,33 @@ func TestLua(t *testing.T) {
 	testScriptDir(t, luaTests, "_lua5.1-tests")
 }
 
+func TestMergingLoadNilBug2(t *testing.T) {
+	// there was a bug where the LOADNIL merging optimisation would merge LOADNILs that were the targets of
+	// JMP instructions, causing the JMP to jump to the wrong location and breaking the logic and resulting in
+	// a panic.
+	s := `
+	id = "foo"
+
+	function get_def()
+	  return {}
+	end
+
+	function test()
+	  local def = id ~= nil and get_def() or nil
+	  if def ~= nil then
+		print("def is not nil")
+	  end
+	end
+
+	test()
+`
+	L := NewState()
+	defer L.Close()
+	if err := L.DoString(s); err != nil {
+		t.Error(err)
+	}
+}
+
 func TestMergingLoadNilBug(t *testing.T) {
 	// there was a bug where a multiple load nils were being incorrectly merged, and the following code exposed it
 	s := `
@@ -185,48 +211,50 @@ func TestMergingLoadNilBug(t *testing.T) {
 	}
 }
 
-func TestMergingLoadNil(t *testing.T) {
-	// multiple nil assignments to consecutive registers should be merged
-	s := `
-		function test()
-			local a = 0
-			local b = 1
-			local c = 2
-
-			-- this should generate just one LOADNIL byte code instruction
-			a = nil
-			b = nil
-			c = nil
-
-			print(a,b,c)
-		end
-
-		test()`
-
-	chunk, err := parse.Parse(strings.NewReader(s), "test")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	compiled, err := Compile(chunk, "test")
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if len(compiled.FunctionPrototypes) != 1 {
-		t.Fatal("expected 1 function prototype")
-	}
-
-	// there should be exactly 1 LOADNIL instruction in the byte code generated for the above
-	// anymore, and the LOADNIL merging is not working correctly
-	count := 0
-	for _, instr := range compiled.FunctionPrototypes[0].Code {
-		if opGetOpCode(instr) == OP_LOADNIL {
-			count++
-		}
-	}
-
-	if count != 1 {
-		t.Fatalf("expected 1 LOADNIL instruction, found %d", count)
-	}
-}
+// This test is disabled because the LOADNIL merging optimisation has been disabled. See the comment in the
+// AddLoadNil() function for more information.
+//func TestMergingLoadNil(t *testing.T) {
+//	// multiple nil assignments to consecutive registers should be merged
+//	s := `
+//		function test()
+//			local a = 0
+//			local b = 1
+//			local c = 2
+//
+//			-- this should generate just one LOADNIL byte code instruction
+//			a = nil
+//			b = nil
+//			c = nil
+//
+//			print(a,b,c)
+//		end
+//
+//		test()`
+//
+//	chunk, err := parse.Parse(strings.NewReader(s), "test")
+//	if err != nil {
+//		t.Fatal(err)
+//	}
+//
+//	compiled, err := Compile(chunk, "test")
+//	if err != nil {
+//		t.Fatal(err)
+//	}
+//
+//	if len(compiled.FunctionPrototypes) != 1 {
+//		t.Fatal("expected 1 function prototype")
+//	}
+//
+//	// there should be exactly 1 LOADNIL instruction in the byte code generated for the above
+//	// anymore, and the LOADNIL merging is not working correctly
+//	count := 0
+//	for _, instr := range compiled.FunctionPrototypes[0].Code {
+//		if opGetOpCode(instr) == OP_LOADNIL {
+//			count++
+//		}
+//	}
+//
+//	if count != 1 {
+//		t.Fatalf("expected 1 LOADNIL instruction, found %d", count)
+//	}
+//}