// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. package hbrt import "testing" func newTestThread() *Thread { vm := NewVM() return vm.NewThread() } // --- Stack operations --- func TestStackPushPop(t *testing.T) { th := newTestThread() th.push(MakeInt(10)) th.push(MakeInt(20)) th.push(MakeInt(30)) if th.sp != 3 { t.Fatalf("sp = %d, want 3", th.sp) } v := th.pop() if v.AsInt() != 30 { t.Errorf("pop = %d, want 30", v.AsInt()) } v = th.pop() if v.AsInt() != 20 { t.Errorf("pop = %d, want 20", v.AsInt()) } v = th.pop() if v.AsInt() != 10 { t.Errorf("pop = %d, want 10", v.AsInt()) } } func TestStackPeek(t *testing.T) { th := newTestThread() th.push(MakeInt(42)) v := th.peek() if v.AsInt() != 42 { t.Errorf("peek = %d, want 42", v.AsInt()) } if th.sp != 1 { t.Error("peek should not change sp") } } func TestStackDup(t *testing.T) { th := newTestThread() th.PushInt(99) th.Dup() if th.sp != 2 { t.Fatalf("sp = %d, want 2", th.sp) } a := th.pop() b := th.pop() if a.AsInt() != 99 || b.AsInt() != 99 { t.Error("Dup should duplicate top") } } // --- Frame and locals --- func TestFrameLocals(t *testing.T) { th := newTestThread() // Simulate: FUNCTION Foo(a, b) with LOCAL c th.push(MakeInt(10)) // arg a th.push(MakeInt(20)) // arg b th.PendingParams2(2) // tell Frame how many args are on stack th.Frame(2, 1) // 2 params, 1 local // Param a = local 1 if th.Local(1).AsInt() != 10 { t.Errorf("local 1 = %d, want 10", th.Local(1).AsInt()) } // Param b = local 2 if th.Local(2).AsInt() != 20 { t.Errorf("local 2 = %d, want 20", th.Local(2).AsInt()) } // Local c = local 3 (NIL) if !th.Local(3).IsNil() { t.Error("local 3 should be NIL") } // Set local 3 th.SetLocal(3, MakeString("hello")) if th.Local(3).AsString() != "hello" { t.Error("local 3 should be 'hello'") } th.EndProc() } func TestLocalSetInt(t *testing.T) { th := newTestThread() th.Frame(0, 2) th.LocalSetInt(1, 42) th.LocalSetInt(2, -99) if th.Local(1).AsInt() != 42 { t.Errorf("local 1 = %d, want 42", th.Local(1).AsInt()) } if th.Local(2).AsInt() != -99 { t.Errorf("local 2 = %d, want -99", th.Local(2).AsInt()) } th.EndProc() } func TestPushPopLocal(t *testing.T) { th := newTestThread() th.Frame(0, 2) th.LocalSetInt(1, 100) th.PushLocal(1) if th.peek().AsInt() != 100 { t.Error("PushLocal should push local value") } th.PushInt(200) th.PopLocal(2) if th.Local(2).AsInt() != 200 { t.Error("PopLocal should set local from stack") } th.EndProc() } // --- Return value --- func TestRetValue(t *testing.T) { th := newTestThread() th.Frame(0, 0) th.PushInt(42) th.RetValue() if th.GetRetValue().AsInt() != 42 { t.Errorf("RetValue = %d, want 42", th.GetRetValue().AsInt()) } th.EndProc() } func TestRetInt(t *testing.T) { th := newTestThread() th.Frame(0, 0) th.RetInt(999) if th.GetRetValue().AsLong() != 999 { t.Errorf("RetInt = %d, want 999", th.GetRetValue().AsLong()) } th.EndProc() } // --- Function call --- func TestFunctionCall(t *testing.T) { vm := NewVM() // Register a simple function: FUNCTION Double(n) → n * 2 mod := NewModule("TEST", Sym("DOUBLE", FsPublic|FsLocal, func(th *Thread) { th.Frame(1, 0) defer th.EndProc() n := th.Local(1).AsNumInt() th.RetInt(n * 2) }), ) vm.RegisterModule(mod) th := vm.NewThread() th.Frame(0, 0) // Call: Double(21) th.PushSymbol(mod.At(0)) th.PushNil() th.PushInt(21) th.Function(1) result := th.pop() if result.AsLong() != 42 { t.Errorf("Double(21) = %d, want 42", result.AsLong()) } th.EndProc() } func TestNestedFunctionCall(t *testing.T) { vm := NewVM() // FUNCTION Add(a, b) → a + b (simplified) addSym := Sym("ADD", FsPublic|FsLocal, func(th *Thread) { th.Frame(2, 0) defer th.EndProc() a := th.Local(1).AsNumInt() b := th.Local(2).AsNumInt() th.RetInt(a + b) }) // FUNCTION Main() → Add(10, Add(20, 30)) mainSym := Sym("MAIN", FsPublic|FsLocal|FsFirst, func(th *Thread) { th.Frame(0, 0) defer th.EndProc() // Inner call: Add(20, 30) th.PushSymbol(vm.FindSymbol("ADD")) th.PushNil() th.PushInt(20) th.PushInt(30) th.Function(2) // → 50 on stack // Outer call: Add(10, ) innerResult := th.pop() th.PushSymbol(vm.FindSymbol("ADD")) th.PushNil() th.PushInt(10) th.PushValue(innerResult) th.Function(2) // → 60 on stack th.RetValue() }) mod := NewModule("TEST", addSym, mainSym) vm.RegisterModule(mod) result := vm.Run("MAIN") if result.AsLong() != 60 { t.Errorf("Main() = %d, want 60", result.AsLong()) } } // --- Static variables --- func TestStaticVariables(t *testing.T) { th := newTestThread() statics := []Value{MakeInt(0), MakeString("hello")} th.RegisterStatics("MOD1", statics) th.Frame(0, 0) th.PushStatic("MOD1", 1) if th.pop().AsInt() != 0 { t.Error("static 1 should be 0") } th.PushInt(42) th.PopStatic("MOD1", 1) th.PushStatic("MOD1", 1) if th.pop().AsInt() != 42 { t.Error("static 1 should be 42 after PopStatic") } th.EndProc() } // --- Panic recovery --- func TestStackUnderflowPanic(t *testing.T) { th := newTestThread() defer func() { r := recover() if r == nil { t.Error("expected panic on stack underflow") } }() th.pop() // should panic }