Merge branch 'master' into fix_aot

This commit is contained in:
2026-02-21 13:31:33 -06:00
146 changed files with 57219 additions and 124492 deletions

View File

@@ -8,7 +8,7 @@ function use_embed(name) {
var fd = use_embed('internal_fd')
var json_mod = use_embed('json')
var crypto = use_embed('crypto')
var crypto = use_embed('internal_crypto')
function content_hash(content) {
var data = content
@@ -34,7 +34,7 @@ function boot_load(name) {
var mcode_blob = null
var mach_blob = null
if (!fd.is_file(mcode_path)) {
print("error: missing seed: " + name + "\n")
os.print("error: missing seed: " + name + "\n")
disrupt
}
mcode_blob = fd.slurp(mcode_path)
@@ -60,9 +60,9 @@ function analyze(src, filename) {
e = ast.errors[_i]
msg = e.message
if (e.line != null && e.column != null)
print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${msg}`)
os.print(`${filename}:${text(e.line)}:${text(e.column)}: error: ${msg}\n`)
else
print(`${filename}: error: ${msg}`)
os.print(`${filename}: error: ${msg}\n`)
_i = _i + 1
}
disrupt
@@ -105,4 +105,4 @@ while (_i < length(seed_files)) {
compile_and_cache(entry.name, core_path + '/' + entry.path)
_i = _i + 1
}
print("bootstrap: cache seeded\n")
os.print("bootstrap: cache seeded\n")

247
internal/crypto.c Normal file
View File

@@ -0,0 +1,247 @@
#include "cell.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "monocypher.h"
/*
Crypto Module Documentation
This module provides cryptographic functions using the Monocypher library.
All inputs and outputs are Blobs.
Functions:
- keypair() -> { public: Blob(256 bits), private: Blob(256 bits) }
Generates a new random X25519 keypair.
- shared(public_key, private_key) -> Blob(256 bits)
Computes a shared secret from your private key and another's public key (X25519).
Input keys must be 256 bits (32 bytes).
- blake2(data, [hash_size_bytes=32]) -> Blob
Computes the BLAKE2b hash of the data.
Default hash size is 32 bytes (256 bits). Supports 1-64 bytes.
- sign(secret_key, message) -> Blob(512 bits)
Signs a message using EdDSA.
secret_key must be 512 bits (64 bytes).
(Note: If you have a 32-byte seed, extend it first or use appropriate key generation).
Returns a 64-byte signature.
- verify(signature, public_key, message) -> bool
Verifies an EdDSA signature.
signature: 512 bits (64 bytes).
public_key: 256 bits (32 bytes).
Returns true if valid, false otherwise.
- lock(key, nonce, message, [ad]) -> Blob
Encrypts and authenticates a message using XChaCha20-Poly1305.
key: 256 bits (32 bytes).
nonce: 192 bits (24 bytes).
ad: Optional associated data (Blob).
Returns a blob containing the ciphertext followed by the 16-byte MAC.
- unlock(key, nonce, ciphertext_with_mac, [ad]) -> Blob or null
Decrypts and verifies a message.
key: 256 bits (32 bytes).
nonce: 192 bits (24 bytes).
ciphertext_with_mac: Must include the 16-byte MAC at the end.
ad: Optional associated data (Blob).
Returns the plaintext Blob if successful, or null if verification fails.
*/
// Helper to get blob data and check exact bit length
static void *get_blob_check_bits(JSContext *js, JSValue val, size_t expected_bits, const char *name) {
size_t bits;
void* result = js_get_blob_data_bits(js, &bits, val);
if (result == (void *)-1) {
return NULL; // Exception already thrown by js_get_blob_data_bits
}
if (bits != expected_bits) {
JS_RaiseDisrupt(js, "%s: expected %zu bits, got %zu", name, expected_bits, bits);
return NULL;
}
return result;
}
// Helper to get any blob data (checking it is a stoned blob)
static void *get_blob_any(JSContext *js, JSValue val, size_t *out_bits, const char *name) {
void *result = js_get_blob_data_bits(js, out_bits, val);
if (result == (void *)-1)
return NULL;
return result;
}
JSValue js_crypto_shared(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 2) {
return JS_RaiseDisrupt(js, "crypto.shared: expected public_key, private_key");
}
uint8_t *pub = get_blob_check_bits(js, argv[0], 256, "crypto.shared public_key");
if (!pub) return JS_EXCEPTION;
uint8_t *priv = get_blob_check_bits(js, argv[1], 256, "crypto.shared private_key");
if (!priv) return JS_EXCEPTION;
uint8_t shared[32];
crypto_x25519(shared, priv, pub);
return js_new_blob_stoned_copy(js, shared, 32);
}
JSValue js_crypto_blake2(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 1)
return JS_RaiseDisrupt(js, "crypto.blake2: expected data blob");
size_t data_bits;
uint8_t *data = get_blob_any(js, argv[0], &data_bits, "crypto.blake2 data");
if (!data) return JS_EXCEPTION;
int32_t hash_len = 32;
if (argc > 1) {
if (JS_ToInt32(js, &hash_len, argv[1]))
return JS_EXCEPTION;
if (hash_len < 1 || hash_len > 64)
return JS_RaiseDisrupt(js, "crypto.blake2: hash length must be between 1 and 64 bytes");
}
uint8_t hash[64];
// Use (bits + 7) / 8 to get byte length covering all bits
crypto_blake2b(hash, hash_len, data, (data_bits + 7) / 8);
return js_new_blob_stoned_copy(js, hash, hash_len);
}
JSValue js_crypto_sign(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 2) return JS_RaiseDisrupt(js, "crypto.sign: expected secret_key, message");
uint8_t *sk = get_blob_check_bits(js, argv[0], 512, "crypto.sign secret_key");
if (!sk) return JS_EXCEPTION;
size_t msg_bits;
uint8_t *msg = get_blob_any(js, argv[1], &msg_bits, "crypto.sign message");
if (!msg) return JS_EXCEPTION;
uint8_t sig[64];
crypto_eddsa_sign(sig, sk, msg, (msg_bits + 7) / 8);
return js_new_blob_stoned_copy(js, sig, 64);
}
JSValue js_crypto_verify(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.verify: expected signature, public_key, message");
uint8_t *sig = get_blob_check_bits(js, argv[0], 512, "crypto.verify signature");
if (!sig) return JS_EXCEPTION;
uint8_t *pk = get_blob_check_bits(js, argv[1], 256, "crypto.verify public_key");
if (!pk) return JS_EXCEPTION;
size_t msg_bits;
uint8_t *msg = get_blob_any(js, argv[2], &msg_bits, "crypto.verify message");
if (!msg) return JS_EXCEPTION;
int ret = crypto_eddsa_check(sig, pk, msg, (msg_bits + 7) / 8);
return JS_NewBool(js, ret == 0);
}
JSValue js_crypto_lock(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.lock: expected key, nonce, message, [ad]");
uint8_t *key = get_blob_check_bits(js, argv[0], 256, "crypto.lock key");
if (!key) return JS_EXCEPTION;
uint8_t *nonce = get_blob_check_bits(js, argv[1], 192, "crypto.lock nonce");
if (!nonce) return JS_EXCEPTION;
size_t msg_bits;
uint8_t *msg = get_blob_any(js, argv[2], &msg_bits, "crypto.lock message");
if (!msg) return JS_EXCEPTION;
size_t msg_len = (msg_bits + 7) / 8;
size_t ad_len = 0;
uint8_t *ad = NULL;
if (argc > 3 && !JS_IsNull(argv[3])) {
size_t ad_bits;
ad = get_blob_any(js, argv[3], &ad_bits, "crypto.lock ad");
if (!ad) return JS_EXCEPTION;
ad_len = (ad_bits + 7) / 8;
}
size_t out_len = msg_len + 16;
uint8_t *out = malloc(out_len);
if (!out) return JS_RaiseOOM(js);
// Output: [Ciphertext (msg_len)] [MAC (16)]
crypto_aead_lock(out, out + msg_len, key, nonce, ad, ad_len, msg, msg_len);
JSValue ret = js_new_blob_stoned_copy(js, out, out_len);
free(out);
return ret;
}
JSValue js_crypto_unlock(JSContext *js, JSValue self, int argc, JSValue *argv) {
if (argc < 3) return JS_RaiseDisrupt(js, "crypto.unlock: expected key, nonce, ciphertext, [ad]");
uint8_t *key = get_blob_check_bits(js, argv[0], 256, "crypto.unlock key");
if (!key) return JS_EXCEPTION;
uint8_t *nonce = get_blob_check_bits(js, argv[1], 192, "crypto.unlock nonce");
if (!nonce) return JS_EXCEPTION;
size_t cipher_bits;
uint8_t *cipher = get_blob_any(js, argv[2], &cipher_bits, "crypto.unlock ciphertext");
if (!cipher) return JS_EXCEPTION;
size_t cipher_len = (cipher_bits + 7) / 8;
if (cipher_len < 16) return JS_RaiseDisrupt(js, "crypto.unlock: ciphertext too short (min 16 bytes)");
size_t msg_len = cipher_len - 16;
size_t ad_len = 0;
uint8_t *ad = NULL;
if (argc > 3 && !JS_IsNull(argv[3])) {
size_t ad_bits;
ad = get_blob_any(js, argv[3], &ad_bits, "crypto.unlock ad");
if (!ad) return JS_EXCEPTION;
ad_len = (ad_bits + 7) / 8;
}
uint8_t *out = malloc(msg_len > 0 ? msg_len : 1);
if (!out) return JS_RaiseOOM(js);
// MAC is at cipher + msg_len
const uint8_t *mac = cipher + msg_len;
if (crypto_aead_unlock(out, mac, key, nonce, ad, ad_len, cipher, msg_len) != 0) {
free(out);
return JS_NULL;
}
JSValue ret = js_new_blob_stoned_copy(js, out, msg_len);
free(out);
return ret;
}
static const JSCFunctionListEntry js_crypto_funcs[] = {
JS_CFUNC_DEF("shared", 2, js_crypto_shared),
JS_CFUNC_DEF("blake2", 2, js_crypto_blake2),
JS_CFUNC_DEF("sign", 2, js_crypto_sign),
JS_CFUNC_DEF("verify", 3, js_crypto_verify),
JS_CFUNC_DEF("lock", 3, js_crypto_lock),
JS_CFUNC_DEF("unlock", 3, js_crypto_unlock),
};
JSValue js_core_internal_crypto_use(JSContext *js)
{
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_crypto_funcs, sizeof(js_crypto_funcs)/sizeof(js_crypto_funcs[0]));
JS_RETURN(mod.val);
}

View File

@@ -25,7 +25,7 @@ function use_embed(name) {
var fd = use_embed('internal_fd')
var js = use_embed('js')
var crypto = use_embed('crypto')
var crypto = use_embed('internal_crypto')
// core_path and shop_path come from env (C runtime passes them through)
// shop_path may be null if --core was used without --shop
@@ -58,7 +58,7 @@ function boot_load(name) {
var mcode_blob = null
var mach_blob = null
if (!fd.is_file(mcode_path)) {
print("error: missing boot seed: " + name + "\n")
os.print("error: missing boot seed: " + name + "\n")
disrupt
}
mcode_blob = fd.slurp(mcode_path)
@@ -103,7 +103,7 @@ function load_pipeline_module(name, env) {
tok_result = boot_tok(src, source_path)
ast = boot_par(tok_result.tokens, src, source_path, boot_tok)
if (ast.errors != null && length(ast.errors) > 0) {
print("error: failed to compile pipeline module: " + name + "\n")
os.print("error: failed to compile pipeline module: " + name + "\n")
disrupt
}
ast = boot_fld(ast)
@@ -126,7 +126,7 @@ function load_pipeline_module(name, env) {
mach_blob = mach_compile_mcode_bin(name, text(mcode_blob))
return mach_load(mach_blob, env)
}
print("error: cannot load pipeline module: " + name + "\n")
os.print("error: cannot load pipeline module: " + name + "\n")
disrupt
}
@@ -158,6 +158,7 @@ function analyze(src, filename) {
var line = null
var col = null
var has_errors = _ast.errors != null && length(_ast.errors) > 0
var folded = null
if (has_errors) {
while (_i < length(_ast.errors)) {
e = _ast.errors[_i]
@@ -166,9 +167,9 @@ function analyze(src, filename) {
col = e.column
if (msg != prev_msg || line != prev_line) {
if (line != null && col != null)
print(`${filename}:${text(line)}:${text(col)}: error: ${msg}`)
os.print(`${filename}:${text(line)}:${text(col)}: error: ${msg}\n`)
else
print(`${filename}: error: ${msg}`)
os.print(`${filename}: error: ${msg}\n`)
}
prev_line = line
prev_msg = msg
@@ -176,15 +177,126 @@ function analyze(src, filename) {
}
disrupt
}
return fold_mod(_ast)
folded = fold_mod(_ast)
if (!_no_warn && folded._diagnostics != null && length(folded._diagnostics) > 0) {
_i = 0
while (_i < length(folded._diagnostics)) {
e = folded._diagnostics[_i]
os.print(`${filename}:${text(e.line)}:${text(e.col)}: ${e.severity}: ${e.message}\n`)
_i = _i + 1
}
}
folded._diagnostics = null
return folded
}
// Lazy-loaded verify_ir module (loaded on first use)
var _verify_ir_mod = null
// Module summary extraction for cross-program analysis.
// Scans mcode IR for use() call patterns and attaches summaries.
// _summary_resolver is set after shop loads (null during bootstrap).
var _summary_resolver = null
function extract_module_summaries(compiled, ctx) {
if (_summary_resolver == null) return null
var instrs = null
var summaries = []
var unresolved = []
var i = 0
var j = 0
var n = 0
var instr = null
var prev = null
var op = null
var use_slots = {}
var frame_map = {}
var arg_map = {}
var val_slot = 0
var f_slot = 0
var path = null
var result_slot = 0
var summary = null
var inv_n = 0
if (compiled.main == null) return null
instrs = compiled.main.instructions
if (instrs == null) return null
n = length(instrs)
// Pass 1: find access(slot, {make:"intrinsic", name:"use"})
i = 0
while (i < n) {
instr = instrs[i]
if (is_array(instr) && instr[0] == "access") {
if (is_object(instr[2]) && instr[2].make == "intrinsic" && instr[2].name == "use") {
use_slots[text(instr[1])] = true
}
}
i = i + 1
}
// Pass 2: find frame(frame_slot, use_slot), setarg with string, invoke
i = 0
while (i < n) {
instr = instrs[i]
if (is_array(instr)) {
op = instr[0]
if (op == "frame" || op == "goframe") {
if (use_slots[text(instr[2])] == true) {
frame_map[text(instr[1])] = true
}
} else if (op == "setarg") {
if (frame_map[text(instr[1])] == true) {
val_slot = instr[3]
j = i - 1
while (j >= 0) {
prev = instrs[j]
if (is_array(prev) && prev[0] == "access" && prev[1] == val_slot && is_text(prev[2])) {
arg_map[text(instr[1])] = prev[2]
break
}
j = j - 1
}
}
} else if (op == "invoke" || op == "tail_invoke") {
f_slot = instr[1]
path = arg_map[text(f_slot)]
if (path != null) {
result_slot = instr[2]
summary = _summary_resolver(path, ctx)
if (summary != null) {
if (summary._native != true) {
summaries[] = {slot: result_slot, summary: summary}
}
} else {
inv_n = length(instr)
unresolved[] = {path: path, line: instr[inv_n - 2], col: instr[inv_n - 1]}
}
}
}
}
i = i + 1
}
if (length(summaries) > 0 || length(unresolved) > 0) {
return {summaries: summaries, unresolved: unresolved}
}
return null
}
// Run AST through mcode pipeline -> register VM
function run_ast_fn(name, ast, env) {
function run_ast_fn(name, ast, env, pkg) {
var compiled = mcode_mod(ast)
var ms = null
var _ui = 0
var _ur = null
var optimized = null
var _di = 0
var _diag = null
var _has_errors = false
var mcode_json = null
var mach_blob = null
if (os._verify_ir) {
if (_verify_ir_mod == null) {
_verify_ir_mod = load_pipeline_module('verify_ir', pipeline_env)
@@ -192,13 +304,45 @@ function run_ast_fn(name, ast, env) {
compiled._verify = true
compiled._verify_mod = _verify_ir_mod
}
var optimized = streamline_mod(compiled)
if (!_no_warn) {
compiled._warn = true
ms = extract_module_summaries(compiled, pkg)
if (ms != null) {
if (length(ms.summaries) > 0) {
compiled._module_summaries = ms.summaries
}
if (length(ms.unresolved) > 0) {
compiled._unresolved_imports = ms.unresolved
}
}
}
if (compiled._unresolved_imports != null) {
_ui = 0
while (_ui < length(compiled._unresolved_imports)) {
_ur = compiled._unresolved_imports[_ui]
os.print(`${name}:${text(_ur.line)}:${text(_ur.col)}: error: cannot resolve module '${_ur.path}'\n`)
_ui = _ui + 1
}
disrupt
}
optimized = streamline_mod(compiled)
if (optimized._verify) {
delete optimized._verify
delete optimized._verify_mod
}
var mcode_json = json.encode(optimized)
var mach_blob = mach_compile_mcode_bin(name, mcode_json)
if (optimized._diagnostics != null && length(optimized._diagnostics) > 0) {
_di = 0
_has_errors = false
while (_di < length(optimized._diagnostics)) {
_diag = optimized._diagnostics[_di]
os.print(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}\n`)
if (_diag.severity == "error") _has_errors = true
_di = _di + 1
}
if (_has_errors) disrupt
}
mcode_json = json.encode(optimized)
mach_blob = mach_compile_mcode_bin(name, mcode_json)
return mach_load(mach_blob, env)
}
@@ -217,6 +361,52 @@ function compile_to_blob(name, ast) {
return mach_compile_mcode_bin(name, json.encode(optimized))
}
// Compile user program AST to blob with diagnostics
function compile_user_blob(name, ast, pkg) {
var compiled = mcode_mod(ast)
var ms = null
var _ui = 0
var _ur = null
var optimized = null
var _di = 0
var _diag = null
var _has_errors = false
if (!_no_warn) {
compiled._warn = true
ms = extract_module_summaries(compiled, pkg)
if (ms != null) {
if (length(ms.summaries) > 0) {
compiled._module_summaries = ms.summaries
}
if (length(ms.unresolved) > 0) {
compiled._unresolved_imports = ms.unresolved
}
}
}
if (compiled._unresolved_imports != null) {
_ui = 0
while (_ui < length(compiled._unresolved_imports)) {
_ur = compiled._unresolved_imports[_ui]
os.print(`${name}:${text(_ur.line)}:${text(_ur.col)}: error: cannot resolve module '${_ur.path}'\n`)
_ui = _ui + 1
}
disrupt
}
optimized = streamline_mod(compiled)
if (optimized._diagnostics != null && length(optimized._diagnostics) > 0) {
_di = 0
_has_errors = false
while (_di < length(optimized._diagnostics)) {
_diag = optimized._diagnostics[_di]
os.print(`${_diag.file}:${text(_diag.line)}:${text(_diag.col)}: ${_diag.severity}: ${_diag.message}\n`)
if (_diag.severity == "error") _has_errors = true
_di = _di + 1
}
if (_has_errors) disrupt
}
return mach_compile_mcode_bin(name, json.encode(optimized))
}
// If loaded directly by C runtime (not via bootstrap), convert args -> init
var _program = null
var _user_args = []
@@ -227,6 +417,9 @@ var _init = init
if (_init != null && _init.native_mode)
native_mode = true
// Inherit warn mode from init (set by C for --no-warn)
var _no_warn = (_init != null && _init.no_warn) ? true : false
// CLI path: convert args to init record
if (args != null && (_init == null || !_init.program)) {
_program = args[0]
@@ -242,7 +435,7 @@ if (args != null && (_init == null || !_init.program)) {
}
}
use_cache['core/os'] = os
use_cache['core/internal/os'] = os
// Extra env properties added as engine initializes (log, runtime fns, etc.)
var core_extras = {}
@@ -291,7 +484,7 @@ function use_core(path) {
result = mach_load(mach_blob, env)
}
} disruption {
print("use('" + path + "'): failed to compile or load " + file_path + "\n")
os.print("use('" + path + "'): failed to compile or load " + file_path + "\n")
disrupt
}
_load_mod()
@@ -314,8 +507,8 @@ function actor() {
}
var actor_mod = use_core('actor')
var wota = use_core('wota')
var nota = use_core('nota')
var wota = use_core('internal/wota')
var nota = use_core('internal/nota')
var ENETSERVICE = 0.1
@@ -324,17 +517,37 @@ var REPLYTIMEOUT = 60 // seconds before replies are ignored
// --- Logging system (bootstrap phase) ---
// Early log: prints to console before toml/time/json are loaded.
// Upgraded to full sink-based system after config loads (see load_log_config below).
// The bootstrap log forwards to _log_full once the full system is ready, so that
// modules loaded early (like shop.cm) get full logging even though they captured
// the bootstrap function reference.
var log_config = null
var channel_sinks = {}
var wildcard_sinks = []
var warned_channels = {}
var stack_channels = {}
var _log_full = null
var log_quiet_channels = { shop: true }
function log(name, args) {
if (_log_full) return _log_full(name, args)
if (log_quiet_channels[name]) return
var msg = args[0]
var stk = null
var i = 0
var fr = null
if (msg == null) msg = ""
os.print(`[${text(_cell.id, 0, 5)}] [${name}]: ${msg}\n`)
if (name == "error") {
stk = os.stack(2)
if (stk && length(stk) > 0) {
for (i = 0; i < length(stk); i = i + 1) {
fr = stk[i]
os.print(` at ${fr.fn} (${fr.file}:${text(fr.line)}:${text(fr.col)})\n`)
}
}
}
}
function actor_die(err)
@@ -420,7 +633,23 @@ core_extras.native_mode = native_mode
// NOW load shop -- it receives all of the above via env
var shop = use_core('internal/shop')
if (native_mode) use_core('build')
use_core('build')
// Wire up module summary resolver now that shop is available
_summary_resolver = function(path, ctx) {
var info = shop.resolve_import_info(path, ctx)
if (info == null) return null
if (info.type == 'native') return {_native: true}
var resolved = info.resolved_path
if (resolved == null) return null
var summary_fn = function() {
return shop.summary_file(resolved)
} disruption {
return null
}
return summary_fn()
}
var time = use_core('time')
var toml = use_core('toml')
@@ -447,6 +676,7 @@ function build_sink_routing() {
var names = array(log_config.sink)
arrfor(names, function(name) {
var sink = log_config.sink[name]
if (!sink || !is_object(sink)) return
sink._name = name
if (!is_array(sink.channels)) sink.channels = []
if (is_text(sink.exclude)) sink.exclude = [sink.exclude]
@@ -476,13 +706,13 @@ function load_log_config() {
log_config = toml.decode(text(fd.slurp(log_path)))
}
}
if (!log_config || !log_config.sink) {
if (!log_config || !log_config.sink || length(array(log_config.sink)) == 0) {
log_config = {
sink: {
terminal: {
type: "console",
format: "pretty",
channels: ["console", "error", "system"],
channels: ["*"],
stack: ["error"]
}
}
@@ -498,10 +728,8 @@ function pretty_format(rec) {
var out = null
var i = 0
var fr = null
if (rec.source && rec.source.file)
src = rec.source.file + ":" + text(rec.source.line)
ev = is_text(rec.event) ? rec.event : json.encode(rec.event, false)
out = `[${aid}] [${rec.channel}] ${src} ${ev}\n`
out = `[${aid}] [${rec.channel}] ${ev}\n`
if (rec.stack && length(rec.stack) > 0) {
for (i = 0; i < length(rec.stack); i = i + 1) {
fr = rec.stack[i]
@@ -561,13 +789,7 @@ log = function(name, args) {
var stack = null
var rec = null
if (!sinks && length(wildcard_sinks) == 0) {
if (!warned_channels[name]) {
warned_channels[name] = true
os.print(`[warn] log channel '${name}' has no sinks configured\n`)
}
return
}
if (!sinks && length(wildcard_sinks) == 0) return
// C-provided stack (from JS_Log callback) overrides caller_info/os.stack
if (c_stack && length(c_stack) > 0) {
@@ -594,6 +816,10 @@ log = function(name, args) {
// Wire C-level JS_Log through the ƿit log system
actor_mod.set_log(log)
// Let the bootstrap log forward to the full system — modules loaded early
// (before the full log was ready) captured the bootstrap function reference.
_log_full = log
var pronto = use_core('pronto')
var fallback = pronto.fallback
var parallel = pronto.parallel
@@ -1209,27 +1435,38 @@ if (ends_with(prog, '.ce')) prog = text(prog, 0, -3)
var package = use_core('package')
// Find the .ce file
var prog_path = prog + ".ce"
var pkg_dir = null
var core_dir = null
if (!fd.is_file(prog_path)) {
pkg_dir = package.find_package_dir(".")
if (pkg_dir)
prog_path = pkg_dir + '/' + prog + '.ce'
}
if (!fd.is_file(prog_path)) {
// Check core packages
core_dir = core_path
prog_path = core_dir + '/' + prog + '.ce'
}
if (!fd.is_file(prog_path)) {
os.print(`Main program ${prog} could not be found\n`)
os.exit(1)
// Find the .ce file using unified resolver
var cwd_package = package.find_package_dir(".")
var prog_info = shop.resolve_program ? shop.resolve_program(prog, cwd_package) : null
var prog_path = null
if (prog_info) {
prog_path = prog_info.path
} else {
// Fallback: check CWD, package dir, and core
prog_path = prog + ".ce"
if (!fd.is_file(prog_path) && cwd_package)
prog_path = cwd_package + '/' + prog + '.ce'
if (!fd.is_file(prog_path))
prog_path = core_path + '/' + prog + '.ce'
if (!fd.is_file(prog_path)) {
os.print(`Main program ${prog} could not be found\n`)
os.exit(1)
}
}
$_.clock(_ => {
var file_info = shop.file_info ? shop.file_info(prog_path) : null
var _file_info_ok = false
var file_info = null
var _try_fi = function() {
file_info = shop.file_info ? shop.file_info(prog_path) : null
_file_info_ok = true
} disruption {}
_try_fi()
if (!_file_info_ok || !file_info)
file_info = {path: prog_path, is_module: false, is_actor: true, package: null, name: prog}
// If the unified resolver found the package, use that as the authoritative source
if (prog_info && prog_info.pkg)
file_info.package = prog_info.pkg
var inject = shop.script_inject_for ? shop.script_inject_for(file_info) : []
// Build env with runtime functions + capability injections
@@ -1248,12 +1485,49 @@ $_.clock(_ => {
}
var pkg = file_info ? file_info.package : null
// Verify all transitive dependency packages are present, auto-install if missing
var _deps = null
var _di = 0
var _dep_dir = null
var _auto_install = null
if (pkg) {
_deps = package.gather_dependencies(pkg)
_di = 0
while (_di < length(_deps)) {
_dep_dir = package.get_dir(_deps[_di])
if (!fd.is_dir(_dep_dir)) {
log.console('installing missing dependency: ' + _deps[_di])
_auto_install = function() {
shop.sync(_deps[_di])
} disruption {
log.error('failed to install dependency: ' + _deps[_di])
disrupt
}
_auto_install()
}
_di = _di + 1
}
}
env.use = function(path) {
var ck = 'core/' + path
var _use_core_result = null
var _use_core_ok = false
if (use_cache[ck]) return use_cache[ck]
var core_mod = use_core(path)
if (core_mod) return core_mod
return shop.use(path, pkg)
var _try_core = function() {
_use_core_result = use_core(path)
_use_core_ok = true
} disruption {}
_try_core()
if (_use_core_ok && _use_core_result) return _use_core_result
var _shop_use = function() {
return shop.use(path, pkg)
} disruption {
log.error(`use('${path}') failed (package: ${pkg})`)
disrupt
}
return _shop_use()
}
env.args = _cell.args.arg
env.log = log
@@ -1293,7 +1567,7 @@ $_.clock(_ => {
} else {
script = text(source_blob)
ast = analyze(script, prog_path)
mach_blob = compile_to_blob(prog, ast)
mach_blob = compile_user_blob(prog, ast, pkg)
if (cached_path) {
ensure_build_dir()
fd.slurpwrite(cached_path, mach_blob)

View File

@@ -40,7 +40,8 @@ static int js2fd(JSContext *ctx, JSValueConst val)
// Helper function for writing
static ssize_t js_fd_write_helper(JSContext *js, int fd, JSValue val)
{
(void)js; (void)fd; (void)val;
return -1;
}
@@ -85,7 +86,7 @@ JSC_CCALL(fd_write,
JS_FreeCString(js, data);
} else {
void *data = js_get_blob_data(js, &len, argv[1]);
if (data == -1)
if (data == (void *)-1)
return JS_EXCEPTION;
wrote = write(fd, data, len);
}
@@ -101,18 +102,15 @@ JSC_CCALL(fd_read,
if (argc > 1)
size = js2number(js, argv[1]);
void *buf = malloc(size);
if (!buf)
return JS_RaiseDisrupt(js, "malloc failed");
ssize_t bytes_read = read(fd, buf, size);
if (bytes_read < 0) {
free(buf);
void *out;
ret = js_new_blob_alloc(js, size, &out);
if (JS_IsException(ret)) return ret;
ssize_t bytes_read = read(fd, out, size);
if (bytes_read < 0)
return JS_RaiseDisrupt(js, "read failed: %s", strerror(errno));
}
ret = js_new_blob_stoned_copy(js, buf, bytes_read);
free(buf);
js_blob_stone(ret, bytes_read);
return ret;
)
@@ -127,39 +125,48 @@ JSC_SCALL(fd_slurp,
size_t size = st.st_size;
if (size == 0)
return js_new_blob_stoned_copy(js, NULL, 0);
#ifndef _WIN32
int fd = open(str, O_RDONLY);
if (fd < 0)
return JS_RaiseDisrupt(js, "open failed: %s", strerror(errno));
void *data = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (data == MAP_FAILED) {
close(fd);
return JS_RaiseDisrupt(js, "mmap failed: %s", strerror(errno));
void *out;
ret = js_new_blob_alloc(js, size, &out);
if (JS_IsException(ret)) { close(fd); return ret; }
size_t total = 0;
while (total < size) {
ssize_t n = read(fd, (uint8_t *)out + total, size - total);
if (n < 0) {
if (errno == EINTR) continue;
close(fd);
return JS_RaiseDisrupt(js, "read failed: %s", strerror(errno));
}
if (n == 0) break;
total += n;
}
ret = js_new_blob_stoned_copy(js, data, size);
munmap(data, size);
close(fd);
js_blob_stone(ret, total);
#else
// Windows: use memory mapping for optimal performance
HANDLE hFile = CreateFileA(str, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
return JS_RaiseDisrupt(js, "CreateFile failed: %lu", GetLastError());
HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (hMapping == NULL) {
CloseHandle(hFile);
return JS_RaiseDisrupt(js, "CreateFileMapping failed: %lu", GetLastError());
}
void *data = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (data == NULL) {
CloseHandle(hMapping);
CloseHandle(hFile);
return JS_RaiseDisrupt(js, "MapViewOfFile failed: %lu", GetLastError());
}
ret = js_new_blob_stoned_copy(js, data, size);
UnmapViewOfFile(data);
CloseHandle(hMapping);

515
internal/fd_playdate.c Normal file
View File

@@ -0,0 +1,515 @@
#include "cell.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "pd_api.h"
// Global Playdate API pointers - defined in main_playdate.c
extern const struct playdate_file *pd_file;
// --- Helper Functions ---
// Convert JS value to SDFile* (casting via intptr_t/int)
static SDFile* js2fd(JSContext *ctx, JSValueConst val)
{
int32_t fd_int;
if (JS_ToInt32(ctx, &fd_int, val) < 0) {
JS_RaiseDisrupt(ctx, "Expected file descriptor number");
return NULL;
}
return (SDFile*)(intptr_t)fd_int;
}
static time_t pd_time_to_unix(FileStat *fs) {
struct tm t = {0};
t.tm_year = fs->m_year - 1900;
t.tm_mon = fs->m_month - 1;
t.tm_mday = fs->m_day;
t.tm_hour = fs->m_hour;
t.tm_min = fs->m_minute;
t.tm_sec = fs->m_second;
return mktime(&t);
}
// --- Implementation ---
JSC_SCALL(fd_open,
FileOptions flags = kFileRead;
// Parse optional flags argument
if (argc > 1 && JS_IsText(argv[1])) {
const char *flag_str = JS_ToCString(js, argv[1]);
flags = 0;
if (strchr(flag_str, 'r')) flags |= kFileRead;
if (strchr(flag_str, 'w')) flags |= kFileWrite;
if (strchr(flag_str, 'a')) flags |= kFileAppend;
if (strchr(flag_str, '+')) {
// Read+Write not directly mapped, assume Write covers it or combine?
// Playdate docs say kFileRead|kFileWrite is valid
flags |= kFileRead | kFileWrite;
}
JS_FreeCString(js, flag_str);
}
SDFile* file = pd_file->open(str, flags);
if (!file) {
const char* err = pd_file->geterr();
ret = JS_RaiseDisrupt(js, "open failed: %s", err ? err : "unknown error");
} else {
ret = JS_NewInt32(js, (int32_t)(intptr_t)file);
}
)
JSC_CCALL(fd_write,
SDFile* fd = js2fd(js, argv[0]);
if (!fd) return JS_EXCEPTION;
size_t len;
int wrote;
if (JS_IsText(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]);
if (!data) return JS_EXCEPTION;
wrote = pd_file->write(fd, data, (unsigned int)len);
JS_FreeCString(js, data);
} else {
void *data = js_get_blob_data(js, &len, argv[1]);
if (data == (void*)-1)
return JS_EXCEPTION;
wrote = pd_file->write(fd, data, (unsigned int)len);
}
if (wrote < 0) {
const char* err = pd_file->geterr();
return JS_RaiseDisrupt(js, "write failed: %s", err ? err : "unknown error");
}
return JS_NewInt64(js, wrote);
)
JSC_CCALL(fd_read,
SDFile* fd = js2fd(js, argv[0]);
if (!fd) return JS_EXCEPTION;
size_t size = 4096;
if (argc > 1)
size = js2number(js, argv[1]);
void *out;
ret = js_new_blob_alloc(js, size, &out);
if (JS_IsException(ret)) return ret;
int bytes_read = pd_file->read(fd, out, (unsigned int)size);
if (bytes_read < 0) {
const char* err = pd_file->geterr();
return JS_RaiseDisrupt(js, "read failed: %s", err ? err : "unknown error");
}
js_blob_stone(ret, bytes_read);
return ret;
)
JSC_SCALL(fd_slurp,
FileStat st;
if (pd_file->stat(str, &st) != 0) {
// const char* err = pd_file->geterr(); // stat usually returns -1 on error
return JS_RaiseDisrupt(js, "stat failed for %s", str);
}
if (st.isdir)
return JS_RaiseDisrupt(js, "path is a directory");
size_t size = st.size;
if (size == 0)
return js_new_blob_stoned_copy(js, NULL, 0);
SDFile* fd = pd_file->open(str, kFileRead);
if (!fd) {
const char* err = pd_file->geterr();
return JS_RaiseDisrupt(js, "open failed: %s", err ? err : "unknown error");
}
void *out;
ret = js_new_blob_alloc(js, size, &out);
if (JS_IsException(ret)) { pd_file->close(fd); return ret; }
int bytes_read = pd_file->read(fd, out, (unsigned int)size);
pd_file->close(fd);
if (bytes_read < 0)
return JS_RaiseDisrupt(js, "read failed");
js_blob_stone(ret, bytes_read);
)
JSC_CCALL(fd_lseek,
SDFile* fd = js2fd(js, argv[0]);
if (!fd) return JS_EXCEPTION;
int offset = (int)js2number(js, argv[1]);
int whence = SEEK_SET;
if (argc > 2) {
const char *whence_str = JS_ToCString(js, argv[2]);
if (strcmp(whence_str, "cur") == 0) whence = SEEK_CUR;
else if (strcmp(whence_str, "end") == 0) whence = SEEK_END;
JS_FreeCString(js, whence_str);
}
if (pd_file->seek(fd, offset, whence) != 0) {
const char* err = pd_file->geterr();
return JS_RaiseDisrupt(js, "seek failed: %s", err ? err : "unknown error");
}
// tell to get new pos
int new_pos = pd_file->tell(fd);
return JS_NewInt64(js, new_pos);
)
JSC_CCALL(fd_getcwd,
// Playdate is always at root of its sandbox
return JS_NewString(js, "/");
)
JSC_SCALL(fd_rmdir,
int recursive = 0;
if (argc > 1)
recursive = JS_ToBool(js, argv[1]);
if (pd_file->unlink(str, recursive) != 0) {
const char* err = pd_file->geterr();
ret = JS_RaiseDisrupt(js, "could not remove %s: %s", str, err ? err : "unknown error");
}
)
JSC_SCALL(fd_mkdir,
if (pd_file->mkdir(str) != 0) {
const char* err = pd_file->geterr();
ret = JS_RaiseDisrupt(js, "could not make directory %s: %s", str, err ? err : "unknown error");
}
)
JSC_SCALL(fd_mv,
if (argc < 2)
ret = JS_RaiseDisrupt(js, "fd.mv requires 2 arguments: old path and new path");
else if (!JS_IsText(argv[1]))
ret = JS_RaiseDisrupt(js, "second argument must be a string (new path)");
else {
const char *new_path = JS_ToCString(js, argv[1]);
if (pd_file->rename(str, new_path) != 0) {
const char* err = pd_file->geterr();
ret = JS_RaiseDisrupt(js, "could not rename %s to %s: %s", str, new_path, err ? err : "unknown error");
}
JS_FreeCString(js, new_path);
}
)
JSC_SCALL(fd_symlink,
// Not supported
if (argc >= 2 && JS_IsText(argv[1])) {
// consume arg
JS_FreeCString(js, JS_ToCString(js, argv[1]));
}
ret = JS_RaiseDisrupt(js, "symlink not supported on Playdate");
)
JSC_SCALL(fd_unlink,
if (pd_file->unlink(str, 0) != 0) {
const char* err = pd_file->geterr();
ret = JS_RaiseDisrupt(js, "could not remove file %s: %s", str, err ? err : "unknown error");
}
)
JSC_SCALL(fd_rm,
// Recursive remove
if (pd_file->unlink(str, 1) != 0) {
const char* err = pd_file->geterr();
ret = JS_RaiseDisrupt(js, "could not remove %s: %s", str, err ? err : "unknown error");
}
)
JSC_CCALL(fd_fsync,
SDFile* fd = js2fd(js, argv[0]);
if (!fd) return JS_EXCEPTION;
if (pd_file->flush(fd) != 0) {
const char* err = pd_file->geterr();
return JS_RaiseDisrupt(js, "fsync failed: %s", err ? err : "unknown error");
}
return JS_NULL;
)
JSC_CCALL(fd_close,
SDFile* fd = js2fd(js, argv[0]);
if (!fd) return JS_EXCEPTION;
if (pd_file->close(fd) != 0) {
const char* err = pd_file->geterr();
return JS_RaiseDisrupt(js, "close failed: %s", err ? err : "unknown error");
}
return JS_NULL;
)
JSC_CCALL(fd_fstat,
// fstat not supported by Playdate API
SDFile* fd = js2fd(js, argv[0]);
if (!fd) return JS_EXCEPTION;
// We can't get stat from SDFile*. Return minimal object or error?
// fd.c returns full object.
// Return empty object with error property? Or just size 0?
// Let's return empty object.
JSValue obj = JS_NewObject(js);
return obj;
)
JSC_SCALL(fd_stat,
FileStat st;
if (pd_file->stat(str, &st) != 0) {
return JS_NewObject(js); // Return empty on failure/not found
}
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.size));
// Approximate mode
JS_SetPropertyStr(js, obj, "mode", JS_NewInt32(js, st.isdir ? 0040755 : 0100644));
JS_SetPropertyStr(js, obj, "uid", JS_NewInt32(js, 0));
JS_SetPropertyStr(js, obj, "gid", JS_NewInt32(js, 0));
time_t ts = pd_time_to_unix(&st);
JS_SetPropertyStr(js, obj, "atime", JS_NewInt64(js, ts));
JS_SetPropertyStr(js, obj, "mtime", JS_NewInt64(js, ts));
JS_SetPropertyStr(js, obj, "ctime", JS_NewInt64(js, ts));
JS_SetPropertyStr(js, obj, "nlink", JS_NewInt32(js, 1));
JS_SetPropertyStr(js, obj, "ino", JS_NewInt64(js, 0));
JS_SetPropertyStr(js, obj, "dev", JS_NewInt32(js, 0));
JS_SetPropertyStr(js, obj, "rdev", JS_NewInt32(js, 0));
JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, 512));
JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, (st.size + 511) / 512));
JS_SetPropertyStr(js, obj, "isFile", JS_NewBool(js, !st.isdir));
JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, st.isdir));
JS_SetPropertyStr(js, obj, "isSymlink", JS_NewBool(js, 0));
JS_SetPropertyStr(js, obj, "isFIFO", JS_NewBool(js, 0));
JS_SetPropertyStr(js, obj, "isSocket", JS_NewBool(js, 0));
JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, 0));
JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, 0));
return obj;
)
struct listfiles_ctx {
JSContext *js;
JSValue array;
int index;
};
static void listfiles_cb(const char *path, void *userdata) {
struct listfiles_ctx *ctx = (struct listfiles_ctx*)userdata;
if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) return;
// Playdate listfiles returns just the name, but sometimes with slash for dir?
// Docs say "names of files".
JS_SetPropertyNumber(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path));
}
JSC_SCALL(fd_readdir,
JSValue ret_arr = JS_NewArray(js);
struct listfiles_ctx ctx = { js, ret_arr, 0 };
if (pd_file->listfiles(str, listfiles_cb, &ctx, 0) != 0) {
const char* err = pd_file->geterr();
JS_FreeValue(js, ret_arr);
return JS_RaiseDisrupt(js, "listfiles failed: %s", err ? err : "unknown error");
}
ret = ret_arr;
)
JSC_CCALL(fd_is_file,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
FileStat st;
int res = pd_file->stat(path, &st);
JS_FreeCString(js, path);
if (res != 0) return JS_NewBool(js, 0);
return JS_NewBool(js, !st.isdir);
)
JSC_CCALL(fd_is_dir,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
FileStat st;
int res = pd_file->stat(path, &st);
JS_FreeCString(js, path);
if (res != 0) return JS_NewBool(js, 0);
return JS_NewBool(js, st.isdir);
)
JSC_CCALL(fd_slurpwrite,
size_t len;
const char *data = js_get_blob_data(js, &len, argv[1]);
if (data == (const char *)-1)
return JS_EXCEPTION;
const char *str = JS_ToCString(js, argv[0]);
if (!str) return JS_EXCEPTION;
SDFile* fd = pd_file->open(str, kFileWrite | kFileRead); // Truncates by default? Playdate docs: kFileWrite truncates.
JS_FreeCString(js, str);
if (!fd) {
const char* err = pd_file->geterr();
return JS_RaiseDisrupt(js, "open failed: %s", err ? err : "unknown error");
}
int written = pd_file->write(fd, data, (unsigned int)len);
pd_file->close(fd);
if (written != (int)len) {
const char* err = pd_file->geterr();
return JS_RaiseDisrupt(js, "write failed: %s", err ? err : "unknown error");
}
return JS_NULL;
)
// Helper for recursive enumeration
static void visit_directory_pd(JSContext *js, JSValue results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) {
// We can't use listfiles with arbitrary userdata easily if we need to pass multiple args without struct
// We'll reuse listfiles but we need a way to pass context.
// The context struct can hold current path info.
// Actually, listfiles takes a callback.
// We need to implement recursion.
// But listfiles doesn't give us full paths in callback, just names.
// So we can reconstruct.
// We need a nested struct or helper.
}
struct enum_ctx {
JSContext *js;
JSValue results;
int *count;
const char *curr_path;
const char *rel_prefix;
int recurse;
};
static void enum_cb(const char *name, void *userdata) {
struct enum_ctx *ctx = (struct enum_ctx*)userdata;
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) return;
char item_rel[512]; // PATH_MAX
if (ctx->rel_prefix && strlen(ctx->rel_prefix) > 0) {
snprintf(item_rel, sizeof(item_rel), "%s/%s", ctx->rel_prefix, name);
} else {
strcpy(item_rel, name);
}
JS_SetPropertyNumber(ctx->js, ctx->results, (*ctx->count)++, JS_NewString(ctx->js, item_rel));
if (ctx->recurse) {
// Check if directory
char child_path[512];
snprintf(child_path, sizeof(child_path), "%s/%s", ctx->curr_path, name);
// Remove trailing slash if name has it? Playdate names sometimes have trailing slash for dirs
// But stat should handle it.
FileStat st;
if (pd_file->stat(child_path, &st) == 0 && st.isdir) {
struct enum_ctx subctx = *ctx;
subctx.curr_path = child_path;
subctx.rel_prefix = item_rel;
pd_file->listfiles(child_path, enum_cb, &subctx, 0);
}
}
}
JSC_SCALL(fd_enumerate,
const char *path = str;
if (!path) path = ".";
int recurse = 0;
if (argc > 1)
recurse = JS_ToBool(js, argv[1]);
JSValue results = JS_NewArray(js);
int result_count = 0;
FileStat st;
if (pd_file->stat(path, &st) == 0 && st.isdir) {
struct enum_ctx ctx = { js, results, &result_count, path, "", recurse };
pd_file->listfiles(path, enum_cb, &ctx, 0);
}
ret = results;
)
JSC_CCALL(fd_realpath,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
// Playdate has no realpath, just return path
JSValue res = JS_NewString(js, path);
JS_FreeCString(js, path);
return res;
)
JSC_CCALL(fd_is_link,
const char *path = JS_ToCString(js, argv[0]);
JS_FreeCString(js, path);
return JS_NewBool(js, 0);
)
JSC_CCALL(fd_readlink,
const char *path = JS_ToCString(js, argv[0]);
JS_FreeCString(js, path);
return JS_RaiseDisrupt(js, "readlink not supported");
)
static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, open, 2),
MIST_FUNC_DEF(fd, write, 2),
MIST_FUNC_DEF(fd, read, 2),
MIST_FUNC_DEF(fd, slurp, 1),
MIST_FUNC_DEF(fd, slurpwrite, 2),
MIST_FUNC_DEF(fd, lseek, 3),
MIST_FUNC_DEF(fd, getcwd, 0),
MIST_FUNC_DEF(fd, rmdir, 2),
MIST_FUNC_DEF(fd, unlink, 1),
MIST_FUNC_DEF(fd, mkdir, 1),
MIST_FUNC_DEF(fd, mv, 2),
MIST_FUNC_DEF(fd, rm, 1),
MIST_FUNC_DEF(fd, fsync, 1),
MIST_FUNC_DEF(fd, close, 1),
MIST_FUNC_DEF(fd, stat, 1),
MIST_FUNC_DEF(fd, fstat, 1),
MIST_FUNC_DEF(fd, readdir, 1),
MIST_FUNC_DEF(fd, is_file, 1),
MIST_FUNC_DEF(fd, is_dir, 1),
MIST_FUNC_DEF(fd, is_link, 1),
MIST_FUNC_DEF(fd, enumerate, 2),
MIST_FUNC_DEF(fd, symlink, 2),
MIST_FUNC_DEF(fd, realpath, 1),
MIST_FUNC_DEF(fd, readlink, 1),
};
JSValue js_fd_use(JSContext *js) {
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js, mod, js_fd_funcs, countof(js_fd_funcs));
return mod;
}

View File

@@ -8,26 +8,25 @@
JSC_CCALL(kim_encode,
const char *utf8_str = JS_ToCString(js, argv[0]);
if (!utf8_str) return JS_EXCEPTION;
// Count runes to estimate kim buffer size
int rune_count = utf8_count(utf8_str);
// Allocate kim buffer (worst case: 5 bytes per rune)
// Allocate blob (worst case: 5 bytes per rune)
size_t kim_size = rune_count * 5;
char *kim_buffer = malloc(kim_size);
char *kim_ptr = kim_buffer;
// Encode utf8 to kim
void *out;
ret = js_new_blob_alloc(js, kim_size, &out);
if (JS_IsException(ret)) { JS_FreeCString(js, utf8_str); return ret; }
// Encode utf8 to kim directly into blob
char *kim_ptr = (char *)out;
long long runes_encoded;
utf8_to_kim(&utf8_str, &kim_ptr, &runes_encoded);
// Calculate actual size used
size_t actual_size = kim_ptr - kim_buffer;
// Create blob with the encoded data
ret = js_new_blob_stoned_copy(js, kim_buffer, actual_size);
free(kim_buffer);
// Stone with actual size used
size_t actual_size = kim_ptr - (char *)out;
js_blob_stone(ret, actual_size);
JS_FreeCString(js, utf8_str);
)
@@ -73,7 +72,7 @@ static const JSCFunctionListEntry js_kim_funcs[] = {
MIST_FUNC_DEF(kim, decode, 1),
};
JSValue js_core_kim_use(JSContext *js)
JSValue js_core_internal_kim_use(JSContext *js)
{
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));

431
internal/nota.c Normal file
View File

@@ -0,0 +1,431 @@
#define NOTA_IMPLEMENTATION
#include "quickjs-internal.h"
#include "cell.h"
static int nota_get_arr_len (JSContext *ctx, JSValue arr) {
int64_t len;
JS_GetLength (ctx, arr, &len);
return (int)len;
}
typedef struct NotaVisitedNode {
JSGCRef ref;
struct NotaVisitedNode *next;
} NotaVisitedNode;
typedef struct NotaEncodeContext {
JSContext *ctx;
NotaVisitedNode *visited_list;
NotaBuffer nb;
int cycle;
JSGCRef *replacer_ref; /* pointer to GC-rooted ref */
} NotaEncodeContext;
static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) {
NotaVisitedNode *node = (NotaVisitedNode *)sys_malloc (sizeof (NotaVisitedNode));
JS_PushGCRef (enc->ctx, &node->ref);
node->ref.val = JS_DupValue (enc->ctx, val);
node->next = enc->visited_list;
enc->visited_list = node;
}
static void nota_stack_pop (NotaEncodeContext *enc) {
NotaVisitedNode *node = enc->visited_list;
enc->visited_list = node->next;
JS_FreeValue (enc->ctx, node->ref.val);
JS_PopGCRef (enc->ctx, &node->ref);
sys_free (node);
}
static int nota_stack_has (NotaEncodeContext *enc, JSValueConst val) {
NotaVisitedNode *node = enc->visited_list;
while (node) {
if (JS_StrictEq (enc->ctx, node->ref.val, val))
return 1;
node = node->next;
}
return 0;
}
static JSValue nota_apply_replacer (NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
if (!enc->replacer_ref || JS_IsNull (enc->replacer_ref->val)) return JS_DupValue (enc->ctx, val);
JSValue args[2] = { JS_DupValue (enc->ctx, key), JS_DupValue (enc->ctx, val) };
JSValue result = JS_Call (enc->ctx, enc->replacer_ref->val, holder, 2, args);
JS_FreeValue (enc->ctx, args[0]);
JS_FreeValue (enc->ctx, args[1]);
if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
return result;
}
static char *js_do_nota_decode (JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
int type = nota_type (nota);
JSValue ret2;
long long n;
double d;
int b;
char *str;
uint8_t *blob;
switch (type) {
case NOTA_BLOB:
nota = nota_read_blob (&n, (char **)&blob, nota);
*tmp = js_new_blob_stoned_copy (js, blob, n);
sys_free (blob);
break;
case NOTA_TEXT:
nota = nota_read_text (&str, nota);
*tmp = JS_NewString (js, str);
sys_free (str);
break;
case NOTA_ARR:
nota = nota_read_array (&n, nota);
*tmp = JS_NewArrayLen (js, n);
for (int i = 0; i < n; i++) {
nota = js_do_nota_decode (js, &ret2, nota, *tmp, JS_NewInt32 (js, i), reviver);
JS_SetPropertyNumber (js, *tmp, i, ret2);
}
break;
case NOTA_REC:
nota = nota_read_record (&n, nota);
*tmp = JS_NewObject (js);
for (int i = 0; i < n; i++) {
JSGCRef prop_key_ref, sub_val_ref;
JS_PushGCRef (js, &prop_key_ref);
JS_PushGCRef (js, &sub_val_ref);
nota = nota_read_text (&str, nota);
prop_key_ref.val = JS_NewString (js, str);
sub_val_ref.val = JS_NULL;
nota = js_do_nota_decode (js, &sub_val_ref.val, nota, *tmp, prop_key_ref.val, reviver);
JS_SetPropertyStr (js, *tmp, str, sub_val_ref.val);
JS_PopGCRef (js, &sub_val_ref);
JS_PopGCRef (js, &prop_key_ref);
sys_free (str);
}
break;
case NOTA_INT:
nota = nota_read_int (&n, nota);
*tmp = JS_NewInt64 (js, n);
break;
case NOTA_SYM:
nota = nota_read_sym (&b, nota);
if (b == NOTA_PRIVATE) {
JSGCRef inner_ref, obj_ref2;
JS_PushGCRef (js, &inner_ref);
JS_PushGCRef (js, &obj_ref2);
inner_ref.val = JS_NULL;
nota = js_do_nota_decode (js, &inner_ref.val, nota, holder, JS_NULL, reviver);
obj_ref2.val = JS_NewObject (js);
if (!JS_IsNull (js->actor_sym))
JS_SetPropertyKey (js, obj_ref2.val, js->actor_sym, inner_ref.val);
JS_CellStone (js, obj_ref2.val);
*tmp = obj_ref2.val;
JS_PopGCRef (js, &obj_ref2);
JS_PopGCRef (js, &inner_ref);
} else {
switch (b) {
case NOTA_NULL: *tmp = JS_NULL; break;
case NOTA_FALSE: *tmp = JS_NewBool (js, 0); break;
case NOTA_TRUE: *tmp = JS_NewBool (js, 1); break;
default: *tmp = JS_NULL; break;
}
}
break;
default:
case NOTA_FLOAT:
nota = nota_read_float (&d, nota);
*tmp = JS_NewFloat64 (js, d);
break;
}
if (!JS_IsNull (reviver)) {
JSValue args[2] = { JS_DupValue (js, key), JS_DupValue (js, *tmp) };
JSValue revived = JS_Call (js, reviver, holder, 2, args);
JS_FreeValue (js, args[0]);
JS_FreeValue (js, args[1]);
if (!JS_IsException (revived)) {
JS_FreeValue (js, *tmp);
*tmp = revived;
} else {
JS_FreeValue (js, revived);
}
}
return nota;
}
static void nota_encode_value (NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
JSContext *ctx = enc->ctx;
JSGCRef replaced_ref, keys_ref, elem_ref, prop_ref;
JS_PushGCRef (ctx, &replaced_ref);
replaced_ref.val = nota_apply_replacer (enc, holder, key, val);
int tag = JS_VALUE_GET_TAG (replaced_ref.val);
switch (tag) {
case JS_TAG_INT:
case JS_TAG_FLOAT64: {
double d;
JS_ToFloat64 (ctx, &d, replaced_ref.val);
nota_write_number (&enc->nb, d);
break;
}
case JS_TAG_STRING: {
const char *str = JS_ToCString (ctx, replaced_ref.val);
nota_write_text (&enc->nb, str);
JS_FreeCString (ctx, str);
break;
}
case JS_TAG_BOOL:
if (JS_VALUE_GET_BOOL (replaced_ref.val)) nota_write_sym (&enc->nb, NOTA_TRUE);
else nota_write_sym (&enc->nb, NOTA_FALSE);
break;
case JS_TAG_NULL:
nota_write_sym (&enc->nb, NOTA_NULL);
break;
case JS_TAG_PTR: {
if (JS_IsText (replaced_ref.val)) {
const char *str = JS_ToCString (ctx, replaced_ref.val);
nota_write_text (&enc->nb, str);
JS_FreeCString (ctx, str);
break;
}
if (js_is_blob (ctx, replaced_ref.val)) {
size_t buf_len;
void *buf_data = js_get_blob_data (ctx, &buf_len, replaced_ref.val);
if (buf_data == (void *)-1) {
JS_PopGCRef (ctx, &replaced_ref);
return;
}
nota_write_blob (&enc->nb, (unsigned long long)buf_len * 8, (const char *)buf_data);
break;
}
if (JS_IsArray (replaced_ref.val)) {
if (nota_stack_has (enc, replaced_ref.val)) {
enc->cycle = 1;
break;
}
nota_stack_push (enc, replaced_ref.val);
int arr_len = nota_get_arr_len (ctx, replaced_ref.val);
nota_write_array (&enc->nb, arr_len);
JS_PushGCRef (ctx, &elem_ref);
for (int i = 0; i < arr_len; i++) {
elem_ref.val = JS_GetPropertyNumber (ctx, replaced_ref.val, i);
JSValue elem_key = JS_NewInt32 (ctx, i);
nota_encode_value (enc, elem_ref.val, replaced_ref.val, elem_key);
}
JS_PopGCRef (ctx, &elem_ref);
nota_stack_pop (enc);
break;
}
JSValue adata = JS_NULL;
if (!JS_IsNull (ctx->actor_sym)) {
int has = JS_HasPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
if (has > 0) adata = JS_GetPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
}
if (!JS_IsNull (adata)) {
nota_write_sym (&enc->nb, NOTA_PRIVATE);
nota_encode_value (enc, adata, replaced_ref.val, JS_NULL);
JS_FreeValue (ctx, adata);
break;
}
JS_FreeValue (ctx, adata);
if (nota_stack_has (enc, replaced_ref.val)) {
enc->cycle = 1;
break;
}
nota_stack_push (enc, replaced_ref.val);
JSValue to_json = JS_GetPropertyStr (ctx, replaced_ref.val, "toJSON");
if (JS_IsFunction (to_json)) {
JSValue result = JS_Call (ctx, to_json, replaced_ref.val, 0, NULL);
if (!JS_IsException (result)) {
nota_encode_value (enc, result, holder, key);
} else {
nota_write_sym (&enc->nb, NOTA_NULL);
}
nota_stack_pop (enc);
break;
}
JS_PushGCRef (ctx, &keys_ref);
keys_ref.val = JS_GetOwnPropertyNames (ctx, replaced_ref.val);
if (JS_IsException (keys_ref.val)) {
nota_write_sym (&enc->nb, NOTA_NULL);
nota_stack_pop (enc);
JS_PopGCRef (ctx, &keys_ref);
break;
}
int64_t plen64;
if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
nota_write_sym (&enc->nb, NOTA_NULL);
nota_stack_pop (enc);
JS_PopGCRef (ctx, &keys_ref);
break;
}
uint32_t plen = (uint32_t)plen64;
JS_PushGCRef (ctx, &prop_ref);
JS_PushGCRef (ctx, &elem_ref);
uint32_t non_function_count = 0;
for (uint32_t i = 0; i < plen; i++) {
elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val);
if (!JS_IsFunction (prop_ref.val)) non_function_count++;
}
nota_write_record (&enc->nb, non_function_count);
for (uint32_t i = 0; i < plen; i++) {
elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val);
if (!JS_IsFunction (prop_ref.val)) {
const char *prop_name = JS_ToCString (ctx, elem_ref.val);
nota_write_text (&enc->nb, prop_name ? prop_name : "");
nota_encode_value (enc, prop_ref.val, replaced_ref.val, elem_ref.val);
JS_FreeCString (ctx, prop_name);
}
}
JS_PopGCRef (ctx, &elem_ref);
JS_PopGCRef (ctx, &prop_ref);
JS_PopGCRef (ctx, &keys_ref);
nota_stack_pop (enc);
break;
}
default:
nota_write_sym (&enc->nb, NOTA_NULL);
break;
}
JS_PopGCRef (ctx, &replaced_ref);
}
void *value2nota (JSContext *ctx, JSValue v) {
JSGCRef val_ref, key_ref;
JS_PushGCRef (ctx, &val_ref);
JS_PushGCRef (ctx, &key_ref);
val_ref.val = v;
NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visited_list = NULL;
enc->cycle = 0;
enc->replacer_ref = NULL;
nota_buffer_init (&enc->nb, 128);
key_ref.val = JS_NewString (ctx, "");
nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val);
if (enc->cycle) {
JS_PopGCRef (ctx, &key_ref);
JS_PopGCRef (ctx, &val_ref);
nota_buffer_free (&enc->nb);
return NULL;
}
JS_PopGCRef (ctx, &key_ref);
JS_PopGCRef (ctx, &val_ref);
void *data_ptr = enc->nb.data;
enc->nb.data = NULL;
nota_buffer_free (&enc->nb);
return data_ptr;
}
JSValue nota2value (JSContext *js, void *nota) {
if (!nota) return JS_NULL;
JSGCRef holder_ref, key_ref, ret_ref;
JS_PushGCRef (js, &holder_ref);
JS_PushGCRef (js, &key_ref);
JS_PushGCRef (js, &ret_ref);
holder_ref.val = JS_NewObject (js);
key_ref.val = JS_NewString (js, "");
ret_ref.val = JS_NULL;
js_do_nota_decode (js, &ret_ref.val, nota, holder_ref.val, key_ref.val, JS_NULL);
JSValue result = ret_ref.val;
JS_PopGCRef (js, &ret_ref);
JS_PopGCRef (js, &key_ref);
JS_PopGCRef (js, &holder_ref);
return result;
}
static JSValue js_nota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_RaiseDisrupt (ctx, "nota.encode requires at least 1 argument");
JSGCRef val_ref, replacer_ref, key_ref;
JS_PushGCRef (ctx, &val_ref);
JS_PushGCRef (ctx, &replacer_ref);
JS_PushGCRef (ctx, &key_ref);
val_ref.val = argv[0];
replacer_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visited_list = NULL;
enc->cycle = 0;
enc->replacer_ref = &replacer_ref;
nota_buffer_init (&enc->nb, 128);
key_ref.val = JS_NewString (ctx, "");
nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val);
JSValue ret;
if (enc->cycle) {
nota_buffer_free (&enc->nb);
ret = JS_RaiseDisrupt (ctx, "Tried to encode something to nota with a cycle.");
} else {
size_t total_len = enc->nb.size;
void *data_ptr = enc->nb.data;
ret = js_new_blob_stoned_copy (ctx, (uint8_t *)data_ptr, total_len);
nota_buffer_free (&enc->nb);
}
JS_PopGCRef (ctx, &key_ref);
JS_PopGCRef (ctx, &replacer_ref);
JS_PopGCRef (ctx, &val_ref);
return ret;
}
static JSValue js_nota_decode (JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
if (argc < 1) return JS_NULL;
size_t len;
unsigned char *nota = js_get_blob_data (js, &len, argv[0]);
if (nota == (unsigned char *)-1) return JS_EXCEPTION;
if (!nota) return JS_NULL;
JSGCRef holder_ref, key_ref, ret_ref, reviver_ref;
JS_PushGCRef (js, &holder_ref);
JS_PushGCRef (js, &key_ref);
JS_PushGCRef (js, &ret_ref);
JS_PushGCRef (js, &reviver_ref);
reviver_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
holder_ref.val = JS_NewObject (js);
key_ref.val = JS_NewString (js, "");
ret_ref.val = JS_NULL;
js_do_nota_decode (js, &ret_ref.val, (char *)nota, holder_ref.val, key_ref.val, reviver_ref.val);
JSValue result = ret_ref.val;
JS_PopGCRef (js, &reviver_ref);
JS_PopGCRef (js, &ret_ref);
JS_PopGCRef (js, &key_ref);
JS_PopGCRef (js, &holder_ref);
return result;
}
static const JSCFunctionListEntry js_nota_funcs[] = {
JS_CFUNC_DEF ("encode", 1, js_nota_encode),
JS_CFUNC_DEF ("decode", 1, js_nota_decode),
};
JSValue js_core_internal_nota_use (JSContext *js) {
JSGCRef export_ref;
JS_PushGCRef (js, &export_ref);
export_ref.val = JS_NewObject (js);
JS_SetPropertyFunctionList (js, export_ref.val, js_nota_funcs, sizeof (js_nota_funcs) / sizeof (JSCFunctionListEntry));
JSValue result = export_ref.val;
JS_PopGCRef (js, &export_ref);
return result;
}

View File

@@ -34,14 +34,10 @@
static JSClassID js_dylib_class_id;
static void js_dylib_finalizer(JSRuntime *rt, JSValue val) {
void *handle = JS_GetOpaque(val, js_dylib_class_id);
if (handle) {
#ifdef _WIN32
FreeLibrary((HMODULE)handle);
#else
dlclose(handle);
#endif
}
/* Do NOT dlclose here. Loaded dylibs contain finalizer functions for other
JS objects; if the dylib is freed before those objects during
JS_FreeContext teardown, calling their finalizers would SEGV.
The OS reclaims all loaded libraries on process exit. */
}
static JSClassDef js_dylib_class = {
@@ -740,7 +736,7 @@ static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, stack, 1),
};
JSValue js_core_os_use(JSContext *js) {
JSValue js_core_internal_os_use(JSContext *js) {
JS_NewClassID(&js_dylib_class_id);
JS_NewClass(js, js_dylib_class_id, &js_dylib_class);

469
internal/qop.c Normal file
View File

@@ -0,0 +1,469 @@
#define QOP_IMPLEMENTATION
#include "qop.h"
#include "cell.h"
static JSClassID js_qop_archive_class_id;
static void js_qop_archive_finalizer(JSRuntime *rt, JSValue val) {
qop_desc *qop = JS_GetOpaque(val, js_qop_archive_class_id);
if (qop) {
if (qop->hashmap) {
js_free_rt(qop->hashmap);
}
qop_close(qop);
js_free_rt(qop);
}
}
static JSClassDef js_qop_archive_class = {
"qop archive",
.finalizer = js_qop_archive_finalizer,
};
static JSClassID js_qop_writer_class_id;
typedef struct {
FILE *fh;
qop_file *files;
int len;
int capacity;
unsigned int size;
} qop_writer;
static void js_qop_writer_finalizer(JSRuntime *rt, JSValue val) {
qop_writer *w = JS_GetOpaque(val, js_qop_writer_class_id);
if (w) {
if (w->fh) fclose(w->fh);
if (w->files) js_free_rt(w->files);
js_free_rt(w);
}
}
static JSClassDef js_qop_writer_class = {
"qop writer",
.finalizer = js_qop_writer_finalizer,
};
static qop_writer *js2writer(JSContext *js, JSValue v) {
return JS_GetOpaque(v, js_qop_writer_class_id);
}
// Helper functions for writer
static void write_16(unsigned int v, FILE *fh) {
unsigned char b[2];
b[0] = 0xff & (v);
b[1] = 0xff & (v >> 8);
fwrite(b, 2, 1, fh);
}
static void write_32(unsigned int v, FILE *fh) {
unsigned char b[4];
b[0] = 0xff & (v);
b[1] = 0xff & (v >> 8);
b[2] = 0xff & (v >> 16);
b[3] = 0xff & (v >> 24);
fwrite(b, 4, 1, fh);
}
static void write_64(unsigned long long v, FILE *fh) {
unsigned char b[8];
b[0] = 0xff & (v);
b[1] = 0xff & (v >> 8);
b[2] = 0xff & (v >> 16);
b[3] = 0xff & (v >> 24);
b[4] = 0xff & (v >> 32);
b[5] = 0xff & (v >> 40);
b[6] = 0xff & (v >> 48);
b[7] = 0xff & (v >> 56);
fwrite(b, 8, 1, fh);
}
static qop_desc *js2qop(JSContext *js, JSValue v) {
return JS_GetOpaque(v, js_qop_archive_class_id);
}
static int js_qop_ensure_index(JSContext *js, qop_desc *qop) {
if (qop->hashmap != NULL) return 1;
void *buffer = js_malloc_rt(qop->hashmap_size);
if (!buffer) return 0;
int num = qop_read_index(qop, buffer);
if (num == 0) {
js_free_rt(buffer);
return 0;
}
return 1;
}
JSC_CCALL(qop_open,
size_t len;
void *data = js_get_blob_data(js, &len, argv[0]);
if (data == (void *)-1)
ret = JS_EXCEPTION;
else if (!data)
ret = JS_RaiseDisrupt(js, "Empty blob");
else {
qop_desc *qop = js_malloc_rt(sizeof(qop_desc));
if (!qop)
ret = JS_RaiseOOM(js);
else {
int size = qop_open_data((const unsigned char *)data, len, qop);
if (size == 0) {
js_free_rt(qop);
ret = JS_ThrowReferenceError(js, "Failed to open QOP archive from blob");
} else {
JSValue obj = JS_NewObjectClass(js, js_qop_archive_class_id);
JS_SetOpaque(obj, qop);
ret = obj;
}
}
}
)
JSC_CCALL(qop_write,
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
FILE *fh = fopen(path, "wb");
JS_FreeCString(js, path);
if (!fh) return JS_RaiseDisrupt(js, "Could not open file for writing");
qop_writer *w = js_malloc_rt(sizeof(qop_writer));
if (!w) {
fclose(fh);
return JS_RaiseOOM(js);
}
w->fh = fh;
w->capacity = 1024;
w->len = 0;
w->size = 0;
w->files = js_malloc_rt(sizeof(qop_file) * w->capacity);
if (!w->files) {
fclose(fh);
js_free_rt(w);
return JS_RaiseOOM(js);
}
JSValue obj = JS_NewObjectClass(js, js_qop_writer_class_id);
JS_SetOpaque(obj, w);
ret = obj;
)
static JSValue js_qop_close(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
if (!qop)
return JS_RaiseDisrupt(js, "Invalid QOP archive");
qop_close(qop);
JS_SetOpaque(self, NULL); // Prevent double free
return JS_NULL;
}
static JSValue js_qop_read(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
if (!qop)
return JS_RaiseDisrupt(js, "Invalid QOP archive");
const char *path = JS_ToCString(js, argv[0]);
if (!path)
return JS_EXCEPTION;
if (!js_qop_ensure_index(js, qop)) {
JS_FreeCString(js, path);
return JS_RaiseDisrupt(js, "Failed to read QOP index");
}
qop_file *file = qop_find(qop, path);
JS_FreeCString(js, path);
if (!file) {
return JS_NULL;
}
void *out;
JSValue blob = js_new_blob_alloc(js, file->size, &out);
if (JS_IsException(blob)) return blob;
int bytes = qop_read(qop, file, out);
if (bytes == 0)
return JS_RaiseDisrupt(js, "Failed to read file");
js_blob_stone(blob, bytes);
return blob;
}
static JSValue js_qop_read_ex(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
if (!qop)
return JS_RaiseDisrupt(js, "Invalid QOP archive");
const char *path = JS_ToCString(js, argv[0]);
if (!path)
return JS_EXCEPTION;
if (!js_qop_ensure_index(js, qop)) {
JS_FreeCString(js, path);
return JS_RaiseDisrupt(js, "Failed to read QOP index");
}
qop_file *file = qop_find(qop, path);
JS_FreeCString(js, path);
if (!file)
return JS_NULL;
unsigned int start;
unsigned int len;
if (JS_ToUint32(js, &start, argv[1]) < 0 || JS_ToUint32(js, &len, argv[2]) < 0)
return JS_RaiseDisrupt(js, "Invalid start or len");
void *out;
JSValue blob = js_new_blob_alloc(js, len, &out);
if (JS_IsException(blob)) return blob;
int bytes = qop_read_ex(qop, file, out, start, len);
if (bytes == 0)
return JS_RaiseDisrupt(js, "Failed to read file part");
js_blob_stone(blob, bytes);
return blob;
}
static JSValue js_qop_list(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
if (!qop)
return JS_RaiseDisrupt(js, "Invalid QOP archive");
if (!js_qop_ensure_index(js, qop)) {
return JS_RaiseDisrupt(js, "Failed to read QOP index");
}
JSValue arr = JS_NewArray(js);
int count = 0;
for (unsigned int i = 0; i < qop->hashmap_len; i++) {
qop_file *file = &qop->hashmap[i];
if (file->size == 0) continue; // empty slot
char *path = js_malloc_rt(file->path_len);
if (!path) {
return JS_RaiseOOM(js);
}
int len = qop_read_path(qop, file, path);
if (len == 0) {
js_free_rt(path);
continue; // skip on error
}
JSValue str = JS_NewStringLen(js, path, len - 1); // -1 for null terminator
js_free_rt(path);
JS_SetPropertyNumber(js, arr, count++, str);
}
return arr;
}
static JSValue js_qop_stat(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
if (!qop) return JS_RaiseDisrupt(js, "Invalid QOP archive");
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
if (!js_qop_ensure_index(js, qop)) {
JS_FreeCString(js, path);
return JS_RaiseDisrupt(js, "Failed to read QOP index");
}
qop_file *file = qop_find(qop, path);
JS_FreeCString(js, path);
if (!file) return JS_NULL;
JSValue obj = JS_NewObject(js);
JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, file->size));
JS_SetPropertyStr(js, obj, "modtime", JS_NewInt64(js, 0));
JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, 0));
return obj;
}
static JSValue js_qop_is_directory(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_desc *qop = js2qop(js, self);
if (!qop) return JS_RaiseDisrupt(js, "Invalid QOP archive");
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
if (!js_qop_ensure_index(js, qop)) {
JS_FreeCString(js, path);
return JS_RaiseDisrupt(js, "Failed to read QOP index");
}
// Check if any file starts with path + "/"
size_t path_len = strlen(path);
char *prefix = alloca(path_len + 2);
memcpy(prefix, path, path_len);
prefix[path_len] = '/';
prefix[path_len + 1] = '\0';
int found = 0;
// This is inefficient but simple. QOP doesn't have a directory structure.
// We iterate all files to check prefixes.
for (unsigned int i = 0; i < qop->hashmap_len; i++) {
qop_file *file = &qop->hashmap[i];
if (file->size == 0) continue;
// We need to read the path to check it
// Optimization: check if we can read just the prefix?
// qop_read_path reads the whole path.
// Let's read the path.
char file_path[1024]; // MAX_PATH_LEN
if (file->path_len > 1024) continue; // Should not happen based on spec
qop_read_path(qop, file, file_path);
if (strncmp(file_path, prefix, path_len + 1) == 0) {
found = 1;
break;
}
}
JS_FreeCString(js, path);
return JS_NewBool(js, found);
}
// Writer methods
static JSValue js_writer_add_file(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_writer *w = js2writer(js, self);
if (!w) return JS_RaiseDisrupt(js, "Invalid QOP writer");
const char *path = JS_ToCString(js, argv[0]);
if (!path) return JS_EXCEPTION;
size_t data_len;
void *data = js_get_blob_data(js, &data_len, argv[1]);
if (data == (void*)-1) {
JS_FreeCString(js, path);
return JS_EXCEPTION;
}
if (!data) {
JS_FreeCString(js, path);
return JS_RaiseDisrupt(js, "No blob data present");
}
if (w->len >= w->capacity) {
w->capacity *= 2;
qop_file *new_files = realloc(w->files, sizeof(qop_file) * w->capacity);
if (!new_files) {
JS_FreeCString(js, path);
return JS_RaiseOOM(js);
}
w->files = new_files;
}
// Strip leading "./"
const char *archive_path = path;
if (path[0] == '.' && path[1] == '/') {
archive_path = path + 2;
}
unsigned long long hash = qop_hash(archive_path);
int path_len = strlen(archive_path) + 1;
// Write path
fwrite(archive_path, 1, path_len, w->fh);
// Write data
fwrite(data, 1, data_len, w->fh);
w->files[w->len] = (qop_file){
.hash = hash,
.offset = w->size,
.size = (unsigned int)data_len,
.path_len = (unsigned short)path_len,
.flags = QOP_FLAG_NONE
};
w->size += data_len + path_len;
w->len++;
JS_FreeCString(js, path);
return JS_NULL;
}
static JSValue js_writer_finalize(JSContext *js, JSValue self, int argc, JSValue *argv) {
qop_writer *w = js2writer(js, self);
if (!w || !w->fh) return JS_RaiseDisrupt(js, "Invalid QOP writer or already closed");
unsigned int total_size = w->size + 12; // Header size
for (int i = 0; i < w->len; i++) {
write_64(w->files[i].hash, w->fh);
write_32(w->files[i].offset, w->fh);
write_32(w->files[i].size, w->fh);
write_16(w->files[i].path_len, w->fh);
write_16(w->files[i].flags, w->fh);
total_size += 20;
}
// Magic "qopf"
unsigned int magic = (((unsigned int)'q') << 0 | ((unsigned int)'o') << 8 |
((unsigned int)'p') << 16 | ((unsigned int)'f') << 24);
write_32(w->len, w->fh);
write_32(total_size, w->fh);
write_32(magic, w->fh);
fclose(w->fh);
w->fh = NULL;
return JS_NULL;
}
static const JSCFunctionListEntry js_qop_archive_funcs[] = {
JS_CFUNC_DEF("close", 0, js_qop_close),
JS_CFUNC_DEF("list", 0, js_qop_list),
JS_CFUNC_DEF("read", 1, js_qop_read),
JS_CFUNC_DEF("read_ex", 3, js_qop_read_ex),
JS_CFUNC_DEF("stat", 1, js_qop_stat),
JS_CFUNC_DEF("is_directory", 1, js_qop_is_directory),
};
static const JSCFunctionListEntry js_qop_writer_funcs[] = {
JS_CFUNC_DEF("add_file", 2, js_writer_add_file),
JS_CFUNC_DEF("finalize", 0, js_writer_finalize),
};
static const JSCFunctionListEntry js_qop_funcs[] = {
MIST_FUNC_DEF(qop, open, 1),
MIST_FUNC_DEF(qop, write, 1),
JS_PROP_INT32_DEF("FLAG_NONE", QOP_FLAG_NONE, 0),
JS_PROP_INT32_DEF("FLAG_COMPRESSED_ZSTD", QOP_FLAG_COMPRESSED_ZSTD, 0),
JS_PROP_INT32_DEF("FLAG_COMPRESSED_DEFLATE", QOP_FLAG_COMPRESSED_DEFLATE, 0),
JS_PROP_INT32_DEF("FLAG_ENCRYPTED", QOP_FLAG_ENCRYPTED, 0),
};
JSValue js_core_internal_qop_use(JSContext *js) {
JS_FRAME(js);
JS_NewClassID(&js_qop_archive_class_id);
JS_NewClass(js, js_qop_archive_class_id, &js_qop_archive_class);
JS_ROOT(archive_proto, JS_NewObject(js));
JS_SetPropertyFunctionList(js, archive_proto.val, js_qop_archive_funcs, countof(js_qop_archive_funcs));
JS_SetClassProto(js, js_qop_archive_class_id, archive_proto.val);
JS_NewClassID(&js_qop_writer_class_id);
JS_NewClass(js, js_qop_writer_class_id, &js_qop_writer_class);
JS_ROOT(writer_proto, JS_NewObject(js));
JS_SetPropertyFunctionList(js, writer_proto.val, js_qop_writer_funcs, countof(js_qop_writer_funcs));
JS_SetClassProto(js, js_qop_writer_class_id, writer_proto.val);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_qop_funcs, countof(js_qop_funcs));
JS_RETURN(mod.val);
}

File diff suppressed because it is too large Load Diff

View File

@@ -33,26 +33,54 @@ function get_pkg_dir(package_name) {
return shop.get_package_dir(package_name)
}
// Ensure directory exists
function ensure_dir(path) {
if (fd.is_dir(path)) return true
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
// Deep comparison of two values (handles arrays and objects)
function values_equal(a, b) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.is_dir(current)) {
fd.mkdir(current)
var ka = null
var kb = null
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
i = 0
while (i < length(a)) {
if (!values_equal(a[i], b[i])) return false
i = i + 1
}
return true
}
return true
if (is_object(a) && is_object(b)) {
ka = array(a)
kb = array(b)
if (length(ka) != length(kb)) return false
i = 0
while (i < length(ka)) {
if (!values_equal(a[ka[i]], b[ka[i]])) return false
i = i + 1
}
return true
}
return false
}
// Describe a value for error messages
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
if (is_array(val)) return `[array length=${text(length(val))}]`
if (is_object(val)) return `{record keys=${text(length(array(val)))}}`
return "<unknown>"
}
return {
is_valid_package: is_valid_package,
get_current_package_name: get_current_package_name,
get_pkg_dir: get_pkg_dir,
ensure_dir: ensure_dir
ensure_dir: fd.ensure_dir,
values_equal: values_equal,
describe: describe
}

75
internal/time_playdate.c Normal file
View File

@@ -0,0 +1,75 @@
// time_playdate.c - Time module for Playdate
#include "cell.h"
#include "pd_api.h"
// Global Playdate API pointers - defined in main_playdate.c
extern const struct playdate_sys *pd_sys;
/* ---------------------------------------------------------------- *\
Helpers
\* ---------------------------------------------------------------- */
static inline double playdate_now(void)
{
if (pd_sys) {
unsigned int ms = 0;
unsigned int secs = pd_sys->getSecondsSinceEpoch(&ms);
return (double)secs + ms / 1000.0;
}
return 0.0;
}
/* ---------------------------------------------------------------- *\
JS bindings
\* ---------------------------------------------------------------- */
static JSValue
js_time_now(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
return JS_NewFloat64(ctx, playdate_now());
}
static JSValue
js_time_computer_dst(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
// Playdate doesn't provide DST info directly
return JS_NewBool(ctx, 0);
}
static JSValue
js_time_computer_zone(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv)
{
#if TARGET_EXTENSION
if (pd_sys) {
int32_t offset = pd_sys->getTimezoneOffset();
// Playdate returns offset in seconds, convert to hours
return JS_NewFloat64(ctx, (double)offset / 3600.0);
}
#endif
return JS_NewFloat64(ctx, 0.0);
}
/* ---------------------------------------------------------------- *\
registration
\* ---------------------------------------------------------------- */
static const JSCFunctionListEntry js_time_funcs[] = {
JS_CFUNC_DEF("now", 0, js_time_now),
JS_CFUNC_DEF("computer_dst", 0, js_time_computer_dst),
JS_CFUNC_DEF("computer_zone", 0, js_time_computer_zone),
};
JSValue
js_time_use(JSContext *ctx)
{
JSValue obj = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, obj,
js_time_funcs,
sizeof(js_time_funcs) /
sizeof(js_time_funcs[0]));
return obj;
}

36
internal/wildstar.c Normal file
View File

@@ -0,0 +1,36 @@
#include "cell.h"
#include "wildmatch.h"
JSC_CCALL(wildstar_match,
const char *pattern = JS_ToCString(js, argv[0]);
const char *string = JS_ToCString(js, argv[1]);
int flags = 0;
if (argc > 2)
flags = js2number(js, argv[2]);
int result = wildmatch(pattern, string, flags);
JS_FreeCString(js, pattern);
JS_FreeCString(js, string);
return JS_NewBool(js, result == WM_MATCH);
)
static const JSCFunctionListEntry js_wildstar_funcs[] = {
MIST_FUNC_DEF(wildstar, match, 3),
JS_PROP_INT32_DEF("WM_MATCH", WM_MATCH, 0),
JS_PROP_INT32_DEF("WM_NOMATCH", WM_NOMATCH, 0),
JS_PROP_INT32_DEF("WM_NOESCAPE", WM_NOESCAPE, 0),
JS_PROP_INT32_DEF("WM_PATHNAME", WM_PATHNAME, 0),
JS_PROP_INT32_DEF("WM_PERIOD", WM_PERIOD, 0),
JS_PROP_INT32_DEF("WM_LEADING_DIR", WM_LEADING_DIR, 0),
JS_PROP_INT32_DEF("WM_CASEFOLD", WM_CASEFOLD, 0),
JS_PROP_INT32_DEF("WM_WILDSTAR", WM_WILDSTAR, 0),
};
JSValue js_core_internal_wildstar_use(JSContext *js) {
JS_FRAME(js);
JS_ROOT(mod, JS_NewObject(js));
JS_SetPropertyFunctionList(js, mod.val, js_wildstar_funcs, countof(js_wildstar_funcs));
JS_RETURN(mod.val);
}

461
internal/wota.c Normal file
View File

@@ -0,0 +1,461 @@
#define WOTA_IMPLEMENTATION
#include "quickjs-internal.h"
#include "cell.h"
typedef struct ObjectRef {
void *ptr;
struct ObjectRef *next;
} ObjectRef;
typedef struct WotaEncodeContext {
JSContext *ctx;
ObjectRef *visited_stack;
WotaBuffer wb;
int cycle;
JSValue replacer;
} WotaEncodeContext;
static void wota_stack_push (WotaEncodeContext *enc, JSValueConst val) {
(void)enc; (void)val;
/* Cycle detection disabled for performance */
}
static void wota_stack_pop (WotaEncodeContext *enc) {
if (!enc->visited_stack) return;
ObjectRef *top = enc->visited_stack;
enc->visited_stack = top->next;
sys_free (top);
}
static int wota_stack_has (WotaEncodeContext *enc, JSValueConst val) {
(void)enc; (void)val;
return 0;
/* Cycle detection disabled for performance */
}
static void wota_stack_free (WotaEncodeContext *enc) {
while (enc->visited_stack) {
wota_stack_pop (enc);
}
}
static JSValue wota_apply_replacer (WotaEncodeContext *enc, JSValueConst holder, JSValue key, JSValueConst val) {
if (JS_IsNull (enc->replacer)) return JS_DupValue (enc->ctx, val);
JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (enc->ctx, key);
JSValue args[2] = { key_val, JS_DupValue (enc->ctx, val) };
JSValue result = JS_Call (enc->ctx, enc->replacer, holder, 2, args);
JS_FreeValue (enc->ctx, args[0]);
JS_FreeValue (enc->ctx, args[1]);
if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
return result;
}
static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key);
static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder) {
JSContext *ctx = enc->ctx;
/* Root the input value to protect it during property enumeration */
JSGCRef val_ref, keys_ref;
JS_PushGCRef (ctx, &val_ref);
JS_PushGCRef (ctx, &keys_ref);
val_ref.val = JS_DupValue (ctx, val);
keys_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val);
if (JS_IsException (keys_ref.val)) {
wota_write_sym (&enc->wb, WOTA_NULL);
JS_FreeValue (ctx, val_ref.val);
JS_PopGCRef (ctx, &keys_ref);
JS_PopGCRef (ctx, &val_ref);
return;
}
int64_t plen64;
if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
JS_FreeValue (ctx, keys_ref.val);
JS_FreeValue (ctx, val_ref.val);
wota_write_sym (&enc->wb, WOTA_NULL);
JS_PopGCRef (ctx, &keys_ref);
JS_PopGCRef (ctx, &val_ref);
return;
}
uint32_t plen = (uint32_t)plen64;
uint32_t non_function_count = 0;
/* Allocate GC-rooted arrays for props and keys */
JSGCRef *prop_refs = sys_malloc (sizeof (JSGCRef) * plen);
JSGCRef *key_refs = sys_malloc (sizeof (JSGCRef) * plen);
for (uint32_t i = 0; i < plen; i++) {
JS_PushGCRef (ctx, &prop_refs[i]);
JS_PushGCRef (ctx, &key_refs[i]);
prop_refs[i].val = JS_NULL;
key_refs[i].val = JS_NULL;
}
for (uint32_t i = 0; i < plen; i++) {
/* Store key into its GCRef slot immediately so it's rooted before
JS_GetProperty can trigger GC and relocate the string. */
key_refs[i].val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
JSValue prop_val = JS_GetProperty (ctx, val_ref.val, key_refs[i].val);
if (!JS_IsFunction (prop_val)) {
if (i != non_function_count) {
key_refs[non_function_count].val = key_refs[i].val;
key_refs[i].val = JS_NULL;
}
prop_refs[non_function_count].val = prop_val;
non_function_count++;
} else {
JS_FreeValue (ctx, prop_val);
JS_FreeValue (ctx, key_refs[i].val);
key_refs[i].val = JS_NULL;
}
}
JS_FreeValue (ctx, keys_ref.val);
wota_write_record (&enc->wb, non_function_count);
for (uint32_t i = 0; i < non_function_count; i++) {
size_t klen;
const char *prop_name = JS_ToCStringLen (ctx, &klen, key_refs[i].val);
wota_write_text_len (&enc->wb, prop_name ? prop_name : "", prop_name ? klen : 0);
wota_encode_value (enc, prop_refs[i].val, val_ref.val, key_refs[i].val);
JS_FreeCString (ctx, prop_name);
JS_FreeValue (ctx, prop_refs[i].val);
JS_FreeValue (ctx, key_refs[i].val);
}
/* Pop all GC refs in reverse order */
for (int i = plen - 1; i >= 0; i--) {
JS_PopGCRef (ctx, &key_refs[i]);
JS_PopGCRef (ctx, &prop_refs[i]);
}
sys_free (prop_refs);
sys_free (key_refs);
JS_FreeValue (ctx, val_ref.val);
JS_PopGCRef (ctx, &keys_ref);
JS_PopGCRef (ctx, &val_ref);
}
static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key) {
JSContext *ctx = enc->ctx;
JSValue replaced;
if (!JS_IsNull (enc->replacer) && !JS_IsNull (key))
replaced = wota_apply_replacer (enc, holder, key, val);
else
replaced = JS_DupValue (enc->ctx, val);
int tag = JS_VALUE_GET_TAG (replaced);
switch (tag) {
case JS_TAG_INT: {
int32_t d;
JS_ToInt32 (ctx, &d, replaced);
wota_write_int_word (&enc->wb, d);
break;
}
case JS_TAG_FLOAT64: {
double d;
if (JS_ToFloat64 (ctx, &d, replaced) < 0) {
wota_write_sym (&enc->wb, WOTA_NULL);
break;
}
wota_write_float_word (&enc->wb, d);
break;
}
case JS_TAG_STRING: {
size_t plen;
const char *str = JS_ToCStringLen (ctx, &plen, replaced);
wota_write_text_len (&enc->wb, str ? str : "", str ? plen : 0);
JS_FreeCString (ctx, str);
break;
}
case JS_TAG_BOOL:
wota_write_sym (&enc->wb, JS_VALUE_GET_BOOL (replaced) ? WOTA_TRUE : WOTA_FALSE);
break;
case JS_TAG_NULL:
wota_write_sym (&enc->wb, WOTA_NULL);
break;
case JS_TAG_PTR: {
if (JS_IsText (replaced)) {
size_t plen;
const char *str = JS_ToCStringLen (ctx, &plen, replaced);
wota_write_text_len (&enc->wb, str ? str : "", str ? plen : 0);
JS_FreeCString (ctx, str);
break;
}
if (js_is_blob (ctx, replaced)) {
size_t buf_len;
void *buf_data = js_get_blob_data (ctx, &buf_len, replaced);
if (buf_data == (void *)-1) {
JS_FreeValue (ctx, replaced);
return;
}
if (buf_len == 0) {
wota_write_blob (&enc->wb, 0, "");
} else {
wota_write_blob (&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data);
}
break;
}
if (JS_IsArray (replaced)) {
if (wota_stack_has (enc, replaced)) {
enc->cycle = 1;
break;
}
wota_stack_push (enc, replaced);
int64_t arr_len;
JS_GetLength (ctx, replaced, &arr_len);
wota_write_array (&enc->wb, arr_len);
for (int64_t i = 0; i < arr_len; i++) {
JSValue elem_val = JS_GetPropertyNumber (ctx, replaced, i);
wota_encode_value (enc, elem_val, replaced, JS_NewInt32 (ctx, (int32_t)i));
JS_FreeValue (ctx, elem_val);
}
wota_stack_pop (enc);
break;
}
JSValue adata = JS_NULL;
if (!JS_IsNull (ctx->actor_sym)) {
int has = JS_HasPropertyKey (ctx, replaced, ctx->actor_sym);
if (has > 0) adata = JS_GetPropertyKey (ctx, replaced, ctx->actor_sym);
}
if (!JS_IsNull (adata)) {
wota_write_sym (&enc->wb, WOTA_PRIVATE);
wota_encode_value (enc, adata, replaced, JS_NULL);
JS_FreeValue (ctx, adata);
break;
}
JS_FreeValue (ctx, adata);
if (wota_stack_has (enc, replaced)) {
enc->cycle = 1;
break;
}
wota_stack_push (enc, replaced);
JSValue to_json = JS_GetPropertyStr (ctx, replaced, "toJSON");
if (JS_IsFunction (to_json)) {
JSValue result = JS_Call (ctx, to_json, replaced, 0, NULL);
JS_FreeValue (ctx, to_json);
if (!JS_IsException (result)) {
wota_encode_value (enc, result, holder, key);
JS_FreeValue (ctx, result);
} else
wota_write_sym (&enc->wb, WOTA_NULL);
wota_stack_pop (enc);
break;
}
JS_FreeValue (ctx, to_json);
encode_object_properties (enc, replaced, holder);
wota_stack_pop (enc);
break;
}
default:
wota_write_sym (&enc->wb, WOTA_NULL);
break;
}
JS_FreeValue (ctx, replaced);
}
static char *decode_wota_value (JSContext *ctx, char *data_ptr, JSValue *out_val, JSValue holder, JSValue key, JSValue reviver) {
uint64_t first_word = *(uint64_t *)data_ptr;
int type = (int)(first_word & 0xffU);
switch (type) {
case WOTA_INT: {
long long val;
data_ptr = wota_read_int (&val, data_ptr);
*out_val = JS_NewInt64 (ctx, val);
break;
}
case WOTA_FLOAT: {
double d;
data_ptr = wota_read_float (&d, data_ptr);
*out_val = JS_NewFloat64 (ctx, d);
break;
}
case WOTA_SYM: {
int scode;
data_ptr = wota_read_sym (&scode, data_ptr);
if (scode == WOTA_PRIVATE) {
JSGCRef inner_ref, obj_ref2;
JS_PushGCRef (ctx, &inner_ref);
JS_PushGCRef (ctx, &obj_ref2);
inner_ref.val = JS_NULL;
data_ptr = decode_wota_value (ctx, data_ptr, &inner_ref.val, holder, JS_NULL, reviver);
obj_ref2.val = JS_NewObject (ctx);
if (!JS_IsNull (ctx->actor_sym))
JS_SetPropertyKey (ctx, obj_ref2.val, ctx->actor_sym, inner_ref.val);
JS_CellStone (ctx, obj_ref2.val);
*out_val = obj_ref2.val;
JS_PopGCRef (ctx, &obj_ref2);
JS_PopGCRef (ctx, &inner_ref);
} else if (scode == WOTA_NULL) *out_val = JS_NULL;
else if (scode == WOTA_FALSE) *out_val = JS_NewBool (ctx, 0);
else if (scode == WOTA_TRUE) *out_val = JS_NewBool (ctx, 1);
else *out_val = JS_NULL;
break;
}
case WOTA_BLOB: {
long long blen;
char *bdata = NULL;
data_ptr = wota_read_blob (&blen, &bdata, data_ptr);
*out_val = bdata ? js_new_blob_stoned_copy (ctx, (uint8_t *)bdata, (size_t)blen) : js_new_blob_stoned_copy (ctx, NULL, 0);
if (bdata) sys_free (bdata);
break;
}
case WOTA_TEXT: {
char *utf8 = NULL;
data_ptr = wota_read_text (&utf8, data_ptr);
*out_val = JS_NewString (ctx, utf8 ? utf8 : "");
if (utf8) sys_free (utf8);
break;
}
case WOTA_ARR: {
long long c;
data_ptr = wota_read_array (&c, data_ptr);
JSGCRef arr_ref;
JS_PushGCRef (ctx, &arr_ref);
arr_ref.val = JS_NewArrayLen (ctx, c);
for (long long i = 0; i < c; i++) {
JSGCRef elem_ref;
JS_PushGCRef (ctx, &elem_ref);
elem_ref.val = JS_NULL;
JSValue idx_key = JS_NewInt32 (ctx, (int32_t)i);
data_ptr = decode_wota_value (ctx, data_ptr, &elem_ref.val, arr_ref.val, idx_key, reviver);
JS_SetPropertyNumber (ctx, arr_ref.val, i, elem_ref.val);
JS_PopGCRef (ctx, &elem_ref);
}
*out_val = arr_ref.val;
JS_PopGCRef (ctx, &arr_ref);
break;
}
case WOTA_REC: {
long long c;
data_ptr = wota_read_record (&c, data_ptr);
JSGCRef obj_ref;
JS_PushGCRef (ctx, &obj_ref);
obj_ref.val = JS_NewObject (ctx);
for (long long i = 0; i < c; i++) {
char *tkey = NULL;
size_t key_len;
data_ptr = wota_read_text_len (&key_len, &tkey, data_ptr);
if (!tkey) continue;
JSGCRef prop_key_ref, sub_val_ref;
JS_PushGCRef (ctx, &prop_key_ref);
JS_PushGCRef (ctx, &sub_val_ref);
prop_key_ref.val = JS_NewStringLen (ctx, tkey, key_len);
sub_val_ref.val = JS_NULL;
data_ptr = decode_wota_value (ctx, data_ptr, &sub_val_ref.val, obj_ref.val, prop_key_ref.val, reviver);
JS_SetPropertyStr (ctx, obj_ref.val, tkey, sub_val_ref.val);
JS_PopGCRef (ctx, &sub_val_ref);
JS_PopGCRef (ctx, &prop_key_ref);
sys_free (tkey);
}
*out_val = obj_ref.val;
JS_PopGCRef (ctx, &obj_ref);
break;
}
default:
data_ptr += 8;
*out_val = JS_NULL;
break;
}
if (!JS_IsNull (reviver)) {
JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (ctx, key);
JSValue args[2] = { key_val, JS_DupValue (ctx, *out_val) };
JSValue revived = JS_Call (ctx, reviver, holder, 2, args);
JS_FreeValue (ctx, args[0]);
JS_FreeValue (ctx, args[1]);
if (!JS_IsException (revived)) {
JS_FreeValue (ctx, *out_val);
*out_val = revived;
} else
JS_FreeValue (ctx, revived);
}
return data_ptr;
}
void *value2wota (JSContext *ctx, JSValue v, JSValue replacer, size_t *bytes) {
JSGCRef val_ref, rep_ref;
JS_PushGCRef (ctx, &val_ref);
JS_PushGCRef (ctx, &rep_ref);
val_ref.val = v;
rep_ref.val = replacer;
WotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visited_stack = NULL;
enc->cycle = 0;
enc->replacer = rep_ref.val;
wota_buffer_init (&enc->wb, 16);
wota_encode_value (enc, val_ref.val, JS_NULL, JS_NULL);
if (enc->cycle) {
wota_stack_free (enc);
wota_buffer_free (&enc->wb);
JS_PopGCRef (ctx, &rep_ref);
JS_PopGCRef (ctx, &val_ref);
return NULL;
}
wota_stack_free (enc);
size_t total_bytes = enc->wb.size * sizeof (uint64_t);
void *wota = sys_realloc (enc->wb.data, total_bytes);
if (bytes) *bytes = total_bytes;
JS_PopGCRef (ctx, &rep_ref);
JS_PopGCRef (ctx, &val_ref);
return wota;
}
JSValue wota2value (JSContext *ctx, void *wota) {
JSGCRef holder_ref, result_ref;
JS_PushGCRef (ctx, &holder_ref);
JS_PushGCRef (ctx, &result_ref);
result_ref.val = JS_NULL;
holder_ref.val = JS_NewObject (ctx);
decode_wota_value (ctx, wota, &result_ref.val, holder_ref.val, JS_NULL, JS_NULL);
JSValue result = result_ref.val;
JS_PopGCRef (ctx, &result_ref);
JS_PopGCRef (ctx, &holder_ref);
return result;
}
static JSValue js_wota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_RaiseDisrupt (ctx, "wota.encode requires at least 1 argument");
size_t total_bytes;
void *wota = value2wota (ctx, argv[0], JS_IsFunction (argv[1]) ? argv[1] : JS_NULL, &total_bytes);
JSValue ret = js_new_blob_stoned_copy (ctx, wota, total_bytes);
sys_free (wota);
return ret;
}
static JSValue js_wota_decode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_NULL;
size_t len;
uint8_t *buf = js_get_blob_data (ctx, &len, argv[0]);
if (buf == (uint8_t *)-1) return JS_EXCEPTION;
if (!buf || len == 0) return JS_RaiseDisrupt (ctx, "No blob data present");
JSValue reviver = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
char *data_ptr = (char *)buf;
JSGCRef result_ref, holder_ref, empty_key_ref;
JS_PushGCRef (ctx, &result_ref);
JS_PushGCRef (ctx, &holder_ref);
JS_PushGCRef (ctx, &empty_key_ref);
result_ref.val = JS_NULL;
holder_ref.val = JS_NewObject (ctx);
empty_key_ref.val = JS_NewString (ctx, "");
decode_wota_value (ctx, data_ptr, &result_ref.val, holder_ref.val, empty_key_ref.val, reviver);
JSValue result = result_ref.val;
JS_PopGCRef (ctx, &empty_key_ref);
JS_PopGCRef (ctx, &holder_ref);
JS_PopGCRef (ctx, &result_ref);
return result;
}
static const JSCFunctionListEntry js_wota_funcs[] = {
JS_CFUNC_DEF ("encode", 2, js_wota_encode),
JS_CFUNC_DEF ("decode", 2, js_wota_decode),
};
JSValue js_core_internal_wota_use (JSContext *ctx) {
JSGCRef exports_ref;
JS_PushGCRef (ctx, &exports_ref);
exports_ref.val = JS_NewObject (ctx);
JS_SetPropertyFunctionList (ctx, exports_ref.val, js_wota_funcs, sizeof (js_wota_funcs) / sizeof (js_wota_funcs[0]));
JSValue result = exports_ref.val;
JS_PopGCRef (ctx, &exports_ref);
return result;
}