From 0d47002167c8f1c352b50fa2a38ff936ee9bf0dd Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Tue, 10 Feb 2026 18:35:18 -0600 Subject: [PATCH] add compile script --- compile.ce | 100 ++++++++++++++++++++++++++++++++ qbe_rt.c | 154 ++++++++++++++++++++++++++++++++++++++++++++++++++ run_native.ce | 69 ++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 compile.ce create mode 100644 qbe_rt.c create mode 100644 run_native.ce diff --git a/compile.ce b/compile.ce new file mode 100644 index 00000000..0b85e607 --- /dev/null +++ b/compile.ce @@ -0,0 +1,100 @@ +// compile.ce — compile a .cm module to native .dylib via QBE +// +// Usage: +// cell --core . compile.ce +// +// Produces .dylib in the current directory. + +var fd = use('fd') +var os = use('os') + +if (length(args) < 1) { + print('usage: cell --core . compile.ce ') + return +} + +var file = args[0] +var base = file +if (ends_with(base, '.cm')) { + base = text(base, 0, length(base) - 3) +} + +var safe = replace(replace(base, '/', '_'), '-', '_') +var symbol = 'js_' + safe + '_use' +var tmp = '/tmp/qbe_' + safe +var ssa_path = tmp + '.ssa' +var s_path = tmp + '.s' +var o_path = tmp + '.o' +var rt_o_path = '/tmp/qbe_rt.o' +var dylib_path = base + '.dylib' +var cwd = fd.getcwd() +var rc = 0 + +// Step 1: emit QBE IL +print('emit qbe...') +rc = os.system('cd ' + cwd + ' && ./cell --core . --emit-qbe ' + file + ' > ' + ssa_path) +if (rc != 0) { + print('failed to emit qbe il') + return +} + +// Step 2: post-process — insert dead labels after ret/jmp, append wrapper +// Use awk via shell to avoid blob/slurpwrite issues with long strings +print('post-process...') +var awk_cmd = `awk ' + /^[[:space:]]*ret / || /^[[:space:]]*jmp / { need_label=1; print; next } + need_label && /^[[:space:]]*[^@}]/ && NF > 0 { + print "@_dead_" dead_id; dead_id++; need_label=0 + } + /^@/ || /^}/ || NF==0 { need_label=0 } + { print } +' ` + ssa_path + ` > ` + tmp + `_fixed.ssa` +rc = os.system(awk_cmd) +if (rc != 0) { + print('post-process failed') + return +} + +// Append wrapper function +var wrapper_cmd = `printf '\nexport function l $` + symbol + `(l %%ctx) {\n@entry\n %%frame =l alloc8 4096\n %%result =l call $cell_main(l %%ctx, l %%frame)\n ret %%result\n}\n' >> ` + tmp + `_fixed.ssa` +rc = os.system(wrapper_cmd) +if (rc != 0) { + print('wrapper append failed') + return +} + +// Step 3: compile QBE IL to assembly +print('qbe compile...') +rc = os.system('~/.local/bin/qbe -o ' + s_path + ' ' + tmp + '_fixed.ssa') +if (rc != 0) { + print('qbe compilation failed') + return +} + +// Step 4: assemble +print('assemble...') +rc = os.system('cc -c ' + s_path + ' -o ' + o_path) +if (rc != 0) { + print('assembly failed') + return +} + +// Step 5: compile runtime stubs (cached — skip if already built) +if (!fd.is_file(rt_o_path)) { + print('compile runtime stubs...') + rc = os.system('cc -c ' + cwd + '/qbe_rt.c -o ' + rt_o_path + ' -fPIC') + if (rc != 0) { + print('runtime stubs compilation failed') + return + } +} + +// Step 6: link dylib +print('link...') +rc = os.system('cc -shared -fPIC -undefined dynamic_lookup ' + o_path + ' ' + rt_o_path + ' -o ' + cwd + '/' + dylib_path) +if (rc != 0) { + print('linking failed') + return +} + +print('built: ' + dylib_path) diff --git a/qbe_rt.c b/qbe_rt.c new file mode 100644 index 00000000..be2f9421 --- /dev/null +++ b/qbe_rt.c @@ -0,0 +1,154 @@ +/* + * qbe_rt.c - Runtime support for QBE-compiled ƿit modules + * + * Provides non-inline versions of static-inline quickjs functions + * (which QBE-generated code calls as external symbols) and stub + * implementations of cell_rt_* helper functions. + */ + +#include +#include +#include + +typedef uint64_t JSValue; +typedef struct JSContext JSContext; + +#define JS_TAG_SHORT_FLOAT 5 +#define JS_TAG_NULL 7 +#define JS_VAL_NULL 7 + +/* ============================================================ + Non-inline wrappers for static-inline quickjs functions + ============================================================ */ + +/* + * __JS_NewFloat64 — encode double as tagged JSValue + * Short float: [sign:1][exp:8][mantissa:52][tag:3] + * Returns tagged int if value is an exact integer in int32 range + */ +JSValue __JS_NewFloat64(JSContext *ctx, double d) { + union { double d; uint64_t u; } u; + u.d = d; + + uint64_t sign = u.u >> 63; + int exp = (u.u >> 52) & 0x7FF; + uint64_t mantissa = u.u & ((1ULL << 52) - 1); + + /* Zero */ + if (exp == 0 && mantissa == 0) + return JS_TAG_SHORT_FLOAT; + /* NaN/Inf → null */ + if (exp == 0x7FF) + return JS_VAL_NULL; + /* Subnormals → zero */ + if (exp == 0) + return (sign << 63) | JS_TAG_SHORT_FLOAT; + + int short_exp = exp - 1023 + 127; + if (short_exp < 1 || short_exp > 254) + return JS_VAL_NULL; + + /* Prefer integer if exact */ + if (d >= (double)(-2147483647 - 1) && d <= (double)2147483647) { + int32_t i = (int32_t)d; + if ((double)i == d) + return (uint64_t)(uint32_t)i << 1; + } + + return (sign << 63) + | ((uint64_t)short_exp << 55) + | (mantissa << 3) + | JS_TAG_SHORT_FLOAT; +} + +/* + * JS_IsNumber — check if value is tagged int or short float + */ +int JS_IsNumber(JSValue v) { + int is_int = (v & 1) == 0; + int is_float = (v & 7) == JS_TAG_SHORT_FLOAT; + return is_int || is_float; +} + +/* + * JS_NewString — create string from C string (wraps JS_NewStringLen) + */ +extern JSValue JS_NewStringLen(JSContext *ctx, const char *str, size_t len); + +JSValue JS_NewString(JSContext *ctx, const char *str) { + return JS_NewStringLen(ctx, str, strlen(str)); +} + +/* ============================================================ + cell_rt_* stubs — error/fallback paths for QBE-compiled code + These are called from type-mismatch branches that should not + be reached in pure numeric code. + ============================================================ */ + +extern JSValue JS_ThrowTypeError(JSContext *ctx, const char *fmt, ...); + +void cell_rt_disrupt(JSContext *ctx) { + JS_ThrowTypeError(ctx, "type error in native code"); +} + +JSValue cell_rt_lt_text(JSContext *ctx, JSValue a, JSValue b) { + return JS_VAL_NULL; +} +JSValue cell_rt_gt_text(JSContext *ctx, JSValue a, JSValue b) { + return JS_VAL_NULL; +} +JSValue cell_rt_le_text(JSContext *ctx, JSValue a, JSValue b) { + return JS_VAL_NULL; +} +JSValue cell_rt_ge_text(JSContext *ctx, JSValue a, JSValue b) { + return JS_VAL_NULL; +} +JSValue cell_rt_eq_tol(JSContext *ctx, JSValue a, JSValue b) { + return JS_VAL_NULL; +} +JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b) { + return JS_VAL_NULL; +} + +JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) { + return JS_VAL_NULL; +} +JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name) { + return JS_VAL_NULL; +} +JSValue cell_rt_load_dynamic(JSContext *ctx, JSValue obj, JSValue key) { + return JS_VAL_NULL; +} +JSValue cell_rt_load_index(JSContext *ctx, JSValue arr, JSValue idx) { + return JS_VAL_NULL; +} +void cell_rt_store_field(JSContext *ctx, JSValue val, JSValue obj, + const char *name) {} +void cell_rt_store_dynamic(JSContext *ctx, JSValue val, JSValue obj, + JSValue key) {} +void cell_rt_store_index(JSContext *ctx, JSValue val, JSValue arr, + JSValue idx) {} +JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth, + int64_t index) { + return JS_VAL_NULL; +} +void cell_rt_put_closure(JSContext *ctx, void *fp, JSValue val, int64_t depth, + int64_t index) {} +JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) { + return JS_VAL_NULL; +} +void cell_rt_setarg(JSValue frame, int64_t idx, JSValue val) {} +JSValue cell_rt_invoke(JSContext *ctx, JSValue frame) { return JS_VAL_NULL; } +JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int64_t nargs) { + return JS_VAL_NULL; +} +void cell_rt_goinvoke(JSContext *ctx, JSValue frame) {} +JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx) { + return JS_VAL_NULL; +} +void cell_rt_push(JSContext *ctx, JSValue arr, JSValue val) {} +JSValue cell_rt_pop(JSContext *ctx, JSValue arr) { return JS_VAL_NULL; } +JSValue cell_rt_delete(JSContext *ctx, JSValue obj, JSValue key) { + return JS_VAL_NULL; +} +JSValue cell_rt_typeof(JSContext *ctx, JSValue val) { return JS_VAL_NULL; } diff --git a/run_native.ce b/run_native.ce new file mode 100644 index 00000000..ae7fc656 --- /dev/null +++ b/run_native.ce @@ -0,0 +1,69 @@ +// run_native.ce — load a module both interpreted and native, compare speed +// +// Usage: +// cell --core . run_native.ce +// +// Loads .cm via use() (interpreted) and .dylib (native), +// runs both and compares results and timing. + +var os = use('os') + +if (length(args) < 1) { + print('usage: cell --core . run_native.ce ') + print(' e.g. cell --core . run_native.ce num_torture') + return +} + +var name = args[0] +if (ends_with(name, '.cm')) { + name = text(name, 0, length(name) - 3) +} + +var safe = replace(replace(name, '/', '_'), '-', '_') +var symbol = 'js_' + safe + '_use' +var dylib_path = './' + name + '.dylib' +var fd = use('fd') + +// --- Interpreted run --- +print('--- interpreted ---') +var t1 = os.now() +var result_interp = use(name) +var t2 = os.now() +var ms_interp = (t2 - t1) / 1000000 +print('result: ' + text(result_interp)) +print('time: ' + text(ms_interp) + ' ms') + +// --- Native run --- +if (!fd.is_file(dylib_path)) { + print('\nno ' + dylib_path + ' found — run compile.ce first') + return +} + +print('\n--- native ---') +var t3 = os.now() +var lib = os.dylib_open(dylib_path) +var t4 = os.now() +var result_native = os.dylib_symbol(lib, symbol) +var t5 = os.now() +var ms_load = (t4 - t3) / 1000000 +var ms_exec = (t5 - t4) / 1000000 +var ms_native = (t5 - t3) / 1000000 +print('result: ' + text(result_native)) +print('load: ' + text(ms_load) + ' ms') +print('exec: ' + text(ms_exec) + ' ms') +print('total: ' + text(ms_native) + ' ms') + +// --- Comparison --- +print('\n--- comparison ---') +var match = result_interp == result_native +var speedup = 0 +var speedup_exec = 0 +print('match: ' + text(match)) +if (ms_native > 0) { + speedup = ms_interp / ms_native + print('speedup: ' + text(speedup) + 'x (total)') +} +if (ms_exec > 0) { + speedup_exec = ms_interp / ms_exec + print('speedup: ' + text(speedup_exec) + 'x (exec only)') +}