Files
five/docs/frb.md
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

12 KiB

FRB — Five Runtime Binary

Why Five uses FRB instead of Harbour's HRB

Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com). All rights reserved.

Overview

FRB (Five Runtime Binary) is Five's dynamic module format for loading and executing compiled PRG code at runtime. It replaces Harbour's HRB (Harbour Runtime Binary) with a dual-mode architecture: native compilation for maximum performance and pcode interpretation for maximum portability.

The Problem with HRB

Harbour's HRB format stores pcode bytecode — an intermediate representation that must be interpreted by the Harbour Virtual Machine at runtime:

PRG Source → Harbour Compiler → HRB (pcode bytecode) → VM Interpreter → Execution

This architecture has inherent limitations:

  1. Performance: Every instruction passes through the interpreter loop — decode, dispatch, execute. For compute-heavy code, this overhead is significant.
  2. No optimization: Pcode bypasses CPU branch prediction, register allocation, and instruction-level parallelism.
  3. No concurrency: HRB modules cannot use threads safely due to Harbour's chronic threading limitations.
  4. No native integration: HRB cannot call Go (or C) functions directly without marshaling through the VM layer.

The FRB Solution

Five's FRB provides two execution modes in a single format:

Mode 1: Native (Go Plugin)

PRG Source → Five Compiler → Go Source → Go Compiler → Native Plugin (.so)
                                                            ↓
                                                      FRB Container (4.7 MB)

The .frb file contains a compiled Go shared library. When loaded, code executes at full native speed — identical to a statically compiled binary.

Mode 2: Pcode (Interpreter)

PRG Source → Five Compiler → Pcode Bytecode → FRB Container (175 bytes)
                                                    ↓
                                            Five Pcode Interpreter

The .frb file contains compact bytecode. No Go compiler needed on the target machine. The pcode interpreter calls the same Thread operations as native code, ensuring identical behavior.

Dual-Mode Architecture

The key insight: the same PRG source compiles to both modes. The developer chooses at build time; the runtime transparently handles either format.

# Native mode — maximum performance (requires Go on build machine only)
five frb module.prg -o module.frb

# Pcode mode — maximum portability (runs anywhere Five runs)
five frb module.prg -o module.frb --pcode

Both produce valid .frb files. FrbLoad() detects the mode automatically.

Architecture Comparison

Aspect HRB (Harbour) FRB Native FRB Pcode
Content Harbour pcode Go native .so Five pcode
Execution Harbour VM Direct CPU Five interpreter
Speed Baseline 10-100x faster ~1x (same class)
File size Small ~4.7 MB 175 bytes
Go needed (build) N/A Yes Yes (five CLI)
Go needed (run) No No No
Platform (run) All Harbour Linux All Five
Goroutines No Yes Yes
Go interop No Native Via RTL

File Format

Offset  Size   Field        Description
0       4      Magic        0xC0 'F' 'R' 'B'
4       2      Version      uint16 LE (currently 2)
6       1      Mode         0x01 = Native, 0x02 = Pcode
7       1      Reserved     0x00
8       4      SymCount     uint32 LE (number of functions)
12      ...    Payload      Mode-dependent content

Native payload: Embedded Go plugin binary (ELF .so)

Pcode payload: Serialized function table:

uint16  funcCount
For each function:
  uint16  nameLen + name (null-free)
  uint16  params
  uint16  locals
  uint32  codeLen + bytecode

The FRB header is deliberately similar to HRB's 0xC0 'H' 'R' 'B' for familiarity, with 'F' replacing 'H' to indicate the Five format.

Pcode Instruction Set

Five's pcode maps 1:1 to Thread stack operations, making the bytecode a direct serialization of what the native compiler generates as Go function calls:

Opcode Hex Description
PcOpPushNil 0x01 Push NIL
PcOpPushInt 0x04 Push int64 (8 bytes LE)
PcOpPushString 0x06 Push string (uint16 len + bytes)
PcOpPushLocal 0x07 Push local variable
PcOpPopLocal 0x08 Pop to local variable
PcOpPlus 0x10 Add top two stack values
PcOpEqual 0x20 Compare equality
PcOpJumpFalse 0x31 Conditional jump
PcOpPushSymbol 0x40 Push function symbol by name
PcOpFunction 0x42 Call function with N args
PcOpReturn 0x33 Return from function

Full opcode set: 40+ opcodes covering arithmetic, comparison, logic, flow control, function calls, OOP, arrays, and blocks.

API Reference

Command Line

# Build native FRB (maximum speed)
five frb mymodule.prg -o mylib.frb

# Build pcode FRB (maximum portability, no Go needed to run)
five frb mymodule.prg -o mylib.frb --pcode

File-Based Loading

// Load FRB module (auto-detects native vs pcode)
pMod := FrbLoad("mylib.frb")

// Call functions from loaded module
result := FrbDo(pMod, "MYFUNC", arg1, arg2)

// Unload when done
FrbUnload(pMod)

In-Memory Compilation

// Compile PRG source string at runtime
// Falls back to pcode mode automatically if Go is not installed
cSource := 'FUNCTION Double(n)' + Chr(10) + ;
           '   RETURN n * 2'

pMod := FrbCompile(cSource)
? FrbDo(pMod, "DOUBLE", 21)    // → 42
FrbUnload(pMod)

One-Shot Execution

// Compile + run Main() + unload in one call
cProgram := 'FUNCTION Main()' + Chr(10) + ;
            '   RETURN 6 * 7'

? FrbExec(cProgram)    // → 42

Function Reference

Function Description
FrbLoad(cFile) Load .frb file (native or pcode), return module handle
FrbDo(pMod, cFunc, ...) Call function in loaded module
FrbUnload(pMod) Unload module, free resources
FrbRun(cFile, ...) Load + run Main() + unload
FrbCompile(cSource) Compile PRG source string (auto-selects mode)
FrbExec(cSource, ...) Compile + run Main() + unload

Use Cases

Plugin Architecture

// Application loads plugins at startup
LOCAL aPlugins := Directory("plugins/*.frb")
FOR EACH cFile IN aPlugins
   LOCAL pPlugin := FrbLoad("plugins/" + cFile[1])
   FrbDo(pPlugin, "INIT")
NEXT

Hot Code Reload

// Read PRG source from database or network
cSource := MemoRead("custom_report.prg")
pMod := FrbCompile(cSource)
FrbDo(pMod, "GENERATEREPORT", dStart, dEnd)
FrbUnload(pMod)

User-Defined Business Rules

// Store business rules as PRG text in database
cRule := GetRuleFromDB("DISCOUNT_CALC")
pRule := FrbCompile(cRule)
nDiscount := FrbDo(pRule, "CALCULATE", nAmount, cCustomerType)
FrbUnload(pRule)

Dynamic Code with Goroutines

// Compile a worker function at runtime, run it in a goroutine
cWorker := 'FUNCTION Worker(ch, n)' + Chr(10) + ;
           '   ChSend(ch, n * n)' + Chr(10) + ;
           '   RETURN NIL'
FrbCompile(cWorker)
ch := Channel(1)
Go("WORKER", ch, 42)
? ChReceive(ch)    // → 1764

Deployment Strategy

Scenario Recommended Mode Reason
Performance-critical server Native Maximum speed
End-user distribution Pcode No Go dependency
Development / testing Native Faster iteration
Cross-platform plugins Pcode Works everywhere
Embedded business rules Pcode Tiny file size
Compute-heavy algorithms Native CPU-bound benefit
  1. Development: Use native mode for fast debugging with full Go optimization
  2. Distribution: Ship pcode .frb files alongside the compiled Five binary
  3. Hot reload: Use FrbCompile() — auto-falls back to pcode if Go unavailable

Symbol Scoping and Isolation

FRB modules operate in an isolated scope to prevent name collisions between the host program and loaded modules. This is critical for plugin architectures where multiple modules may define functions with the same name.

Scoping Rules

Scenario Behavior
FrbDo(pMod, "FUNC") Module scope first, then VM global
Module defines Main() Always module-local; never registered in VM
Module function = host function (same name) Host function preserved; module function accessible only via FrbDo()
Module function = new name (not in host) Registered in VM global scope; callable directly from host
FrbUnload(pMod) Newly registered symbols removed; overwritten symbols restored

How It Works

When FrbLoad() loads a module:

  1. All module functions are stored in the module's local symbol table.
  2. Main() is never exported to the VM — it stays module-private.
  3. For each non-Main function:
    • If a function with the same name already exists in the VM: skip (host function protected).
    • If the name is new: register in the VM global scope.
  4. The module records what it registered and what it would have overwritten.

When FrbDo(pMod, "FUNC", ...) is called:

  1. First searches the module's local scope — finds module-private functions.
  2. If not found locally, falls back to the VM global scope.
  3. This means FrbDo() always reaches the module's version of a function, even if the host has a different function with the same name.

When FrbUnload(pMod) is called:

  1. All symbols the module registered globally are removed from the VM.
  2. Any host symbols that were saved are restored to their original state.
  3. The VM returns to exactly the state it had before FrbLoad().

Example: Name Collision

// Host program defines Add() as (a+b)*10
FUNCTION Add(a, b)
   RETURN (a + b) * 10

FUNCTION Main()
   ? Add(1, 2)                        // → 30 (host function)

   pMod := FrbLoad("mathlib.frb")     // Module also defines Add() as a+b

   ? Add(1, 2)                        // → 30 (host function still works!)
   ? FrbDo(pMod, "ADD", 100, 200)     // → 300 (module's Add via FrbDo)

   FrbUnload(pMod)
   ? Add(1, 2)                        // → 30 (fully restored)
   RETURN NIL

Comparison with Harbour HRB Binding Modes

Harbour HRB Five FRB Description
HB_HRB_BIND_DEFAULT Default behavior Don't overwrite existing functions
HB_HRB_BIND_OVERLOAD (not needed) FrbDo() always reaches module scope
HB_HRB_BIND_FORCELOCAL Default for Main() Entry point always module-private

Five simplifies Harbour's binding modes into a single intuitive behavior: module functions are always accessible via FrbDo(), host functions are always protected, and FrbUnload() cleanly restores the original state.

Limitations

Native Mode

  • Linux only (Go plugin limitation)
  • FRB and host binary must use same Go version
  • Go plugins cannot be truly unloaded from memory
  • Larger file size (~4.7 MB per module)

Pcode Mode

  • Slower than native (interpreter overhead)
  • Advanced features may have limited pcode support
  • No direct Go interop from pcode (uses RTL functions)

Migration from Harbour HRB

Harbour Five
hb_hrbLoad(cFile) FrbLoad(cFile)
hb_hrbDo(pHrb, ...) FrbDo(pMod, cFunc, ...)
hb_hrbUnload(pHrb) FrbUnload(pMod)
hb_hrbRun(cFile, ...) FrbRun(cFile, ...)
hb_compileFromBuf(cSrc) FrbCompile(cSrc)
N/A FrbExec(cSrc, ...)
.hrb extension .frb extension
Pcode only Native + Pcode dual mode

The API is deliberately similar to Harbour's for easy migration. Existing HRB workflows translate directly to FRB with the added benefit of choosing between native speed and universal portability.

Verified Test Results

=== FRB Pcode Mode Test ===

Hello: Hello, World! (from FRB module)     175 bytes, no Go needed
Add: 300                                    Arithmetic works
Factorial: 3628800                          Recursion works

=== FRB Native Mode Test ===

Hello: Hello, Five! (from FRB module)       4.7 MB, native speed
Add(100, 200): 300                          Direct Go execution
Factorial(10): 3628800                      Compiled recursion