# Porffor AOT Rendering Integration Guide

## Overview

Porffor AOT (Ahead-Of-Time) compilation for vue-renderer:

- **Single dylib**: Compile ONCE, use many times
- **Native performance**: JavaScript → C → dylib
- **Zero-copy**: Direct FFI calls to compiled functions

## Feature Comparison: C Compile (2c) vs Wasm Compile

> **CRITICAL**: Porffor's C compilation path via `2c` (Wasm→C transpiler) is **experimental** and **intentionally limited**. For production, prefer Wasm target.

### Unimplemented Wasm Opcodes in 2c

| Wasm Opcode | Status | Description |
|-------------|--------|-------------|
| `call_indirect` (0x11) | ⚠️ Stubbed (returns `0, 0`) | Indirect function calls through table |
| `br_table` (0x0e) | ❌ Unimplemented | Branch table for multi-target jumps |
| `memory_size` (0x3f) | ❌ Unimplemented | Get current memory page count |
| `i32_rotr` (0x78) | ❌ Unimplemented | Rotate right (only `rotl` exists) |
| `f64_copysign` (0xa6) | ❌ Unimplemented | Copy sign bit |
| `f64_sqrt` (0x9f) | ❌ Unimplemented | Square root |
| `i64_*` comparisons | ❌ Mostly missing | Only `i64_eqz` implemented |
| `try`/`catch`/`catch_all` | ⚠️ Partial | Only `throw` handled (exits on throw) |

### Disabled Features for C/Native Target

From `compiler/index.js:85`:

```javascript
Prefs.passiveData = false; // disabled as unsupported by 2c for now
```

| Feature | Wasm | C/Native (2c) |
|---------|------|---------------|
| Passive data segments | ✅ | ❌ Disabled |
| Exception catching | ✅ | ❌ TODO (line 830) |
| Indirect calls | ✅ | ⚠️ Stubbed |
| SIMD instructions | ✅ | ❌ Not in switch |
| Atomic operations | ✅ | ❌ Not in switch |

### What This Means for Your Code

**AVOID** these patterns in JS intended for C compilation:

1. **Dynamic function calls** - Uses `call_indirect` (stubbed)
2. **Large switch statements** - May compile to `br_table`
3. **try/catch blocks** - Only throw works, catching crashes
4. **Memory growth queries** - `memory_size` not implemented
5. **i64 operations** - Most i64 ops missing
6. **Advanced math** - `Math.sqrt`, `Math.copysign` not implemented

**SAFE** patterns for C compilation:

- Direct function calls
- Basic arithmetic (+, -, *, /)
- String operations (concatenation, indexOf, substring)
- Array operations (push, pop, basic iteration)
- If/else conditionals
- Loops (for, while)

### Recommendation

```bash
# For development/testing (Wasm target - full feature support)
porf wasm libporffor.js out.wasm

# For production dylib (C target - limited features)
porf c libporffor.js libporffor.c
```

If your code uses unimplemented features, 2c will log:

```text
warning: 2c: unimplemented op: br_table (functionName)
warning: 2c: unimplemented op: call_indirect (functionName)
```

### Verification

Before deploying C-compiled dylib:

```bash
# Check for unimplemented opcode warnings
porf c libporffor.js libporffor.c 2>&1 | grep "unimplemented"
```

If no warnings appear, your code is 2c-compatible.

## Architecture

```text
libporffor.js --[Porffor AOT]--> libporffor.c + libporffor-init.c --[clang]--> libporffor.dylib
                                                                                   |
                                                                                   v
                                                               test.lua (FFI) --> lib.funcCall()
                                                               all test functions
```

## Files

### Source (Input)

- **`libporffor.js`** - JavaScript source code (single file, no ES6 imports)
  - HTML parser (no regex, Porffor-compatible)
  - `renderBig(paramStr)` - Main function
  - Test functions: `testInlineCode()`, `testArrayOps()`, `testComplexInline()`, etc.
  - Export via `globalThis.funcName = funcName`
  - Must call to prevent tree-shaking

### Build

- **`xmake.lua`** - Build configuration
  - Runs Porffor: `node /path/to/porffor/runtime/index.js c libporffor.js`
  - Removes `main()` function (uses `porffor_init()` from C)
  - Compiles C to dylib with clang
  - Fails build if Porffor compilation fails
  - Output: `/Volumes/nc/bin/bin_osx_arm64/libporffor.dylib`

- **`libporffor-init.c`** - C helper
  - `porfforInit(int pageCount, int porfforStart, int reservedBytes)` - Initialize Porffor memory
  - `porfforGetMemory()` - Get memory buffer pointer
  - `porfforGetCurrentPtr()` - Get current allocator position
  - `porfforWriteData()` - C function to write data to Lua buffer
  - `porfforWritePattern()` - C function to write pattern to Lua buffer

### Compiled (Output)

- **`libporffor.dylib`** - Native shared library
  - Loaded via `ffi.load(path)` or `ffi.loadDll("porffor")`
  - Contains compiled JavaScript functions

### Usage

- **`porffor-javascript-test.lua`** - All-in-one test file

## Building

```bash
cd /Volumes/nc/nc-plugin/plugin/vue-renderer/renderer/porffor
xmake -vr
```

This:

1. Bundles JS with rolldown
2. Compiles JS to C with Porffor
3. Renames `main()` to `_main()` to preserve string literal initialization
4. Compiles to dylib with clang
5. Moves to output: `/Volumes/nc/bin/bin_osx_arm64/libporffor.dylib`

## Memory and Allocator Initialization

### Critical: Allocator State Must Match Memory

Porffor uses a bump-pointer allocator with these global variables (from libporffor.c):

```c
char* _memory;              // Memory buffer pointer
u32 _memoryPages = 14;      // Page count (INITIALIZED TO 14!)
i32 jjporfjjcurrentPtr = 0; // Current allocation offset
i32 jjporfjjendPtr = 0;     // Allocation limit
```

### The calloc() vs realloc() Issue

**WHAT DOES calloc() DO?**

- Allocates FRESH zeroed memory
- Returns NEW pointer assigned to `_memory`
- Does NOT touch `jjporfjjcurrentPtr` or `jjporfjjendPtr`

**WHAT DOES realloc() DO?**

- Can return SAME or DIFFERENT pointer
- If moved, OLD pointer becomes INVALID
- Updates GLOBAL `_memory` variable
- **BUG**: Lua's cached `mem` variable becomes STALE

### Initialization Sequence (libporffor-init.c)

```c
int porfforInit(int pageCount, int porfforStart, int reservedBytes) {
    // 1. Validate parameters
    int totalSize = pageCount * 65536;
    if (pageCount < 13 || porfforStart < 0 || reservedBytes < 1024 || reservedBytes > totalSize) return -1;
    // 2. Allocate memory (64KB pages)
    _memory = (char *)calloc(pageCount, 65536);
    // 3. Update Porffor's page count (CRITICAL!)
    _memoryPages = pageCount;
    // 4. Initialize allocator state - Porffor starts at porfforStart
    jjporfjjcurrentPtr = porfforStart;  // Start AFTER reserved zone
    jjporfjjendPtr = totalSize;
    // 5. Initialize string literals (uses allocator)
    _main();
    return 0;
}
```

**C Parameters:**

- `pageCount`: Number of 64KB pages (e.g., 256 = 16MB)
- `porfforStart`: Where Porffor allocator begins (usually = reservedBytes)
- `reservedBytes`: Size of Lua zone (validates against totalSize)

**Memory Layout After Init:**

```text
[0................reservedBytes..................][reservedBytes.......totalSize]
  ^ Lua parameters zone                           ^ Porffor allocator zone
  nextParamOffset starts at 0                    jjporfjjcurrentPtr starts here
```

**WHY THIS ORDER?**

1. calloc() allocates memory but doesn't init allocator state
2. Must update `_memoryPages` to match actual allocation (was 14, need 256)
3. Set `jjporfjjcurrentPtr = reservedBytes` to reserve space for Lua
4. Must init allocator BEFORE `_main()` so string allocation works
5. After `_main()`, `jjporfjjcurrentPtr` points past string literals
6. Later Porffor allocations start AFTER reserved zone (safe from Lua writes)

### What Happens When __Porffor_malloc Runs

```c
i32 __Porffor_malloc(i32 size) {
    if ((jjporfjjcurrentPtr + size) >= jjporfjjendPtr) {
        // Realloc path: out of memory
        const u32 _oldPages = _memoryPages;
        _memoryPages += 16;
        _memory = realloc(_memory, _memoryPages * 65536);  // MAY CHANGE POINTER!
        memset(_memory + _oldPages * 65536, 0, 16 * 65536);
        jjporfjjcurrentPtr = (_oldPages * 65536) + size;
        jjporfjjendPtr = (jjporfjjcurrentPtr + 1048576) - size;
        return jjporfjjcurrentPtr - size;
    }

    // Fast path: enough space
    jjporfjjcurrentPtr = jjporfjjcurrentPtr + size;
    return jjporfjjcurrentPtr;
}
```

**POTENTIAL BUG**: If `realloc()` moves memory, Lua's `mem` variable points to OLD (freed) memory!

## Using Compiled Dylib

### Lua Wrapper for porfforInit (Recommended)

The Lua wrapper `porfforInit(totalMemKB, reservedKB)` provides:

- **KB parameters** (easier than bytes)
- **Validation** (64KB multiples, minimum 13 pages)
- **Overhead reporting** (shows Porffor string literal usage)

```lua
-- Wrapper: validates, calculates pages, calls C
local function porfforInit(totalMemKB, reservedKB)
    local pageSizeKB = 64
    local reservedZone = reservedKB * 1024

    -- Validate
    if totalMemKB % pageSizeKB ~= 0 then
        util.printError("memSize must be multiple of 64KB")
        return -1
    end
    if totalMemKB < 13 * pageSizeKB then
        util.printError("memSize must be >= 13 pages (832KB)")
        return -1
    end

    local pageCount = totalMemKB / pageSizeKB
    local result = lib.porfforInit(pageCount, reservedZone, reservedZone)
    if result ~= 0 then return -1 end

    -- Report overhead
    local ptr = lib.porfforGetCurrentPtr()
    local overhead = ptr - reservedZone
    local freeMem = (totalMemKB * 1024) - ptr
    util.print("Porffor allocator: " .. ptr .. " (overhead=" .. overhead .. ", free=" .. freeMem .. ")")

    return 0
end

-- Usage: 16MB total, 1MB reserved
porfforInit(16 * 1024, 1 * 1024)
```

**Output example:**

```text
Init: 16384KB total (256 pages), 1024KB reserved, Porffor start=1048576
Porffor allocator: 2277376 (overhead=1228800, free=14499840)
```

**Overhead meaning:** Porffor uses ~1.2MB for string literals (initialized by `_main()`)

### In Lua (via FFI)

```lua
local ffi = require("ffi")

-- Load dylib
local jslib = ffi.load("/Volumes/nc/bin/bin_osx_arm64/libporffor.dylib")

-- FFI definitions
ffi.cdef [[
    typedef struct ReturnValue {
        f64 value;
        i32 type;
    } ReturnValue;

    ReturnValue renderBig(f64 newtarget, i32 newtargetType,
                         f64 thisptr, i32 thisType,
                         f64 param, i32 paramType);
    int porfforInit(int pageCount, int porfforStart, int reservedBytes);
    char* porfforGetMemory();
    int porfforGetCurrentPtr();
]]

-- Initialize with Lua wrapper
porfforInit(16 * 1024, 1 * 1024)  -- 16MB total, 1MB reserved

-- Call function
local result = jslib.renderBig(0, 0, 0, 0, 0, 0)

-- Get result from memory
local mem = jslib.porfforGetMemory()
local resultStr = ffi.string(mem + result.value, result.type)
```

## Function Signature Pattern

All compiled JavaScript functions receive 6 parameters:

1. `newtarget` (f64) - Constructor target value (usually 0)
2. `newtargetType` (i32) - Constructor target type (usually 0)
3. `thisptr` (f64) - This pointer value (usually 0)
4. `thisType` (i32) - This type (usually 0)
5. Parameter value (f64) - Actual parameter (memory offset for strings)
6. Parameter type (i32) - Parameter type tag

Return: `ReturnValue` struct with `{value, type}`

## String Memory Format

### Type 195 (String Literal)

Memory layout:

```text
[1 byte: length][3 bytes: PADDING][string data...]
```

**CRITICAL**: The 3-byte padding is for memory alignment. MUST skip 4 bytes total.

Read example:

```lua
local u8Len = ffi.cast("u8*", mem + offset)[0]  -- Read 1-byte length
local str = ffi.string(mem + offset + 4, u8Len) -- Skip 4 bytes (len + 3 pads)
```

Raw memory example:

```text
Offset: 15, 0, 0, 0, 104, 101, 108, 108, 111, 32, 119, ...
Meaning: [len=15][pad][pad][pad]['h']['e']['l']['l']['o'][' ']['w']...
```

### Type 67 (ByteString/Computed)

Memory layout:

```text
[4 bytes: i32 length][string data...]
```

Read example:

```lua
local len32 = ffi.cast("i32*", mem + offset)[0]  -- Read 4-byte length
local str = ffi.string(mem + offset + 4, len32)  -- Skip 4 bytes
```

**Key Difference**: Type 67 has no padding, data immediately follows 4-byte length.

## Type Tags

| Tag | Type | Description |
|-----|------|-------------|
| 1 | Number | f64 number |
| 2 | Boolean | true/false |
| 67 | ByteString | Computed string |
| 195 | String | Literal string |

### How to Choose: Type 67 vs Type 195

**Decision Tree:**

```mermaid
flowchart TD
    A[Need to pass string to Porffor] --> B{Where is string created?}
    B -->|In JavaScript source| C[Type 195: String Literal]
    B -->|At runtime from Lua| D[Type 0: ByteString]
    C --> E[Porffor handles automatically]
    D --> F[Use setStringParam - returns offset, type=0]
```

**Rule: You NEVER choose manually**

| Scenario | Who Sets Type | Value |
|----------|---------------|-------|
| `const s = "hello"` in `.js` file | Porffor compiler | **195** |
| `s = a + b` (concatenation) | Porffor runtime | **67** |
| `setStringParam(str)` in Lua | Always **0** (hardcoded) | **0** |
| Reading Porffor return value | Porffor returns | **Either** |

**Why TWO types?**

- **Type 195 (Literal)**: Stored in bytecode, read-only, 1-byte length prefix + 3-byte padding
- **Type 67 (ByteString)**: Computed at runtime (concatenation), 4-byte length prefix, no padding
- **Type 0 (for Lua params)**: Passed from Lua, treated as type 67 by Porffor

**Both types store 1-byte-per-char data** (not UTF-16). The difference is in the length prefix format.

**Code Example:**

```lua
-- setStringParam returns just the offset (type is always 0)
local offset = setStringParam(myStr)
local result = lib.renderBig(0, 0, 0, 0, offset, 0)

-- WHEN Porffor returns string:
local result = lib.renderBig(0, 0, 0, 0, 0, 0)
-- result.type is 195 OR 67 - Porffor decides!
local str = porfforGetString(result)     -- ✅ Handles both types
```

## Porffor Compatibility Rules

1. **NO regex** - Use manual string parsing with `indexOf`, `substring`
2. **NO ES6 imports** - All code in one file (single-file approach)
3. **Export via globalThis** - `globalThis.funcName = funcName`
4. **Prevent tree-shaking** - Call each function: `funcName(defaultParam)`
5. **JSON for complex data** - Pass/return JSON strings
6. **Simple types** - Numbers work best, strings via memory offsets

## Testing

### Run Test

```bash
cd /Volumes/nc/nc-plugin/plugin/vue-renderer/renderer/porffor
lj porffor-javascript-test.lua
```

### Test Coverage

| Test | Description | Status |
|------|-------------|--------|
| `initTooFewPages` | 5 pages should fail | ✅ Pass |
| `init` | 256 pages should succeed | ✅ Pass |
| `minimalMem` | 13 pages (832KB) absolute minimum | ✅ Pass |
| `smallMem` | 20 pages (1.3MB) | ✅ Pass |
| `biggerMem` | 50 pages (3.2MB) | ✅ Pass |
| `inlineCode` | Basic arithmetic (42 + 10 = 52) | ✅ Pass |
| `returnString` | Returns "hello world" + random number | ✅ Pass |
| `arrayOps` | Array sum [1,2,3,4,5] = 15 | ✅ Pass |
| `complexInline` | (100 * 2) + 50 = 250 | ✅ Pass |
| `nestedCalls` | multiply(add(5,3), 2) = 16 | ✅ Pass |
| `conditionals` | If value > 50 return 1 | ✅ Pass |
| `largeArray` | Sum 0..999 = 499500 | ✅ Pass |
| `bigComputation` | Sum 0..9999 = 49995000 | ✅ Pass |
| `cBigDataSend` | C writes 100000 bytes to Lua buffer | ✅ Pass |
| `cPatternSend` | C writes pattern to Lua buffer | ✅ Pass |
| `es6Import` | ES6 import (10 + 20 = 30) | ✅ Pass |
| `stringParamNull` | Pass null parameter | ✅ Pass |
| `stringParamNumber` | Pass number parameter | ✅ Pass |
| `stringParamEmpty` | Empty string parameter | ✅ Pass |
| `stringParamByteString` | ByteString type 67 (6000 chars) | ✅ Pass |

**Current: 21/21 passing** ✅

All string reading works correctly after fixing the 3-byte padding issue in type 195 strings.

### Memory Scaling Tests

| Test | Total | Reserved | Overhead | Free |
|------|-------|----------|----------|------|
| minimalMem | 832KB | 1KB | 0 (re-init) | 850KB |
| smallMem | 1.3MB | 640KB | 0 (re-init) | 640KB |
| biggerMem | 3.2MB | 1.6MB | 0 (re-init) | 1.6MB |
| init | 16MB | 1MB | 1.2MB | 14MB |

**Note:** Re-init shows overhead=0 because `_main()` is called again with fresh allocator state.

## Troubleshooting

### Dylib not found

```text
error: failed to load library 'libporffor.dylib'
```

**Solution:** Run `xmake` to build dylib

### Init fails (returns -1)

```text
porfforInit returns -1
```

**Causes:**

1. `totalMemKB` not multiple of 64KB
2. `totalMemKB < 832KB` (13 pages minimum)
3. `reservedKB >= totalMemKB`
4. `reservedKB < 1KB`

**Solution:** Use the Lua wrapper with KB parameters

```lua
-- WRONG (C function directly)
lib.porfforInit(5, 0, 1000000)     -- pageCount too small
lib.porfforInit(256, 0, 100)       -- reservedBytes too small
lib.porfforInit(13, 0, 10000000)   -- reservedBytes > totalSize

-- CORRECT (Lua wrapper with KB)
porfforInit(16 * 1024, 1 * 1024)   -- 16MB total, 1MB reserved
porfforInit(832, 1)                -- 832KB total, 1KB reserved (minimal)
```

### Empty result from function

```text
result.value = 0, result.type = 0
```

**Solution:**

1. Check `porfforInit()` was called
2. Check function is exported via `globalThis`
3. Check function is called to prevent tree-shaking

### Porffor compilation fails

**Solution:** Check JS code follows Porffor rules (no regex, no ES6 imports)

### Crash on malloc (Segmentation fault)

```text
Segmentation fault (exit code 139)
```

**Possible causes:**

1. Allocator state not initialized (`jjporfjjcurrentPtr`, `jjporfjjendPtr` = 0)
2. `_memoryPages` not updated (still 14 instead of actual pageCount)
3. Realloc moved memory, Lua has stale pointer

**Solution:** Check `libporffor-init.c` has:

```c
_memoryPages = pageCount;              // Must match calloc size
jjporfjjcurrentPtr = 0;                // Init BEFORE _main()
jjporfjjendPtr = pageCount * 65536;
_main();                               // _main uses allocator
```

### String returns "hello worldNaN"

**Cause:** `Math.random()` has no return statement in Porffor

**Solution:** Don't use `Math.random()` in return values

## Performance Expectations

| Operation | SpiderMonkey JIT | Porffor AOT | Speedup |
|-----------|------------------|-------------|---------|
| First render | 50ms | 100ms | 0.5x (compile overhead) |
| Cached render | 50ms | 5ms | **10x** |
| 100k rows | 500ms | 50ms | **10x** |
| Data transfer | copy | zero-copy | **infinite** |

## Known Issues

1. **Type 195 3-byte padding** - String literals have 3 padding bytes after length (for alignment)
2. **Math.random()** - Returns NaN (no return statement in generated code)
3. **realloc() pointer invalidation** - If realloc moves memory, Lua's cached pointer becomes stale

## Future Work

1. Implement safe memory refresh after realloc
2. Add BigInt support for large integers
3. Optimize JSON serialization
4. Investigate Porffor's pointer table system for alternative string passing

## See Also

- Porffor: <https://github.com/connorjsoap/porffor>
- LuaJIT FFI: <http://luajit.org/ext_ffi.html>
