13 KiB
title, description
| title | description |
|---|---|
| C Runtime for Native Code | Minimum C runtime surface for QBE-generated native code |
Overview
QBE-generated native code calls into a C runtime for anything that touches the heap, dispatches dynamically, or requires GC awareness. The design principle: native code handles control flow and integer math directly; everything else is a runtime call.
This document defines the runtime boundary — what must be in C, what QBE handles inline, and how to organize the C code to serve both the mcode interpreter and native code cleanly.
The Boundary
What native code does inline (no C calls)
These operations compile to straight QBE instructions with no runtime involvement:
- Integer arithmetic:
add,sub,mulon NaN-boxed ints (shift right 1, operate, shift left 1) - Integer comparisons: extract int with shift, compare, produce tagged bool
- Control flow: jumps, branches, labels, function entry/exit
- Slot access: load/store to frame slots via
%fp+ offset - NaN-box tagging: integer tagging (
n << 1), bool constants (0x03/0x23), null (0x07) - Type tests:
JS_IsInt(LSB check),JS_IsNumber,JS_IsText,JS_IsNull— these are bit tests on the value, no heap access needed
What requires a C call
Anything that:
- Allocates (arrays, records, strings, frames, function objects)
- Touches the heap (property get/set, array indexing, closure access)
- Dispatches on type at runtime (dynamic load/store, polymorphic arithmetic)
- Calls user functions (frame setup, argument passing, invocation)
- Does string operations (concatenation, comparison, conversion)
Runtime Functions
Tier 1: Essential (must exist for any program to run)
These are called by virtually every QBE program.
Intrinsic Lookup
// Look up a built-in function by name. Called once per intrinsic per callsite.
JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name);
Maps name → C function pointer wrapped in JSValue. This is the primary entry point for all built-in functions (print, text, length, is_array, etc). The native code never calls intrinsics directly — it always goes through get_intrinsic → frame → invoke.
Function Calls
// Allocate a call frame with space for nr_args arguments.
JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int nr_args);
// Set argument idx in the frame.
void cell_rt_setarg(JSValue frame, int idx, JSValue val);
// Execute the function. Returns the result.
JSValue cell_rt_invoke(JSContext *ctx, JSValue frame);
This is the universal calling convention. Every function call — user functions, intrinsics, methods — goes through frame/setarg/invoke. The frame allocates a JSFrameRegister on the GC heap, setarg fills slots, invoke dispatches.
Tail call variants:
JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int nr_args);
void cell_rt_goinvoke(JSContext *ctx, JSValue frame);
Same as frame/invoke but reuse the caller's stack position.
Tier 2: Property Access (needed by any program using records or arrays)
// Record field by constant name.
JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name);
void cell_rt_store_field(JSContext *ctx, JSValue obj, JSValue val, const char *name);
// Array element by integer index.
JSValue cell_rt_load_index(JSContext *ctx, JSValue obj, JSValue idx);
void cell_rt_store_index(JSContext *ctx, JSValue obj, JSValue idx, JSValue val);
// Dynamic — type of key unknown at compile time.
JSValue cell_rt_load_dynamic(JSContext *ctx, JSValue obj, JSValue key);
void cell_rt_store_dynamic(JSContext *ctx, JSValue obj, JSValue key, JSValue val);
The typed variants (load_field/load_index) skip the key-type dispatch that load_dynamic must do. When parse and fold provide type information, QBE emit selects the typed variant and the streamline optimizer can narrow dynamic → typed.
Implementation: These are thin wrappers around existing JS_GetPropertyStr/JS_GetPropertyNumber/JS_GetProperty and their Set counterparts.
Tier 3: Closures (needed by programs with nested functions)
// Walk depth levels up the frame chain, read slot.
JSValue cell_rt_get_closure(JSContext *ctx, JSValue fp, int depth, int slot);
// Walk depth levels up, write slot.
void cell_rt_put_closure(JSContext *ctx, JSValue fp, JSValue val, int depth, int slot);
Closure variables live in outer frames. depth is how many caller links to follow; slot is the register index in that frame.
Tier 4: Object Construction (needed by programs creating arrays/records/functions)
// Create a function object from a compiled function index.
// The native code loader must maintain a function table.
JSValue cell_rt_make_function(JSContext *ctx, int fn_id);
Array and record literals are currently compiled as intrinsic calls (array(...), direct {...} construction) which go through the frame/invoke path. A future optimization could add:
// Fast paths (optional, not yet needed)
JSValue cell_rt_new_array(JSContext *ctx, int len);
JSValue cell_rt_new_record(JSContext *ctx);
Tier 5: Collection Operations
// a[] = val (push) and var v = a[] (pop)
void cell_rt_push(JSContext *ctx, JSValue arr, JSValue val);
JSValue cell_rt_pop(JSContext *ctx, JSValue arr);
Tier 6: Error Handling
// Trigger disruption. Jumps to the disrupt handler or unwinds.
void cell_rt_disrupt(JSContext *ctx);
Tier 7: Miscellaneous
JSValue cell_rt_delete(JSContext *ctx, JSValue obj, JSValue key);
JSValue cell_rt_typeof(JSContext *ctx, JSValue val);
Tier 8: String and Float Helpers (called from QBE inline code, not from qbe_emit)
These are called from the QBE IL that qbe.cm generates inline for arithmetic and comparison operations. They're not cell_rt_ prefixed — they're lower-level:
// Float arithmetic (when operands aren't both ints)
JSValue qbe_float_add(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_sub(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_mul(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_div(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_mod(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_pow(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_neg(JSContext *ctx, JSValue v);
JSValue qbe_float_inc(JSContext *ctx, JSValue v);
JSValue qbe_float_dec(JSContext *ctx, JSValue v);
// Float comparison (returns C int 0/1 for QBE branching)
int qbe_float_cmp(JSContext *ctx, int op, JSValue a, JSValue b);
// Bitwise ops on non-int values (convert to int32 first)
JSValue qbe_bnot(JSContext *ctx, JSValue v);
JSValue qbe_bitwise_and(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_bitwise_or(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_bitwise_xor(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_shift_shl(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_shift_sar(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_shift_shr(JSContext *ctx, JSValue a, JSValue b);
// String operations
JSValue JS_ConcatString(JSContext *ctx, JSValue a, JSValue b);
int js_string_compare_value(JSContext *ctx, JSValue a, JSValue b, int eq_only);
JSValue JS_NewString(JSContext *ctx, const char *str);
JSValue __JS_NewFloat64(JSContext *ctx, double d);
int JS_ToBool(JSContext *ctx, JSValue v);
// String/number type tests (inline-able but currently calls)
int JS_IsText(JSValue v);
int JS_IsNumber(JSValue v);
// Tolerant equality (== on mixed types)
JSValue cell_rt_eq_tol(JSContext *ctx, JSValue a, JSValue b);
JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b);
// Text ordering comparisons
JSValue cell_rt_lt_text(JSContext *ctx, JSValue a, JSValue b);
JSValue cell_rt_le_text(JSContext *ctx, JSValue a, JSValue b);
JSValue cell_rt_gt_text(JSContext *ctx, JSValue a, JSValue b);
JSValue cell_rt_ge_text(JSContext *ctx, JSValue a, JSValue b);
What Exists vs What Needs Writing
Already exists (in qbe_helpers.c)
All qbe_float_*, qbe_bnot, qbe_bitwise_*, qbe_shift_*, qbe_to_bool — these are implemented and working.
Already exists (in runtime.c / quickjs.c) but not yet wrapped
The underlying operations exist but aren't exposed with the cell_rt_ names:
| Runtime function | Underlying implementation |
|---|---|
cell_rt_load_field |
JS_GetPropertyStr(ctx, obj, name) |
cell_rt_load_index |
JS_GetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(idx)) |
cell_rt_load_dynamic |
JS_GetProperty(ctx, obj, key) |
cell_rt_store_field |
JS_SetPropertyStr(ctx, obj, name, val) |
cell_rt_store_index |
JS_SetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(idx), val) |
cell_rt_store_dynamic |
JS_SetProperty(ctx, obj, key, val) |
cell_rt_delete |
JS_DeleteProperty(ctx, obj, key) |
cell_rt_push |
JS_ArrayPush(ctx, &arr, val) |
cell_rt_pop |
JS_ArrayPop(ctx, arr) |
cell_rt_typeof |
type tag switch → JS_NewString |
cell_rt_disrupt |
JS_Throw(ctx, ...) |
cell_rt_eq_tol / cell_rt_ne_tol |
comparison logic in mcode.c eq_tol/ne_tol handler |
cell_rt_lt_text etc. |
js_string_compare_value + wrap result |
Needs new code
| Runtime function | What's needed |
|---|---|
cell_rt_get_intrinsic |
Look up intrinsic by name string, return JSValue function. Currently scattered across js_cell_intrinsic_get and the mcode handler. Needs a clean single entry point. |
cell_rt_frame |
Allocate JSFrameRegister, set function slot, set argc. Exists in mcode.c frame handler but not as a callable function. |
cell_rt_setarg |
Write to frame slot. Trivial: frame->slots[idx + 1] = val (slot 0 is this). |
cell_rt_invoke |
Call the function in the frame. Needs to dispatch: native C function vs mach bytecode vs mcode. This is the critical piece — it must handle all function types. |
cell_rt_goframe / cell_rt_goinvoke |
Tail call variants. Similar to frame/invoke but reuse caller frame. |
cell_rt_make_function |
Create function object from index. Needs a function table (populated by the native loader). |
cell_rt_get_closure / cell_rt_put_closure |
Walk frame chain. Exists inline in mcode.c get/put handlers. |
Recommended C File Organization
source/
cell_runtime.c — NEW: all cell_rt_* functions (the native code API)
qbe_helpers.c — existing: float/bitwise/shift helpers for inline QBE
runtime.c — existing: JS_GetProperty, JS_SetProperty, etc.
quickjs.c — existing: core VM, GC, value representation
mcode.c — existing: mcode interpreter (can delegate to cell_runtime.c)
cell_runtime.c is the single file that defines the native code contract. It should:
- Include
quickjs-internal.hfor access to value representation and heap types - Export all
cell_rt_*functions with C linkage (nostatic) - Keep each function thin — delegate to existing
JS_*functions where possible - Handle GC safety: after any allocation (frame, string, array), callers' frames may have moved
Implementation Priority
Phase 1 — Get "hello world" running natively:
cell_rt_get_intrinsic(to findprintandtext)cell_rt_frame,cell_rt_setarg,cell_rt_invoke(to call them)- A loader that takes QBE output → assembles → links → calls
cell_main
Phase 2 — Variables and arithmetic:
- All property access (
load_field,load_index,store_*,load_dynamic) cell_rt_make_function,cell_rt_get_closure,cell_rt_put_closure
Phase 3 — Full language:
cell_rt_push,cell_rt_pop,cell_rt_delete,cell_rt_typeofcell_rt_disruptcell_rt_goframe,cell_rt_goinvoke- Text comparison wrappers (
cell_rt_lt_text, etc.) - Tolerant equality (
cell_rt_eq_tol,cell_rt_ne_tol)
Calling Convention
All cell_rt_* functions follow the same pattern:
- First argument is always
JSContext *ctx - Values are passed/returned as
JSValue(64-bit, by value) - Frame pointers are
JSValue(tagged pointer toJSFrameRegister) - String names are
const char *(pointer to data section label) - Integer constants (slot indices, arg counts) are
int/long
Native code maintains %ctx (JSContext) and %fp (current frame pointer) as persistent values across the function body. All slot reads/writes go through %fp + offset.
What Should NOT Be in the C Runtime
These are handled entirely by QBE-generated code:
- Integer arithmetic and comparisons — bit operations on NaN-boxed values
- Control flow — branches, loops, labels, jumps
- Boolean logic —
and/or/noton tagged values - Constant loading — integer constants are immediate, strings are data labels
- Type guard branches — the
is_int/is_text/is_nullchecks are inline bit tests; the branch to the float or text path is just a QBEjnz
The qbe.cm macros already handle all of this. The arithmetic path looks like:
check both ints? → yes → inline int add → done
→ no → call qbe_float_add (or JS_ConcatString for text)
The C runtime is only called on the slow paths (float, text, dynamic dispatch). The fast path (integer arithmetic, comparisons, branching) is fully native.