From b586df63ad1625dfb0e4a6978f9604366665de33 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Mon, 29 Dec 2025 14:28:54 -0600 Subject: [PATCH] move internals to pure C --- internal/array.cm | 303 ---- internal/engine.cm | 91 +- internal/fn.cm | 22 - internal/number.cm | 169 --- internal/object.cm | 93 -- internal/text.c | 306 ---- internal/text.cm | 602 -------- meson.build | 2 - source/quickjs.c | 3315 +++++++++++++++++++++++++++++++++++++++++++- test.ce | 2 +- time.cm | 10 +- 11 files changed, 3310 insertions(+), 1605 deletions(-) delete mode 100644 internal/array.cm delete mode 100644 internal/fn.cm delete mode 100644 internal/number.cm delete mode 100644 internal/object.cm delete mode 100644 internal/text.c delete mode 100644 internal/text.cm diff --git a/internal/array.cm b/internal/array.cm deleted file mode 100644 index 7327e5f6..00000000 --- a/internal/array.cm +++ /dev/null @@ -1,303 +0,0 @@ -/* array.cm - array creation and manipulation utilities */ - -var _isArray = Array.isArray -var _slice = Array.prototype.slice -var _push = Array.prototype.push -var _sort = Array.prototype.sort -var _keys = Object.keys -var _from = Array.from - -function array(arg, arg2, arg3, arg4) { - // array(number) - create array of size with nulls - // array(number, initial_value) - create array with initial values - if (typeof arg == 'number') { - if (arg < 0) return null - var len = number.floor(arg) - var result = [] - - if (arg2 == null) { - result.length = 100 - } else if (typeof arg2 == 'function') { - var arity = arg2.length - for (var i = 0; i < len; i++) { - result[i] = arity >= 1 ? arg2(i) : arg2() - } - } else { - for (var i = 0; i < len; i++) result[i] = arg2 - } - - return result - } - - // array(array) - copy - // array(array, function, reverse, exit) - map - // array(array, another_array) - concat - // array(array, from, to) - slice - if (_isArray(arg)) { - if (arg2 == null) { - // Copy - return _slice.call(arg) - } - - if (typeof arg2 == 'function') { - // Map - var fn = arg2 - var reverse = arg3 == true - var exit = arg4 - var result = [] - - if (reverse) { - for (var i = arg.length - 1; i >= 0; i--) { - var val = fn(arg[i], i) - if (exit != null && val == exit) break - result[i] = val - } - } else { - for (var i = 0; i < arg.length; i++) { - var val = fn(arg[i], i) - if (exit != null && val == exit) break - _push.call(result, val) - } - } - - return result - } - - if (_isArray(arg2)) { - // Concat - var result = _slice.call(arg) - for (var i = 0; i < arg2.length; i++) { - _push.call(result, arg2[i]) - } - return result - } - - if (typeof arg2 == 'number') { - // Slice - var from = arg2 - var to = arg3 - var len = arg.length - - if (from < 0) from += len - if (to == null) to = len - if (to < 0) to += len - - if (from < 0 || from > to || to > len) return null - - return _slice.call(arg, from, to) - } - - return null - } - - // array(object) - keys - if (typeof arg == 'object' && arg != null && !_isArray(arg)) { - if (arg instanceof Set) { - return _from(arg) - } - return _keys(arg) - } - - // array(text) - split into grapheme clusters - // array(text, separator) - split by separator - // array(text, length) - dice into chunks - if (typeof arg == 'string') { - if (arg2 == null) { - // Split into grapheme clusters (simplified: split into characters) - var result = [] - for (var i = 0; i < arg.length; i++) { - _push.call(result, arg[i]) - } - return result - } - - if (typeof arg2 == 'string') { - // Split by separator - return arg.split(arg2) - } - - if (typeof arg2 == 'number') { - // Dice into chunks - var len = number.floor(arg2) - if (len <= 0) return null - var result = [] - for (var i = 0; i < arg.length; i += len) { - _push.call(result, arg.substring(i, i + len)) - } - return result - } - - return null - } - - return null -} - -array.reduce = function(arr, fn, initial, reverse) { - if (!_isArray(arr)) return null - if (typeof fn != 'function') return null - - var len = arr.length - - if (initial == null) { - if (len == 0) return null - if (len == 1) return arr[0] - - if (reverse == true) { - var acc = arr[len - 1] - for (var i = len - 2; i >= 0; i--) { - acc = fn(acc, arr[i]) - } - return acc - } else { - var acc = arr[0] - for (var i = 1; i < len; i++) { - acc = fn(acc, arr[i]) - } - return acc - } - } else { - if (len == 0) return initial - - if (reverse == true) { - var acc = initial - for (var i = len - 1; i >= 0; i--) { - acc = fn(acc, arr[i]) - } - return acc - } else { - var acc = initial - for (var i = 0; i < len; i++) { - acc = fn(acc, arr[i]) - } - return acc - } - } -} - -array.for = function(arr, fn, reverse, exit) { - if (!_isArray(arr)) return null - if (arr.length == 0) return null - if (typeof fn != 'function') return null - - if (reverse == true) { - for (var i = arr.length - 1; i >= 0; i--) { - var result = fn(arr[i], i) - if (exit != null && result == exit) return exit - } - } else { - for (var i = 0; i < arr.length; i++) { - var result = fn(arr[i], i) - if (exit != null && result == exit) return exit - } - } - - return null -} - -array.find = function(arr, fn, reverse, from) { - if (!_isArray(arr)) return null - - var len = arr.length - - if (typeof fn != 'function') { - // Compare exactly - var target = fn - if (reverse == true) { - var start = from != null ? from : len - 1 - for (var i = start; i >= 0; i--) { - if (arr[i] == target) return i - } - } else { - var start = from != null ? from : 0 - for (var i = start; i < len; i++) { - if (arr[i] == target) return i - } - } - return null - } - - if (reverse == true) { - var start = from != null ? from : len - 1 - for (var i = start; i >= 0; i--) { - if (fn(arr[i], i) == true) return i - } - } else { - var start = from != null ? from : 0 - for (var i = start; i < len; i++) { - if (fn(arr[i], i) == true) return i - } - } - - return null -} - -array.filter = function(arr, fn) { - if (!_isArray(arr)) return null - if (typeof fn != 'function') return null - - var result = [] - - for (var i = 0; i < arr.length; i++) { - var val = fn(arr[i], i) - if (val == true) { - _push.call(result, arr[i]) - } else if (val != false) { - return null - } - } - - return result -} - -array.sort = function(arr, select) { - if (!_isArray(arr)) return null - - var result = _slice.call(arr) - var keys = [] - - // Extract keys - for (var i = 0; i < result.length; i++) { - var key - if (select == null) { - key = result[i] - } else if (typeof select == 'string' || typeof select == 'number') { - key = result[i][select] - } else if (_isArray(select)) { - key = select[i] - } else { - return null - } - - if (typeof key != 'number' && typeof key != 'string') return null - keys[i] = key - } - - // Check all keys are same type - if (keys.length > 0) { - var keyType = typeof keys[0] - for (var i = 1; i < keys.length; i++) { - if (typeof keys[i] != keyType) return null - } - } - - // Create index array and sort - var indices = [] - for (var i = 0; i < result.length; i++) indices[i] = i - - // Stable sort using indices - _sort.call(indices, function(a, b) { - if (keys[a] < keys[b]) return -1 - if (keys[a] > keys[b]) return 1 - return a - b // stable - }) - - var sorted = [] - for (var i = 0; i < indices.length; i++) { - sorted[i] = result[indices[i]] - } - - return sorted -} - -return array diff --git a/internal/engine.cm b/internal/engine.cm index bf09ba22..9107b249 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -32,7 +32,7 @@ globalThis.meme = function(obj, ...mixins) { var result = _ObjectCreate(obj) array.for(mixins, mix => { - if (isa(mix, object)) { + if (is_object(mix)) { for (var key in mix) result[key] = mix[key] } @@ -102,100 +102,34 @@ function use_core(path) { } var blob = use_core('blob') -var blob_stone = blob.prototype.stone -var blob_stonep = blob.prototype.stonep; -delete blob.prototype.stone; -delete blob.prototype.stonep; // Capture Object and Array methods before they're deleted var _Object = Object var _ObjectKeys = Object.keys -var _ObjectFreeze = Object.freeze var _ObjectIsFrozen = Object.isFrozen var _ObjectDefineProperty = Object.defineProperty var _ObjectGetPrototypeOf = Object.getPrototypeOf var _ObjectCreate = Object.create -var _ArrayIsArray = Array.isArray Object.prototype.toString = function() { return json.encode(this) } -function deepFreeze(object) { - if (object instanceof blob) - blob_stone.call(object); - - var propNames = _ObjectKeys(object); - - for (var name of propNames) { - var value = object[name]; - - if ((value && typeof value == "object") || typeof value == "function") - deepFreeze(value); - } - - return _ObjectFreeze(object); -} - globalThis.actor = function() { } -globalThis.stone = deepFreeze -stone.p = function(object) -{ - if (object instanceof blob) - return blob_stonep.call(object) - - return _ObjectIsFrozen(object) -} - var actor_mod = use_core('actor') var wota = use_core('wota') var nota = use_core('nota') -// Load internal modules for global functions -globalThis.text = use_core('internal/text') -globalThis.number = use_core('internal/number') -globalThis.array = use_core('internal/array') -globalThis.object = use_core('internal/object') -globalThis.fn = use_core('internal/fn') - // Global utility functions (use already-captured references) -var _isArray = _ArrayIsArray var _keys = _ObjectKeys var _getPrototypeOf = _ObjectGetPrototypeOf var _create = _ObjectCreate -globalThis.length = function(value) { - if (value == null) return null - - // For functions, return arity - if (typeof value == 'function') return value.length - - // For strings, return codepoint count - if (typeof value == 'string') return value.length - - // For arrays, return element count - if (_isArray(value)) return value.length - - // For blobs, return bit count - if (value instanceof blob && typeof value.length == 'number') return value.length - - // For records with length field - if (typeof value == 'object' && value != null) { - if ('length' in value) { - var len = value.length - if (typeof len == 'function') return len.call(value) - if (typeof len == 'number') return len - } - } - - return null -} - globalThis.reverse = function(value) { if (_isArray(value)) { var result = [] @@ -229,14 +163,14 @@ globalThis.isa = function(value, master) { // isa(value, function) - check if function.prototype is in chain if (typeof master == 'function') { // Special type checks - if (master == stone) return _ObjectIsFrozen(value) || typeof value != 'object' - if (master == number) return typeof value == 'number' - if (master == text) return typeof value == 'string' - if (master == logical) return typeof value == 'boolean' - if (master == array) return _isArray(value) - if (master == object) return typeof value == 'object' && value != null && !_isArray(value) - if (master == fn) return typeof value == 'function' - if (master == actor) return isa(value, object) && value[ACTORDATA] + if (master == stone) return is_stone(value) + if (master == number) return is_number(value) + if (master == text) return is_text(value) + if (master == logical) return is_logical(value) + if (master == array) return is_array(value) + if (master == object) return is_object(value) + if (master == fn) return is_function(value) + if (master == actor) return is_object(value) && value[ACTORDATA] // Check prototype chain if (master.prototype) { @@ -413,8 +347,6 @@ globalThis.parallel = pronto.parallel globalThis.race = pronto.race globalThis.sequence = pronto.sequence - - $_.time_limit = function(requestor, seconds) { if (!pronto.is_requestor(requestor)) @@ -983,12 +915,9 @@ if (!locator) // Store references we need internally before deleting var _Array = Array var _String = String -var _Number = Number -var _Boolean = Boolean var _Math = Math var _Function = Function -var _Error = Error var _JSON = JSON // juicing these before Math is gone @@ -1043,7 +972,7 @@ delete globalThis.unescape delete globalThis.Intl delete globalThis.RegExp -_ObjectFreeze(globalThis) +stone(globalThis) $_.clock(_ => { // Get capabilities for the main program diff --git a/internal/fn.cm b/internal/fn.cm deleted file mode 100644 index dfa76273..00000000 --- a/internal/fn.cm +++ /dev/null @@ -1,22 +0,0 @@ -/* fn.cm - function utilities */ - -var _apply = Function.prototype.apply -var _isArray = Array.isArray - -var fn = {} - -fn.apply = function(func, args) { - if (typeof func != 'function') return func - - if (!_isArray(args)) { - args = [args] - } - - if (args.length > func.length) { - throw new Error("fn.apply: too many arguments") - } - - return _apply.call(func, null, args) -} - -return fn diff --git a/internal/number.cm b/internal/number.cm deleted file mode 100644 index 6e03266c..00000000 --- a/internal/number.cm +++ /dev/null @@ -1,169 +0,0 @@ -/* number.cm - number conversion and math utilities */ -var _floor = Math.floor -var _ceil = Math.ceil -var _round = Math.round -var _abs = Math.abs -var _trunc = Math.trunc -var _min = Math.min -var _max = Math.max -var _pow = Math.pow -var _parseInt = parseInt -var _parseFloat = parseFloat - -function number(val, format) { - if (val == true) return 1 - if (val == false) return 0 - - if (typeof val == 'number') return val - - if (typeof val == 'string') { - if (typeof format == 'number') { - // radix conversion - if (format < 2 || format > 36) return null - var result = _parseInt(val, format) - if (isNaN(result)) return null - return result - } - - if (typeof format == 'string') { - return parse_formatted(val, format) - } - - // default: parse as decimal - var result = _parseFloat(val) - if (!isa(result, number)) return null - return result - } - - return null -} - -function parse_formatted(str, format) { - if (!format || format == "" || format == "n") { - var result = _parseFloat(str) - if (isNaN(result)) return null - return result - } - - switch (format) { - case "u": // underbar separator - str = str.split('_').join('') - break - case "d": // comma separator - str = str.split(',').join('') - break - case "s": // space separator - str = str.split(' ').join('') - break - case "v": // European style: period separator, comma decimal - str = str.split('.').join('') - str = str.replace(',', '.') - break - case "l": // locale - treat like 'd' for now - str = str.split(',').join('') - break - case "i": // integer with underbar - str = str.split('_').join('') - break - case "b": // binary - return _parseInt(str, 2) - case "o": // octal - return _parseInt(str, 8) - case "h": // hex - return _parseInt(str, 16) - case "t": // base32 - return _parseInt(str, 32) - case "j": // JavaScript style prefix - if (str.startsWith('0x') || str.startsWith('0X')) - return _parseInt(str.slice(2), 16) - if (str.startsWith('0o') || str.startsWith('0O')) - return _parseInt(str.slice(2), 8) - if (str.startsWith('0b') || str.startsWith('0B')) - return _parseInt(str.slice(2), 2) - return _parseFloat(str) - default: - return null - } - - var result = _parseFloat(str) - if (isNaN(result)) return null - return result -} - -number.whole = function(n) { - if (typeof n != 'number') return null - return _trunc(n) -} - -number.fraction = function(n) { - if (typeof n != 'number') return null - return n - _trunc(n) -} - -number.floor = function(n, place) { - if (typeof n != 'number') return null - if (place == null || place == 0) return _floor(n) - var mult = _pow(10, place) - return _floor(n * mult) / mult -} - -number.ceiling = function(n, place) { - if (typeof n != 'number') return null - if (place == null || place == 0) return _ceil(n) - var mult = _pow(10, place) - return _ceil(n * mult) / mult -} - -number.abs = function(n) { - if (typeof n != 'number') return null - return _abs(n) -} - -number.round = function(n, place) { - if (typeof n != 'number') return null - if (place == null || place == 0) return _round(n) - var mult = _pow(10, place) - return _round(n * mult) / mult -} - -number.sign = function(n) { - if (typeof n != 'number') return null - if (n < 0) return -1 - if (n > 0) return 1 - return 0 -} - -number.trunc = function(n, place) { - if (typeof n != 'number') return null - if (place == null || place == 0) return _trunc(n) - var mult = _pow(10, place) - return _trunc(n * mult) / mult -} - -number.min = function(...vals) { - if (vals.length == 0) return null - var result = vals[0] - for (var i = 1; i < vals.length; i++) { - if (typeof vals[i] != 'number') return null - if (vals[i] < result) result = vals[i] - } - return result -} - -number.max = function(...vals) { - if (vals.length == 0) return null - var result = vals[0] - for (var i = 1; i < vals.length; i++) { - if (typeof vals[i] != 'number') return null - if (vals[i] > result) result = vals[i] - } - return result -} - -number.remainder = function(dividend, divisor) { - if (typeof dividend != 'number' || typeof divisor != 'number') return null - if (divisor == 0) return null - return dividend - (_trunc(dividend / divisor) * divisor) -} - -return number diff --git a/internal/object.cm b/internal/object.cm deleted file mode 100644 index c6deb009..00000000 --- a/internal/object.cm +++ /dev/null @@ -1,93 +0,0 @@ -/* object.cm - object creation and manipulation utilities */ - -var _keys = array -var _create = meme -var _assign = Object.assign -var _isArray = function(val) { return isa(val, array) } -var _values = Object.values - -function object(arg, arg2) { - // object(object) - shallow mutable copy - if (isa(arg, object) && arg2 == null) { - var result = {} - var keys = _keys(arg) - for (var i = 0; i < keys.length; i++) { - result[keys[i]] = arg[keys[i]] - } - return result - } - - // object(object, another_object) - combine - if (isa(arg, object) && isa(arg2, object)) { - var result = {} - var keys = _keys(arg) - for (var i = 0; i < keys.length; i++) { - result[keys[i]] = arg[keys[i]] - } - keys = _keys(arg2) - for (var i = 0; i < keys.length; i++) { - result[keys[i]] = arg2[keys[i]] - } - return result - } - - // object(object, array_of_keys) - select - if (isa(arg, object) && _isArray(arg2)) { - var result = {} - for (var i = 0; i < arg2.length; i++) { - var key = arg2[i] - if (typeof key == 'string' && key in arg) { - result[key] = arg[key] - } - } - return result - } - - // object(array_of_keys) - set with true values - if (_isArray(arg) && arg2 == null) { - var result = {} - for (var i = 0; i < arg.length; i++) { - var key = arg[i] - if (typeof key == 'string') { - result[key] = true - } - } - return result - } - - // object(array_of_keys, value) - value set - // object(array_of_keys, function) - functional value set - if (_isArray(arg) && arg2 != null) { - var result = {} - if (typeof arg2 == 'function') { - for (var i = 0; i < arg.length; i++) { - var key = arg[i] - if (typeof key == 'string') { - result[key] = arg2(key) - } - } - } else { - for (var i = 0; i < arg.length; i++) { - var key = arg[i] - if (typeof key == 'string') { - result[key] = arg2 - } - } - } - return result - } - - return null -} - -object.values = function(obj) -{ - return _values(obj) -} - -object.assign = function(obj, ...args) -{ - return _assign(obj, ...args) -} - -return object diff --git a/internal/text.c b/internal/text.c deleted file mode 100644 index 7318b39f..00000000 --- a/internal/text.c +++ /dev/null @@ -1,306 +0,0 @@ -#include "cell.h" -#include -#include - -JSC_CCALL(text_blob_to_hex, - size_t blob_len; - void *blob_data = js_get_blob_data(js, &blob_len, argv[0]); - if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob"); - uint8_t *bytes = (uint8_t *)blob_data; - size_t hex_len = blob_len * 2; - char *hex_str = malloc(hex_len + 1); - if (!hex_str) return JS_ThrowOutOfMemory(js); - static const char hex_digits[] = "0123456789ABCDEF"; - for (size_t i = 0; i < blob_len; ++i) { - hex_str[i * 2] = hex_digits[(bytes[i] >> 4) & 0xF]; - hex_str[i * 2 + 1] = hex_digits[bytes[i] & 0xF]; - } - hex_str[hex_len] = '\0'; - JSValue val = JS_NewString(js, hex_str); - free(hex_str); - return val; -) - -JSC_CCALL(text_blob_to_base32, - size_t blob_len; - void *blob_data = js_get_blob_data(js, &blob_len, argv[0]); - if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob"); - uint8_t *bytes = (uint8_t *)blob_data; - static const char b32_digits[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - - // Calculate exact output length needed - size_t groups = (blob_len + 4) / 5; // Round up to next group of 5 - size_t b32_len = groups * 8; - char *b32_str = malloc(b32_len + 1); - if (!b32_str) return JS_ThrowOutOfMemory(js); - - size_t in_idx = 0; - size_t out_idx = 0; - - while (in_idx < blob_len) { - // Read up to 5 bytes into a 40-bit buffer - uint64_t buf = 0; - int bytes_to_read = (blob_len - in_idx < 5) ? (blob_len - in_idx) : 5; - - for (int i = 0; i < bytes_to_read; ++i) { - buf = (buf << 8) | bytes[in_idx++]; - } - - // Pad buffer to 40 bits if we read fewer than 5 bytes - buf <<= 8 * (5 - bytes_to_read); - - // Extract 8 groups of 5 bits each - for (int i = 0; i < 8; ++i) { - b32_str[out_idx++] = b32_digits[(buf >> (35 - i * 5)) & 0x1F]; - } - } - - // Add padding if necessary - if (blob_len % 5 != 0) { - static const int pad_count[] = {0, 6, 4, 3, 1}; // padding for 0,1,2,3,4 bytes - int padding = pad_count[blob_len % 5]; - for (int i = 0; i < padding; ++i) { - b32_str[b32_len - 1 - i] = '='; - } - } - - b32_str[b32_len] = '\0'; - JSValue val = JS_NewString(js, b32_str); - free(b32_str); - return val; -) - -static int base32_char_to_val(char c) { - if (c >= 'A' && c <= 'Z') return c - 'A'; - if (c >= 'a' && c <= 'z') return c - 'a'; - if (c >= '2' && c <= '7') return c - '2' + 26; - return -1; -} - -JSC_CCALL(text_base32_to_blob, - const char *str = JS_ToCString(js, argv[0]); - if (!str) return JS_ThrowTypeError(js, "Expected string"); - size_t str_len = strlen(str); - if (str_len == 0) { - JS_FreeCString(js, str); - return JS_ThrowTypeError(js, "Empty base32 string"); - } - - // Remove padding to get effective length - size_t effective_len = str_len; - while (effective_len > 0 && str[effective_len - 1] == '=') { - effective_len--; - } - - // Calculate output length: each group of 8 base32 chars -> 5 bytes - size_t output_len = (effective_len * 5) / 8; - uint8_t *output = malloc(output_len); - if (!output) { - JS_FreeCString(js, str); - return JS_ThrowOutOfMemory(js); - } - - size_t in_idx = 0; - size_t out_idx = 0; - - // Process in groups of 8 characters (40 bits -> 5 bytes) - while (in_idx < effective_len) { - uint64_t buf = 0; - int chars_to_read = (effective_len - in_idx < 8) ? (effective_len - in_idx) : 8; - - // Read up to 8 base32 characters into buffer - for (int i = 0; i < chars_to_read; ++i) { - int val = base32_char_to_val(str[in_idx++]); - if (val < 0) { - free(output); - JS_FreeCString(js, str); - return JS_ThrowTypeError(js, "Invalid base32 character"); - } - buf = (buf << 5) | val; - } - - // Calculate how many bytes we can extract - int bytes_to_extract = (chars_to_read * 5) / 8; - - // Shift buffer to align the most significant bits - buf <<= (40 - chars_to_read * 5); - - // Extract bytes from most significant to least significant - for (int i = 0; i < bytes_to_extract && out_idx < output_len; ++i) { - output[out_idx++] = (buf >> (32 - i * 8)) & 0xFF; - } - } - - JSValue val = js_new_blob_stoned_copy(js, output, output_len); - free(output); - JS_FreeCString(js, str); - return val; -) - -static int base64_char_to_val_standard(char c) { - if (c >= 'A' && c <= 'Z') return c - 'A'; - if (c >= 'a' && c <= 'z') return c - 'a' + 26; - if (c >= '0' && c <= '9') return c - '0' + 52; - if (c == '+') return 62; - if (c == '/') return 63; - return -1; -} -static int base64_char_to_val_url(char c) { - if (c >= 'A' && c <= 'Z') return c - 'A'; - if (c >= 'a' && c <= 'z') return c - 'a' + 26; - if (c >= '0' && c <= '9') return c - '0' + 52; - if (c == '-') return 62; - if (c == '_') return 63; - return -1; -} - -/*─── blob → Base64 (standard, with ‘+’ and ‘/’, padded) ───────────────────*/ -JSC_CCALL(text_blob_to_base64, - size_t blob_len; - void *blob_data = js_get_blob_data(js, &blob_len, argv[0]); - if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob"); - const uint8_t *bytes = blob_data; - static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; - size_t out_len = ((blob_len + 2) / 3) * 4; - char *out = malloc(out_len + 1); - if (!out) return JS_ThrowOutOfMemory(js); - - size_t in_i = 0, out_i = 0; - while (in_i < blob_len) { - uint32_t buf = 0; - int to_read = (blob_len - in_i < 3 ? blob_len - in_i : 3); - for (int j = 0; j < to_read; ++j) { - buf = (buf << 8) | bytes[in_i++]; - } - buf <<= 8 * (3 - to_read); - out[out_i++] = b64[(buf >> 18) & 0x3F]; - out[out_i++] = b64[(buf >> 12) & 0x3F]; - out[out_i++] = (to_read > 1 ? b64[(buf >> 6) & 0x3F] : '='); - out[out_i++] = (to_read > 2 ? b64[ buf & 0x3F] : '='); - } - out[out_len] = '\0'; - JSValue v = JS_NewString(js, out); - free(out); - return v; -) - -/*─── Base64 → blob (standard, expects ‘+’ and ‘/’, pads allowed) ────────────*/ -JSC_CCALL(text_base64_to_blob, - const char *str = JS_ToCString(js, argv[0]); - if (!str) return JS_ThrowTypeError(js, "Expected string"); - size_t len = strlen(str); - // strip padding for length calculation - size_t eff = len; - while (eff > 0 && str[eff-1] == '=') eff--; - size_t out_len = (eff * 6) / 8; - uint8_t *out = malloc(out_len); - if (!out) { JS_FreeCString(js, str); return JS_ThrowOutOfMemory(js); } - - size_t in_i = 0, out_i = 0; - while (in_i < eff) { - uint32_t buf = 0; - int to_read = (eff - in_i < 4 ? eff - in_i : 4); - for (int j = 0; j < to_read; ++j) { - int v = base64_char_to_val_standard(str[in_i++]); - if (v < 0) { free(out); JS_FreeCString(js, str); - return JS_ThrowTypeError(js, "Invalid base64 character"); } - buf = (buf << 6) | v; - } - buf <<= 6 * (4 - to_read); - int bytes_out = (to_read * 6) / 8; - for (int j = 0; j < bytes_out && out_i < out_len; ++j) { - out[out_i++] = (buf >> (16 - 8*j)) & 0xFF; - } - } - - JSValue v = js_new_blob_stoned_copy(js, out, out_len); - free(out); - JS_FreeCString(js, str); - return v; -) - -/*─── blob → Base64URL (no padding, ‘-’ and ‘_’) ─────────────────────────────*/ -JSC_CCALL(text_blob_to_base64url, - size_t blob_len; - void *blob_data = js_get_blob_data(js, &blob_len, argv[0]); - if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob"); - const uint8_t *bytes = blob_data; - static const char b64url[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789-_"; - size_t raw_len = ((blob_len + 2) / 3) * 4; - // we’ll drop any trailing '=' - char *out = malloc(raw_len + 1); - if (!out) return JS_ThrowOutOfMemory(js); - - size_t in_i = 0, out_i = 0; - while (in_i < blob_len) { - uint32_t buf = 0; - int to_read = (blob_len - in_i < 3 ? blob_len - in_i : 3); - for (int j = 0; j < to_read; ++j) { - buf = (buf << 8) | bytes[in_i++]; - } - buf <<= 8 * (3 - to_read); - out[out_i++] = b64url[(buf >> 18) & 0x3F]; - out[out_i++] = b64url[(buf >> 12) & 0x3F]; - if (to_read > 1) out[out_i++] = b64url[(buf >> 6) & 0x3F]; - if (to_read > 2) out[out_i++] = b64url[ buf & 0x3F]; - } - out[out_i] = '\0'; - JSValue v = JS_NewString(js, out); - free(out); - return v; -) - -/*─── Base64URL → blob (accepts ‘-’ / ‘_’, no padding needed) ─────────────────*/ -JSC_CCALL(text_base64url_to_blob, - const char *str = JS_ToCString(js, argv[0]); - if (!str) return JS_ThrowTypeError(js, "Expected string"); - size_t len = strlen(str); - size_t eff = len; // no '=' in URL‐safe, but strip if present - while (eff > 0 && str[eff-1] == '=') eff--; - size_t out_len = (eff * 6) / 8; - uint8_t *out = malloc(out_len); - if (!out) { JS_FreeCString(js, str); return JS_ThrowOutOfMemory(js); } - - size_t in_i = 0, out_i = 0; - while (in_i < eff) { - uint32_t buf = 0; - int to_read = (eff - in_i < 4 ? eff - in_i : 4); - for (int j = 0; j < to_read; ++j) { - int v = base64_char_to_val_url(str[in_i++]); - if (v < 0) { free(out); JS_FreeCString(js, str); - return JS_ThrowTypeError(js, "Invalid base64url character"); } - buf = (buf << 6) | v; - } - buf <<= 6 * (4 - to_read); - int bytes_out = (to_read * 6) / 8; - for (int j = 0; j < bytes_out && out_i < out_len; ++j) { - out[out_i++] = (buf >> (16 - 8*j)) & 0xFF; - } - } - - JSValue v = js_new_blob_stoned_copy(js, out, out_len); - free(out); - JS_FreeCString(js, str); - return v; -) - -static const JSCFunctionListEntry js_text_funcs[] = { - MIST_FUNC_DEF(text, blob_to_hex, 1), - MIST_FUNC_DEF(text, blob_to_base32, 1), - MIST_FUNC_DEF(text, base32_to_blob, 1), - MIST_FUNC_DEF(text, blob_to_base64, 1), - MIST_FUNC_DEF(text, base64_to_blob, 1), - MIST_FUNC_DEF(text, blob_to_base64url, 1), - MIST_FUNC_DEF(text, base64url_to_blob, 1), -}; - -JSValue js_internal_text_use(JSContext *js) -{ - JSValue mod = JS_NewObject(js); - JS_SetPropertyFunctionList(js, mod, js_text_funcs, countof(js_text_funcs)); - return mod; -} \ No newline at end of file diff --git a/internal/text.cm b/internal/text.cm deleted file mode 100644 index 7fad685d..00000000 --- a/internal/text.cm +++ /dev/null @@ -1,602 +0,0 @@ -/* text.cm - text conversion and formatting utilities */ -var blob = use('blob') -var utf8 = use('utf8') - -var _toLowerCase = String.prototype.toLowerCase -var _toUpperCase = String.prototype.toUpperCase -var _trim = String.prototype.trim -var _indexOf = String.prototype.indexOf -var _lastIndexOf = String.prototype.lastIndexOf -var _replace = String.prototype.replace -var _normalize = String.prototype.normalize -var _substring = String.prototype.substring -var _charCodeAt = String.prototype.charCodeAt -var _codePointAt = String.prototype.codePointAt - -var _String = String - -var that = this - -// Convert number to string with given radix -function to_radix(num, radix) { - if (radix < 2 || radix > 36) return null; - - var digits = "0123456789abcdefghijklmnopqrstuvwxyz"; - var result = ""; - var n = number.whole(num); - var negative = n < 0; - n = number.abs(n); - - if (n == 0) return "0"; - - while (n > 0) { - result = digits[n % radix] + result; - n = number.floor(n / radix); - } - - return negative ? "-" + result : result; -} - -// Insert separator every n digits from right -function add_separator(str, sep, n) { - if (!n || n == 0) return str; - - var negative = str[0] == '-'; - if (negative) str = str.substring(1); - - var parts = str.split('.'); - var integer = parts[0]; - var decimal = parts[1] || ''; - - // Add separators to integer part - var result = ""; - for (var i = integer.length - 1, count = 0; i >= 0; i--) { - if (count == n && i != integer.length - 1) { - result = sep + result; - count = 0; - } - result = integer[i] + result; - count++; - } - - if (decimal) result += '.' + decimal; - return negative ? '-' + result : result; -} - -// Format number with separator from left -function add_separator_left(str, sep, n) { - if (!n || n == 0) return str; - - var negative = str[0] == '-'; - if (negative) str = str.substring(1); - - var result = ""; - for (var i = 0, count = 0; i < str.length; i++) { - if (count == n && i != 0) { - result += sep; - count = 0; - } - result += str[i]; - count++; - } - - return negative ? '-' + result : result; -} - -/* -------- main text function --------------------------------------- */ - -function text(...arguments) { - var arg = arguments[0]; - - // Handle blob conversion - if (arg instanceof blob) { - if (!stone.p(arg)) - throw new Error("text: blob must be stone for reading"); - - var format = arguments[1]; - var bit_length = arg.length; - var result = ""; - - if (typeof format == 'string') { - // Extract style from format - var style = ''; - for (var i = 0; i < format.length; i++) { - if ((format[i] >= 'a' && format[i] <= 'z') || (format[i] >= 'A' && format[i] <= 'Z')) { - style = format[i]; - break; - } - } - - // Handle blob encoding styles - switch (style) { - case 'h': // hexadecimal - return that.blob_to_hex(arg); - - case 't': // base32 - return that.blob_to_base32(arg); - - case 'b': // binary - for (var i = 0; i < bit_length; i++) { - result += arg.read_logical(i) ? '1' : '0'; - } - return result; - - case 'o': // octal - var bits = 0; - var value = 0; - - for (var i = 0; i < bit_length; i++) { - var bit = arg.read_logical(i); - value = (value << 1) | (bit ? 1 : 0); - bits++; - - if (bits == 3) { - result += value.toString(); - bits = 0; - value = 0; - } - } - - // Handle remaining bits - if (bits > 0) { - value = value << (3 - bits); - result += value.toString(); - } - - return result; - } - } - - // Default: interpret as UTF-8 text - // Use the utf8 module to decode the blob - if (arg.length == 0) return "" - return utf8.decode(arg); - } - - // Handle array conversion - if (isa(arg, array)) { - var separator = arguments[1] || ""; - - // Check if all items are valid codepoints - var all_codepoints = true; - for (var i = 0; i < arg.length; i++) { - var item = arg[i]; - if (!(typeof item == 'number' && item >= 0 && item <= 0x10FFFF && item == number.floor(item))) { - all_codepoints = false; - break; - } - } - - if (all_codepoints && separator == "") { - // Use utf8 module to convert codepoints to string - return utf8.from_codepoints(arg); - } else { - // General array to string conversion - var result = ""; - for (var i = 0; i < arg.length; i++) { - if (i > 0) result += separator; - - var item = arg[i]; - if (typeof item == 'number' && item >= 0 && item <= 0x10FFFF && item == number.floor(item)) { - // Single codepoint - use utf8 module - result += utf8.from_codepoints([item]); - } else { - result += String(item); - } - } - return result; - } - } - - // Handle number conversion - if (typeof arg == 'number') { - var format = arguments[1]; - - // Simple radix conversion - if (typeof format == 'number') { - return to_radix(arg, format); - } - - // Format string conversion - if (typeof format == 'string') { - return format_number(arg, format); - } - - // Default conversion - return _String(arg); - } - - // Handle text operations - if (typeof arg == 'string') { - if (arguments.length == 1) return arg; - - var from = arguments[1]; - var to = arguments[2]; - - if (typeof from != 'number' || typeof to != 'number') return arg; - - var len = arg.length; - - // Adjust negative indices - if (from < 0) from += len; - if (to < 0) to += len; - - // Default values - if (from == null) from = 0; - if (to == null) to = len; - - // Validate range - if (from < 0 || from > to || to > len) return null; - - return arg.substring(from, to); - } - - return null; -} - -/* -------- number formatting ---------------------------------------- */ - -function format_number(num, format) { - // Parse format string - var separation = 0; - var style = ''; - var places = 0; - - var i = 0; - - // Parse separation digit - if (i < format.length && format[i] >= '0' && format[i] <= '9') { - separation = number(format[i]); - i++; - } - - // Parse style letter - if (i < format.length) { - style = format[i]; - i++; - } else { - return null; - } - - // Parse places digits - if (i < format.length && format[i] >= '0' && format[i] <= '9') { - places = number(format[i]); - i++; - if (i < format.length && format[i] >= '0' && format[i] <= '9') { - places = places * 10 + number(format[i]); - i++; - } - } - - // Invalid format if there's more - if (i < format.length) return null; - - // Real number styles - if (style == 'e' || style == 'n' || style == 's' || - style == 'u' || style == 'd' || style == 'v' || style == 'l') { - - var decimal_point = '.'; - var separator = ''; - var default_separation = 0; - var default_places = 0; - - switch (style) { - case 'e': // exponential - decimal_point = '.'; - separator = ''; - default_separation = 0; - default_places = 0; - break; - case 'n': // number - decimal_point = '.'; - separator = ''; - default_separation = 0; - default_places = 0; - break; - case 's': // space - decimal_point = '.'; - separator = ' '; - default_separation = 3; - default_places = 0; - break; - case 'u': // underbar - decimal_point = '.'; - separator = '_'; - default_separation = 0; - default_places = 0; - break; - case 'd': // decimal - decimal_point = '.'; - separator = ','; - default_separation = 3; - default_places = 2; - break; - case 'v': // comma (European style) - decimal_point = ','; - separator = '.'; - default_separation = 0; - default_places = 0; - break; - case 'l': // locale (default to 'd' style for now) - decimal_point = '.'; - separator = ','; - default_separation = 3; - default_places = 2; - break; - } - - if (separation == 0) separation = default_separation; - if (places == 0 && style != 'e' && style != 'n') places = default_places; - - // Format the number - if (style == 'e') { - // Scientific notation - var str = places > 0 ? num.toExponential(places) : num.toExponential(); - return str; - } else if (style == 'n' && (number.abs(num) >= 1e21 || (number.abs(num) < 1e-6 && num != 0))) { - // Use scientific notation for extreme values - return num.toExponential(); - } else { - // Regular decimal formatting - var str; - if (places > 0) { - str = num.toFixed(places); - } else { - str = num.toString(); - } - - // Replace decimal point if needed - if (decimal_point != '.') { - str = str.replace('.', decimal_point); - } - - // Add separators - if (separation > 0 && separator) { - str = add_separator(str, separator, separation); - } - - return str; - } - } - - // Integer styles - if (style == 'i' || style == 'b' || style == 'o' || - style == 'h' || style == 't') { - - var radix = 10; - var default_separation = 0; - var default_places = 1; - - switch (style) { - case 'i': // integer - radix = 10; - default_separation = 0; - default_places = 1; - break; - case 'b': // binary - radix = 2; - default_separation = 0; - default_places = 1; - break; - case 'o': // octal - radix = 8; - default_separation = 0; - default_places = 1; - break; - case 'h': // hexadecimal - radix = 16; - default_separation = 0; - default_places = 1; - break; - case 't': // base32 - radix = 32; - default_separation = 0; - default_places = 1; - break; - } - - if (separation == 0) separation = default_separation; - if (places == 0) places = default_places; - - // Convert to integer - var n = number.whole(num); - var str = to_radix(n, radix).toUpperCase(); - - // Pad with zeros if needed - var negative = str[0] == '-'; - if (negative) str = str.substring(1); - - while (str.length < places) { - str = '0' + str; - } - - // Add separators - if (separation > 0) { - str = add_separator_left(str, '_', separation); - } - - return negative ? '-' + str : str; - } - - return null; -} - -/* -------- text sub-functions --------------------------------------- */ - -text.lower = function(str) { - if (typeof str != 'string') return null - return _toLowerCase.call(str) -} - -text.upper = function(str) { - if (typeof str != 'string') return null - return _toUpperCase.call(str) -} - -text.trim = function(str, reject) { - if (typeof str != 'string') return null - if (reject == null) return _trim.call(str) - - // Custom trim with reject characters - var start = 0 - var end = str.length - - while (start < end && reject.indexOf(str[start]) >= 0) start++ - while (end > start && reject.indexOf(str[end - 1]) >= 0) end-- - - return _substring.call(str, start, end) -} - -text.normalize = function(str) { - if (typeof str != 'string') return null - return _normalize.call(str, 'NFC') -} - -text.codepoint = function(str) { - if (typeof str != 'string' || str.length == 0) return null - return _codePointAt.call(str, 0) -} - -text.search = function(str, target, from) { - if (typeof str != 'string') return null - if (typeof target != 'string') return null - - if (from == null) from = 0 - if (from < 0) from += str.length - if (from < 0) from = 0 - - var result = _indexOf.call(str, target, from) - if (result == -1) return null - return result -} - -text.replace = function(str, target, replacement, limit) { - if (typeof str != 'string') return null - if (typeof target != 'string') return null - - if (limit == null) { - // Replace all - var result = str - var pos = 0 - while (true) { - var idx = _indexOf.call(result, target, pos) - if (idx == -1) break - - var rep = replacement - if (typeof replacement == 'function') { - rep = replacement(target, idx) - if (rep == null) { - pos = idx + target.length - continue - } - } - - result = _substring.call(result, 0, idx) + rep + _substring.call(result, idx + target.length) - pos = idx + rep.length - } - return result - } - - // Replace with limit - var result = str - var pos = 0 - var count = 0 - - while (count < limit) { - var idx = _indexOf.call(result, target, pos) - if (idx == -1) break - - var rep = replacement - if (typeof replacement == 'function') { - rep = replacement(target, idx) - if (rep == null) { - pos = idx + target.length - count++ - continue - } - } - - result = _substring.call(result, 0, idx) + rep + _substring.call(result, idx + target.length) - pos = idx + rep.length - count++ - } - - return result -} - -text.format = function(str, collection, transformer) { - if (typeof str != 'string') return null - - var result = "" - var i = 0 - - while (i < str.length) { - if (str[i] == '{') { - var end = _indexOf.call(str, '}', i) - if (end == -1) { - result += str[i] - i++ - continue - } - - var middle = _substring.call(str, i + 1, end) - var colonIdx = _indexOf.call(middle, ':') - var key = colonIdx >= 0 ? _substring.call(middle, 0, colonIdx) : middle - var formatSpec = colonIdx >= 0 ? _substring.call(middle, colonIdx + 1) : "" - - var value = null - if (isa(collection, array)) { - var idx = number(key) - if (!isNaN(idx) && idx >= 0 && idx < collection.length) { - value = collection[idx] - } - } else if (isa(collection, object)) { - value = collection[key] - } - - var substitution = null - - if (transformer != null) { - if (typeof transformer == 'function') { - substitution = transformer(value, formatSpec) - } else if (typeof transformer == 'object') { - var fn = transformer[formatSpec] - if (typeof fn == 'function') { - substitution = fn(value) - } - } - } - - if (substitution == null && typeof value == 'number' && formatSpec) { - // Try number formatting - substitution = String(value) // simplified - } - - if (substitution == null && value != null) { - substitution = String(value) - } - - if (substitution != null) { - result += substitution - } else { - result += _substring.call(str, i, end + 1) - } - - i = end + 1 - } else { - result += str[i] - i++ - } - } - - return result -} - -text.extract = function(str, pattern, from, to) { - // Simplified pattern matching - returns null for now - // Full implementation would require regex or custom pattern language - if (typeof str != 'string') return null - return null -} - -return text \ No newline at end of file diff --git a/meson.build b/meson.build index 95484e44..62684339 100644 --- a/meson.build +++ b/meson.build @@ -38,7 +38,6 @@ endif link_args = link sources = [] src += [ # core - 'qjs_blob.c', 'monocypher.c', 'cell.c', 'wildmatch.c', @@ -58,7 +57,6 @@ scripts = [ 'wildstar.c', 'fit.c', 'crypto.c', - 'internal/text.c', 'utf8.c', 'internal/kim.c', 'time.c', diff --git a/source/quickjs.c b/source/quickjs.c index 2a9d20c7..1b915a0e 100644 --- a/source/quickjs.c +++ b/source/quickjs.c @@ -31,6 +31,7 @@ #include #include #include +#include #if defined(__APPLE__) #include #elif defined(__linux__) || defined(__GLIBC__) @@ -46,6 +47,9 @@ #include "libunicode.h" #include "dtoa.h" +#define BLOB_IMPLEMENTATION +#include "blob.h" + #define OPTIMIZE 1 #define SHORT_OPCODES 1 #if defined(EMSCRIPTEN) @@ -174,7 +178,8 @@ enum { JS_CLASS_STRING_ITERATOR, /* u.array_iterator_data */ JS_CLASS_REGEXP_STRING_ITERATOR, /* u.regexp_string_iterator_data */ JS_CLASS_FINALIZATION_REGISTRY, - + JS_CLASS_BLOB, /* u.opaque (blob *) */ + JS_CLASS_INIT_COUNT, /* last entry for predefined classes */ }; @@ -1497,6 +1502,12 @@ void JS_SetGCThreshold(JSRuntime *rt, size_t gc_threshold) rt->malloc_gc_threshold = gc_threshold; } +/* Helper to call system free (for memory allocated by external libs like blob.h) */ +static void sys_free(void *ptr) +{ + free(ptr); +} + #define malloc(s) malloc_is_forbidden(s) #define free(p) free_is_forbidden(p) #define realloc(p,s) realloc_is_forbidden(p,s) @@ -37097,6 +37108,3193 @@ static int __attribute__((format(printf, 2, 3))) js_throw_URIError(JSContext *ct /* global object */ +/* ============================================================================ + * Cell Script Native Global Functions + * ============================================================================ + * These functions implement the core Cell script primitives: + * - text: string conversion and manipulation + * - number: number conversion and math utilities + * - array: array creation and manipulation + * - object: object creation and manipulation + * - fn: function utilities + * ============================================================================ */ + + +/* ---------------------------------------------------------------------------- + * number function and sub-functions + * ---------------------------------------------------------------------------- */ + +/* number(val, format) - convert to number */ +static JSValue js_cell_number(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_NULL; + + JSValue val = argv[0]; + int tag = JS_VALUE_GET_TAG(val); + + /* Handle boolean */ + if (tag == JS_TAG_BOOL) { + return JS_NewInt32(ctx, JS_VALUE_GET_BOOL(val) ? 1 : 0); + } + + /* Handle number - return as-is */ + if (tag == JS_TAG_INT || tag == JS_TAG_FLOAT64) { + return JS_DupValue(ctx, val); + } + + /* Handle string */ + if (tag == JS_TAG_STRING || tag == JS_TAG_STRING_ROPE) { + const char *str = JS_ToCString(ctx, val); + if (!str) + return JS_EXCEPTION; + + JSValue result; + + /* Check for format argument */ + if (argc > 1 && JS_VALUE_GET_TAG(argv[1]) == JS_TAG_INT) { + /* Radix conversion */ + int radix = JS_VALUE_GET_INT(argv[1]); + if (radix < 2 || radix > 36) { + JS_FreeCString(ctx, str); + return JS_NULL; + } + char *endptr; + long long n = strtoll(str, &endptr, radix); + if (endptr == str || *endptr != '\0') { + JS_FreeCString(ctx, str); + return JS_NULL; + } + result = JS_NewInt64(ctx, n); + } else if (argc > 1 && (JS_VALUE_GET_TAG(argv[1]) == JS_TAG_STRING || + JS_VALUE_GET_TAG(argv[1]) == JS_TAG_STRING_ROPE)) { + /* Format string */ + const char *format = JS_ToCString(ctx, argv[1]); + if (!format) { + JS_FreeCString(ctx, str); + return JS_EXCEPTION; + } + + char *clean = js_malloc(ctx, strlen(str) + 1); + if (!clean) { + JS_FreeCString(ctx, format); + JS_FreeCString(ctx, str); + return JS_EXCEPTION; + } + + const char *p = str; + char *q = clean; + + if (strcmp(format, "u") == 0) { + /* underbar separator */ + while (*p) { + if (*p != '_') *q++ = *p; + p++; + } + } else if (strcmp(format, "d") == 0 || strcmp(format, "l") == 0) { + /* comma separator */ + while (*p) { + if (*p != ',') *q++ = *p; + p++; + } + } else if (strcmp(format, "s") == 0) { + /* space separator */ + while (*p) { + if (*p != ' ') *q++ = *p; + p++; + } + } else if (strcmp(format, "v") == 0) { + /* European style: period separator, comma decimal */ + while (*p) { + if (*p == '.') { p++; continue; } + if (*p == ',') { *q++ = '.'; p++; continue; } + *q++ = *p++; + } + } else if (strcmp(format, "b") == 0) { + *q = '\0'; + char *endptr; + long long n = strtoll(str, &endptr, 2); + js_free(ctx, clean); + JS_FreeCString(ctx, format); + JS_FreeCString(ctx, str); + if (endptr == str) + return JS_NULL; + return JS_NewInt64(ctx, n); + } else if (strcmp(format, "o") == 0) { + *q = '\0'; + char *endptr; + long long n = strtoll(str, &endptr, 8); + js_free(ctx, clean); + JS_FreeCString(ctx, format); + JS_FreeCString(ctx, str); + if (endptr == str) + return JS_NULL; + return JS_NewInt64(ctx, n); + } else if (strcmp(format, "h") == 0) { + *q = '\0'; + char *endptr; + long long n = strtoll(str, &endptr, 16); + js_free(ctx, clean); + JS_FreeCString(ctx, format); + JS_FreeCString(ctx, str); + if (endptr == str) + return JS_NULL; + return JS_NewInt64(ctx, n); + } else if (strcmp(format, "t") == 0) { + *q = '\0'; + char *endptr; + long long n = strtoll(str, &endptr, 32); + js_free(ctx, clean); + JS_FreeCString(ctx, format); + JS_FreeCString(ctx, str); + if (endptr == str) + return JS_NULL; + return JS_NewInt64(ctx, n); + } else if (strcmp(format, "j") == 0) { + /* JavaScript style prefix */ + js_free(ctx, clean); + JS_FreeCString(ctx, format); + int radix = 10; + const char *start = str; + if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) { + radix = 16; start = str + 2; + } else if (str[0] == '0' && (str[1] == 'o' || str[1] == 'O')) { + radix = 8; start = str + 2; + } else if (str[0] == '0' && (str[1] == 'b' || str[1] == 'B')) { + radix = 2; start = str + 2; + } + if (radix != 10) { + char *endptr; + long long n = strtoll(start, &endptr, radix); + JS_FreeCString(ctx, str); + if (endptr == start) + return JS_NULL; + return JS_NewInt64(ctx, n); + } + double d = strtod(str, NULL); + JS_FreeCString(ctx, str); + return JS_NewFloat64(ctx, d); + } else { + /* Unknown format, just copy */ + strcpy(clean, str); + q = clean + strlen(clean); + } + *q = '\0'; + + double d = strtod(clean, NULL); + js_free(ctx, clean); + JS_FreeCString(ctx, format); + JS_FreeCString(ctx, str); + if (isnan(d)) + return JS_NULL; + return JS_NewFloat64(ctx, d); + } else { + /* Default: parse as decimal */ + char *endptr; + double d = strtod(str, &endptr); + JS_FreeCString(ctx, str); + if (endptr == str || isnan(d)) + return JS_NULL; + result = JS_NewFloat64(ctx, d); + } + + return result; + } + + return JS_NULL; +} + +/* number.whole(n) - truncate to integer */ +static JSValue js_cell_number_whole(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double d; + if (JS_ToFloat64(ctx, &d, argv[0])) + return JS_NULL; + return JS_NewFloat64(ctx, trunc(d)); +} + +/* number.fraction(n) - get fractional part */ +static JSValue js_cell_number_fraction(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double d; + if (JS_ToFloat64(ctx, &d, argv[0])) + return JS_NULL; + return JS_NewFloat64(ctx, d - trunc(d)); +} + +/* number.floor(n, place) - floor with optional decimal place */ +static JSValue js_cell_number_floor(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double d; + if (JS_ToFloat64(ctx, &d, argv[0])) + return JS_NULL; + if (argc < 2 || JS_IsNull(argv[1])) { + return JS_NewFloat64(ctx, floor(d)); + } + int place; + if (JS_ToInt32(ctx, &place, argv[1])) + return JS_NULL; + if (place == 0) + return JS_NewFloat64(ctx, floor(d)); + double mult = pow(10, place); + return JS_NewFloat64(ctx, floor(d * mult) / mult); +} + +/* number.ceiling(n, place) - ceiling with optional decimal place */ +static JSValue js_cell_number_ceiling(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double d; + if (JS_ToFloat64(ctx, &d, argv[0])) + return JS_NULL; + if (argc < 2 || JS_IsNull(argv[1])) { + return JS_NewFloat64(ctx, ceil(d)); + } + int place; + if (JS_ToInt32(ctx, &place, argv[1])) + return JS_NULL; + if (place == 0) + return JS_NewFloat64(ctx, ceil(d)); + double mult = pow(10, place); + return JS_NewFloat64(ctx, ceil(d * mult) / mult); +} + +/* number.abs(n) - absolute value */ +static JSValue js_cell_number_abs(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double d; + if (JS_ToFloat64(ctx, &d, argv[0])) + return JS_NULL; + return JS_NewFloat64(ctx, fabs(d)); +} + +/* number.round(n, place) - round with optional decimal place */ +static JSValue js_cell_number_round(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double d; + if (JS_ToFloat64(ctx, &d, argv[0])) + return JS_NULL; + if (argc < 2 || JS_IsNull(argv[1])) { + return JS_NewFloat64(ctx, round(d)); + } + int place; + if (JS_ToInt32(ctx, &place, argv[1])) + return JS_NULL; + if (place == 0) + return JS_NewFloat64(ctx, round(d)); + double mult = pow(10, place); + return JS_NewFloat64(ctx, round(d * mult) / mult); +} + +/* number.sign(n) - return sign (-1, 0, 1) */ +static JSValue js_cell_number_sign(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double d; + if (JS_ToFloat64(ctx, &d, argv[0])) + return JS_NULL; + if (d < 0) return JS_NewInt32(ctx, -1); + if (d > 0) return JS_NewInt32(ctx, 1); + return JS_NewInt32(ctx, 0); +} + +/* number.trunc(n, place) - truncate with optional decimal place */ +static JSValue js_cell_number_trunc(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double d; + if (JS_ToFloat64(ctx, &d, argv[0])) + return JS_NULL; + if (argc < 2 || JS_IsNull(argv[1])) { + return JS_NewFloat64(ctx, trunc(d)); + } + int place; + if (JS_ToInt32(ctx, &place, argv[1])) + return JS_NULL; + if (place == 0) + return JS_NewFloat64(ctx, trunc(d)); + double mult = pow(10, place); + return JS_NewFloat64(ctx, trunc(d * mult) / mult); +} + +/* number.min(...vals) - minimum value */ +static JSValue js_cell_number_min(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc == 0) return JS_NULL; + double result; + if (JS_ToFloat64(ctx, &result, argv[0])) + return JS_NULL; + for (int i = 1; i < argc; i++) { + double d; + if (JS_ToFloat64(ctx, &d, argv[i])) + return JS_NULL; + if (d < result) result = d; + } + return JS_NewFloat64(ctx, result); +} + +/* number.max(...vals) - maximum value */ +static JSValue js_cell_number_max(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc == 0) return JS_NULL; + double result; + if (JS_ToFloat64(ctx, &result, argv[0])) + return JS_NULL; + for (int i = 1; i < argc; i++) { + double d; + if (JS_ToFloat64(ctx, &d, argv[i])) + return JS_NULL; + if (d > result) result = d; + } + return JS_NewFloat64(ctx, result); +} + +/* number.remainder(dividend, divisor) - remainder after division */ +static JSValue js_cell_number_remainder(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_NULL; + double dividend, divisor; + if (JS_ToFloat64(ctx, ÷nd, argv[0])) + return JS_NULL; + if (JS_ToFloat64(ctx, &divisor, argv[1])) + return JS_NULL; + if (divisor == 0) return JS_NULL; + return JS_NewFloat64(ctx, dividend - (trunc(dividend / divisor) * divisor)); +} + +static const JSCFunctionListEntry js_cell_number_funcs[] = { + JS_CFUNC_DEF("whole", 1, js_cell_number_whole), + JS_CFUNC_DEF("fraction", 1, js_cell_number_fraction), + JS_CFUNC_DEF("floor", 2, js_cell_number_floor), + JS_CFUNC_DEF("ceiling", 2, js_cell_number_ceiling), + JS_CFUNC_DEF("abs", 1, js_cell_number_abs), + JS_CFUNC_DEF("round", 2, js_cell_number_round), + JS_CFUNC_DEF("sign", 1, js_cell_number_sign), + JS_CFUNC_DEF("trunc", 2, js_cell_number_trunc), + JS_CFUNC_DEF("min", 0, js_cell_number_min), + JS_CFUNC_DEF("max", 0, js_cell_number_max), + JS_CFUNC_DEF("remainder", 2, js_cell_number_remainder), +}; + +/* ---------------------------------------------------------------------------- + * text function and sub-functions + * ---------------------------------------------------------------------------- */ + +/* Helper: convert number to string with radix */ +static JSValue js_cell_number_to_radix_string(JSContext *ctx, double num, int radix) +{ + if (radix < 2 || radix > 36) return JS_NULL; + + static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; + char buf[70]; + int len = 0; + int negative = 0; + int64_t n = (int64_t)trunc(num); + + if (n < 0) { + negative = 1; + n = -n; + } + + if (n == 0) { + buf[len++] = '0'; + } else { + while (n > 0) { + buf[len++] = digits[n % radix]; + n /= radix; + } + } + + if (negative) { + buf[len++] = '-'; + } + + /* Reverse the string */ + char result[72]; + int j = 0; + for (int i = len - 1; i >= 0; i--) { + result[j++] = buf[i]; + } + result[j] = '\0'; + + return JS_NewString(ctx, result); +} + +/* Helper: add separator every n digits from right */ +static char *add_separator(JSContext *ctx, const char *str, char sep, int n) +{ + if (n <= 0) { + char *result = js_malloc(ctx, strlen(str) + 1); + if (result) strcpy(result, str); + return result; + } + + int negative = (str[0] == '-'); + const char *start = negative ? str + 1 : str; + + /* Find decimal point */ + const char *decimal = strchr(start, '.'); + int int_len = decimal ? (int)(decimal - start) : (int)strlen(start); + + int num_seps = (int_len - 1) / n; + int result_len = strlen(str) + num_seps + 1; + char *result = js_malloc(ctx, result_len); + if (!result) return NULL; + + char *q = result; + if (negative) *q++ = '-'; + + int count = int_len % n; + if (count == 0) count = n; + + for (int i = 0; i < int_len; i++) { + if (i > 0 && count == 0) { + *q++ = sep; + count = n; + } + *q++ = start[i]; + count--; + } + + if (decimal) { + strcpy(q, decimal); + } else { + *q = '\0'; + } + + return result; +} + +/* Helper: format number with format string */ +static JSValue js_cell_format_number(JSContext *ctx, double num, const char *format) +{ + int separation = 0; + char style = '\0'; + int places = 0; + int i = 0; + + /* Parse separation digit */ + if (format[i] >= '0' && format[i] <= '9') { + separation = format[i] - '0'; + i++; + } + + /* Parse style letter */ + if (format[i]) { + style = format[i]; + i++; + } else { + return JS_NULL; + } + + /* Parse places digits */ + if (format[i] >= '0' && format[i] <= '9') { + places = format[i] - '0'; + i++; + if (format[i] >= '0' && format[i] <= '9') { + places = places * 10 + (format[i] - '0'); + i++; + } + } + + /* Invalid if more characters */ + if (format[i] != '\0') return JS_NULL; + + char buf[128]; + char *result_str = NULL; + + switch (style) { + case 'e': { + /* Exponential */ + if (places > 0) + snprintf(buf, sizeof(buf), "%.*e", places, num); + else + snprintf(buf, sizeof(buf), "%e", num); + return JS_NewString(ctx, buf); + } + case 'n': { + /* Number - scientific for extreme values */ + if (fabs(num) >= 1e21 || (fabs(num) < 1e-6 && num != 0)) { + snprintf(buf, sizeof(buf), "%e", num); + } else if (places > 0) { + snprintf(buf, sizeof(buf), "%.*f", places, num); + } else { + snprintf(buf, sizeof(buf), "%g", num); + } + return JS_NewString(ctx, buf); + } + case 's': { + /* Space separated */ + if (separation == 0) separation = 3; + snprintf(buf, sizeof(buf), "%.*f", places, num); + result_str = add_separator(ctx, buf, ' ', separation); + if (!result_str) return JS_EXCEPTION; + JSValue ret = JS_NewString(ctx, result_str); + js_free(ctx, result_str); + return ret; + } + case 'u': { + /* Underbar separated */ + snprintf(buf, sizeof(buf), "%.*f", places, num); + if (separation > 0) { + result_str = add_separator(ctx, buf, '_', separation); + if (!result_str) return JS_EXCEPTION; + JSValue ret = JS_NewString(ctx, result_str); + js_free(ctx, result_str); + return ret; + } + return JS_NewString(ctx, buf); + } + case 'd': + case 'l': { + /* Decimal/locale with comma separator */ + if (separation == 0) separation = 3; + if (places == 0 && style == 'd') places = 2; + snprintf(buf, sizeof(buf), "%.*f", places, num); + result_str = add_separator(ctx, buf, ',', separation); + if (!result_str) return JS_EXCEPTION; + JSValue ret = JS_NewString(ctx, result_str); + js_free(ctx, result_str); + return ret; + } + case 'v': { + /* European style: comma decimal, period separator */ + snprintf(buf, sizeof(buf), "%.*f", places, num); + /* Replace . with , */ + for (char *p = buf; *p; p++) { + if (*p == '.') *p = ','; + } + if (separation > 0) { + result_str = add_separator(ctx, buf, '.', separation); + if (!result_str) return JS_EXCEPTION; + JSValue ret = JS_NewString(ctx, result_str); + js_free(ctx, result_str); + return ret; + } + return JS_NewString(ctx, buf); + } + case 'i': { + /* Integer base 10 */ + if (places == 0) places = 1; + int64_t n = (int64_t)trunc(num); + int neg = n < 0; + if (neg) n = -n; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + int len = strlen(buf); + /* Pad with zeros */ + if (len < places) { + memmove(buf + (places - len), buf, len + 1); + memset(buf, '0', places - len); + } + if (separation > 0) { + result_str = add_separator(ctx, buf, '_', separation); + if (!result_str) return JS_EXCEPTION; + if (neg) { + char *final = js_malloc(ctx, strlen(result_str) + 2); + if (!final) { js_free(ctx, result_str); return JS_EXCEPTION; } + final[0] = '-'; + strcpy(final + 1, result_str); + js_free(ctx, result_str); + JSValue ret = JS_NewString(ctx, final); + js_free(ctx, final); + return ret; + } + JSValue ret = JS_NewString(ctx, result_str); + js_free(ctx, result_str); + return ret; + } + if (neg) { + memmove(buf + 1, buf, strlen(buf) + 1); + buf[0] = '-'; + } + return JS_NewString(ctx, buf); + } + case 'b': { + /* Binary */ + if (places == 0) places = 1; + return js_cell_number_to_radix_string(ctx, num, 2); + } + case 'o': { + /* Octal */ + if (places == 0) places = 1; + int64_t n = (int64_t)trunc(num); + snprintf(buf, sizeof(buf), "%llo", (long long)(n < 0 ? -n : n)); + /* Uppercase and pad */ + for (char *p = buf; *p; p++) *p = toupper(*p); + int len = strlen(buf); + if (len < places) { + memmove(buf + (places - len), buf, len + 1); + memset(buf, '0', places - len); + } + if (n < 0) { + memmove(buf + 1, buf, strlen(buf) + 1); + buf[0] = '-'; + } + return JS_NewString(ctx, buf); + } + case 'h': { + /* Hexadecimal */ + if (places == 0) places = 1; + int64_t n = (int64_t)trunc(num); + snprintf(buf, sizeof(buf), "%llX", (long long)(n < 0 ? -n : n)); + int len = strlen(buf); + if (len < places) { + memmove(buf + (places - len), buf, len + 1); + memset(buf, '0', places - len); + } + if (n < 0) { + memmove(buf + 1, buf, strlen(buf) + 1); + buf[0] = '-'; + } + return JS_NewString(ctx, buf); + } + case 't': { + /* Base32 */ + if (places == 0) places = 1; + return js_cell_number_to_radix_string(ctx, num, 32); + } + } + + return JS_NULL; +} + +/* Forward declaration for blob helper */ +static blob *js_get_blob(JSContext *ctx, JSValueConst val); + +/* text(arg, format) - main text function */ +static JSValue js_cell_text(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_NULL; + + JSValue arg = argv[0]; + int tag = JS_VALUE_GET_TAG(arg); + + /* Handle string */ + if (tag == JS_TAG_STRING || tag == JS_TAG_STRING_ROPE) { + if (argc == 1) + return JS_DupValue(ctx, arg); + + /* text(string, from, to) - substring */ + if (argc >= 3) { + int from, to; + JSString *p = JS_VALUE_GET_STRING(arg); + int len = p->len; + + if (JS_ToInt32(ctx, &from, argv[1])) + return JS_NULL; + if (JS_ToInt32(ctx, &to, argv[2])) + return JS_NULL; + + /* Adjust negative indices */ + if (from < 0) from += len; + if (to < 0) to += len; + + if (from < 0 || from > to || to > len) + return JS_NULL; + + return js_sub_string(ctx, p, from, to); + } + return JS_DupValue(ctx, arg); + } + + /* Handle blob - convert to text representation */ + blob *bd = js_get_blob(ctx, arg); + if (bd) { + if (!bd->is_stone) + return JS_ThrowTypeError(ctx, "text: blob must be stone"); + + char format = '\0'; + if (argc > 1) { + const char *fmt = JS_ToCString(ctx, argv[1]); + if (fmt) { + format = fmt[0]; + JS_FreeCString(ctx, fmt); + } + } + + size_t byte_len = (bd->length + 7) / 8; + const uint8_t *data = bd->data; + + if (format == 'h') { + /* Hexadecimal encoding */ + static const char hex[] = "0123456789abcdef"; + char *result = js_malloc(ctx, byte_len * 2 + 1); + if (!result) return JS_EXCEPTION; + for (size_t i = 0; i < byte_len; i++) { + result[i * 2] = hex[(data[i] >> 4) & 0xF]; + result[i * 2 + 1] = hex[data[i] & 0xF]; + } + result[byte_len * 2] = '\0'; + JSValue ret = JS_NewString(ctx, result); + js_free(ctx, result); + return ret; + } else if (format == 'b') { + /* Binary encoding */ + char *result = js_malloc(ctx, bd->length + 1); + if (!result) return JS_EXCEPTION; + for (size_t i = 0; i < bd->length; i++) { + size_t byte_idx = i / 8; + size_t bit_idx = i % 8; + result[i] = (data[byte_idx] & (1 << bit_idx)) ? '1' : '0'; + } + result[bd->length] = '\0'; + JSValue ret = JS_NewString(ctx, result); + js_free(ctx, result); + return ret; + } else if (format == 'o') { + /* Octal encoding - 3 bits at a time */ + size_t octal_len = (bd->length + 2) / 3; + char *result = js_malloc(ctx, octal_len + 1); + if (!result) return JS_EXCEPTION; + for (size_t i = 0; i < octal_len; i++) { + int val = 0; + for (int j = 0; j < 3; j++) { + size_t bit_pos = i * 3 + j; + if (bit_pos < bd->length) { + size_t byte_idx = bit_pos / 8; + size_t bit_idx = bit_pos % 8; + if (data[byte_idx] & (1 << bit_idx)) + val |= (1 << j); + } + } + result[i] = '0' + val; + } + result[octal_len] = '\0'; + JSValue ret = JS_NewString(ctx, result); + js_free(ctx, result); + return ret; + } else if (format == 't') { + /* Base32 encoding (RFC 4648) */ + static const char b32[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + size_t b32_len = (bd->length + 4) / 5; + char *result = js_malloc(ctx, b32_len + 1); + if (!result) return JS_EXCEPTION; + for (size_t i = 0; i < b32_len; i++) { + int val = 0; + for (int j = 0; j < 5; j++) { + size_t bit_pos = i * 5 + j; + if (bit_pos < bd->length) { + size_t byte_idx = bit_pos / 8; + size_t bit_idx = bit_pos % 8; + if (data[byte_idx] & (1 << bit_idx)) + val |= (1 << j); + } + } + result[i] = b32[val]; + } + result[b32_len] = '\0'; + JSValue ret = JS_NewString(ctx, result); + js_free(ctx, result); + return ret; + } else { + /* Default: UTF-8 (treat bytes as UTF-8 text) */ + if (bd->length % 8 != 0) + return JS_ThrowTypeError(ctx, "text: blob not byte-aligned for UTF-8"); + return JS_NewStringLen(ctx, (const char *)data, byte_len); + } + } + + /* Handle number */ + if (tag == JS_TAG_INT || tag == JS_TAG_FLOAT64) { + double num; + if (JS_ToFloat64(ctx, &num, arg)) + return JS_EXCEPTION; + + if (argc > 1) { + /* Check for radix (number) or format (string) */ + if (JS_VALUE_GET_TAG(argv[1]) == JS_TAG_INT) { + int radix = JS_VALUE_GET_INT(argv[1]); + return js_cell_number_to_radix_string(ctx, num, radix); + } + if (JS_VALUE_GET_TAG(argv[1]) == JS_TAG_STRING || + JS_VALUE_GET_TAG(argv[1]) == JS_TAG_STRING_ROPE) { + const char *format = JS_ToCString(ctx, argv[1]); + if (!format) return JS_EXCEPTION; + JSValue result = js_cell_format_number(ctx, num, format); + JS_FreeCString(ctx, format); + return result; + } + } + + /* Default: convert to string */ + return JS_ToString(ctx, arg); + } + + /* Handle array */ + if (JS_IsArray(ctx, arg)) { + int64_t len; + if (js_get_length64(ctx, &len, arg)) + return JS_EXCEPTION; + + const char *separator = ""; + if (argc > 1 && (JS_VALUE_GET_TAG(argv[1]) == JS_TAG_STRING || + JS_VALUE_GET_TAG(argv[1]) == JS_TAG_STRING_ROPE)) { + separator = JS_ToCString(ctx, argv[1]); + if (!separator) return JS_EXCEPTION; + } + + StringBuffer b_s, *b = &b_s; + string_buffer_init(ctx, b, 0); + + for (int64_t i = 0; i < len; i++) { + if (i > 0 && separator[0]) { + if (string_buffer_puts8(b, separator)) { + string_buffer_free(b); + if (argc > 1) JS_FreeCString(ctx, separator); + return JS_EXCEPTION; + } + } + JSValue item = JS_GetPropertyInt64(ctx, arg, i); + if (JS_IsException(item)) { + string_buffer_free(b); + if (argc > 1) JS_FreeCString(ctx, separator); + return JS_EXCEPTION; + } + JSValue str = JS_ToString(ctx, item); + JS_FreeValue(ctx, item); + if (JS_IsException(str)) { + string_buffer_free(b); + if (argc > 1) JS_FreeCString(ctx, separator); + return JS_EXCEPTION; + } + if (string_buffer_concat_value_free(b, str)) { + string_buffer_free(b); + if (argc > 1) JS_FreeCString(ctx, separator); + return JS_EXCEPTION; + } + } + + if (argc > 1) JS_FreeCString(ctx, separator); + return string_buffer_end(b); + } + + /* Handle null */ + if (JS_IsNull(arg)) + return JS_NULL; + + /* Default: convert to string */ + return JS_ToString(ctx, arg); +} + +/* text.lower(str) - convert to lowercase */ +static JSValue js_cell_text_lower(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_NULL; + int tag = JS_VALUE_GET_TAG(argv[0]); + if (tag != JS_TAG_STRING && tag != JS_TAG_STRING_ROPE) + return JS_NULL; + + JSValue str = JS_ToString(ctx, argv[0]); + if (JS_IsException(str)) return str; + + JSString *p = JS_VALUE_GET_STRING(str); + StringBuffer b_s, *b = &b_s; + string_buffer_init(ctx, b, p->len); + + for (int i = 0; i < p->len; i++) { + uint32_t c = string_get(p, i); + if (c >= 'A' && c <= 'Z') + c = c - 'A' + 'a'; + if (string_buffer_putc(b, c)) { + string_buffer_free(b); + JS_FreeValue(ctx, str); + return JS_EXCEPTION; + } + } + + JS_FreeValue(ctx, str); + return string_buffer_end(b); +} + +/* text.upper(str) - convert to uppercase */ +static JSValue js_cell_text_upper(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_NULL; + int tag = JS_VALUE_GET_TAG(argv[0]); + if (tag != JS_TAG_STRING && tag != JS_TAG_STRING_ROPE) + return JS_NULL; + + JSValue str = JS_ToString(ctx, argv[0]); + if (JS_IsException(str)) return str; + + JSString *p = JS_VALUE_GET_STRING(str); + StringBuffer b_s, *b = &b_s; + string_buffer_init(ctx, b, p->len); + + for (int i = 0; i < p->len; i++) { + uint32_t c = string_get(p, i); + if (c >= 'a' && c <= 'z') + c = c - 'a' + 'A'; + if (string_buffer_putc(b, c)) { + string_buffer_free(b); + JS_FreeValue(ctx, str); + return JS_EXCEPTION; + } + } + + JS_FreeValue(ctx, str); + return string_buffer_end(b); +} + +/* text.trim(str, reject) - trim whitespace or custom characters */ +static JSValue js_cell_text_trim(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_NULL; + int tag = JS_VALUE_GET_TAG(argv[0]); + if (tag != JS_TAG_STRING && tag != JS_TAG_STRING_ROPE) + return JS_NULL; + + JSValue str = JS_ToString(ctx, argv[0]); + if (JS_IsException(str)) return str; + + JSString *p = JS_VALUE_GET_STRING(str); + int start = 0; + int end = p->len; + + if (argc > 1 && !JS_IsNull(argv[1])) { + /* Custom trim with reject characters */ + const char *reject = JS_ToCString(ctx, argv[1]); + if (!reject) { + JS_FreeValue(ctx, str); + return JS_EXCEPTION; + } + size_t reject_len = strlen(reject); + + while (start < end) { + uint32_t c = string_get(p, start); + int found = 0; + for (size_t i = 0; i < reject_len; i++) { + if (c == (uint8_t)reject[i]) { found = 1; break; } + } + if (!found) break; + start++; + } + while (end > start) { + uint32_t c = string_get(p, end - 1); + int found = 0; + for (size_t i = 0; i < reject_len; i++) { + if (c == (uint8_t)reject[i]) { found = 1; break; } + } + if (!found) break; + end--; + } + JS_FreeCString(ctx, reject); + } else { + /* Default: trim whitespace */ + while (start < end && lre_is_space(string_get(p, start))) + start++; + while (end > start && lre_is_space(string_get(p, end - 1))) + end--; + } + + JSValue result = js_sub_string(ctx, p, start, end); + JS_FreeValue(ctx, str); + return result; +} + +/* text.codepoint(str) - get first codepoint */ +static JSValue js_cell_text_codepoint(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_NULL; + int tag = JS_VALUE_GET_TAG(argv[0]); + if (tag != JS_TAG_STRING && tag != JS_TAG_STRING_ROPE) + return JS_NULL; + + JSValue str = JS_ToString(ctx, argv[0]); + if (JS_IsException(str)) return str; + + JSString *p = JS_VALUE_GET_STRING(str); + if (p->len == 0) { + JS_FreeValue(ctx, str); + return JS_NULL; + } + + uint32_t c = string_get(p, 0); + /* Handle surrogate pairs */ + if (c >= 0xD800 && c <= 0xDBFF && p->len > 1) { + uint32_t c2 = string_get(p, 1); + if (c2 >= 0xDC00 && c2 <= 0xDFFF) { + c = 0x10000 + ((c - 0xD800) << 10) + (c2 - 0xDC00); + } + } + + JS_FreeValue(ctx, str); + return JS_NewInt32(ctx, c); +} + +/* text.search(str, target, from) - find substring */ +static JSValue js_cell_text_search(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_NULL; + + int tag1 = JS_VALUE_GET_TAG(argv[0]); + int tag2 = JS_VALUE_GET_TAG(argv[1]); + if ((tag1 != JS_TAG_STRING && tag1 != JS_TAG_STRING_ROPE) || + (tag2 != JS_TAG_STRING && tag2 != JS_TAG_STRING_ROPE)) + return JS_NULL; + + JSValue str = JS_ToString(ctx, argv[0]); + if (JS_IsException(str)) return str; + + JSValue target = JS_ToString(ctx, argv[1]); + if (JS_IsException(target)) { + JS_FreeValue(ctx, str); + return target; + } + + JSString *p = JS_VALUE_GET_STRING(str); + JSString *t = JS_VALUE_GET_STRING(target); + + int from = 0; + if (argc > 2 && !JS_IsNull(argv[2])) { + if (JS_ToInt32(ctx, &from, argv[2])) { + JS_FreeValue(ctx, str); + JS_FreeValue(ctx, target); + return JS_NULL; + } + if (from < 0) from += p->len; + if (from < 0) from = 0; + } + + int result = -1; + int len = p->len; + int t_len = t->len; + + if (len >= t_len) { + for (int i = from; i <= len - t_len; i++) { + if (!string_cmp(p, t, i, 0, t_len)) { + result = i; + break; + } + } + } + + JS_FreeValue(ctx, str); + JS_FreeValue(ctx, target); + + if (result == -1) return JS_NULL; + return JS_NewInt32(ctx, result); +} + +/* text.replace(str, target, replacement, limit) - replace substrings */ +static JSValue js_cell_text_replace(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_NULL; + + int tag1 = JS_VALUE_GET_TAG(argv[0]); + int tag2 = JS_VALUE_GET_TAG(argv[1]); + if ((tag1 != JS_TAG_STRING && tag1 != JS_TAG_STRING_ROPE) || + (tag2 != JS_TAG_STRING && tag2 != JS_TAG_STRING_ROPE)) + return JS_NULL; + + JSValue str = JS_ToString(ctx, argv[0]); + if (JS_IsException(str)) return str; + + JSValue target = JS_ToString(ctx, argv[1]); + if (JS_IsException(target)) { + JS_FreeValue(ctx, str); + return target; + } + + JSString *sp = JS_VALUE_GET_STRING(str); + JSString *tp = JS_VALUE_GET_STRING(target); + + int limit = -1; /* -1 means unlimited */ + if (argc > 3 && !JS_IsNull(argv[3])) { + if (JS_ToInt32(ctx, &limit, argv[3])) { + JS_FreeValue(ctx, str); + JS_FreeValue(ctx, target); + return JS_NULL; + } + } + + StringBuffer b_s, *b = &b_s; + string_buffer_init(ctx, b, sp->len); + + int pos = 0; + int count = 0; + int len = sp->len; + int t_len = tp->len; + + while (pos <= len - t_len && (limit < 0 || count < limit)) { + int found = -1; + for (int i = pos; i <= len - t_len; i++) { + if (!string_cmp(sp, tp, i, 0, t_len)) { + found = i; + break; + } + } + if (found < 0) break; + + /* Copy up to the match */ + if (found > pos) { + JSValue sub = js_sub_string(ctx, sp, pos, found); + if (JS_IsException(sub)) goto fail; + if (string_buffer_concat_value_free(b, sub)) goto fail; + } + + /* Get replacement */ + JSValue rep; + if (argc > 2 && JS_IsFunction(ctx, argv[2])) { + JSValue args[2]; + args[0] = JS_NewStringLen(ctx, (const char *)tp->u.str8, t_len); + args[1] = JS_NewInt32(ctx, found); + rep = JS_Call(ctx, argv[2], JS_NULL, 2, args); + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); + if (JS_IsException(rep)) goto fail; + if (JS_IsNull(rep)) { + pos = found + t_len; + count++; + continue; + } + } else if (argc > 2) { + rep = JS_DupValue(ctx, argv[2]); + } else { + rep = JS_AtomToString(ctx, JS_ATOM_empty_string); + } + + if (!JS_IsNull(rep)) { + JSValue rep_str = JS_ToString(ctx, rep); + JS_FreeValue(ctx, rep); + if (JS_IsException(rep_str)) goto fail; + if (string_buffer_concat_value_free(b, rep_str)) goto fail; + } + + pos = found + t_len; + count++; + } + + /* Copy remainder */ + if (pos < len) { + JSValue sub = js_sub_string(ctx, sp, pos, len); + if (JS_IsException(sub)) goto fail; + if (string_buffer_concat_value_free(b, sub)) goto fail; + } + + JS_FreeValue(ctx, str); + JS_FreeValue(ctx, target); + return string_buffer_end(b); + +fail: + string_buffer_free(b); + JS_FreeValue(ctx, str); + JS_FreeValue(ctx, target); + return JS_EXCEPTION; +} + +static const JSCFunctionListEntry js_cell_text_funcs[] = { + JS_CFUNC_DEF("lower", 1, js_cell_text_lower), + JS_CFUNC_DEF("upper", 1, js_cell_text_upper), + JS_CFUNC_DEF("trim", 2, js_cell_text_trim), + JS_CFUNC_DEF("codepoint", 1, js_cell_text_codepoint), + JS_CFUNC_DEF("search", 3, js_cell_text_search), + JS_CFUNC_DEF("replace", 4, js_cell_text_replace), +}; + +/* ---------------------------------------------------------------------------- + * array function and sub-functions + * ---------------------------------------------------------------------------- */ + +/* array(arg, arg2, arg3, arg4) - main array function */ +static JSValue js_cell_array(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_NULL; + + JSValue arg = argv[0]; + int tag = JS_VALUE_GET_TAG(arg); + + /* array(number) - create array of size */ + /* array(number, initial_value) - create array with initial values */ + if (tag == JS_TAG_INT || tag == JS_TAG_FLOAT64) { + double d; + if (JS_ToFloat64(ctx, &d, arg)) + return JS_NULL; + if (d < 0) return JS_NULL; + int64_t len = (int64_t)floor(d); + + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) return result; + + if (argc < 2 || JS_IsNull(argv[1])) { + /* Just set length */ + JS_SetPropertyStr(ctx, result, "length", JS_NewInt64(ctx, len > 100 ? 100 : len)); + } else if (JS_IsFunction(ctx, argv[1])) { + /* Fill with function results */ + JSValueConst func = argv[1]; + int arity = 0; + JSValue len_val = JS_GetPropertyStr(ctx, func, "length"); + if (!JS_IsException(len_val)) { + JS_ToInt32(ctx, &arity, len_val); + JS_FreeValue(ctx, len_val); + } + + for (int64_t i = 0; i < len; i++) { + JSValue args[1] = { JS_NewInt64(ctx, i) }; + JSValue val = arity >= 1 ? JS_Call(ctx, func, JS_NULL, 1, args) + : JS_Call(ctx, func, JS_NULL, 0, NULL); + JS_FreeValue(ctx, args[0]); + if (JS_IsException(val)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetPropertyInt64(ctx, result, i, val); + } + } else { + /* Fill with value */ + for (int64_t i = 0; i < len; i++) { + JS_SetPropertyInt64(ctx, result, i, JS_DupValue(ctx, argv[1])); + } + } + return result; + } + + /* array(array) - copy */ + /* array(array, function) - map */ + /* array(array, another_array) - concat */ + /* array(array, from, to) - slice */ + if (JS_IsArray(ctx, arg)) { + int64_t len; + if (js_get_length64(ctx, &len, arg)) + return JS_EXCEPTION; + + if (argc < 2 || JS_IsNull(argv[1])) { + /* Copy */ + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) return result; + for (int64_t i = 0; i < len; i++) { + JSValue val = JS_GetPropertyInt64(ctx, arg, i); + if (JS_IsException(val)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetPropertyInt64(ctx, result, i, val); + } + return result; + } + + if (JS_IsFunction(ctx, argv[1])) { + /* Map */ + JSValueConst func = argv[1]; + int reverse = argc > 2 && JS_ToBool(ctx, argv[2]); + JSValue exit_val = argc > 3 ? argv[3] : JS_NULL; + + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) return result; + + if (reverse) { + for (int64_t i = len - 1; i >= 0; i--) { + JSValue item = JS_GetPropertyInt64(ctx, arg, i); + if (JS_IsException(item)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JSValue args[2] = { item, JS_NewInt64(ctx, i) }; + JSValue val = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); + if (JS_IsException(val)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + if (!JS_IsNull(exit_val) && js_strict_eq(ctx, val, exit_val)) { + JS_FreeValue(ctx, val); + break; + } + JS_SetPropertyInt64(ctx, result, i, val); + } + } else { + int64_t out_idx = 0; + for (int64_t i = 0; i < len; i++) { + JSValue item = JS_GetPropertyInt64(ctx, arg, i); + if (JS_IsException(item)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JSValue args[2] = { item, JS_NewInt64(ctx, i) }; + JSValue val = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); + if (JS_IsException(val)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + if (!JS_IsNull(exit_val) && js_strict_eq(ctx, val, exit_val)) { + JS_FreeValue(ctx, val); + break; + } + JS_SetPropertyInt64(ctx, result, out_idx++, val); + } + } + return result; + } + + if (JS_IsArray(ctx, argv[1])) { + /* Concat */ + int64_t len2; + if (js_get_length64(ctx, &len2, argv[1])) + return JS_EXCEPTION; + + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) return result; + + int64_t idx = 0; + for (int64_t i = 0; i < len; i++) { + JSValue val = JS_GetPropertyInt64(ctx, arg, i); + if (JS_IsException(val)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetPropertyInt64(ctx, result, idx++, val); + } + for (int64_t i = 0; i < len2; i++) { + JSValue val = JS_GetPropertyInt64(ctx, argv[1], i); + if (JS_IsException(val)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetPropertyInt64(ctx, result, idx++, val); + } + return result; + } + + if (tag == JS_TAG_INT || tag == JS_TAG_FLOAT64 || + JS_VALUE_GET_TAG(argv[1]) == JS_TAG_INT || + JS_VALUE_GET_TAG(argv[1]) == JS_TAG_FLOAT64) { + /* Slice */ + int from, to; + if (JS_ToInt32(ctx, &from, argv[1])) + return JS_NULL; + if (argc > 2 && !JS_IsNull(argv[2])) { + if (JS_ToInt32(ctx, &to, argv[2])) + return JS_NULL; + } else { + to = len; + } + + if (from < 0) from += len; + if (to < 0) to += len; + if (from < 0 || from > to || to > len) + return JS_NULL; + + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) return result; + + int64_t idx = 0; + for (int i = from; i < to; i++) { + JSValue val = JS_GetPropertyInt64(ctx, arg, i); + if (JS_IsException(val)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetPropertyInt64(ctx, result, idx++, val); + } + return result; + } + + return JS_NULL; + } + + /* array(object) - keys */ + if (JS_IsObject(arg) && !JS_IsArray(ctx, arg)) { + /* Check if it's a Set */ + JSObject *p = JS_VALUE_GET_OBJ(arg); + if (p->class_id == JS_CLASS_SET) { + /* Convert Set to array */ + JSValue iter = JS_GetProperty(ctx, arg, JS_ATOM_Symbol_iterator); + if (JS_IsException(iter)) return iter; + JSValue iter_obj = JS_Call(ctx, iter, arg, 0, NULL); + JS_FreeValue(ctx, iter); + if (JS_IsException(iter_obj)) return iter_obj; + + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) { + JS_FreeValue(ctx, iter_obj); + return result; + } + + int64_t idx = 0; + while (1) { + JSValue next = JS_Call(ctx, JS_GetProperty(ctx, iter_obj, JS_ATOM_next), iter_obj, 0, NULL); + if (JS_IsException(next)) { + JS_FreeValue(ctx, iter_obj); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JSValue done = JS_GetProperty(ctx, next, JS_ATOM_done); + if (JS_ToBool(ctx, done)) { + JS_FreeValue(ctx, done); + JS_FreeValue(ctx, next); + break; + } + JS_FreeValue(ctx, done); + JSValue value = JS_GetProperty(ctx, next, JS_ATOM_value); + JS_FreeValue(ctx, next); + JS_SetPropertyInt64(ctx, result, idx++, value); + } + JS_FreeValue(ctx, iter_obj); + return result; + } + + /* Return object keys */ + return JS_GetOwnPropertyNames2(ctx, arg, + JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK, + JS_ITERATOR_KIND_KEY); + } + + /* array(text) - split into characters */ + /* array(text, separator) - split by separator */ + /* array(text, length) - dice into chunks */ + if (tag == JS_TAG_STRING || tag == JS_TAG_STRING_ROPE) { + JSValue str = JS_ToString(ctx, arg); + if (JS_IsException(str)) return str; + + JSString *p = JS_VALUE_GET_STRING(str); + int len = p->len; + + if (argc < 2 || JS_IsNull(argv[1])) { + /* Split into characters */ + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) { + JS_FreeValue(ctx, str); + return result; + } + for (int i = 0; i < len; i++) { + JSValue ch = js_sub_string(ctx, p, i, i + 1); + if (JS_IsException(ch)) { + JS_FreeValue(ctx, str); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetPropertyInt64(ctx, result, i, ch); + } + JS_FreeValue(ctx, str); + return result; + } + + int tag2 = JS_VALUE_GET_TAG(argv[1]); + if (tag2 == JS_TAG_STRING || tag2 == JS_TAG_STRING_ROPE) { + /* Split by separator */ + const char *cstr = JS_ToCString(ctx, str); + const char *sep = JS_ToCString(ctx, argv[1]); + if (!cstr || !sep) { + if (cstr) JS_FreeCString(ctx, cstr); + if (sep) JS_FreeCString(ctx, sep); + JS_FreeValue(ctx, str); + return JS_EXCEPTION; + } + + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) { + JS_FreeCString(ctx, cstr); + JS_FreeCString(ctx, sep); + JS_FreeValue(ctx, str); + return result; + } + + int64_t idx = 0; + size_t sep_len = strlen(sep); + const char *pos = cstr; + const char *found; + + if (sep_len == 0) { + /* Split into characters */ + for (int i = 0; i < len; i++) { + JSValue ch = js_sub_string(ctx, p, i, i + 1); + JS_SetPropertyInt64(ctx, result, idx++, ch); + } + } else { + while ((found = strstr(pos, sep)) != NULL) { + JSValue part = JS_NewStringLen(ctx, pos, found - pos); + JS_SetPropertyInt64(ctx, result, idx++, part); + pos = found + sep_len; + } + JSValue part = JS_NewString(ctx, pos); + JS_SetPropertyInt64(ctx, result, idx++, part); + } + + JS_FreeCString(ctx, cstr); + JS_FreeCString(ctx, sep); + JS_FreeValue(ctx, str); + return result; + } + + if (tag2 == JS_TAG_INT || tag2 == JS_TAG_FLOAT64) { + /* Dice into chunks */ + int chunk_len; + if (JS_ToInt32(ctx, &chunk_len, argv[1])) { + JS_FreeValue(ctx, str); + return JS_NULL; + } + if (chunk_len <= 0) { + JS_FreeValue(ctx, str); + return JS_NULL; + } + + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) { + JS_FreeValue(ctx, str); + return result; + } + + int64_t idx = 0; + for (int i = 0; i < len; i += chunk_len) { + int end = i + chunk_len; + if (end > len) end = len; + JSValue chunk = js_sub_string(ctx, p, i, end); + if (JS_IsException(chunk)) { + JS_FreeValue(ctx, str); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetPropertyInt64(ctx, result, idx++, chunk); + } + + JS_FreeValue(ctx, str); + return result; + } + + JS_FreeValue(ctx, str); + return JS_NULL; + } + + return JS_NULL; +} + +/* array.reduce(arr, fn, initial, reverse) */ +static JSValue js_cell_array_reduce(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_NULL; + if (!JS_IsArray(ctx, argv[0])) return JS_NULL; + if (!JS_IsFunction(ctx, argv[1])) return JS_NULL; + + JSValueConst arr = argv[0]; + JSValueConst func = argv[1]; + int64_t len; + if (js_get_length64(ctx, &len, arr)) + return JS_EXCEPTION; + + int reverse = argc > 3 && JS_ToBool(ctx, argv[3]); + JSValue acc; + + if (argc < 3 || JS_IsNull(argv[2])) { + if (len == 0) return JS_NULL; + if (len == 1) return JS_GetPropertyInt64(ctx, arr, 0); + + if (reverse) { + acc = JS_GetPropertyInt64(ctx, arr, len - 1); + for (int64_t i = len - 2; i >= 0; i--) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) { + JS_FreeValue(ctx, acc); + return JS_EXCEPTION; + } + JSValue args[2] = { acc, item }; + JSValue new_acc = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, acc); + JS_FreeValue(ctx, item); + if (JS_IsException(new_acc)) return JS_EXCEPTION; + acc = new_acc; + } + } else { + acc = JS_GetPropertyInt64(ctx, arr, 0); + for (int64_t i = 1; i < len; i++) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) { + JS_FreeValue(ctx, acc); + return JS_EXCEPTION; + } + JSValue args[2] = { acc, item }; + JSValue new_acc = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, acc); + JS_FreeValue(ctx, item); + if (JS_IsException(new_acc)) return JS_EXCEPTION; + acc = new_acc; + } + } + } else { + if (len == 0) return JS_DupValue(ctx, argv[2]); + acc = JS_DupValue(ctx, argv[2]); + + if (reverse) { + for (int64_t i = len - 1; i >= 0; i--) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) { + JS_FreeValue(ctx, acc); + return JS_EXCEPTION; + } + JSValue args[2] = { acc, item }; + JSValue new_acc = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, acc); + JS_FreeValue(ctx, item); + if (JS_IsException(new_acc)) return JS_EXCEPTION; + acc = new_acc; + } + } else { + for (int64_t i = 0; i < len; i++) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) { + JS_FreeValue(ctx, acc); + return JS_EXCEPTION; + } + JSValue args[2] = { acc, item }; + JSValue new_acc = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, acc); + JS_FreeValue(ctx, item); + if (JS_IsException(new_acc)) return JS_EXCEPTION; + acc = new_acc; + } + } + } + + return acc; +} + +/* array.for(arr, fn, reverse, exit) */ +static JSValue js_cell_array_for(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_NULL; + if (!JS_IsArray(ctx, argv[0])) return JS_NULL; + if (!JS_IsFunction(ctx, argv[1])) return JS_NULL; + + JSValueConst arr = argv[0]; + JSValueConst func = argv[1]; + int64_t len; + if (js_get_length64(ctx, &len, arr)) + return JS_EXCEPTION; + if (len == 0) return JS_NULL; + + int reverse = argc > 2 && JS_ToBool(ctx, argv[2]); + JSValue exit_val = argc > 3 ? argv[3] : JS_NULL; + + if (reverse) { + for (int64_t i = len - 1; i >= 0; i--) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) return JS_EXCEPTION; + JSValue args[2] = { item, JS_NewInt64(ctx, i) }; + JSValue result = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, args[1]); + if (JS_IsException(result)) return JS_EXCEPTION; + if (!JS_IsNull(exit_val) && js_strict_eq(ctx, result, exit_val)) { + return result; + } + JS_FreeValue(ctx, result); + } + } else { + for (int64_t i = 0; i < len; i++) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) return JS_EXCEPTION; + JSValue args[2] = { item, JS_NewInt64(ctx, i) }; + JSValue result = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, args[1]); + if (JS_IsException(result)) return JS_EXCEPTION; + if (!JS_IsNull(exit_val) && js_strict_eq(ctx, result, exit_val)) { + return result; + } + JS_FreeValue(ctx, result); + } + } + + return JS_NULL; +} + +/* array.find(arr, fn, reverse, from) */ +static JSValue js_cell_array_find(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_NULL; + if (!JS_IsArray(ctx, argv[0])) return JS_NULL; + + JSValueConst arr = argv[0]; + int64_t len; + if (js_get_length64(ctx, &len, arr)) + return JS_EXCEPTION; + + int reverse = argc > 2 && JS_ToBool(ctx, argv[2]); + int64_t from; + if (argc > 3 && !JS_IsNull(argv[3])) { + if (JS_ToInt64(ctx, &from, argv[3])) + return JS_NULL; + } else { + from = reverse ? len - 1 : 0; + } + + if (!JS_IsFunction(ctx, argv[1])) { + /* Compare exactly */ + JSValue target = argv[1]; + if (reverse) { + for (int64_t i = from; i >= 0; i--) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) return JS_EXCEPTION; + if (js_strict_eq(ctx, item, target)) { + JS_FreeValue(ctx, item); + return JS_NewInt64(ctx, i); + } + JS_FreeValue(ctx, item); + } + } else { + for (int64_t i = from; i < len; i++) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) return JS_EXCEPTION; + if (js_strict_eq(ctx, item, target)) { + JS_FreeValue(ctx, item); + return JS_NewInt64(ctx, i); + } + JS_FreeValue(ctx, item); + } + } + return JS_NULL; + } + + /* Use function predicate */ + JSValueConst func = argv[1]; + if (reverse) { + for (int64_t i = from; i >= 0; i--) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) return JS_EXCEPTION; + JSValue args[2] = { item, JS_NewInt64(ctx, i) }; + JSValue result = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, args[1]); + if (JS_IsException(result)) return JS_EXCEPTION; + if (JS_ToBool(ctx, result)) { + JS_FreeValue(ctx, result); + return JS_NewInt64(ctx, i); + } + JS_FreeValue(ctx, result); + } + } else { + for (int64_t i = from; i < len; i++) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) return JS_EXCEPTION; + JSValue args[2] = { item, JS_NewInt64(ctx, i) }; + JSValue result = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, args[1]); + if (JS_IsException(result)) return JS_EXCEPTION; + if (JS_ToBool(ctx, result)) { + JS_FreeValue(ctx, result); + return JS_NewInt64(ctx, i); + } + JS_FreeValue(ctx, result); + } + } + + return JS_NULL; +} + +/* array.filter(arr, fn) */ +static JSValue js_cell_array_filter(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_NULL; + if (!JS_IsArray(ctx, argv[0])) return JS_NULL; + if (!JS_IsFunction(ctx, argv[1])) return JS_NULL; + + JSValueConst arr = argv[0]; + JSValueConst func = argv[1]; + int64_t len; + if (js_get_length64(ctx, &len, arr)) + return JS_EXCEPTION; + + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) return result; + + int64_t out_idx = 0; + for (int64_t i = 0; i < len; i++) { + JSValue item = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(item)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JSValue args[2] = { item, JS_NewInt64(ctx, i) }; + JSValue val = JS_Call(ctx, func, JS_NULL, 2, args); + JS_FreeValue(ctx, args[1]); + if (JS_IsException(val)) { + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + if (JS_VALUE_GET_TAG(val) == JS_TAG_BOOL) { + if (JS_VALUE_GET_BOOL(val)) { + JS_SetPropertyInt64(ctx, result, out_idx++, item); + } else { + JS_FreeValue(ctx, item); + } + } else { + JS_FreeValue(ctx, item); + JS_FreeValue(ctx, val); + JS_FreeValue(ctx, result); + return JS_NULL; + } + JS_FreeValue(ctx, val); + } + + return result; +} + +/* array.sort(arr, select) */ +static JSValue js_cell_array_sort(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_NULL; + if (!JS_IsArray(ctx, argv[0])) return JS_NULL; + + JSValueConst arr = argv[0]; + int64_t len; + if (js_get_length64(ctx, &len, arr)) + return JS_EXCEPTION; + + /* Copy array */ + JSValue result = JS_NewArray(ctx); + if (JS_IsException(result)) return result; + + JSValue *items = js_malloc(ctx, sizeof(JSValue) * len); + double *keys = js_malloc(ctx, sizeof(double) * len); + char **str_keys = NULL; + int is_string = 0; + + if (!items || !keys) { + if (items) js_free(ctx, items); + if (keys) js_free(ctx, keys); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + + /* Extract keys */ + for (int64_t i = 0; i < len; i++) { + items[i] = JS_GetPropertyInt64(ctx, arr, i); + if (JS_IsException(items[i])) { + for (int64_t j = 0; j < i; j++) JS_FreeValue(ctx, items[j]); + js_free(ctx, items); + js_free(ctx, keys); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + + JSValue key; + if (argc < 2 || JS_IsNull(argv[1])) { + key = JS_DupValue(ctx, items[i]); + } else if (JS_VALUE_GET_TAG(argv[1]) == JS_TAG_STRING || + JS_VALUE_GET_TAG(argv[1]) == JS_TAG_INT) { + key = JS_GetProperty(ctx, items[i], JS_ValueToAtom(ctx, argv[1])); + } else if (JS_IsArray(ctx, argv[1])) { + key = JS_GetPropertyInt64(ctx, argv[1], i); + } else { + key = JS_DupValue(ctx, items[i]); + } + + if (JS_IsException(key)) { + for (int64_t j = 0; j <= i; j++) JS_FreeValue(ctx, items[j]); + js_free(ctx, items); + js_free(ctx, keys); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + + int key_tag = JS_VALUE_GET_TAG(key); + if (key_tag == JS_TAG_INT || key_tag == JS_TAG_FLOAT64) { + JS_ToFloat64(ctx, &keys[i], key); + if (i == 0) is_string = 0; + } else if (key_tag == JS_TAG_STRING || key_tag == JS_TAG_STRING_ROPE) { + if (i == 0) { + is_string = 1; + str_keys = js_malloc(ctx, sizeof(char*) * len); + if (!str_keys) { + JS_FreeValue(ctx, key); + for (int64_t j = 0; j <= i; j++) JS_FreeValue(ctx, items[j]); + js_free(ctx, items); + js_free(ctx, keys); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + } + if (is_string) { + str_keys[i] = (char*)JS_ToCString(ctx, key); + } + } else { + JS_FreeValue(ctx, key); + for (int64_t j = 0; j <= i; j++) JS_FreeValue(ctx, items[j]); + js_free(ctx, items); + js_free(ctx, keys); + if (str_keys) { + for (int64_t j = 0; j < i; j++) JS_FreeCString(ctx, str_keys[j]); + js_free(ctx, str_keys); + } + JS_FreeValue(ctx, result); + return JS_NULL; + } + JS_FreeValue(ctx, key); + } + + /* Create index array */ + int64_t *indices = js_malloc(ctx, sizeof(int64_t) * len); + if (!indices) { + for (int64_t j = 0; j < len; j++) JS_FreeValue(ctx, items[j]); + js_free(ctx, items); + js_free(ctx, keys); + if (str_keys) { + for (int64_t j = 0; j < len; j++) JS_FreeCString(ctx, str_keys[j]); + js_free(ctx, str_keys); + } + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + for (int64_t i = 0; i < len; i++) indices[i] = i; + + /* Simple insertion sort (stable) */ + for (int64_t i = 1; i < len; i++) { + int64_t temp = indices[i]; + int64_t j = i - 1; + while (j >= 0) { + int cmp; + if (is_string) { + cmp = strcmp(str_keys[indices[j]], str_keys[temp]); + } else { + double a = keys[indices[j]], b = keys[temp]; + cmp = (a > b) - (a < b); + } + if (cmp <= 0) break; + indices[j + 1] = indices[j]; + j--; + } + indices[j + 1] = temp; + } + + /* Build sorted array */ + for (int64_t i = 0; i < len; i++) { + JS_SetPropertyInt64(ctx, result, i, JS_DupValue(ctx, items[indices[i]])); + } + + /* Cleanup */ + for (int64_t i = 0; i < len; i++) JS_FreeValue(ctx, items[i]); + js_free(ctx, items); + js_free(ctx, keys); + js_free(ctx, indices); + if (str_keys) { + for (int64_t i = 0; i < len; i++) JS_FreeCString(ctx, str_keys[i]); + js_free(ctx, str_keys); + } + + return result; +} + +static const JSCFunctionListEntry js_cell_array_funcs[] = { + JS_CFUNC_DEF("reduce", 4, js_cell_array_reduce), + JS_CFUNC_DEF("for", 4, js_cell_array_for), + JS_CFUNC_DEF("find", 4, js_cell_array_find), + JS_CFUNC_DEF("filter", 2, js_cell_array_filter), + JS_CFUNC_DEF("sort", 2, js_cell_array_sort), +}; + +/* ---------------------------------------------------------------------------- + * object function and sub-functions + * ---------------------------------------------------------------------------- */ + +/* object(arg, arg2) - main object function */ +static JSValue js_cell_object(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_NULL; + + JSValue arg = argv[0]; + + /* object(object) - shallow mutable copy */ + if (JS_IsObject(arg) && !JS_IsArray(ctx, arg) && !JS_IsFunction(ctx, arg)) { + if (argc < 2 || JS_IsNull(argv[1])) { + /* Shallow copy */ + JSValue result = JS_NewObject(ctx); + if (JS_IsException(result)) return result; + + JSPropertyEnum *props; + uint32_t len; + if (JS_GetOwnPropertyNames(ctx, &props, &len, arg, + JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + + for (uint32_t i = 0; i < len; i++) { + JSValue val = JS_GetProperty(ctx, arg, props[i].atom); + if (JS_IsException(val)) { + JS_FreePropertyEnum(ctx, props, len); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetProperty(ctx, result, props[i].atom, val); + } + JS_FreePropertyEnum(ctx, props, len); + return result; + } + + /* object(object, another_object) - combine */ + if (JS_IsObject(argv[1]) && !JS_IsArray(ctx, argv[1])) { + JSValue result = JS_NewObject(ctx); + if (JS_IsException(result)) return result; + + /* Copy from first object */ + JSPropertyEnum *props; + uint32_t len; + if (JS_GetOwnPropertyNames(ctx, &props, &len, arg, + JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + for (uint32_t i = 0; i < len; i++) { + JSValue val = JS_GetProperty(ctx, arg, props[i].atom); + if (JS_IsException(val)) { + JS_FreePropertyEnum(ctx, props, len); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetProperty(ctx, result, props[i].atom, val); + } + JS_FreePropertyEnum(ctx, props, len); + + /* Copy from second object */ + if (JS_GetOwnPropertyNames(ctx, &props, &len, argv[1], + JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + for (uint32_t i = 0; i < len; i++) { + JSValue val = JS_GetProperty(ctx, argv[1], props[i].atom); + if (JS_IsException(val)) { + JS_FreePropertyEnum(ctx, props, len); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + JS_SetProperty(ctx, result, props[i].atom, val); + } + JS_FreePropertyEnum(ctx, props, len); + return result; + } + + /* object(object, array_of_keys) - select */ + if (JS_IsArray(ctx, argv[1])) { + JSValue result = JS_NewObject(ctx); + if (JS_IsException(result)) return result; + + int64_t len; + if (js_get_length64(ctx, &len, argv[1])) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + + for (int64_t i = 0; i < len; i++) { + JSValue key = JS_GetPropertyInt64(ctx, argv[1], i); + if (JS_IsException(key)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + int key_tag = JS_VALUE_GET_TAG(key); + if (key_tag == JS_TAG_STRING || key_tag == JS_TAG_STRING_ROPE) { + JSAtom atom = JS_ValueToAtom(ctx, key); + int has = JS_HasProperty(ctx, arg, atom); + if (has > 0) { + JSValue val = JS_GetProperty(ctx, arg, atom); + if (!JS_IsException(val)) { + JS_SetProperty(ctx, result, atom, val); + } + } + JS_FreeAtom(ctx, atom); + } + JS_FreeValue(ctx, key); + } + return result; + } + } + + /* object(array_of_keys) - set with true values */ + /* object(array_of_keys, value) - value set */ + /* object(array_of_keys, function) - functional value set */ + if (JS_IsArray(ctx, arg)) { + int64_t len; + if (js_get_length64(ctx, &len, arg)) + return JS_EXCEPTION; + + JSValue result = JS_NewObject(ctx); + if (JS_IsException(result)) return result; + + for (int64_t i = 0; i < len; i++) { + JSValue key = JS_GetPropertyInt64(ctx, arg, i); + if (JS_IsException(key)) { + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + int key_tag = JS_VALUE_GET_TAG(key); + if (key_tag == JS_TAG_STRING || key_tag == JS_TAG_STRING_ROPE) { + JSAtom atom = JS_ValueToAtom(ctx, key); + JSValue val; + if (argc < 2 || JS_IsNull(argv[1])) { + val = JS_TRUE; + } else if (JS_IsFunction(ctx, argv[1])) { + val = JS_Call(ctx, argv[1], JS_NULL, 1, &key); + if (JS_IsException(val)) { + JS_FreeAtom(ctx, atom); + JS_FreeValue(ctx, key); + JS_FreeValue(ctx, result); + return JS_EXCEPTION; + } + } else { + val = JS_DupValue(ctx, argv[1]); + } + JS_SetProperty(ctx, result, atom, val); + JS_FreeAtom(ctx, atom); + } + JS_FreeValue(ctx, key); + } + return result; + } + + return JS_NULL; +} + +/* object.values(obj) */ +static JSValue js_cell_object_values(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_NULL; + return JS_GetOwnPropertyNames2(ctx, argv[0], + JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK, + JS_ITERATOR_KIND_VALUE); +} + +/* object.assign(obj, ...args) */ +static JSValue js_cell_object_assign(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_NULL; + return js_object_assign(ctx, this_val, argc, argv); +} + +/* object.has(obj, key) - check if object has own property */ +static JSValue js_cell_object_has(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_NULL; + if (!JS_IsObject(argv[0])) return JS_NULL; + + JSAtom atom = JS_ValueToAtom(ctx, argv[1]); + if (atom == JS_ATOM_NULL) return JS_EXCEPTION; + + int ret = JS_HasProperty(ctx, argv[0], atom); + JS_FreeAtom(ctx, atom); + + if (ret < 0) return JS_EXCEPTION; + return JS_NewBool(ctx, ret); +} + +static const JSCFunctionListEntry js_cell_object_funcs[] = { + JS_CFUNC_DEF("values", 1, js_cell_object_values), + JS_CFUNC_DEF("assign", 2, js_cell_object_assign), + JS_CFUNC_DEF("has", 2, js_cell_object_has), +}; + +/* ---------------------------------------------------------------------------- + * fn function and sub-functions + * ---------------------------------------------------------------------------- */ + +/* fn.apply(func, args) */ +static JSValue js_cell_fn_apply(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_NULL; + if (!JS_IsFunction(ctx, argv[0])) return JS_DupValue(ctx, argv[0]); + + JSValueConst func = argv[0]; + + if (argc < 2) { + return JS_Call(ctx, func, JS_NULL, 0, NULL); + } + + JSValue args_val = argv[1]; + if (!JS_IsArray(ctx, args_val)) { + /* Wrap single value in array */ + return JS_Call(ctx, func, JS_NULL, 1, &args_val); + } + + int64_t len; + if (js_get_length64(ctx, &len, args_val)) + return JS_EXCEPTION; + + /* Check arity */ + JSValue func_len = JS_GetPropertyStr(ctx, func, "length"); + if (!JS_IsException(func_len)) { + int arity; + if (!JS_ToInt32(ctx, &arity, func_len)) { + if (len > arity) { + JS_FreeValue(ctx, func_len); + return JS_ThrowTypeError(ctx, "fn.apply: too many arguments"); + } + } + JS_FreeValue(ctx, func_len); + } + + JSValue *args = js_malloc(ctx, sizeof(JSValue) * (len > 0 ? len : 1)); + if (!args) return JS_EXCEPTION; + + for (int64_t i = 0; i < len; i++) { + args[i] = JS_GetPropertyInt64(ctx, args_val, i); + if (JS_IsException(args[i])) { + for (int64_t j = 0; j < i; j++) JS_FreeValue(ctx, args[j]); + js_free(ctx, args); + return JS_EXCEPTION; + } + } + + JSValue result = JS_Call(ctx, func, JS_NULL, len, args); + + for (int64_t i = 0; i < len; i++) JS_FreeValue(ctx, args[i]); + js_free(ctx, args); + + return result; +} + +static const JSCFunctionListEntry js_cell_fn_funcs[] = { + JS_CFUNC_DEF("apply", 2, js_cell_fn_apply), +}; + +/* ============================================================================ + * Blob Intrinsic Type + * ============================================================================ */ + +/* Helper to check if JSValue is a blob */ +static blob *js_get_blob(JSContext *ctx, JSValueConst val) +{ + if (JS_VALUE_GET_TAG(val) != JS_TAG_OBJECT) + return NULL; + JSObject *p = JS_VALUE_GET_OBJ(val); + if (p->class_id != JS_CLASS_BLOB) + return NULL; + return p->u.opaque; +} + +/* Helper to create a new blob JSValue */ +static JSValue js_new_blob(JSContext *ctx, blob *b) +{ + JSValue obj = JS_NewObjectClass(ctx, JS_CLASS_BLOB); + if (JS_IsException(obj)) { + blob_destroy(b); + return obj; + } + JS_SetOpaque(obj, b); + return obj; +} + +/* Blob finalizer */ +static void js_blob_finalizer(JSRuntime *rt, JSValue val) +{ + blob *b = JS_GetOpaque(val, JS_CLASS_BLOB); + if (b) + blob_destroy(b); +} + +/* blob() constructor */ +static JSValue js_blob_constructor(JSContext *ctx, JSValueConst new_target, + int argc, JSValueConst *argv) +{ + blob *bd = NULL; + + /* blob() - empty blob */ + if (argc == 0) { + bd = blob_new(0); + } + /* blob(capacity) - blob with initial capacity in bits */ + else if (argc == 1 && JS_IsNumber(argv[0])) { + int64_t capacity_bits; + if (JS_ToInt64(ctx, &capacity_bits, argv[0]) < 0) + return JS_EXCEPTION; + if (capacity_bits < 0) capacity_bits = 0; + bd = blob_new((size_t)capacity_bits); + } + /* blob(length, logical/random) - blob with fill or random */ + else if (argc == 2 && JS_IsNumber(argv[0])) { + int64_t length_bits; + if (JS_ToInt64(ctx, &length_bits, argv[0]) < 0) + return JS_EXCEPTION; + if (length_bits < 0) length_bits = 0; + + if (JS_IsBool(argv[1])) { + int is_one = JS_ToBool(ctx, argv[1]); + bd = blob_new_with_fill((size_t)length_bits, is_one); + } else if (JS_IsFunction(ctx, argv[1])) { + /* Random function provided */ + size_t bytes = (length_bits + 7) / 8; + bd = blob_new((size_t)length_bits); + if (bd) { + bd->length = length_bits; + memset(bd->data, 0, bytes); + + size_t bits_written = 0; + while (bits_written < (size_t)length_bits) { + JSValue randval = JS_Call(ctx, argv[1], JS_NULL, 0, NULL); + if (JS_IsException(randval)) { + blob_destroy(bd); + return JS_EXCEPTION; + } + + int64_t fitval; + JS_ToInt64(ctx, &fitval, randval); + JS_FreeValue(ctx, randval); + + size_t bits_to_use = length_bits - bits_written; + if (bits_to_use > 52) bits_to_use = 52; + + for (size_t j = 0; j < bits_to_use; j++) { + size_t bit_pos = bits_written + j; + size_t byte_idx = bit_pos / 8; + size_t bit_idx = bit_pos % 8; + + if (fitval & (1LL << j)) + bd->data[byte_idx] |= (uint8_t)(1 << bit_idx); + else + bd->data[byte_idx] &= (uint8_t)~(1 << bit_idx); + } + bits_written += bits_to_use; + } + } + } else { + return JS_ThrowTypeError(ctx, "Second argument must be boolean or random function"); + } + } + /* blob(blob, from, to) - copy from another blob */ + else if (argc >= 1 && JS_IsObject(argv[0])) { + blob *src = js_get_blob(ctx, argv[0]); + if (!src) + return JS_ThrowTypeError(ctx, "blob constructor: argument 1 not a blob"); + int64_t from = 0, to = (int64_t)src->length; + if (argc >= 2 && JS_IsNumber(argv[1])) { + JS_ToInt64(ctx, &from, argv[1]); + if (from < 0) from = 0; + } + if (argc >= 3 && JS_IsNumber(argv[2])) { + JS_ToInt64(ctx, &to, argv[2]); + if (to < from) to = from; + if (to > (int64_t)src->length) to = (int64_t)src->length; + } + bd = blob_new_from_blob(src, (size_t)from, (size_t)to); + } + else { + return JS_ThrowTypeError(ctx, "blob constructor: invalid arguments"); + } + + if (!bd) + return JS_ThrowOutOfMemory(ctx); + + return js_new_blob(ctx, bd); +} + +/* blob.write_bit(logical) */ +static JSValue js_blob_write_bit(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "write_bit(logical) requires 1 argument"); + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "write_bit: not called on a blob"); + + int bit_val; + if (JS_IsNumber(argv[0])) { + int32_t num; + JS_ToInt32(ctx, &num, argv[0]); + if (num != 0 && num != 1) + return JS_ThrowTypeError(ctx, "write_bit: value must be true, false, 0, or 1"); + bit_val = num; + } else { + bit_val = JS_ToBool(ctx, argv[0]); + } + + if (blob_write_bit(bd, bit_val) < 0) + return JS_ThrowTypeError(ctx, "write_bit: cannot write (maybe stone or OOM)"); + return JS_NULL; +} + +/* blob.write_blob(second_blob) */ +static JSValue js_blob_write_blob(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "write_blob(second_blob) requires 1 argument"); + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "write_blob: not called on a blob"); + blob *second = js_get_blob(ctx, argv[0]); + if (!second) + return JS_ThrowTypeError(ctx, "write_blob: argument must be a blob"); + + if (blob_write_blob(bd, second) < 0) + return JS_ThrowTypeError(ctx, "write_blob: cannot write to stone blob or OOM"); + + return JS_NULL; +} + +/* blob.write_number(number) - write dec64 */ +static JSValue js_blob_write_number(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "write_number(number) requires 1 argument"); + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "write_number: not called on a blob"); + + double d; + if (JS_ToFloat64(ctx, &d, argv[0]) < 0) + return JS_EXCEPTION; + + if (blob_write_dec64(bd, d) < 0) + return JS_ThrowTypeError(ctx, "write_number: cannot write to stone blob or OOM"); + + return JS_NULL; +} + +/* blob.write_fit(value, len) */ +static JSValue js_blob_write_fit(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) + return JS_ThrowTypeError(ctx, "write_fit(value, len) requires 2 arguments"); + + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "write_fit: not called on a blob"); + + int64_t value; + int32_t len; + + if (JS_ToInt64(ctx, &value, argv[0]) < 0) return JS_EXCEPTION; + if (JS_ToInt32(ctx, &len, argv[1]) < 0) return JS_EXCEPTION; + + if (blob_write_fit(bd, value, len) < 0) + return JS_ThrowTypeError(ctx, "write_fit: value doesn't fit or stone blob"); + + return JS_NULL; +} + +/* blob.write_text(text) */ +static JSValue js_blob_write_text(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "write_text(text) requires 1 argument"); + + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "write_text: not called on a blob"); + + const char *str = JS_ToCString(ctx, argv[0]); + if (!str) + return JS_EXCEPTION; + + if (blob_write_text(bd, str) < 0) { + JS_FreeCString(ctx, str); + return JS_ThrowTypeError(ctx, "write_text: cannot write to stone blob or OOM"); + } + + JS_FreeCString(ctx, str); + return JS_NULL; +} + +/* blob.write_pad(block_size) */ +static JSValue js_blob_write_pad(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "write_pad(block_size) requires 1 argument"); + + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "write_pad: not called on a blob"); + + int32_t block_size; + if (JS_ToInt32(ctx, &block_size, argv[0]) < 0) return JS_EXCEPTION; + + if (blob_write_pad(bd, block_size) < 0) + return JS_ThrowTypeError(ctx, "write_pad: cannot write"); + + return JS_NULL; +} + +/* blob.w16(value) - write 16-bit value */ +static JSValue js_blob_w16(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "w16(value) requires 1 argument"); + + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "w16: not called on a blob"); + + int32_t value; + if (JS_ToInt32(ctx, &value, argv[0]) < 0) return JS_EXCEPTION; + + int16_t short_val = (int16_t)value; + if (blob_write_bytes(bd, &short_val, sizeof(int16_t)) < 0) + return JS_ThrowTypeError(ctx, "w16: cannot write"); + + return JS_NULL; +} + +/* blob.wf(value) - write float */ +static JSValue js_blob_wf(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "wf(value) requires 1 argument"); + + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "wf: not called on a blob"); + + double d; + if (JS_ToFloat64(ctx, &d, argv[0]) < 0) return JS_EXCEPTION; + + float f = (float)d; + if (blob_write_bytes(bd, &f, sizeof(float)) < 0) + return JS_ThrowTypeError(ctx, "wf: cannot write"); + + return JS_NULL; +} + +/* blob.read_logical(from) */ +static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "read_logical(from) requires 1 argument"); + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "read_logical: not called on a blob"); + int64_t pos; + if (JS_ToInt64(ctx, &pos, argv[0]) < 0) + return JS_ThrowInternalError(ctx, "must provide a positive bit"); + if (pos < 0) + return JS_ThrowRangeError(ctx, "read_logical: position must be non-negative"); + int bit_val; + if (blob_read_bit(bd, (size_t)pos, &bit_val) < 0) + return JS_ThrowTypeError(ctx, "read_logical: blob must be stone"); + return JS_NewBool(ctx, bit_val); +} + +/* blob.read_blob(from, to) */ +static JSValue js_blob_read_blob(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "read_blob: not called on a blob"); + + if (!bd->is_stone) + return JS_ThrowTypeError(ctx, "read_blob: blob must be stone"); + + int64_t from = 0; + int64_t to = bd->length; + + if (argc >= 1) { + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + if (from < 0) from = 0; + } + if (argc >= 2) { + if (JS_ToInt64(ctx, &to, argv[1]) < 0) return JS_EXCEPTION; + if (to > (int64_t)bd->length) to = bd->length; + } + + blob *new_bd = blob_read_blob(bd, from, to); + if (!new_bd) + return JS_ThrowOutOfMemory(ctx); + + return js_new_blob(ctx, new_bd); +} + +/* blob.read_number(from) */ +static JSValue js_blob_read_number(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_ThrowTypeError(ctx, "read_number(from) requires 1 argument"); + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "read_number: not called on a blob"); + + if (!bd->is_stone) + return JS_ThrowTypeError(ctx, "read_number: blob must be stone"); + + double from; + if (JS_ToFloat64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + + if (from < 0) return JS_ThrowRangeError(ctx, "read_number: position must be non-negative"); + + double d; + if (blob_read_dec64(bd, from, &d) < 0) + return JS_ThrowRangeError(ctx, "read_number: out of range"); + + return JS_NewFloat64(ctx, d); +} + +/* blob.read_fit(from, len) */ +static JSValue js_blob_read_fit(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) + return JS_ThrowTypeError(ctx, "read_fit(from, len) requires 2 arguments"); + + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "read_fit: not called on a blob"); + + if (!bd->is_stone) + return JS_ThrowTypeError(ctx, "read_fit: blob must be stone"); + + int64_t from; + int32_t len; + + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + if (JS_ToInt32(ctx, &len, argv[1]) < 0) return JS_EXCEPTION; + + if (from < 0) + return JS_ThrowRangeError(ctx, "read_fit: position must be non-negative"); + + int64_t value; + if (blob_read_fit(bd, from, len, &value) < 0) + return JS_ThrowRangeError(ctx, "read_fit: out of range or invalid length"); + + return JS_NewInt64(ctx, value); +} + +/* blob.read_text(from) */ +static JSValue js_blob_read_text(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "read_text: not called on a blob"); + + if (!bd->is_stone) + return JS_ThrowTypeError(ctx, "read_text: blob must be stone"); + + int64_t from = 0; + if (argc >= 1) { + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + } + + char *text; + size_t bits_read; + if (blob_read_text(bd, from, &text, &bits_read) < 0) + return JS_ThrowRangeError(ctx, "read_text: out of range or invalid encoding"); + + JSValue result = JS_NewString(ctx, text); + /* Note: blob_read_text uses system malloc, so we use sys_free */ + sys_free(text); + + return result; +} + +/* blob.pad?(from, block_size) */ +static JSValue js_blob_pad_q(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) + return JS_ThrowTypeError(ctx, "pad?(from, block_size) requires 2 arguments"); + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "pad?: not called on a blob"); + + if (!bd->is_stone) + return JS_ThrowTypeError(ctx, "pad?: blob must be stone"); + + int64_t from; + int32_t block_size; + if (JS_ToInt64(ctx, &from, argv[0]) < 0) return JS_EXCEPTION; + if (JS_ToInt32(ctx, &block_size, argv[1]) < 0) return JS_EXCEPTION; + + return JS_NewBool(ctx, blob_pad_check(bd, from, block_size)); +} + +/* blob.length getter */ +static JSValue js_blob_get_length(JSContext *ctx, JSValueConst this_val) +{ + blob *bd = js_get_blob(ctx, this_val); + if (!bd) + return JS_ThrowTypeError(ctx, "length: not called on a blob"); + return JS_NewInt64(ctx, bd->length); +} + +static const JSCFunctionListEntry js_blob_proto_funcs[] = { + /* Write methods */ + JS_CFUNC_DEF("write_bit", 1, js_blob_write_bit), + JS_CFUNC_DEF("write_blob", 1, js_blob_write_blob), + JS_CFUNC_DEF("write_number", 1, js_blob_write_number), + JS_CFUNC_DEF("write_fit", 2, js_blob_write_fit), + JS_CFUNC_DEF("write_text", 1, js_blob_write_text), + JS_CFUNC_DEF("write_pad", 1, js_blob_write_pad), + JS_CFUNC_DEF("wf", 1, js_blob_wf), + JS_CFUNC_DEF("w16", 1, js_blob_w16), + + /* Read methods */ + JS_CFUNC_DEF("read_logical", 1, js_blob_read_logical), + JS_CFUNC_DEF("read_blob", 2, js_blob_read_blob), + JS_CFUNC_DEF("read_number", 1, js_blob_read_number), + JS_CFUNC_DEF("read_fit", 2, js_blob_read_fit), + JS_CFUNC_DEF("read_text", 1, js_blob_read_text), + JS_CFUNC_DEF("pad?", 2, js_blob_pad_q), + + /* Length property getter */ + JS_CGETSET_DEF("length", js_blob_get_length, NULL), +}; + +/* ============================================================================ + * Blob external API functions (called from other files via cell.h) + * ============================================================================ */ + +/* Initialize blob - called during context setup (but we do it in JS_AddIntrinsicBaseObjects now) */ +JSValue js_blob_use(JSContext *js) +{ + /* Blob is now initialized as an intrinsic in JS_AddIntrinsicBaseObjects */ + /* Return the blob constructor for compatibility */ + return JS_GetPropertyStr(js, js->global_obj, "blob"); +} + +/* Create a new blob from raw data, stone it, and return as JSValue */ +JSValue js_new_blob_stoned_copy(JSContext *js, void *data, size_t bytes) +{ + blob *b = blob_new(bytes * 8); + if (!b) + return JS_ThrowOutOfMemory(js); + memcpy(b->data, data, bytes); + b->length = bytes * 8; + blob_make_stone(b); + return js_new_blob(js, b); +} + +/* Get raw data pointer from a blob (must be stone) - returns byte count */ +void *js_get_blob_data(JSContext *js, size_t *size, JSValue v) +{ + blob *b = js_get_blob(js, v); + if (!b) { + JS_ThrowReferenceError(js, "get_blob_data: not called on a blob"); + return NULL; + } + + if (!b->is_stone) { + JS_ThrowReferenceError(js, "attempted to read data from a non-stone blob"); + return NULL; + } + + if (b->length % 8 != 0) { + JS_ThrowReferenceError(js, "attempted to read data from a non-byte aligned blob [length is %zu]", b->length); + return NULL; + } + + *size = (b->length + 7) / 8; + return b->data; +} + +/* Get raw data pointer from a blob (must be stone) - returns bit count */ +void *js_get_blob_data_bits(JSContext *js, size_t *bits, JSValue v) +{ + blob *b = js_get_blob(js, v); + if (!b) { + JS_ThrowReferenceError(js, "get_blob_data_bits: not called on a blob"); + return NULL; + } + if (!b->is_stone) { + JS_ThrowReferenceError(js, "attempted to read data from a non-stone blob"); + return NULL; + } + + if (!b->data) { + JS_ThrowReferenceError(js, "attempted to read data from an empty blob"); + return NULL; + } + + if (b->length % 8 != 0) { + JS_ThrowReferenceError(js, "attempted to read data from a non-byte aligned blob"); + return NULL; + } + + if (b->length == 0) { + JS_ThrowReferenceError(js, "attempted to read data from an empty blob"); + return NULL; + } + + *bits = b->length; + return b->data; +} + +/* Check if a value is a blob */ +int js_is_blob(JSContext *js, JSValue v) +{ + return js_get_blob(js, v) != NULL; +} + +/* ============================================================================ + * stone() function - deep freeze with blob support + * ============================================================================ */ + +static JSValue js_cell_stone_deep(JSContext *ctx, JSValueConst obj); + +static JSValue js_cell_stone_deep(JSContext *ctx, JSValueConst obj) +{ + /* Handle blob specially */ + blob *bd = js_get_blob(ctx, obj); + if (bd) { + if (!bd->is_stone) + blob_make_stone(bd); + return JS_DupValue(ctx, obj); + } + + /* Handle non-objects (primitives are already immutable) */ + if (!JS_IsObject(obj)) + return JS_DupValue(ctx, obj); + + /* Get all property keys */ + JSPropertyEnum *tab; + uint32_t len; + if (JS_GetOwnPropertyNames(ctx, &tab, &len, obj, + JS_GPN_STRING_MASK | JS_GPN_SYMBOL_MASK | JS_GPN_ENUM_ONLY) < 0) { + return JS_EXCEPTION; + } + + /* Recursively freeze all properties */ + for (uint32_t i = 0; i < len; i++) { + JSValue val = JS_GetProperty(ctx, obj, tab[i].atom); + if (JS_IsException(val)) { + for (uint32_t j = i; j < len; j++) + JS_FreeAtom(ctx, tab[j].atom); + js_free(ctx, tab); + return JS_EXCEPTION; + } + + if (JS_IsObject(val) || JS_IsFunction(ctx, val)) { + JSValue frozen = js_cell_stone_deep(ctx, val); + JS_FreeValue(ctx, val); + if (JS_IsException(frozen)) { + for (uint32_t j = i; j < len; j++) + JS_FreeAtom(ctx, tab[j].atom); + js_free(ctx, tab); + return JS_EXCEPTION; + } + JS_FreeValue(ctx, frozen); + } else { + JS_FreeValue(ctx, val); + } + + JS_FreeAtom(ctx, tab[i].atom); + } + js_free(ctx, tab); + + /* Freeze the object itself */ + JSValue args[1] = { JS_DupValue(ctx, obj) }; + JSValue result = js_object_seal(ctx, JS_NULL, 1, args, 1); /* freeze */ + JS_FreeValue(ctx, args[0]); + + if (JS_IsException(result)) + return result; + JS_FreeValue(ctx, result); + + return JS_DupValue(ctx, obj); +} + +/* stone(object) - deep freeze an object */ +static JSValue js_cell_stone(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_NULL; + return js_cell_stone_deep(ctx, argv[0]); +} + +/* stone.p(object) - check if object is frozen/stone */ +static JSValue js_cell_stone_p(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_FALSE; + + JSValue obj = argv[0]; + + /* Check blob */ + blob *bd = js_get_blob(ctx, obj); + if (bd) + return JS_NewBool(ctx, bd->is_stone); + + /* Non-objects are immutable by nature */ + if (!JS_IsObject(obj)) + return JS_TRUE; + + /* Check if object is frozen */ + return JS_NewBool(ctx, JS_IsExtensible(ctx, obj) == 0); +} + +static const JSCFunctionListEntry js_cell_stone_funcs[] = { + JS_CFUNC_DEF("p", 1, js_cell_stone_p), +}; + +/* ============================================================================ + * length() function + * ============================================================================ */ + +static JSValue js_cell_length(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) + return JS_NULL; + + JSValue val = argv[0]; + + /* null returns null */ + if (JS_IsNull(val)) + return JS_NULL; + + /* Functions return arity */ + if (JS_IsFunction(ctx, val)) { + JSValue len = JS_GetPropertyStr(ctx, val, "length"); + if (JS_IsException(len)) + return len; + return len; + } + + int tag = JS_VALUE_GET_TAG(val); + + /* Strings return codepoint count */ + if (tag == JS_TAG_STRING || tag == JS_TAG_STRING_ROPE) { + JSString *p = JS_VALUE_GET_STRING(val); + return JS_NewInt32(ctx, p->len); + } + + /* Check for blob */ + blob *bd = js_get_blob(ctx, val); + if (bd) + return JS_NewInt64(ctx, bd->length); + + /* Arrays return element count */ + if (JS_IsArray(ctx, val)) { + int64_t len; + if (js_get_length64(ctx, &len, val) < 0) + return JS_EXCEPTION; + return JS_NewInt64(ctx, len); + } + + /* Objects with length property */ + if (JS_IsObject(val)) { + JSValue len = JS_GetPropertyStr(ctx, val, "length"); + if (!JS_IsException(len) && !JS_IsNull(len)) { + if (JS_IsFunction(ctx, len)) { + JSValue result = JS_Call(ctx, len, val, 0, NULL); + JS_FreeValue(ctx, len); + return result; + } + if (JS_VALUE_GET_TAG(len) == JS_TAG_INT || JS_VALUE_GET_TAG(len) == JS_TAG_FLOAT64) + return len; + JS_FreeValue(ctx, len); + } else if (JS_IsException(len)) { + return len; + } + } + + return JS_NULL; +} + +/* ============================================================================ + * is_* type checking functions + * ============================================================================ */ + +/* is_array(val) */ +static JSValue js_cell_is_array(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + return JS_NewBool(ctx, JS_IsArray(ctx, argv[0])); +} + +/* is_blob(val) */ +static JSValue js_cell_is_blob(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + return JS_NewBool(ctx, js_get_blob(ctx, argv[0]) != NULL); +} + +/* is_data(val) - check if object is a plain object (data record) */ +static JSValue js_cell_is_data(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + JSValue val = argv[0]; + if (!JS_IsObject(val)) return JS_FALSE; + if (JS_IsArray(ctx, val)) return JS_FALSE; + if (JS_IsFunction(ctx, val)) return JS_FALSE; + if (js_get_blob(ctx, val)) return JS_FALSE; + /* Check if it's a plain object (prototype is Object.prototype or null) */ + return JS_TRUE; +} + +/* is_function(val) */ +static JSValue js_cell_is_function(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + return JS_NewBool(ctx, JS_IsFunction(ctx, argv[0])); +} + +/* is_logical(val) - check if value is a boolean (true or false) */ +static JSValue js_cell_is_logical(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + return JS_NewBool(ctx, JS_VALUE_GET_TAG(argv[0]) == JS_TAG_BOOL); +} + +/* is_integer(val) */ +static JSValue js_cell_is_integer(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + JSValue val = argv[0]; + int tag = JS_VALUE_GET_TAG(val); + if (tag == JS_TAG_INT) return JS_TRUE; + if (tag == JS_TAG_FLOAT64) { + double d = JS_VALUE_GET_FLOAT64(val); + return JS_NewBool(ctx, isfinite(d) && trunc(d) == d); + } + return JS_FALSE; +} + +/* is_null(val) */ +static JSValue js_cell_is_null(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + return JS_NewBool(ctx, JS_IsNull(argv[0])); +} + +/* is_number(val) */ +static JSValue js_cell_is_number(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + return JS_NewBool(ctx, JS_IsNumber(argv[0])); +} + +/* is_object(val) - true for non-array, non-null objects */ +static JSValue js_cell_is_object(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + JSValue val = argv[0]; + if (!JS_IsObject(val)) return JS_FALSE; + if (JS_IsArray(ctx, val)) return JS_FALSE; + return JS_TRUE; +} + +/* is_stone(val) - check if value is immutable */ +static JSValue js_cell_is_stone(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + JSValue val = argv[0]; + + /* Check blob */ + blob *bd = js_get_blob(ctx, val); + if (bd) + return JS_NewBool(ctx, bd->is_stone); + + /* Primitives are always stone */ + if (!JS_IsObject(val)) + return JS_TRUE; + + /* Check frozen */ + return JS_NewBool(ctx, JS_IsExtensible(ctx, val) == 0); +} + +/* is_text(val) */ +static JSValue js_cell_is_text(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 1) return JS_FALSE; + int tag = JS_VALUE_GET_TAG(argv[0]); + return JS_NewBool(ctx, tag == JS_TAG_STRING || tag == JS_TAG_STRING_ROPE); +} + +/* is_proto(val, master) - check if val has master in prototype chain */ +static JSValue js_cell_is_proto(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + if (argc < 2) return JS_FALSE; + JSValue val = argv[0]; + JSValue master = argv[1]; + + if (!JS_IsObject(val) || JS_IsNull(master)) + return JS_FALSE; + + /* Walk prototype chain */ + JSValue proto = JS_GetPrototype(ctx, val); + while (!JS_IsNull(proto) && !JS_IsException(proto)) { + /* If master is a function with prototype property, check that */ + if (JS_IsFunction(ctx, master)) { + JSValue master_proto = JS_GetPropertyStr(ctx, master, "prototype"); + if (!JS_IsException(master_proto) && !JS_IsNull(master_proto)) { + JSObject *p1 = JS_VALUE_GET_OBJ(proto); + JSObject *p2 = JS_VALUE_GET_OBJ(master_proto); + JS_FreeValue(ctx, master_proto); + if (p1 == p2) { + JS_FreeValue(ctx, proto); + return JS_TRUE; + } + } else if (!JS_IsException(master_proto)) { + JS_FreeValue(ctx, master_proto); + } + } + /* Also check if proto == master directly */ + if (JS_IsObject(master)) { + JSObject *p1 = JS_VALUE_GET_OBJ(proto); + JSObject *p2 = JS_VALUE_GET_OBJ(master); + if (p1 == p2) { + JS_FreeValue(ctx, proto); + return JS_TRUE; + } + } + + JSValue next = JS_GetPrototype(ctx, proto); + JS_FreeValue(ctx, proto); + proto = next; + } + if (JS_IsException(proto)) + return proto; + return JS_FALSE; +} + +/* ============================================================================ + * Cell Script Global Function Registration + * ============================================================================ */ + static const JSCFunctionListEntry js_global_funcs[] = { JS_CFUNC_DEF("parseInt", 2, js_parseInt ), JS_CFUNC_DEF("parseFloat", 1, js_parseFloat ), @@ -37282,26 +40480,6 @@ void JS_AddIntrinsicBaseObjects(JSContext *ctx) JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_global_funcs, countof(js_global_funcs)); - /* Number */ - ctx->class_proto[JS_CLASS_NUMBER] = JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], - JS_CLASS_NUMBER); - JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_NUMBER], JS_NewInt32(ctx, 0)); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_NUMBER], - js_number_proto_funcs, - countof(js_number_proto_funcs)); - number_obj = JS_NewGlobalCConstructor(ctx, "Number", js_number_constructor, 1, - ctx->class_proto[JS_CLASS_NUMBER]); - JS_SetPropertyFunctionList(ctx, number_obj, js_number_funcs, countof(js_number_funcs)); - - /* Boolean */ - ctx->class_proto[JS_CLASS_BOOLEAN] = JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], - JS_CLASS_BOOLEAN); - JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_BOOLEAN], JS_NewBool(ctx, FALSE)); - JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_BOOLEAN], js_boolean_proto_funcs, - countof(js_boolean_proto_funcs)); - JS_NewGlobalCConstructor(ctx, "Boolean", js_boolean_constructor, 1, - ctx->class_proto[JS_CLASS_BOOLEAN]); - /* String */ ctx->class_proto[JS_CLASS_STRING] = JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], JS_CLASS_STRING); @@ -37348,6 +40526,101 @@ void JS_AddIntrinsicBaseObjects(JSContext *ctx) JS_DefinePropertyValue(ctx, ctx->global_obj, JS_ATOM_globalThis, JS_DupValue(ctx, ctx->global_obj), JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE); + + /* Cell Script global functions: text, number, array, object, fn */ + { + JSValue text_func = JS_NewCFunction(ctx, js_cell_text, "text", 2); + JS_SetPropertyFunctionList(ctx, text_func, js_cell_text_funcs, countof(js_cell_text_funcs)); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "text", text_func, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + + JSValue number_func = JS_NewCFunction(ctx, js_cell_number, "number", 2); + JS_SetPropertyFunctionList(ctx, number_func, js_cell_number_funcs, countof(js_cell_number_funcs)); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "number", number_func, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + + JSValue array_func = JS_NewCFunction(ctx, js_cell_array, "array", 4); + JS_SetPropertyFunctionList(ctx, array_func, js_cell_array_funcs, countof(js_cell_array_funcs)); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "array", array_func, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + + JSValue object_func = JS_NewCFunction(ctx, js_cell_object, "object", 2); + JS_SetPropertyFunctionList(ctx, object_func, js_cell_object_funcs, countof(js_cell_object_funcs)); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "object", object_func, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + + JSValue fn_obj = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, fn_obj, js_cell_fn_funcs, countof(js_cell_fn_funcs)); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "fn", fn_obj, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + + /* Blob intrinsic type */ + { + JSClassDef blob_class = { + .class_name = "blob", + .finalizer = js_blob_finalizer, + }; + JS_NewClass(JS_GetRuntime(ctx), JS_CLASS_BLOB, &blob_class); + ctx->class_proto[JS_CLASS_BLOB] = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_BLOB], + js_blob_proto_funcs, countof(js_blob_proto_funcs)); + + JSValue blob_ctor = JS_NewCFunction2(ctx, js_blob_constructor, "blob", 3, + JS_CFUNC_constructor, 0); + JS_SetConstructor(ctx, blob_ctor, ctx->class_proto[JS_CLASS_BLOB]); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "blob", blob_ctor, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + } + + /* stone() function */ + JSValue stone_func = JS_NewCFunction(ctx, js_cell_stone, "stone", 1); + JS_SetPropertyFunctionList(ctx, stone_func, js_cell_stone_funcs, countof(js_cell_stone_funcs)); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "stone", stone_func, + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + + /* length() function */ + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "length", + JS_NewCFunction(ctx, js_cell_length, "length", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + + /* is_* type checking functions */ + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_array", + JS_NewCFunction(ctx, js_cell_is_array, "is_array", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_blob", + JS_NewCFunction(ctx, js_cell_is_blob, "is_blob", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_data", + JS_NewCFunction(ctx, js_cell_is_data, "is_data", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_function", + JS_NewCFunction(ctx, js_cell_is_function, "is_function", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_logical", + JS_NewCFunction(ctx, js_cell_is_logical, "is_logical", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_integer", + JS_NewCFunction(ctx, js_cell_is_integer, "is_integer", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_null", + JS_NewCFunction(ctx, js_cell_is_null, "is_null", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_number", + JS_NewCFunction(ctx, js_cell_is_number, "is_number", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_object", + JS_NewCFunction(ctx, js_cell_is_object, "is_object", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_stone", + JS_NewCFunction(ctx, js_cell_is_stone, "is_stone", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_text", + JS_NewCFunction(ctx, js_cell_is_text, "is_text", 1), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + JS_DefinePropertyValueStr(ctx, ctx->global_obj, "is_proto", + JS_NewCFunction(ctx, js_cell_is_proto, "is_proto", 2), + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE); + } } #define STRLEN(s) (sizeof(s)/sizeof(s[0])) diff --git a/test.ce b/test.ce index fa013d79..9c1bb340 100644 --- a/test.ce +++ b/test.ce @@ -567,7 +567,7 @@ if (all_actor_tests.length == 0) { // Generate Reports function function generate_reports(totals) { - var timestamp = number.floor(time.number()).toString() + var timestamp = text(number.floor(time.number())) var report_dir = shop.get_reports_dir() + '/test_' + timestamp ensure_dir(report_dir) diff --git a/time.cm b/time.cm index e93bac3c..a813018d 100644 --- a/time.cm +++ b/time.cm @@ -178,16 +178,16 @@ function time_text(num = now(), /* substitutions */ var full_offset = zone + (dst ? 1 : 0); - fmt = fmt.replaceAll("yyyy", year.toString().padStart(4, "0")); + fmt = fmt.replaceAll("yyyy", text(year, "i4")) fmt = fmt.replaceAll("y", year); fmt = fmt.replaceAll("eee", rec.yday + 1); - fmt = fmt.replaceAll("dd", rec.day.toString().padStart(2, "0")); + fmt = fmt.replaceAll("dd", text(rec.day, "i2")) fmt = fmt.replaceAll("d", rec.day); - fmt = fmt.replaceAll("hh", rec.hour.toString().padStart(2, "0")); + fmt = fmt.replaceAll("hh", text(rec.hour, "i2")); fmt = fmt.replaceAll("h", rec.hour); - fmt = fmt.replaceAll("nn", rec.minute.toString().padStart(2, "0")); + fmt = fmt.replaceAll("nn", text(rec.minute, "i2")); fmt = fmt.replaceAll("n", rec.minute); - fmt = fmt.replaceAll("ss", rec.second.toFixed(2).padStart(2, "0")); + fmt = fmt.replaceAll("ss", text(rec.second, "i2")); fmt = fmt.replaceAll("s", rec.second); fmt = fmt.replaceAll("x", dst ? "DST" : ""); /* new */ fmt = fmt.replaceAll("z", (full_offset >= 0 ? "+" : "") + text(full_offset));