add compile script

This commit is contained in:
2026-02-10 18:35:18 -06:00
parent 2f681fa366
commit 0d47002167
3 changed files with 323 additions and 0 deletions

100
compile.ce Normal file
View File

@@ -0,0 +1,100 @@
// compile.ce — compile a .cm module to native .dylib via QBE
//
// Usage:
// cell --core . compile.ce <file.cm>
//
// Produces <file>.dylib in the current directory.
var fd = use('fd')
var os = use('os')
if (length(args) < 1) {
print('usage: cell --core . compile.ce <file.cm>')
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)

154
qbe_rt.c Normal file
View File

@@ -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 <stdint.h>
#include <string.h>
#include <math.h>
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; }

69
run_native.ce Normal file
View File

@@ -0,0 +1,69 @@
// run_native.ce — load a module both interpreted and native, compare speed
//
// Usage:
// cell --core . run_native.ce <module>
//
// Loads <module>.cm via use() (interpreted) and <module>.dylib (native),
// runs both and compares results and timing.
var os = use('os')
if (length(args) < 1) {
print('usage: cell --core . run_native.ce <module>')
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)')
}