Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2 SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved as a single checkpoint before refactoring the parser to delegate xBase command translation to the preprocessor. Highlights: FiveSql2 engine (_FiveSql2/src/) - prefix-glob index attach -> explicit convention (<table>_pk.ntx, <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop - DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt) - COUNT(DISTINCT col) parsed + aggregated via hSeen hash - UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent) - DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT) - Derived table FROM (SELECT...) + JOIN right-side derived - Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect - LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs) - DATE literal round-trip validation (Feb 29 non-leap rejected) - CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists - AlterTable type dispatcher comma-wrapped (1-char type "A" no longer matches CHARACTER) Compiler / runtime - gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity) - gengo split: emit_block.go, emit_stmt.go, folding.go extracted - parser/stmtreg.go nudges - hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*), windows debug stubs collapsed - thread/vm/value/class/pcinterp tightening from panic traces RDD layer (hbrdd/) - dbf: null bitmap support (null.go + null_test.go), mmap split (mmap_posix.go / mmap_windows.go), byte-level numeric parse - ntx/cdx: windows mmap parity - workarea + mem RDD: cross-area state-bleed fixes RTL (hbrtl/) - errorlog rewrite with platform-specific FD (errorlog_fd_unix / errorlog_fd_other) - sqlscan, sqlhelpers, indexrtl, datetime extensions Gates green at checkpoint: - go test ./... : PASS - FiveSql2 SQL:1999 : 43/43 - Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
164 lines
4.3 KiB
Go
164 lines
4.3 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
package gengo
|
|
|
|
import (
|
|
"five/compiler/parser"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func generate(t *testing.T, source string) string {
|
|
t.Helper()
|
|
file, errs := parser.Parse("test.prg", source)
|
|
if len(errs) > 0 {
|
|
for _, e := range errs {
|
|
t.Errorf("parse error: %s", e)
|
|
}
|
|
t.FailNow()
|
|
}
|
|
return Generate(file)
|
|
}
|
|
|
|
func assertContains(t *testing.T, code, want string) {
|
|
t.Helper()
|
|
if !strings.Contains(code, want) {
|
|
t.Errorf("generated code missing %q\n--- code ---\n%s", want, code)
|
|
}
|
|
}
|
|
|
|
func TestGenerateHelloWorld(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
? "Hello, World!"
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, "package main")
|
|
assertContains(t, code, `import (`)
|
|
assertContains(t, code, `"five/hbrt"`)
|
|
assertContains(t, code, "func FV_MAIN(t *hbrt.Thread)")
|
|
assertContains(t, code, "t.Frame(0, 0)")
|
|
assertContains(t, code, "defer t.EndProc()")
|
|
assertContains(t, code, `t.PushString("Hello, World!")`)
|
|
assertContains(t, code, "t.Function(1)")
|
|
assertContains(t, code, "t.PushNil()")
|
|
assertContains(t, code, "t.RetValue()")
|
|
assertContains(t, code, "func main()")
|
|
assertContains(t, code, `vm.Run("MAIN")`)
|
|
}
|
|
|
|
func TestGenerateArithmetic(t *testing.T) {
|
|
// Const prop (b829ed4) inlines `n` as 10 at its read site. The
|
|
// literal fold pass runs before the ident substitution so the
|
|
// outer `10 + 5` doesn't collapse to `15` — leaves two PushInt +
|
|
// Plus. Dead store for `n` is elided (6974ff9).
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL n := 10
|
|
RETURN n + 5
|
|
`)
|
|
assertContains(t, code, "t.Frame(0, 1)")
|
|
assertContains(t, code, "t.PushInt(10)")
|
|
assertContains(t, code, "t.PushInt(5)")
|
|
assertContains(t, code, "t.Plus()")
|
|
assertContains(t, code, "t.RetValue()")
|
|
}
|
|
|
|
func TestGenerateIfElse(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL n := 10
|
|
IF n > 5
|
|
? "Big"
|
|
ELSE
|
|
? "Small"
|
|
ENDIF
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, "t.Greater()")
|
|
assertContains(t, code, "if t.PopLogical()")
|
|
assertContains(t, code, `t.PushString("Big")`)
|
|
assertContains(t, code, "} else {")
|
|
assertContains(t, code, `t.PushString("Small")`)
|
|
}
|
|
|
|
func TestGenerateDoWhile(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL i := 0
|
|
DO WHILE i < 10
|
|
i++
|
|
ENDDO
|
|
RETURN i
|
|
`)
|
|
assertContains(t, code, "for {")
|
|
assertContains(t, code, "t.Less()")
|
|
assertContains(t, code, "if !t.PopLogical() { break }")
|
|
assertContains(t, code, "t.LocalAddInt(1, 1)") // i++
|
|
}
|
|
|
|
func TestGenerateForNext(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL i, nSum := 0
|
|
FOR i := 1 TO 10
|
|
nSum += i
|
|
NEXT
|
|
RETURN nSum
|
|
`)
|
|
assertContains(t, code, "t.Frame(0, 2)")
|
|
assertContains(t, code, "for {")
|
|
assertContains(t, code, "LocalLessEqualInt(")
|
|
assertContains(t, code, "t.LocalAdd(") // nSum += i
|
|
assertContains(t, code, "t.LocalAddInt(") // i += 1
|
|
}
|
|
|
|
func TestGenerateMultipleFunctions(t *testing.T) {
|
|
// Symbol hoist (1f63c7f) replaced `t.VM().FindSymbol(...)` with a
|
|
// per-file package-level pointer populated lazily via GetSym.
|
|
code := generate(t, `FUNCTION Double(n)
|
|
RETURN n * 2
|
|
|
|
FUNCTION Main()
|
|
? Double(21)
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, "func FV_DOUBLE(t *hbrt.Thread)")
|
|
assertContains(t, code, "func FV_MAIN(t *hbrt.Thread)")
|
|
assertContains(t, code, "t.Frame(1, 0)") // Double has 1 param
|
|
assertContains(t, code, "t.Mult()")
|
|
assertContains(t, code, `t.GetSym(&_sym_test_DOUBLE, "DOUBLE")`)
|
|
}
|
|
|
|
func TestGenerateStringConcat(t *testing.T) {
|
|
// cName propagates to "World" (b829ed4). The string-concat fold
|
|
// (7e4079f) works on literal+literal pairs, which is what the
|
|
// three PushStrings + Plus calls produce.
|
|
code := generate(t, `FUNCTION Main()
|
|
LOCAL cName := "World"
|
|
? "Hello, " + cName + "!"
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, `t.PushString("Hello, ")`)
|
|
assertContains(t, code, `t.PushString("World")`)
|
|
assertContains(t, code, `t.PushString("!")`)
|
|
assertContains(t, code, "t.Plus()")
|
|
}
|
|
|
|
func TestGenerateSymbolTable(t *testing.T) {
|
|
code := generate(t, `FUNCTION Main()
|
|
RETURN NIL
|
|
|
|
FUNCTION Helper()
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, `hbrt.Sym("MAIN"`)
|
|
assertContains(t, code, `hbrt.Sym("HELPER"`)
|
|
assertContains(t, code, "hbrt.FsFirst")
|
|
}
|
|
|
|
func TestGenerateImport(t *testing.T) {
|
|
code := generate(t, `IMPORT "net/http"
|
|
|
|
FUNCTION Main()
|
|
RETURN NIL
|
|
`)
|
|
assertContains(t, code, `"net/http"`)
|
|
}
|