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>
This commit is contained in:
427
compiler/parser/parser_test.go
Normal file
427
compiler/parser/parser_test.go
Normal file
@@ -0,0 +1,427 @@
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user