/* * compat_harbour.prg — Harbour Compatibility Test Suite for Five * * Tests language features that differ between Harbour and Go semantics. * Run: five build tests/compat_harbour.prg -o test_compat && ./test_compat * * Copyright (c) 2026 Charles KWON OhJun */ STATIC s_nPass := 0 STATIC s_nFail := 0 PROCEDURE Main() ? "================================================================" ? " Five — Harbour Compatibility Test Suite" ? "================================================================" ? TestByref() TestShortCircuit() TestForLoop() TestSequence() TestClosure() TestTypes() TestStatic() TestLocalScope() TestArrayHash() ? ? "================================================================" ? " Results:", hb_ntos(s_nPass), "/", hb_ntos(s_nPass + s_nFail), "passed" ? "================================================================" IF s_nFail > 0 ERRORLEVEL(1) ENDIF RETURN /* ====================================================================== */ /* 1. @byref pass-by-reference */ /* ====================================================================== */ STATIC PROCEDURE TestByref() LOCAL n := 10, cStr := "hello" ? "--- 1. @byref ---" // Basic ByrefModify(@n) Assert("1a @byref basic: n changed to 42", n == 42) // Chained n := 100 ByrefMiddle(@n) Assert("1b @byref chained: n changed to 999", n == 999) // Loop accumulation LOCAL nSum := 0, i FOR i := 1 TO 5 ByrefAdd(@nSum, i) NEXT Assert("1c @byref loop: sum 1..5 = 15", nSum == 15) // String ByrefAppend(@cStr, " world") Assert("1d @byref string: 'hello world'", cStr == "hello world") RETURN STATIC FUNCTION ByrefModify(x) x := 42 RETURN NIL STATIC FUNCTION ByrefMiddle(x) ByrefInner(@x) RETURN NIL STATIC FUNCTION ByrefInner(y) y := 999 RETURN NIL STATIC FUNCTION ByrefAdd(nAcc, nVal) nAcc := nAcc + nVal RETURN NIL STATIC FUNCTION ByrefAppend(cP, cS) cP := cP + cS RETURN NIL /* ====================================================================== */ /* 2. Short-circuit AND/OR */ /* ====================================================================== */ STATIC PROCEDURE TestShortCircuit() LOCAL lCalled ? "--- 2. Short-circuit AND/OR ---" // .AND. short-circuits on false left lCalled := .F. IF .F. .AND. SideEffect(@lCalled) ENDIF Assert("2a AND short-circuit: right not called", ! lCalled) // .OR. short-circuits on true left lCalled := .F. IF .T. .OR. SideEffect(@lCalled) ENDIF Assert("2b OR short-circuit: right not called", ! lCalled) // .AND. evaluates right when left is true lCalled := .F. IF .T. .AND. SideEffect(@lCalled) ENDIF Assert("2c AND evaluates right when left=.T.", lCalled) // NIL in condition → .F. Assert("2d NIL .AND. .T. = .F.", ! (NIL .AND. .T.)) RETURN STATIC FUNCTION SideEffect(lFlag) lFlag := .T. RETURN .T. /* ====================================================================== */ /* 3. FOR..NEXT LOOP/EXIT */ /* ====================================================================== */ STATIC PROCEDURE TestForLoop() LOCAL i, n, nLoopCount ? "--- 3. FOR..NEXT LOOP ---" // Basic FOR n := 0 FOR i := 1 TO 5 n += i NEXT Assert("3a FOR sum 1..5 = 15", n == 15) // FOR with EXIT n := 0 FOR i := 1 TO 100 IF i > 5 EXIT ENDIF n += i NEXT Assert("3b FOR EXIT: sum 1..5 = 15", n == 15) // FOR with LOOP (LOOP goes to NEXT, increments counter) nLoopCount := 0 FOR i := 1 TO 5 nLoopCount++ IF i == 3 LOOP // should skip to NEXT (i becomes 4) ENDIF NEXT Assert("3c FOR LOOP: counter = 5 (not infinite)", nLoopCount == 5) // FOR STEP -1 n := 0 FOR i := 5 TO 1 STEP -1 n += i NEXT Assert("3d FOR STEP -1: sum 5..1 = 15", n == 15) RETURN /* ====================================================================== */ /* 4. BEGIN SEQUENCE / RECOVER */ /* ====================================================================== */ STATIC PROCEDURE TestSequence() LOCAL lRecovered, nResult ? "--- 4. BEGIN SEQUENCE ---" // Basic recover lRecovered := .F. BEGIN SEQUENCE nResult := 1 / 0 // division by zero RECOVER lRecovered := .T. END SEQUENCE Assert("4a RECOVER catches error", lRecovered) // Normal flow (no error) lRecovered := .F. nResult := 0 BEGIN SEQUENCE nResult := 42 RECOVER lRecovered := .T. END SEQUENCE Assert("4b No error: result = 42", nResult == 42 .AND. ! lRecovered) // Nested SEQUENCE LOCAL lOuter := .F., lInner := .F. BEGIN SEQUENCE BEGIN SEQUENCE nResult := 1 / 0 RECOVER lInner := .T. END SEQUENCE RECOVER lOuter := .T. END SEQUENCE Assert("4c Nested: inner caught, outer not", lInner .AND. ! lOuter) RETURN /* ====================================================================== */ /* 5. Code block closure capture */ /* ====================================================================== */ STATIC PROCEDURE TestClosure() LOCAL bBlock, nOuter := 10 ? "--- 5. Closure ---" // Basic capture bBlock := {|| nOuter * 2} Assert("5a Closure captures outer: 10*2=20", Eval(bBlock) == 20) // Capture with parameter bBlock := {|x| nOuter + x} Assert("5b Closure with param: 10+5=15", Eval(bBlock, 5) == 15) // Closure returning value bBlock := {|a,b| a + b} Assert("5c Closure 2 params: 3+7=10", Eval(bBlock, 3, 7) == 10) RETURN /* ====================================================================== */ /* 6. Type system */ /* ====================================================================== */ STATIC PROCEDURE TestTypes() ? "--- 6. Types ---" Assert("6a ValType(NIL) = 'U'", ValType(NIL) == "U") Assert("6b ValType(1) = 'N'", ValType(1) == "N") Assert("6c ValType('a') = 'C'", ValType("a") == "C") Assert("6d ValType(.T.) = 'L'", ValType(.T.) == "L") Assert("6e ValType({}) = 'A'", ValType({}) == "A") Assert("6f ValType({=>}) = 'H'", ValType({=>}) == "H") Assert("6g ValType({||}) = 'B'", ValType({|| NIL}) == "B") Assert("6h NIL == NIL", NIL == NIL) Assert("6i NIL != 0", !( NIL == 0 )) Assert("6j Empty('')", Empty("")) Assert("6k Empty(0)", Empty(0)) Assert("6l ! Empty(1)", ! Empty(1)) RETURN /* ====================================================================== */ /* 7. STATIC variables */ /* ====================================================================== */ STATIC PROCEDURE TestStatic() ? "--- 7. STATIC ---" Assert("7a STATIC counter 1st call = 1", StaticCounter() == 1) Assert("7b STATIC counter 2nd call = 2", StaticCounter() == 2) Assert("7c STATIC counter 3rd call = 3", StaticCounter() == 3) RETURN STATIC s_nCounter := 0 STATIC FUNCTION StaticCounter() s_nCounter++ RETURN s_nCounter /* ====================================================================== */ /* 8. LOCAL scope */ /* ====================================================================== */ STATIC PROCEDURE TestLocalScope() LOCAL x := "outer" ? "--- 8. LOCAL scope ---" Assert("8a LOCAL before IF: 'outer'", x == "outer") Assert("8b LOCAL after assignment: unchanged", x == "outer") // Multiple LOCALs in same function LOCAL y := 99 Assert("8b Multiple LOCALs: y=99", y == 99) Assert("8c x still 'outer'", x == "outer") RETURN /* ====================================================================== */ /* 9. Array + Hash operations */ /* ====================================================================== */ STATIC PROCEDURE TestArrayHash() LOCAL a, h, i, nSum ? "--- 9. Array + Hash ---" // Array basics a := {1, 2, 3} Assert("9a Array literal: Len=3", Len(a) == 3) AAdd(a, 4) Assert("9b AAdd: Len=4", Len(a) == 4) // ASort a := {3, 1, 2} ASort(a) Assert("9c ASort: {1,2,3}", a[1] == 1 .AND. a[2] == 2 .AND. a[3] == 3) // ASort with block a := {3, 1, 2} ASort(a,,, {|x,y| x > y}) Assert("9d ASort desc: {3,2,1}", a[1] == 3 .AND. a[2] == 2 .AND. a[3] == 1) // ASort dates (default, no block — formerly no-op, now sorts julian) a := { CToD("2026-03-15"), CToD("2024-01-10"), CToD("2025-07-01") } ASort(a) Assert("9c1 ASort dates ascending", ; a[1] == CToD("2024-01-10") .AND. ; a[2] == CToD("2025-07-01") .AND. ; a[3] == CToD("2026-03-15")) // ASort logicals (default — .F. < .T.) a := { .T., .F., .T., .F. } ASort(a) Assert("9c2 ASort logicals: F,F,T,T", ; !a[1] .AND. !a[2] .AND. a[3] .AND. a[4]) // AScan a := {"alice", "bob", "charlie"} Assert("9e AScan: found 'bob' at 2", AScan(a, "bob") == 2) Assert("9f AScan: 'dave' not found", AScan(a, "dave") == 0) // AScan numeric fast-path a := { 10, 20, 30, 40 } Assert("9e1 AScan int found", AScan(a, 30) == 3) Assert("9e2 AScan int cross-type (double lookup)", AScan(a, 30.0) == 3) Assert("9e3 AScan int not found", AScan(a, 99) == 0) // AEval with mutable closure capture (Harbour: closures share outer locals) nSum := 0 AEval({10, 20, 30}, {|x| nSum += x}) Assert("9g AEval closure sum: 60", nSum == 60) // Hash basics h := {=>} h["name"] := "Alice" h["age"] := 30 Assert("9h Hash set/get: 'Alice'", h["name"] == "Alice") Assert("9i Hash Len: 2", Len(h) == 2) Assert("9j hb_HHasKey: .T.", hb_HHasKey(h, "name")) Assert("9k hb_HHasKey missing: .F.", ! hb_HHasKey(h, "xyz")) // Hash iteration LOCAL aKeys := hb_HKeys(h) Assert("9l hb_HKeys Len: 2", Len(aKeys) == 2) // hb_HClone LOCAL h2 := hb_HClone(h) h2["name"] := "Bob" Assert("9m hb_HClone: orig unchanged", h["name"] == "Alice") Assert("9n hb_HClone: clone changed", h2["name"] == "Bob") RETURN /* ====================================================================== */ /* Assert helper */ /* ====================================================================== */ STATIC FUNCTION Assert(cLabel, lOK) IF lOK s_nPass++ ? " PASS:", cLabel ELSE s_nFail++ ? " FAIL:", cLabel ENDIF RETURN NIL