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