- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
428 lines
9.2 KiB
Go
428 lines
9.2 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
package parser
|
|
|
|
import (
|
|
"five/compiler/ast"
|
|
"five/compiler/token"
|
|
"testing"
|
|
)
|
|
|
|
func parseOK(t *testing.T, source string) *ast.File {
|
|
t.Helper()
|
|
file, errs := Parse("test.prg", source)
|
|
if len(errs) > 0 {
|
|
for _, e := range errs {
|
|
t.Errorf("parse error: %s", e)
|
|
}
|
|
t.FailNow()
|
|
}
|
|
return file
|
|
}
|
|
|
|
// --- Function declaration ---
|
|
|
|
func TestParseSimpleFunction(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
RETURN NIL
|
|
`)
|
|
if len(file.Decls) != 1 {
|
|
t.Fatalf("expected 1 decl, got %d", len(file.Decls))
|
|
}
|
|
fn, ok := file.Decls[0].(*ast.FuncDecl)
|
|
if !ok {
|
|
t.Fatalf("expected FuncDecl, got %T", file.Decls[0])
|
|
}
|
|
if fn.Name != "Main" {
|
|
t.Errorf("name = %q, want %q", fn.Name, "Main")
|
|
}
|
|
if fn.IsProc {
|
|
t.Error("should not be PROCEDURE")
|
|
}
|
|
}
|
|
|
|
func TestParseFunctionWithLocals(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Foo(a, b)
|
|
LOCAL n := 10
|
|
LOCAL cName := "hello", x
|
|
RETURN n
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
if len(fn.Params) != 2 {
|
|
t.Errorf("params = %d, want 2", len(fn.Params))
|
|
}
|
|
if len(fn.Decls) != 2 {
|
|
t.Errorf("decls = %d, want 2 (two LOCAL statements)", len(fn.Decls))
|
|
}
|
|
// Check second LOCAL has 2 vars
|
|
vd := fn.Decls[1].(*ast.VarDecl)
|
|
if len(vd.Vars) != 2 {
|
|
t.Errorf("second LOCAL vars = %d, want 2", len(vd.Vars))
|
|
}
|
|
}
|
|
|
|
func TestParseProcedure(t *testing.T) {
|
|
file := parseOK(t, `PROCEDURE DoStuff()
|
|
RETURN
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
if !fn.IsProc {
|
|
t.Error("should be PROCEDURE")
|
|
}
|
|
}
|
|
|
|
// --- Expressions ---
|
|
|
|
func TestParseArithmetic(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
RETURN 1 + 2 * 3
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
ret := fn.Body[0].(*ast.ReturnStmt)
|
|
// Should be: 1 + (2 * 3) due to precedence
|
|
bin, ok := ret.Value.(*ast.BinaryExpr)
|
|
if !ok {
|
|
t.Fatalf("expected BinaryExpr, got %T", ret.Value)
|
|
}
|
|
if bin.Op != token.PLUS {
|
|
t.Errorf("top op = %v, want PLUS", bin.Op)
|
|
}
|
|
// Right side should be 2 * 3
|
|
right, ok := bin.Right.(*ast.BinaryExpr)
|
|
if !ok {
|
|
t.Fatalf("right should be BinaryExpr, got %T", bin.Right)
|
|
}
|
|
if right.Op != token.STAR {
|
|
t.Errorf("right op = %v, want STAR", right.Op)
|
|
}
|
|
}
|
|
|
|
func TestParseAssignment(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
LOCAL n
|
|
n := 10
|
|
RETURN n
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
// Body[0] should be assignment: n := 10
|
|
es := fn.Body[0].(*ast.ExprStmt)
|
|
assign, ok := es.X.(*ast.AssignExpr)
|
|
if !ok {
|
|
t.Fatalf("expected AssignExpr, got %T", es.X)
|
|
}
|
|
if assign.Op != token.ASSIGN {
|
|
t.Errorf("assign op = %v, want ASSIGN", assign.Op)
|
|
}
|
|
}
|
|
|
|
func TestParseFunctionCall(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
RETURN Str(42)
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
ret := fn.Body[0].(*ast.ReturnStmt)
|
|
call, ok := ret.Value.(*ast.CallExpr)
|
|
if !ok {
|
|
t.Fatalf("expected CallExpr, got %T", ret.Value)
|
|
}
|
|
ident := call.Func.(*ast.IdentExpr)
|
|
if ident.Name != "Str" {
|
|
t.Errorf("func name = %q, want Str", ident.Name)
|
|
}
|
|
if len(call.Args) != 1 {
|
|
t.Errorf("args = %d, want 1", len(call.Args))
|
|
}
|
|
}
|
|
|
|
func TestParseStringConcat(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
RETURN "Hello, " + "World!"
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
ret := fn.Body[0].(*ast.ReturnStmt)
|
|
bin := ret.Value.(*ast.BinaryExpr)
|
|
if bin.Op != token.PLUS {
|
|
t.Errorf("op = %v, want PLUS", bin.Op)
|
|
}
|
|
}
|
|
|
|
// --- Control flow ---
|
|
|
|
func TestParseIfElse(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
LOCAL n := 10
|
|
IF n > 5
|
|
RETURN .T.
|
|
ELSE
|
|
RETURN .F.
|
|
ENDIF
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
ifStmt, ok := fn.Body[0].(*ast.IfStmt)
|
|
if !ok {
|
|
t.Fatalf("expected IfStmt, got %T", fn.Body[0])
|
|
}
|
|
if len(ifStmt.Body) != 1 {
|
|
t.Errorf("if body = %d stmts", len(ifStmt.Body))
|
|
}
|
|
if len(ifStmt.ElseBody) != 1 {
|
|
t.Errorf("else body = %d stmts", len(ifStmt.ElseBody))
|
|
}
|
|
}
|
|
|
|
func TestParseIfElseIf(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
LOCAL n := 10
|
|
IF n > 10
|
|
RETURN 1
|
|
ELSEIF n > 5
|
|
RETURN 2
|
|
ELSEIF n > 0
|
|
RETURN 3
|
|
ELSE
|
|
RETURN 0
|
|
ENDIF
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
ifStmt := fn.Body[0].(*ast.IfStmt)
|
|
if len(ifStmt.ElseIfs) != 2 {
|
|
t.Errorf("elseifs = %d, want 2", len(ifStmt.ElseIfs))
|
|
}
|
|
}
|
|
|
|
func TestParseDoWhile(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
LOCAL i := 0
|
|
DO WHILE i < 10
|
|
i++
|
|
ENDDO
|
|
RETURN i
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
dw, ok := fn.Body[0].(*ast.DoWhileStmt)
|
|
if !ok {
|
|
t.Fatalf("expected DoWhileStmt, got %T", fn.Body[0])
|
|
}
|
|
if len(dw.Body) != 1 {
|
|
t.Errorf("body = %d stmts", len(dw.Body))
|
|
}
|
|
}
|
|
|
|
func TestParseForNext(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
LOCAL i
|
|
FOR i := 1 TO 10
|
|
? i
|
|
NEXT
|
|
RETURN NIL
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
forStmt, ok := fn.Body[0].(*ast.ForStmt)
|
|
if !ok {
|
|
t.Fatalf("expected ForStmt, got %T", fn.Body[0])
|
|
}
|
|
if forStmt.Var != "i" {
|
|
t.Errorf("var = %q, want i", forStmt.Var)
|
|
}
|
|
}
|
|
|
|
func TestParseForEach(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
LOCAL x
|
|
FOR EACH x IN {1, 2, 3}
|
|
? x
|
|
NEXT
|
|
RETURN NIL
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
fe, ok := fn.Body[0].(*ast.ForEachStmt)
|
|
if !ok {
|
|
t.Fatalf("expected ForEachStmt, got %T", fn.Body[0])
|
|
}
|
|
if fe.Var != "x" {
|
|
t.Errorf("var = %q, want x", fe.Var)
|
|
}
|
|
}
|
|
|
|
// --- QOut ---
|
|
|
|
func TestParseQOut(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
? "Hello"
|
|
? 1 + 2, "World"
|
|
RETURN NIL
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
q1, ok := fn.Body[0].(*ast.QOutStmt)
|
|
if !ok {
|
|
t.Fatalf("expected QOutStmt, got %T", fn.Body[0])
|
|
}
|
|
if len(q1.Exprs) != 1 {
|
|
t.Errorf("? args = %d, want 1", len(q1.Exprs))
|
|
}
|
|
q2 := fn.Body[1].(*ast.QOutStmt)
|
|
if len(q2.Exprs) != 2 {
|
|
t.Errorf("? args = %d, want 2", len(q2.Exprs))
|
|
}
|
|
}
|
|
|
|
// --- xBase commands ---
|
|
|
|
func TestParseUse(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
USE "customers" VIA DBFCDX ALIAS cust
|
|
RETURN NIL
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
use, ok := fn.Body[0].(*ast.UseCmd)
|
|
if !ok {
|
|
t.Fatalf("expected UseCmd, got %T", fn.Body[0])
|
|
}
|
|
if use.Via != "DBFCDX" {
|
|
t.Errorf("via = %q, want DBFCDX", use.Via)
|
|
}
|
|
if use.Alias != "cust" {
|
|
t.Errorf("alias = %q, want cust", use.Alias)
|
|
}
|
|
}
|
|
|
|
func TestParseGoTop(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
GO TOP
|
|
RETURN NIL
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
goCmd, ok := fn.Body[0].(*ast.GoCmd)
|
|
if !ok {
|
|
t.Fatalf("expected GoCmd, got %T", fn.Body[0])
|
|
}
|
|
if goCmd.Direction != "TOP" {
|
|
t.Errorf("direction = %q, want TOP", goCmd.Direction)
|
|
}
|
|
}
|
|
|
|
func TestParseSeek(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
SEEK "SMITH"
|
|
RETURN NIL
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
seek, ok := fn.Body[0].(*ast.SeekCmd)
|
|
if !ok {
|
|
t.Fatalf("expected SeekCmd, got %T", fn.Body[0])
|
|
}
|
|
lit := seek.Key.(*ast.LiteralExpr)
|
|
if lit.Value != "SMITH" {
|
|
t.Errorf("key = %q, want SMITH", lit.Value)
|
|
}
|
|
}
|
|
|
|
func TestParseReplace(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
REPLACE name WITH "Kim", salary WITH 50000
|
|
RETURN NIL
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
rep, ok := fn.Body[0].(*ast.ReplaceCmd)
|
|
if !ok {
|
|
t.Fatalf("expected ReplaceCmd, got %T", fn.Body[0])
|
|
}
|
|
if len(rep.Fields) != 2 {
|
|
t.Errorf("fields = %d, want 2", len(rep.Fields))
|
|
}
|
|
}
|
|
|
|
// --- Array and Hash literals ---
|
|
|
|
func TestParseArrayLiteral(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
RETURN {1, 2, 3}
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
ret := fn.Body[0].(*ast.ReturnStmt)
|
|
arr, ok := ret.Value.(*ast.ArrayLitExpr)
|
|
if !ok {
|
|
t.Fatalf("expected ArrayLitExpr, got %T", ret.Value)
|
|
}
|
|
if len(arr.Items) != 3 {
|
|
t.Errorf("items = %d, want 3", len(arr.Items))
|
|
}
|
|
}
|
|
|
|
func TestParseHashLiteral(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
RETURN {"a" => 1, "b" => 2}
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
ret := fn.Body[0].(*ast.ReturnStmt)
|
|
hash, ok := ret.Value.(*ast.HashLitExpr)
|
|
if !ok {
|
|
t.Fatalf("expected HashLitExpr, got %T", ret.Value)
|
|
}
|
|
if len(hash.Keys) != 2 {
|
|
t.Errorf("keys = %d, want 2", len(hash.Keys))
|
|
}
|
|
}
|
|
|
|
func TestParseCodeBlock(t *testing.T) {
|
|
file := parseOK(t, `FUNCTION Main()
|
|
RETURN {|x| x + 1}
|
|
`)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
ret := fn.Body[0].(*ast.ReturnStmt)
|
|
blk, ok := ret.Value.(*ast.BlockExpr)
|
|
if !ok {
|
|
t.Fatalf("expected BlockExpr, got %T", ret.Value)
|
|
}
|
|
if len(blk.Params) != 1 || blk.Params[0] != "x" {
|
|
t.Errorf("params = %v, want [x]", blk.Params)
|
|
}
|
|
}
|
|
|
|
// --- IMPORT ---
|
|
|
|
func TestParseImport(t *testing.T) {
|
|
file := parseOK(t, `IMPORT "net/http"
|
|
|
|
FUNCTION Main()
|
|
RETURN NIL
|
|
`)
|
|
if len(file.Imports) != 1 {
|
|
t.Fatalf("imports = %d, want 1", len(file.Imports))
|
|
}
|
|
if file.Imports[0].Path != "net/http" {
|
|
t.Errorf("import path = %q, want net/http", file.Imports[0].Path)
|
|
}
|
|
}
|
|
|
|
// --- Full program ---
|
|
|
|
func TestParseFullProgram(t *testing.T) {
|
|
src := `FUNCTION Main()
|
|
LOCAL nSum := 0, i
|
|
FOR i := 1 TO 10
|
|
nSum += i
|
|
NEXT
|
|
? "Sum =", nSum
|
|
IF nSum > 50
|
|
? "Big"
|
|
ELSE
|
|
? "Small"
|
|
ENDIF
|
|
RETURN nSum
|
|
`
|
|
file := parseOK(t, src)
|
|
fn := file.Decls[0].(*ast.FuncDecl)
|
|
if fn.Name != "Main" {
|
|
t.Errorf("name = %q", fn.Name)
|
|
}
|
|
if len(fn.Decls) != 1 {
|
|
t.Errorf("decls = %d, want 1 (LOCAL)", len(fn.Decls))
|
|
}
|
|
// Body: FOR + ? + IF + RETURN
|
|
if len(fn.Body) < 3 {
|
|
t.Errorf("body stmts = %d, want at least 3", len(fn.Body))
|
|
}
|
|
}
|