Files
five/compiler/parser/parser_test.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- 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>
2026-03-31 09:41:50 +09:00

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))
}
}