This plan outlines the work needed to make ZPack and the Runtime SDK robust enough for real-world native standard library development. The goal is to enable frontend implementers (like Haxe, Python, etc.) to build comprehensive standard libraries with:
- Type-safe FFI between generated code and native runtime
- Generic container support (Array, Map<K,V>, etc.)
- Memory management that's correct and efficient
- Cross-platform distribution via ZPack
- Developer ergonomics with good tooling and macros
- Basic ZRTL loading -
ZrtlPlugin::load()successfully loads.zrtlfiles - Symbol resolution - Function pointers are correctly passed to JIT/AOT
- Basic DynamicValue - Type tags, primitive boxing/unboxing
- C SDK header -
zrtl.hwith type tags and DynamicBox - Rust macros -
zrtl_plugin!and#[zrtl_export]for basic exports - ZPack format - Archive creation and loading works
Generic type reification- ✅ EXISTS:GenericTypeArgs,GenericValuein zrtl.rs- Object/struct field access - Partial:
ZrtlFieldDescriptorexists but no runtime reflection API - Method dispatch - No vtable or interface dispatch support in SDK
Memory lifecycle hooks- ✅ EXISTS:DropFn, dropper callbacks inTypeMeta/DynamicBox- String interop - Inconsistent:
ZrtlString(ptr/len/cap) vs reflaxe (length-prefixed i32) Error handling- ✅ EXISTS:ZRTL_CAT_RESULT,ZrtlOptional,zrtl_gbox_result()- Iterator protocol - No standard iteration interface in SDK
- Async runtime - Compiler has async support but SDK has no runtime hooks
- Debugging support - No DWARF generation for runtime functions
- Testing infrastructure - No runtime test harness or
ZRTL_TESTmacros
| Gap | Priority | Description |
|---|---|---|
| String format inconsistency | High | reflaxe uses i32 length + bytes, SDK defines ZrtlString {ptr, len, cap} |
| Method dispatch in SDK | Medium | No vtable support for interface calls from runtime functions |
| Iterator protocol | Medium | No standard next()/hasNext() interface |
| Async runtime hooks | Low | SDK doesn't expose async executor integration |
| DWARF for runtime | Low | No debug info generation for .zrtl functions |
| Test harness | Medium | No ZRTL_TEST macros or test runner |
| Runtime reflection API | Medium | Field descriptors exist but no query functions |
Problem: Currently we have multiple string representations:
- Length-prefixed (
i32length + bytes) - Struct with ptr/len/cap
Solution: Define canonical representations in zrtl.h:
// Canonical string - always heap allocated with capacity
typedef struct {
uint8_t* data; // UTF-8 bytes (NOT null-terminated)
uint32_t length; // Byte length
uint32_t capacity; // Allocated capacity
} ZrtlString;
// Canonical array - generic over element type
typedef struct {
void* data; // Element storage
uint32_t length; // Number of elements
uint32_t capacity; // Allocated slots
uint32_t elem_size; // Sizeof one element
ZrtlTypeTag elem_type; // Element type tag
} ZrtlArray;
// Canonical map
typedef struct {
void* buckets; // Hash buckets
uint32_t length; // Number of entries
uint32_t capacity; // Number of buckets
ZrtlTypeTag key_type;
ZrtlTypeTag value_type;
} ZrtlMap;Tasks:
- Finalize canonical type layouts in
zrtl.h - Update
DynamicValuein Rust to use same layouts - Migrate
reflaxe.zyntax/runtimeto use canonical types - Migrate
haxe_zyntax_runtimeto use canonical types - Add conversion helpers in SDK
Problem: Custom types (structs, classes) have no runtime type info.
Solution: Add a type registry that plugins can populate:
// In zrtl.rs
pub struct TypeRegistry {
types: Vec<TypeDescriptor>,
name_to_id: HashMap<String, TypeId>,
}
pub struct TypeDescriptor {
pub name: String,
pub size: u32,
pub alignment: u32,
pub dropper: Option<DropFn>,
pub fields: Vec<FieldDescriptor>,
pub methods: Vec<MethodDescriptor>,
}
pub struct FieldDescriptor {
pub name: String,
pub offset: u32,
pub type_id: TypeId,
}
pub struct MethodDescriptor {
pub name: String,
pub symbol: String, // Runtime symbol name
pub signature: FunctionSignature,
}Tasks:
- Implement
TypeRegistryinzrtl.rs - Add
ZRTL_REGISTER_TYPE(name, TypeDescriptor)macro in C - Add
#[zrtl_type]derive macro in Rust - Generate type registrations from frontend type info
- Support querying field offsets at runtime
Problem: Array<Int> and Array<String> need different handling but share code.
Solution: Generic containers carry element type info:
// GenericArray with element type tracking
#[repr(C)]
pub struct GenericArray {
pub data: *mut u8,
pub length: u32,
pub capacity: u32,
pub elem_size: u32,
pub elem_type: TypeTag,
pub elem_dropper: Option<DropFn>, // Called on each element during cleanup
}
impl GenericArray {
pub fn new(elem_type: TypeTag, elem_size: u32) -> Self { ... }
pub fn push(&mut self, elem: *const u8) { ... }
pub fn get(&self, index: u32) -> *const u8 { ... }
pub fn drop_all(&mut self) { ... } // Calls elem_dropper on each
}Tasks:
- Define
GenericArray,GenericMapinzrtl.rs - Add C equivalents in
zrtl.h - Implement type-aware
push/get/setoperations - Add element drop propagation
- Test with nested generics (
Array<Array<Int>>)
Problem: No clear ownership semantics - who frees memory?
Solution: Adopt clear ownership rules:
- Runtime-allocated = Runtime-freed: If ZRTL allocates, ZRTL frees
- Caller-owns-result: Functions returning pointers transfer ownership
- Borrowing via const:
const T*means "borrowed, do not free"
Add explicit ownership markers:
// Ownership annotations in function signatures
typedef struct {
ZrtlArray* arr; // OWNED - caller must free
} OwnedArray;
typedef struct {
const ZrtlArray* arr; // BORROWED - caller must NOT free
} BorrowedArray;
// Runtime function ownership convention
ZRTL_EXPORT ZrtlArray* Array_copy(const ZrtlArray* src); // Returns owned
ZRTL_EXPORT void Array_sort_inplace(ZrtlArray* arr); // Mutates, no ownership changeTasks:
- Document ownership conventions in SDK
- Add
ZRTL_OWNED,ZRTL_BORROWEDattribute macros - Update all runtime functions with ownership docs
- Add ownership verification in debug builds
Problem: Nested structures need recursive cleanup.
Solution: Type descriptors include drop functions that handle children:
// Drop chain example for Array<String>
extern "C" fn drop_string_array(arr: *mut GenericArray) {
unsafe {
let arr = &mut *arr;
// Drop each string element
for i in 0..arr.length {
let elem_ptr = arr.get(i) as *mut ZrtlString;
if !elem_ptr.is_null() {
drop_zrtl_string(elem_ptr);
}
}
// Free array storage
libc::free(arr.data as *mut _);
}
}Tasks:
- Add
dropperfield to all container types - Generate drop functions for parameterized types
- Handle cycles (weak refs) to prevent leaks
- Add debug mode leak detection
Problem: Some objects need shared ownership (closures, callbacks).
Solution: Optional ARC wrapper for shared values:
#[repr(C)]
pub struct ArcHeader {
pub strong_count: AtomicU32,
pub weak_count: AtomicU32,
pub dropper: DropFn,
}
#[repr(C)]
pub struct ArcBox<T> {
header: ArcHeader,
value: T,
}
// C API
ZRTL_EXPORT void* zrtl_arc_new(size_t size, ZrtlDropFn dropper);
ZRTL_EXPORT void zrtl_arc_retain(void* arc_ptr);
ZRTL_EXPORT void zrtl_arc_release(void* arc_ptr);
ZRTL_EXPORT void* zrtl_arc_weak_upgrade(void* weak_ptr);Tasks:
- Implement
ArcBoxinzrtl.rs - Add C wrappers in
zrtl.h - Integrate with frontend's object model
- Handle weak references for cycles
Problem: Instance methods need receiver + method resolution.
Solution: Method symbols encode receiver type:
Symbol naming: $<Type>$<method>
Examples:
$Array$push - Array.push(elem)
$String$length - String.length property
$MyClass$doThing - MyClass.doThing()
Compiler generates calls:
; obj.push(42) where obj: Array<Int>
%result = call i32 @"$Array$push"(%Array* %obj, i32 42)Tasks:
- Document method symbol naming convention
- Generate correct symbols from frontend
- Support property getter/setter disambiguation
Problem: Interface calls need vtable lookup.
Solution: Vtables for interface types:
// Vtable structure for an interface
#[repr(C)]
pub struct IteratorVtable {
pub next: extern "C" fn(*mut u8) -> DynamicValue,
pub has_next: extern "C" fn(*const u8) -> bool,
}
// Trait object representation
#[repr(C)]
pub struct TraitObject {
pub data: *mut u8,
pub vtable: *const (),
}Tasks:
- Define vtable layout convention
- Generate vtables for implementing types
- Add
TraitObjectto ZRTL - Implement interface dispatch in HIR lowering
Problem: Haxe's Dynamic type needs runtime method lookup.
Solution: Method table attached to object metadata:
// Object header for Dynamic-capable objects
#[repr(C)]
pub struct ObjectHeader {
pub type_id: TypeId,
pub method_table: *const MethodTable,
}
// Method lookup table
pub struct MethodTable {
pub entries: Vec<MethodEntry>,
}
pub struct MethodEntry {
pub name_hash: u64,
pub name: &'static str,
pub ptr: *const u8,
}
// Runtime lookup
extern "C" fn dynamic_method_lookup(
obj: *const ObjectHeader,
name: *const c_char,
) -> *const u8;Tasks:
- Implement
ObjectHeaderandMethodTable - Add method table generation to frontend
- Implement
dynamic_method_lookupfunction - Cache lookups for performance
// Complete string API
pub trait ZrtlStringOps {
fn new_empty() -> ZrtlString;
fn from_utf8(bytes: &[u8]) -> ZrtlString;
fn from_cstr(cstr: *const c_char) -> ZrtlString;
fn length(&self) -> u32;
fn char_at(&self, index: u32) -> u32; // Returns codepoint
fn substring(&self, start: u32, end: u32) -> ZrtlString;
fn concat(&self, other: &ZrtlString) -> ZrtlString;
fn split(&self, delimiter: &ZrtlString) -> GenericArray;
fn to_lowercase(&self) -> ZrtlString;
fn to_uppercase(&self) -> ZrtlString;
fn trim(&self) -> ZrtlString;
fn index_of(&self, needle: &ZrtlString) -> i32;
fn replace(&self, from: &ZrtlString, to: &ZrtlString) -> ZrtlString;
}Tasks:
- Implement all string ops in
zrtl_string.rs - Export as
$String$*symbols - Handle Unicode correctly (grapheme clusters)
- Add string interning for small strings
// Math library
extern "C" fn math_sin(x: f64) -> f64;
extern "C" fn math_cos(x: f64) -> f64;
extern "C" fn math_sqrt(x: f64) -> f64;
extern "C" fn math_pow(base: f64, exp: f64) -> f64;
extern "C" fn math_floor(x: f64) -> f64;
extern "C" fn math_ceil(x: f64) -> f64;
extern "C" fn math_round(x: f64) -> f64;
extern "C" fn math_random() -> f64;
extern "C" fn math_abs_int(x: i64) -> i64;
extern "C" fn math_abs_float(x: f64) -> f64;Tasks:
- Implement math functions (can wrap libc)
- Export as
$Math$*symbols - Handle edge cases (NaN, Inf)
// File I/O
extern "C" fn file_open(path: *const ZrtlString, mode: i32) -> *mut FileHandle;
extern "C" fn file_read(handle: *mut FileHandle, buf: *mut u8, len: u32) -> i32;
extern "C" fn file_write(handle: *mut FileHandle, buf: *const u8, len: u32) -> i32;
extern "C" fn file_close(handle: *mut FileHandle);
// Console I/O
extern "C" fn console_print(s: *const ZrtlString);
extern "C" fn console_println(s: *const ZrtlString);
extern "C" fn console_read_line() -> ZrtlString;Tasks:
- Implement file ops with error handling
- Console I/O with buffering
- Export as
$Sys$*/$File$*symbols
extern "C" fn date_now() -> i64; // Unix timestamp ms
extern "C" fn date_from_timestamp(ts: i64) -> *mut DateTime;
extern "C" fn date_year(dt: *const DateTime) -> i32;
extern "C" fn date_month(dt: *const DateTime) -> i32;
// ... etcTasks:
- Implement DateTime type
- Time zone handling
- Formatting and parsing
// Type definition macro
#[zrtl_struct("MyPoint")]
#[derive(Clone)]
pub struct MyPoint {
pub x: f64,
pub y: f64,
}
// Auto-generates:
// - TypeDescriptor registration
// - Field accessors as symbols
// - Serialization hooks
// Method export with full metadata
#[zrtl_method(
symbol = "$MyPoint$distance",
receiver = "MyPoint",
args = [("other", "MyPoint")],
returns = "f64"
)]
pub extern "C" fn my_point_distance(self_: *const MyPoint, other: *const MyPoint) -> f64 {
// ...
}Tasks:
- Implement
#[zrtl_struct]derive macro - Implement
#[zrtl_method]with metadata - Generate type descriptors automatically
- Support generic type parameters
// Enhanced C macros
ZRTL_DEFINE_STRUCT(MyPoint,
ZRTL_FIELD(x, f64),
ZRTL_FIELD(y, f64)
);
ZRTL_METHOD(MyPoint, distance, f64,
ZRTL_ARG(other, MyPoint*)
) {
return sqrt(pow(self->x - other->x, 2) + pow(self->y - other->y, 2));
}Tasks:
- Add struct definition macros
- Add method definition macros
- Auto-generate symbol table from macros
- Add compile-time type checking where possible
# Pack a runtime
zyntax pack create \
--name haxe-std \
--version 1.0.0 \
--language haxe \
--runtime lib/darwin-arm64/runtime.zrtl \
--runtime lib/linux-x64/runtime.zrtl \
--output haxe-std.zpack
# Inspect a pack
zyntax pack info haxe-std.zpack
# Extract contents
zyntax pack extract haxe-std.zpack --output ./extracted/
# List symbols
zyntax pack symbols haxe-std.zpack
# Verify pack integrity
zyntax pack verify haxe-std.zpackTasks:
- Implement
zyntax pack createcommand - Implement
zyntax pack infocommand - Implement
zyntax pack extractcommand - Implement
zyntax pack symbolscommand - Implement
zyntax pack verifycommand
# Check if a runtime provides required symbols
zyntax symbols check \
--required ./generated/symbols.txt \
--runtime ./runtime.zrtl
# Generate symbol requirements from compiled program
zyntax symbols list \
--source ./compiled.zbc \
--output ./required-symbols.txtTasks:
- Extract required symbols from bytecode
- Verify runtime provides all required symbols
- Report missing symbols with suggestions
// In sdk/zrtl_test/
pub struct TestRunner {
tests: Vec<TestCase>,
}
pub struct TestCase {
name: String,
test_fn: extern "C" fn() -> bool,
}
// C API for test registration
ZRTL_TEST(test_array_push) {
ZrtlArray* arr = Array_new();
arr = Array_push(arr, 42);
ZRTL_ASSERT_EQ(Array_length(arr), 1);
ZRTL_ASSERT_EQ(Array_get(arr, 0), 42);
Array_free(arr);
return true;
}
ZRTL_TEST_MAIN() // Generates main that runs all testsTasks:
- Create
zrtl_testcrate - Implement test registration macros
- Add assertion macros
- Add memory leak detection in tests
- CI integration
# Fuzz test array operations
cargo fuzz run array_ops -- -max_len=1000
# Fuzz string parsing
cargo fuzz run string_parseTasks:
- Set up cargo-fuzz for runtime
- Create fuzz targets for each data type
- Add sanitizers (ASan, UBSan)
- Continuous fuzzing in CI
// Benchmark comparisons
fn bench_array_push(c: &mut Criterion) {
c.bench_function("array_push_1000", |b| {
b.iter(|| {
let mut arr = Array_new();
for i in 0..1000 {
arr = Array_push(arr, i);
}
Array_free(arr);
});
});
}Tasks:
- Set up criterion benchmarks
- Benchmark all core operations
- Compare with HashLink/Haxe-interp
- Track regressions in CI
- Complete API documentation for
zrtl.h - Rust crate documentation with examples
- Memory management guide
- Type system reference
Step-by-step guide covering:
- Project setup
- Implementing basic types
- Adding methods
- Memory management
- Testing
- Packaging
- Distribution
- Type representation diagrams
- Memory layout documentation
- Symbol naming conventions
- Vtable structure
- Standardized type representations (Phase 1.1)
- Generic container protocol (Phase 1.3)
- Destructor chain (Phase 2.2)
- Direct method calls (Phase 3.1)
- String operations (Phase 4.1)
- Type registry & reflection (Phase 1.2)
- Ownership model documentation (Phase 2.1)
- Reference counting (Phase 2.3)
- Math operations (Phase 4.2)
- Enhanced Rust macros (Phase 5.1)
- Virtual dispatch (Phase 3.2)
- Dynamic dispatch (Phase 3.3)
- I/O operations (Phase 4.3)
- Date/Time (Phase 4.4)
- Full tooling suite (Phase 5.3-5.4)
| Phase | Effort | Dependencies |
|---|---|---|
| Phase 1 | 2-3 weeks | None |
| Phase 2 | 2 weeks | Phase 1 |
| Phase 3.1 | 1 week | Phase 1 |
| Phase 3.2-3.3 | 2 weeks | Phase 3.1 |
| Phase 4 | 3-4 weeks | Phase 1-2 |
| Phase 5 | 2-3 weeks | Phase 1-4 |
| Phase 6 | 2 weeks | Phase 4-5 |
| Phase 7 | Ongoing | All |
Total: ~14-17 weeks for full implementation
- Haxe standard library compiles and runs - All basic types work
- No memory leaks in test suite - Valgrind/ASan clean
- Cross-platform builds - macOS, Linux, Windows all work
- Performance parity - Within 2x of HashLink for benchmarks
- Developer adoption - At least one external frontend uses SDK