82 Commits

Author SHA1 Message Date
John Alanbrook
d029037b01 fix bytecode error 2026-02-04 19:23:16 -06:00
John Alanbrook
ed7dd91c3f rm global 2026-02-04 18:57:45 -06:00
John Alanbrook
3abe20fee0 merge 2026-02-04 18:38:46 -06:00
John Alanbrook
a92a96118e remove eval parser; consolidate addintrinsic 2026-02-04 17:15:03 -06:00
John Alanbrook
4e407fe301 migrate nota, wota into quickjs.c 2026-02-04 17:03:48 -06:00
John Alanbrook
ab74cdc173 merge warningfix 2026-02-04 16:17:52 -06:00
John Alanbrook
2c9d039271 massive cleanup 2026-02-04 14:26:17 -06:00
John Alanbrook
80d314c58f Merge templatefix branch
Use PPretext for parser string building to avoid GC issues during parsing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 14:21:25 -06:00
John Alanbrook
611fba2b6f fix regexp parsing 2026-02-04 14:19:39 -06:00
John Alanbrook
f5fad52d47 Rewrite template literals with OP_format_template
Replace complex template literal handling with a simple format-based
approach. Template literals like `hello ${x}` now compile to:
  <push x>
  OP_format_template expr_count=1, cpool_idx=N
where cpool[N] = "hello {0}"

The opcode handler parses the format string, substitutes {N} placeholders
with stringified stack values, and produces the result string.

Key implementation details:
- Uses PPretext (parser pretext) with pjs_malloc to avoid GC issues
- Re-reads b->cpool[cpool_idx] after any GC-triggering operation
- Opcode layout is u16 expr_count followed by u32 cpool_idx - the u16
  must come first because compute_stack_size reads the pop count from
  position 1 for npop_u16 format opcodes

Removed:
- OP_template_concat opcode and handler
- Tagged template literal support (users can use format() directly)
- FuncCallType enum (FUNC_CALL_TEMPLATE case no longer needed)
- Complex template object creation logic in js_parse_template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 14:18:02 -06:00
John Alanbrook
2fc7d333ad new environment tact for engine 2026-02-04 14:12:57 -06:00
John Alanbrook
d4635f2a75 remove unused vars, fix warnings 2026-02-04 13:49:43 -06:00
John Alanbrook
19576533d9 no more global obj; eval w/ env 2026-02-03 23:07:30 -06:00
John Alanbrook
c08249b6f1 clean up var ref 2026-02-03 22:01:42 -06:00
John Alanbrook
dc348d023f optimize lref 2026-02-03 21:37:17 -06:00
John Alanbrook
fd5e4d155e varref cleanup 2026-02-03 20:09:35 -06:00
John Alanbrook
e734353722 Begin removal of varref 2026-02-03 19:13:17 -06:00
John Alanbrook
94f1645be1 new closure model 2026-02-03 19:04:38 -06:00
John Alanbrook
41e3a6d91a closures work 2026-02-03 18:42:14 -06:00
John Alanbrook
04c569eab1 fix parser 2026-02-03 18:24:06 -06:00
John Alanbrook
dc3c474b3a link 2026-02-03 18:01:31 -06:00
John Alanbrook
f1117bbd41 get_global 2026-02-03 17:35:24 -06:00
John Alanbrook
43faad95e0 new opcodes 2026-02-03 17:21:41 -06:00
John Alanbrook
acc9878b36 bytecode dump 2026-02-03 17:08:21 -06:00
John Alanbrook
03c45ee8b0 lexer 2026-02-03 16:59:20 -06:00
John Alanbrook
a171a0d2af -e flag run script 2026-02-03 16:06:18 -06:00
John Alanbrook
bb8d3930b3 fix many internals wrt gc 2026-02-03 06:55:26 -06:00
John Alanbrook
522ae6128a add FORCE_GC_AT_MALLOC logic 2026-02-03 03:00:46 -06:00
John Alanbrook
16c26e4bf2 fix object fwd growth 2026-02-03 02:53:06 -06:00
John Alanbrook
a9804785e0 fix gc 2026-02-03 02:44:52 -06:00
John Alanbrook
11ae703693 suite.c roots 2026-02-03 02:40:40 -06:00
John Alanbrook
f203278c3e more gc correctness 2026-02-03 02:31:14 -06:00
John Alanbrook
a80557283a fix poison heap 2026-02-03 02:06:29 -06:00
John Alanbrook
3e40885e07 extensive debugging; fixed forwarding error 2026-02-03 01:50:30 -06:00
John Alanbrook
e4b7de46f6 add function aware to gc 2026-02-03 01:20:24 -06:00
John Alanbrook
e680439a9b gc asan poison 2026-02-03 01:03:23 -06:00
John Alanbrook
ae11504e00 copy gc 2026-02-03 00:34:07 -06:00
John Alanbrook
893deaec23 suite.c all works 2026-02-02 23:39:12 -06:00
John Alanbrook
69b032d3dc rm gc 2026-02-02 23:02:50 -06:00
John Alanbrook
256a00c501 fix mem errors 2026-02-02 22:54:38 -06:00
John Alanbrook
8e166b8f98 gc aware 2026-02-02 22:46:07 -06:00
John Alanbrook
ddbdd00496 update text and object to be gc aware 2026-02-02 22:37:42 -06:00
John Alanbrook
bdf0461e1f extensive C testing 2026-02-02 21:26:46 -06:00
John Alanbrook
0b86af1d4c array gc 2026-02-02 20:33:11 -06:00
John Alanbrook
beac9608ea chase 2026-02-02 18:54:15 -06:00
John Alanbrook
a04bebd0d7 fix str 2026-02-02 10:38:48 -06:00
John Alanbrook
ce74f726dd compiles + runs 2026-02-02 10:24:02 -06:00
John Alanbrook
4d1ab60852 rm notion of object values 2026-02-02 09:54:36 -06:00
John Alanbrook
22ab6c8098 rm gc fns 2026-02-02 09:22:51 -06:00
John Alanbrook
9a9775690f rm function proto 2026-02-02 08:35:14 -06:00
John Alanbrook
be71ae3bba remove **Free type functions 2026-02-02 08:32:11 -06:00
John Alanbrook
f2a76cbb55 fix number rep 2026-02-02 07:56:53 -06:00
John Alanbrook
2d834c37b3 simplify eq 2026-02-02 06:56:46 -06:00
John Alanbrook
ba1b92aa78 rm freevalue and dupvalue 2026-02-02 06:27:08 -06:00
John Alanbrook
c356fe462d compiles 2026-02-02 06:15:10 -06:00
John Alanbrook
b23b918f97 compiles 2026-02-01 23:03:01 -06:00
John Alanbrook
e720152bcd gc plan 2026-02-01 20:58:42 -06:00
John Alanbrook
f093e6f5a3 reformat 2026-02-01 19:36:20 -06:00
John Alanbrook
4fc904de63 rm 2026-02-01 19:33:19 -06:00
John Alanbrook
e53f55fb23 pretext 2026-02-01 18:59:52 -06:00
John Alanbrook
6150406905 pretext 2026-02-01 18:34:35 -06:00
John Alanbrook
a189440769 stringbuffer 2026-02-01 17:58:11 -06:00
John Alanbrook
6c3c492446 rm jsstring and string_buffer 2026-02-01 12:34:38 -06:00
John Alanbrook
bb83327a52 transformation 2026-02-01 07:45:44 -06:00
John Alanbrook
c74bee89a7 plan 2026-01-31 14:59:31 -06:00
John Alanbrook
271a3d6724 before gc rewrite 2026-01-31 14:43:21 -06:00
John Alanbrook
c5ccc66e51 simpler enumeration of property names 2026-01-31 12:13:35 -06:00
John Alanbrook
3a0ea31896 rm dump profile 2026-01-31 12:01:33 -06:00
John Alanbrook
67e82fd12c merge objects and recs 2026-01-31 11:40:16 -06:00
John Alanbrook
3c59087c0c merge jsobject to jsrecord 2026-01-31 11:36:07 -06:00
John Alanbrook
b79e07f57b more atom removal 2026-01-31 11:14:02 -06:00
John Alanbrook
fdcb374403 fix formatting 2026-01-31 10:33:29 -06:00
John Alanbrook
03feb370fd rm atoms 2026-01-31 06:51:19 -06:00
John Alanbrook
6712755940 edit lexer atoms 2026-01-30 21:33:10 -06:00
John Alanbrook
b3f3bc8a5f rm atoms 2026-01-30 20:16:08 -06:00
John Alanbrook
a49b94e0a1 phase3 2026-01-30 17:02:50 -06:00
John Alanbrook
24ecff3f1c rm atom' 2026-01-30 09:58:02 -06:00
John Alanbrook
3ccaf68a5b begin rm atoms 2026-01-30 02:12:05 -06:00
John Alanbrook
64933260d4 docs 2026-01-29 22:07:48 -06:00
John Alanbrook
561ab9d917 mem 2026-01-29 20:33:38 -06:00
John Alanbrook
bcd6e641a5 initial try 2026-01-27 19:17:44 -06:00
John Alanbrook
2857581271 cleanup 2026-01-27 10:42:49 -06:00
33 changed files with 27862 additions and 29075 deletions

View File

@@ -1,9 +1,20 @@
BasedOnStyle: GNU BasedOnStyle: GNU
Language: C
IndentWidth: 2 IndentWidth: 2
TabWidth: 2 TabWidth: 2
UseTab: Never UseTab: Never
ContinuationIndentWidth: 2 # Indents continuation lines by 2 spaces ContinuationIndentWidth: 2
AllowShortFunctionsOnASingleLine: true AllowShortFunctionsOnASingleLine: true
AllowShortBlocksOnASingleLine: true AllowShortBlocksOnASingleLine: true
AllowShortIfStatementsOnASingleLine: true AllowShortIfStatementsOnASingleLine: true
BreakBeforeBraces: Attach BreakBeforeBraces: Attach
ColumnLimit: 0
BreakFunctionDefinitionParameters: false
BinPackParameters: false
BinPackArguments: false
# --- Fix the "static T\nname(...)" style ---
AlwaysBreakAfterDefinitionReturnType: None
BreakAfterReturnType: None

View File

@@ -9,6 +9,8 @@
CELL_SHOP = $(HOME)/.cell CELL_SHOP = $(HOME)/.cell
CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core
maker: install
makecell: makecell:
cell pack core -o cell cell pack core -o cell
cp cell /opt/homebrew/bin/ cp cell /opt/homebrew/bin/
@@ -56,7 +58,7 @@ static:
# Bootstrap: build cell from scratch using meson (only needed once) # Bootstrap: build cell from scratch using meson (only needed once)
# Also installs core scripts to ~/.cell/core # Also installs core scripts to ~/.cell/core
bootstrap: bootstrap:
meson setup build_bootstrap -Dbuildtype=debugoptimized meson setup build_bootstrap -Dbuildtype=debug -Db_sanitize=address
meson compile -C build_bootstrap meson compile -C build_bootstrap
cp build_bootstrap/cell . cp build_bootstrap/cell .
cp build_bootstrap/libcell_runtime.dylib . cp build_bootstrap/libcell_runtime.dylib .

View File

@@ -8,14 +8,14 @@ static JSClassID js_writer_class_id;
static void js_reader_finalizer(JSRuntime *rt, JSValue val) { static void js_reader_finalizer(JSRuntime *rt, JSValue val) {
mz_zip_archive *zip = JS_GetOpaque(val, js_reader_class_id); mz_zip_archive *zip = JS_GetOpaque(val, js_reader_class_id);
mz_zip_reader_end(zip); mz_zip_reader_end(zip);
js_free_rt(rt,zip); js_free_rt(zip);
} }
static void js_writer_finalizer(JSRuntime *rt, JSValue val) { static void js_writer_finalizer(JSRuntime *rt, JSValue val) {
mz_zip_archive *zip = JS_GetOpaque(val, js_writer_class_id); mz_zip_archive *zip = JS_GetOpaque(val, js_writer_class_id);
mz_zip_writer_finalize_archive(zip); mz_zip_writer_finalize_archive(zip);
mz_zip_writer_end(zip); mz_zip_writer_end(zip);
js_free_rt(rt,zip); js_free_rt(zip);
} }
static JSClassDef js_reader_class = { static JSClassDef js_reader_class = {
@@ -101,7 +101,7 @@ static JSValue js_miniz_compress(JSContext *js, JSValue this_val,
size_t in_len = 0; size_t in_len = 0;
const void *in_ptr = NULL; const void *in_ptr = NULL;
if (JS_IsString(argv[0])) { if (JS_IsText(argv[0])) {
/* String → UTF-8 bytes without the terminating NUL */ /* String → UTF-8 bytes without the terminating NUL */
cstring = JS_ToCStringLen(js, &in_len, argv[0]); cstring = JS_ToCStringLen(js, &in_len, argv[0]);
if (!cstring) if (!cstring)

View File

@@ -1,8 +1,6 @@
#include "cell.h" #include "cell.h"
JSC_CCALL(os_gc, JS_RunGC(JS_GetRuntime(js)) )
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0]))) JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_gc_threshold, JS_SetGCThreshold(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(JS_GetRuntime(js), js2number(js,argv[0]))) JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(JS_GetRuntime(js), js2number(js,argv[0])))
// Compute the approximate size of a single JS value in memory. // Compute the approximate size of a single JS value in memory.
@@ -15,8 +13,7 @@ JSC_CCALL(os_calc_mem,
JS_SetPropertyStr(js,ret,"memory_used_size",number2js(js,mu.memory_used_size)); JS_SetPropertyStr(js,ret,"memory_used_size",number2js(js,mu.memory_used_size));
JS_SetPropertyStr(js,ret,"malloc_count",number2js(js,mu.malloc_count)); JS_SetPropertyStr(js,ret,"malloc_count",number2js(js,mu.malloc_count));
JS_SetPropertyStr(js,ret,"memory_used_count",number2js(js,mu.memory_used_count)); JS_SetPropertyStr(js,ret,"memory_used_count",number2js(js,mu.memory_used_count));
JS_SetPropertyStr(js,ret,"atom_count",number2js(js,mu.atom_count)); /* atom_count and atom_size removed - atoms are now just strings */
JS_SetPropertyStr(js,ret,"atom_size",number2js(js,mu.atom_size));
JS_SetPropertyStr(js,ret,"str_count",number2js(js,mu.str_count)); JS_SetPropertyStr(js,ret,"str_count",number2js(js,mu.str_count));
JS_SetPropertyStr(js,ret,"str_size",number2js(js,mu.str_size)); JS_SetPropertyStr(js,ret,"str_size",number2js(js,mu.str_size));
JS_SetPropertyStr(js,ret,"obj_count",number2js(js,mu.obj_count)); JS_SetPropertyStr(js,ret,"obj_count",number2js(js,mu.obj_count));
@@ -42,20 +39,22 @@ JSC_CCALL(os_calc_mem,
JSC_SSCALL(os_eval, JSC_SSCALL(os_eval,
if (!str2) return JS_ThrowReferenceError(js, "Second argument should be the script."); if (!str2) return JS_ThrowReferenceError(js, "Second argument should be the script.");
if (!str) return JS_ThrowReferenceError(js, "First argument should be the name of the script."); if (!str) return JS_ThrowReferenceError(js, "First argument should be the name of the script.");
ret = JS_Eval(js,str2,strlen(str2),str, 0); JSValue bytecode = JS_Compile(js, str2, strlen(str2), str);
if (JS_IsException(bytecode)) return bytecode;
ret = JS_Integrate(js, bytecode, JS_NULL);
) )
// Compile a string of JavaScript code into a function object. // Compile a string of JavaScript code into a function object.
JSC_SSCALL(js_compile, JSC_SSCALL(js_compile,
if (!str2) return JS_ThrowReferenceError(js, "Second argument should be the script."); if (!str2) return JS_ThrowReferenceError(js, "Second argument should be the script.");
if (!str) return JS_ThrowReferenceError(js, "First argument should be the name of the script."); if (!str) return JS_ThrowReferenceError(js, "First argument should be the name of the script.");
ret = JS_Eval(js, str2, strlen(str2), str, JS_EVAL_FLAG_COMPILE_ONLY | JS_EVAL_FLAG_BACKTRACE_BARRIER); ret = JS_Compile(js, str2, strlen(str2), str);
) )
// Evaluate a function object in the current QuickJS context. // Link compiled bytecode with environment and execute.
JSC_CCALL(js_eval_compile, JSC_CCALL(js_integrate,
JS_DupValue(js,argv[0]); JSValue env = (argc > 1 && !JS_IsNull(argv[1])) ? argv[1] : JS_NULL;
ret = JS_EvalFunction(js, argv[0]); ret = JS_Integrate(js, argv[0], env);
) )
// Compile a function object into a bytecode blob. // Compile a function object into a bytecode blob.
@@ -92,12 +91,10 @@ JSC_CCALL(js_fn_info,
static const JSCFunctionListEntry js_js_funcs[] = { static const JSCFunctionListEntry js_js_funcs[] = {
MIST_FUNC_DEF(os, calc_mem, 0), MIST_FUNC_DEF(os, calc_mem, 0),
MIST_FUNC_DEF(os, mem_limit, 1), MIST_FUNC_DEF(os, mem_limit, 1),
MIST_FUNC_DEF(os, gc_threshold, 1),
MIST_FUNC_DEF(os, max_stacksize, 1), MIST_FUNC_DEF(os, max_stacksize, 1),
MIST_FUNC_DEF(os, gc, 0),
MIST_FUNC_DEF(os, eval, 2), MIST_FUNC_DEF(os, eval, 2),
MIST_FUNC_DEF(js, compile, 2), MIST_FUNC_DEF(js, compile, 2),
MIST_FUNC_DEF(js, eval_compile, 1), MIST_FUNC_DEF(js, integrate, 2),
MIST_FUNC_DEF(js, compile_blob, 1), MIST_FUNC_DEF(js, compile_blob, 1),
MIST_FUNC_DEF(js, compile_unblob, 1), MIST_FUNC_DEF(js, compile_unblob, 1),
MIST_FUNC_DEF(js, disassemble, 1), MIST_FUNC_DEF(js, disassemble, 1),

921
docs/functions.md Normal file
View File

@@ -0,0 +1,921 @@
# Cell Functions
The intrinsics are constants and functions that are built into the language. The use statement is not needed to access them.
A programmer is not obliged to consult the list of intrinsics before naming a new variable or input.
New intrinsics may be added to Misty without breaking existing programs.
Constants
false
This is the value of 1 = 0. The false value is one of the two logical values.
true
This is the value of 1 = 1. The true value is one of the two logical values.
null
This is the value of 1 / 0. The null value is an empty immutable object. All attempts to obtain a value from null by refinement will produce null.
Any attempt to modify null will disrupt. Any attempt to call null as a function will disrupt.
null is the value of missing input values, missing fields in records, and invalid numbers. The |default operator can detect the presence of null and substitute another value.
pi
This is an approximation of the circle expression circumference / diameter, or to be precisely approximate, 3.1415926535897932.
Creator Functions
The creator functions are used to make new objects. Some of them can take various types. All of these functions can return null if their inputs are not suitable.
Array
array(number)
Make an array. All of the elements are initialized to null.
number is a non-negative integer, the intended length of the new array.
array(number, initial_value)
Make an array. All of the elements are initialized to initial_value.
number is a non-negative integer, the intended length of the new array.
If initial_value is a function, then the function is called for each element to produce initialization values. If the function has an arity of 1 or more, it is passed the element number.
array(array)
Copy. Make a mutable copy of the array.
array(array, function, reverse, exit)
Map. Call the function with each element of the array, collecting the return values in a new array. The function is passed each element and its element number.
function (element, element_nr)
If reverse is true, then it starts with the last element and works backwards.
If exit is not null, then when the function returns the exit value, then the array function returns early. The exit value will not be stored into the new array. If the array was processed normally, then the returned array will be shorter than the input array. If the array was processed in reverse, then the returned array will have the same length as the input array, and the first elements will be null. The elements in the new array that were not set due to an early exit will be set to null.
This is like the for function except that the return values are collected into a new array.
array(array, another_array)
Concat. Produce a new array that concatenates the array and another_array.
array(array, from, to)
Slice. Make a mutable copy of all or part of an array.
array: the array to copy
from: the position at which to start copying. Default: 0, the beginning. If negative, add length(array).
to: the position at which to stop copying. Default: length(array), the end. If negative, add length(array).
If, after adjustment, from and to are not valid integers in the proper range, then it returns null. from must be positive and less than or equal to to. to must be less than or equal to length(array).
array(record)
Keys. Make an array containing all of the text keys in the record. The keys are not guaranteed to be in any particular order.
array(text)
Split the text into grapheme clusters. A grapheme cluster is a Unicode codepoint and its contributing combining characters, if any.
array(text, separator)
Split the text into an array of subtexts. The separator can be a text or pattern.
array(text, length)
Dice the text into an array of subtexts of a given length.
Logical
logical(value)
if value = 0 \/ value = false \/ value = "false" \/ value = null
return false
fi
if value = 1 \/ value = true \/ value = "true"
return true
fi
return null
Number
number(logical)
The result is 1 or 0.
number(number)
The number is returned.
number(text, radix)
Convert a text to a number. The optional radix is an integer from 2 thru 37. (See Base 32.) The default radix is 10.
number(text, format)
number format
format radix separator decimal point
"" 10 .period
"n"
"u" _underbar
"d" ,comma
"s" space
"v" .period ,comma
"l" dependent on locale
"i" _underbar
"b" 2
"o" 8
"h" 16
"t" 32
"j" 0x- base 16
0o- base 8
0b- base 2
otherwise base 10
The number function converts a text into a number.
If it is unable to (possibly because of a formatting error), it returns null. The format character determines how the text is interpreted. If the format is not one of those listed, then null is returned.
Examples:
assign result: number("123,456,789.10", "d") # result is 123456789.1
assign result: number("123.456.789,10", "v") # result is 123456789.1
assign result: number("123.456.789,10", "d") # result is null
assign result: number("123 456 789.10", "s") # result is 123456789.1
assign result: number("12.350") # result is 12.35
assign result: number("12.350", "v") # result is 12350
assign result: number("12.350", "i") # result is null
assign result: number("666") # result is 666
assign result: number("666", "b") # result is null
assign result: number("666", "o") # result is 438
assign result: number("666", "h") # result is 1638
assign result: number("666", "t") # result is 6342
assign result: number("0666") # result is 666
Record
record(record)
Copy. Make a mutable copy.
record(record, another_record)
Combine. Make a copy of a record, and then put all the fields of another_record into the copy.
record(record, array_of_keys)
Select. Make a new record containing only the fields that are named by the array_of_keys.
record(array_of_keys)
Set. Make a record using the array as the source of the keys. Each field value is true.
record(array_of_keys, value)
Value Set. Make a record using the array as the source of the keys. Each field value is value.
record(array_of_keys, function)
Functional Value Set. Make a record using the array as the source of the keys. The function is called for each key, yielding the field values.
Text
text(array)
Convert an array to text. The array can contain text and unicode codepoints. All are concatenated together to make a single text.
text(array, separator)
Convert an array to text. The array can contain text and unicode codepoints. All are concatenated together to make a single text. The separator text is inserted between each piece. The default separator is the empty text.
text(number, radix)
Convert a number to text. The optional radix is an integer from 2 thru 37. (See Base 32.) The default radix is 10.
text(number, format)
The format of the format text is
format
format_separation format_style format_places
format_separation
""
digit
format_style
'b'
'c'
'e'
'h'
'i'
'l'
'n'
'o'
's'
't'
'u'
format_places
digit digit
digit
Convert a number to formatted text. The text function converts a number to a text. It takes a format text input.
A format text contains a style letter that controls how a text is produced from the number. It is optionally preceded by a separation digit, and optionally followed by a places digit. There are real styles and integer styles. If the format input is not a proper format text, then null is returned.
Separation is a character that is placed between digits to improve readability. If separation is 0, then there is no separation. If separation is 3, then a character is inserted before the quadrillions, trillions, billions, millions, and thousands.
Places is the number of places to display after the decimal point (in real styles) or the minimum number of digits to display with zero-fill (in integer styles). If places is 0, then as many digits as necessary are displayed. Places can be zero or one or two digits.
real style base default
separation default
places decimal
point separator
e exponential 10 0 0 .period
n number
s space 3 space
u underbar _underbar
d decimal 2 ,comma
c comma ,comma .period
l locale determined by the locale
The real format options are
"e" uses scientific notation. One digit is placed before the decimal point, and all of the remaining digits after, followed by e and the exponent.
"n" uses .period as the decimal point and no separator. It is the format used for numbers in Misty source programs and JSON. Scientific notation is used if the number value is extreme.
"s" uses .period as the decimal point and a space as the separator.
"u" uses .period as the decimal point and _underbar as the separator.
"d" uses .period as the decimal point and ,comma as the separator.
"c" uses ,comma as the decimal point and .period as the separator.
"l" depends on the locale to determine the characters to use as the decimal point and the separator.
The optional places determines the number of digits after the decimal point. The default is determined by the format, as seen in the table. If the places is 0, then the number of decimal places will be the fewest to exactly display the number without truncating. If the places is larger, then the field is padded if necessary with trailing 0.
The optional separation determines the spacing of the separator character. For example, to place a separator between billions, millions, and thousands (that is, every 3 digits) then separation should be 3. If separation is zero, then there is no separation. The default is determined by the style.
integer style base default
separation minimum
places separator
i integer 10 0 1 _underbar
b binary 2
o octal 8
h hexadecimal 16
t Base32 32
The integer styles first trunc the number. The fractional part of the number is ignored. The separation character is _underbar.
The optional places determines the minimum number of digits to show. More leading 0 may be shown if necessary. The default is determined by the format, as seen in the table.
The optional separation determines the spacing of the separator character. For example, to place a separator between billions, millions, and thousands (that is, every 3 digits) then separation should be 3. If separation is zero, then there is no separation. The default is determined by the format, as seen in the table.
Examples:
def data: 0123456789.1
assign result: text(data) # result is "123456789.1"
assign result: text(data, "n") # result is "123456789.1"
assign result: text(data, "3s4") # result is "123 456 789.1000"
assign result: text(data, "s") # result is "123 456 789.1"
assign result: text(data, "d2") # result is "123,456,789.10"
assign result: text(data, "4d0") # result is "1,2345,6789.1"
assign result: text(data, "v2") # result is "123.456.789,10"
assign result: text(data, "e") # result is "1.234567891e8"
assign result: text(data, "e4") # result is "1.2345e8"
assign result: text(data, "i") # result is "123456789"
assign result: text(data, "8b") # result is "111_01011011_11001101_00010101"
assign result: text(data, "o") # result is "726746425"
assign result: text(data, "h") # result is "75BCD15"
assign result: text(data, "t") # result is "3NQK8N"
assign result: text(12) # result is "12"
assign result: text(12, 8) # result is "14"
assign result: text(12, 32) # result is "C"
assign result: text(12, "4b8") # result is "0000_1100"
assign result: text(12, "o3") # result is "014"
assign result: text(12, "h4") # result is "000C"
assign result: text(12, "t2") # result is "0C"
text(text)
Return the text. The text is not altered.
text(text, from, to)
Make a copy of part of a text.
text: the text to copy.
from: the position at which to start copying. Default: 0, the beginning. If negative, add length(text).
to: the position at which to stop copying. Default: length(text), the end. If negative, add length(text).
If, after adjustment, from and to are not valid integers in the proper range, then it returns null. from must be positive and less than or equal to to. to must be less than or equal to length(text).
assign my_text: "miskatonic"
text(my_text, 0, 3) # "mis" # the first 3
text(my_text, 3, 6) # "kat" # from 3 to 6
text(my_text, 5) # "tonic" # exclude the first 5
text(my_text, 0, -4) # "miskat" # exclude the last 4
text(my_text, -3) # "nic" # the last 3
text(my_text, 0, 0) # ""
text(my_text, 10) # ""
text(my_text, 11) # null
text(my_text, 2, 1) # null
Sensory Functions
The sensory functions end with ?question mark. They always return a logical value.
actor?(value)
Is the value an actor address object?
actor?(me!) # true
actor?("actor") # false
actor?({actor: true}) # false
array?(value)
Is the value an array? If the value is an array, the result is true. Otherwise, the result is false.
array?(0) # false
array?({}) # false
array?([]) # true
not(array?([])) # false
array?(pattern (1- {letter digit "_-%"})) # false
array?(null) # false
array?("array") # false
blob?(value)
Is the value a blob? If the value is a blob, the result is true. Otherwise, the result is false.
blob?(0) # false
blob?("blob") # false
blob?(blob()) # true
character?(value)
Is the value a character? If the value is a text with a length of 1, then the result is true. Otherwise, the result is false.
character?(1) # false
character?("1") # true
character?("character") # false
character?("") # false
character?("\u{FFFE}") # true
character?() # false
character?("/q") # false
character?("\q") # true
character?(<<">>) # true
character?(null) # false
data?(value)
Is the value data? If the value is a text, number, logical, array, blob, or record, then the result is true. If the value is a function, pattern, or null, the result is false.
data?(0) # true
data?("") # true
data?(["0"]) # true
data?({}) # true
data?(null) # false
digit?(value)
Is the value a digit? If the value is a text with a length of 1 and is one of the 10 digit characters, then the result is true. Otherwise, the result is false.
digit?(0) # false
digit?("0") # true
digit?("9") # true
digit?("09") # false
digit?("digit") # false
digit?("") # false
digit?(1) # false
digit?(["0"]) # false
digit?("Z") # false
false?(value)
Is the value false?
fit?(number)
Is the number a fit number? A number is a fit number if it is an integer that fits in 56 bits. All fit numbers are integers in the range -36028797018963968 thru 36028797018963967. Only fit numbers can be given to the fit functions. Misty has additional integers that are too big to fit.
function?(value)
Is the value a function? If the value is a function, then the result is true. Otherwise, the result is false.
function?(0) # false
function?(function () (null)) # true
function?("function") # false
function?(null) # false
function?(function?) # true
integer?(value)
Is the value an integer? If the value is a number and if its fraction part is zero, then the result is true. Otherwise, the result is false.
integer?(0) # true
integer?(13 / 4) # false
integer?(16 / 4) # true
integer?(65.0000000) # true
integer?(65.0000001) # false
integer?(null) # false
integer?(true) # false
integer?(1) # true
integer?(36028797018963968) # true
integer?(1.00001e100) # true
letter?(value)
Is the value a letter? If the value is a text with a length of 1 and is a letter, then the result is true. Otherwise, the result is false.
letter?(0) # false
letter?("0") # false
letter?("letter") # false
letter?("l") # true
letter?("L") # true
letter?("") # false
letter?(null) # false
logical?(value)
Is the value a logical? A logical is either a false or a true. All other values are not logical.
logical?(false) # true
logical?(true) # true
logical?(0) # false
logical?() # false
logical?(null) # false
lower?(value)
Is the value a lower case letter? If the value is a text with a length of 1 and is a lower case letter, then the result is true. Otherwise, the result is false.
lower?(0) # false
lower?("0") # false
lower?("lower") # false
lower?("l") # true
lower?("L") # false
lower?("") # false
null?(value)
Is the value null? This does the same thing as value = null.
number?(value)
Is the value a number? If the value is a number, then the result is true. Otherwise, the result is false.
number?(0) # true
number?((13 / 4)) # true
number?((13 / 0)) # false
number?(98.6) # true
number?("0") # false
number?(1) # true
pattern?(value)
Is the value a pattern? If the value is a pattern, then the result is true. Otherwise, the result is false.
pattern?(pattern (1- {letter digit "_-%"})) # true
record?(value)
Is the value a record? If the value is a record, then the result is true. Otherwise, the result is false.
record?(0) # false
record?({}) # true
record?([]) # false
record?("record") # false
record?("{}") # false
record?(function () ({})) # false
record?(pattern (1- {letter digit "_-%"})) # false
record?(@) # true
stone?(value)
Is the value stone?
stone?("false") # true
stone?(9) # true
stone?(null) # true
stone?({}) # false
stone?(stone({})) # true
text?(value)
Is the value a text? If the value is a text, then the result is true. Otherwise, the result is false.
text?(0) # false
text?("0") # true
text?("number") # true
text?("") # true
text?(null) # false
true?(value)
Is the value true?
upper?(value)
Is the value an upper case letter? If the value is a text with a length of 1 and is an upper case letter, then the result is true. Otherwise, the result is false.
upper?(0) # false
upper?("0") # false
upper?("UPPER") # false
upper?("u") # false
upper?("U") # true
upper?("") # false
whitespace?(value)
Is the value whitespace? If the value is a nonempty text containing only whitespace characters, then the result is true. Otherwise, the result is false.
whitespace?(0) # false
whitespace?(32) # false
whitespace?(char(32)) # true
whitespace?("0") # false
whitespace?(" ") # true
whitespace?("\t") # true
whitespace?("\r") # true
whitespace?("\r\n") # true
whitespace?("space") # false
whitespace?(" ") # true
whitespace?(" L") # false
whitespace?("") # false
Standard Functions
abs(number)
Absolute value. Return the positive form of the number. If the input value is not a number, the result is null.
apply(function, array)
Apply. Execute the function and return its return value. Pass the elements of the array as input values. See proxy.
If the first input value is not a function, apply returns its first input value.
If length(array) is greater than length(function), it disrupts.
If the second argument is not an array, it is used as a single input value.
ceiling(number, place)
If place is 0 or null, the number is rounded up to the smallest integer that is greater than or equal to the number. If place is a small positive integer, then the number is rounded up to that decimal place.
Examples:
assign result: ceiling(12.3775) # result is 13
assign result: ceiling(12.3775, 0) # result is 13
assign result: ceiling(12.3775, 1) # result is 20
assign result: ceiling(12.3775, -2) # result is 12.38
assign result: ceiling(-12.3775, -2) # result is -12.37
assign result: ceiling(-12.3775) # result is -12
character(value)
If the value is a text, it returns the first character. If the value is a non-negative 32-bit integer, it returns the character from that codepoint. Otherwise, it returns the empty string.
codepoint(text)
The codepoint function returns the codepoint number of the first character of the text. If the input value is not a text, or if it is the empty text, then it returns null.
extract(text, pattern, from, to)
The text is matched to the pattern. If it does not match, the result is null. If the pattern does match, then the result is a record containing the saved fields.
fallback(requestor_array)
The fallback requestor factory returns a requestor function that tries each of the requestors in the requstor_array until it gets a success. When the requestor is called, it calls the first requestor in requestor_array. If that is eventually successful, its value is passed to the callback. But if that requestor fails, the next requestor is called, and so on. If none of the requestors is successful, then the fallback fails. If any one succeeds, then the fallback succeeds.
The fallback requestor returns a cancel function that can be called when the result is no longer needed.
filter(array, function)
The filter function calls a function for every element in the array, passing each element and its element number.
(element, element_nr)
When the function's return value is true, then the element is copied into a new array. If the function's return value is false, then the element is not copied into the new array. If the return value is not a logical, then the filter returns null.
It returns a new array. The length of the new array is between 0 thru length(array). It returns null if the function input is not a function.
Example:
def data: [0, 1.25, 2, 3.5, 4, 5.75]
def integers: filter(data, integer?) # integers is [0, 2, 4]
find(array, function, reverse, from)
Call the function for each element of the array, passing each element and its element number.
(element, element_nr)
If the function returns true, then find returns the element number of the current element.
If the second input value is not a function, then it is compared exactly to the elements.
If the reverse input value is true, then search begins at the end of the array and works backward.
The from input value gives the element number to search first. The default is 0 unless reverse is true, when the default is length(array) - 1.
find returns the element number of the found value. If nothing is found, find returns null.
floor(number, place)
If place is 0 or null, the number is rounded down to the greatest integer that is less than or equal to the number. If place is a small positive integer, then the number is rounded down to that many decimal places. For positive numbers, this is like discarding decimal places.
Examples:
assign result: floor(12.3775) # result is 12
assign result: floor(12.3775, -2) # result is 12.37
assign result: floor(-12.3775, 0) # result is -13
assign result: floor(-12.3775, -2) # result is -12.38
for(array, function, reverse, exit)
For each. Call the function with each element of the array. The function is passed each element and its element number.
(element, element_nr)
If reverse is true, then it starts with the last element and works backwards.
If exit is not null, then when the function returns the exit value, then the for function returns early. The exit value is usually true or false, but it may be anything. If exit is null, then every element is processed.
The for function returns null unless it did an early exit, when it returns the exit value.
format(text, collection, transformer)
The format function makes a new text with substitutions in an original text. A collection is either an array of texts or a record of texts.
A search is made for {left brace and }right brace in the text. If they are found, the middle text between them is examined. If the collection is an array, the middle text is used as a number, and then the matched {left brace and middle and }right brace are replaced with the text at that subscript in the array. If the collection is a record, and if the middle text is the key of a member of the collection with a text value, then the value of the member is used in the substitution. Unmatched text is not altered.
The text between {left brace and }right brace is broken on the :colon character. The left text will be used as a number or name to select a value from the collection. (The value need not be a text.)
If a transformer input is a function, then it is called with the collection[left text] and right text as inputs. If it returns a text, then the substitution is made.
If a transformer input is a record, then the right text is used to select a function from the transformer. That function is passed the value from the collection. If the return value is a text, that text will substitute. If there is no colon, then the empty text is used to select the function from the transformer. If the transformer does not produce a function, or if the function does not return a text, then no replacement occurs. If transformer[right text](collection[left text]) produces a text, then the substitution is made.
If the substitution is not made, and if collection[left text] is a number, then the right text is used as a format input in calling collection[left text].text(right text). If it returns a text, then the substitution is made.
Example:
var result: format("{0} in {1}!", ["Malmborg", "Plano"])
# result is "Malmborg in Plano!"
fraction(number)
The fraction function returns the fractional part of a number It returns null for non-numbers. Also see whole.
length(value)
The length function
value result
array number of elements
blob number of bits
logical null
function number of named inputs
null null
number null
record record.length()
text number of codepoints
Length. Find the length of an array in elements, a blob in bits, or a text in codepoints. For functions, it is the arity (or number of inputs).
If the value is a record containing a length field
If the length field contains a function, then length(my_record) has the same effect as my_record.length().
If the length field contains a number, return the number.
All other values produce null.
lower(text)
The lower function returns a text in which all uppercase characters are converted to lowercase.
Example:
assign result: lower("Carl Hollywood") # result is "carl hollywood"
max(number, number)
Maximum. The result is the larger of the two numbers. If either input value is not a number, the result is null.
max(3, 4) # 4
max can be used with min to constrain values within an acceptable range.
min(max(2, 3), 7) # 3
min(max(4, 3), 7) # 4
min(max(8, 3), 7) # 7
max(1, null) # null
max(null, 1) # null
min(number,number)
Minimum. The result is the smaller of the two numbers. If either input value is not a number, the result is null.
min(3, 4) # 3
modulo(dividend, divisor)
The result of modulo(dividend, divisor) is dividend - (divisor * floor(dividend / divisor)). The result has the sign of the divisor.
If dividend is 0, then the result is 0. If divisor is 0, or if either operand is not a number, then the result is null. dividend and divisor are not required to be integers.
If both input values are integers, and if the divisor is a positive power of two, then it is the same as and(dividend, divisor - 1).
neg(number)
Negate. Reverse the sign of a number. If the input value is not a number, the result is null. Note that the -minus sign is not used as a prefix negation operator.
normalize(text)
Unicode normalize. Return a text whose textual value is the same as text, but whose binary representation is in the specified Unicode normalization form. The two texts will display the same, but might not be equal.
not(logical)
Not. Return the opposite logical. If the input value is not a logical, it returns null.
parallel(requestor_array, throttle, need)
Parallel. The parallel requestor factory returns a resquestor function. When the requestor function is called with a callback function and a value, every requestor in the requestor_array is called with the value, and the results are placed in corresponding elements of the results array. This all happens in parallel. The value produced by the first element of the requestor_array provides the first element of the result. The completed results array is passed to the callback function.
By default, it starts all of the requestors in the requestor_array at once, each in its own turn so that they do not interfere with each other. This can shock some systems by unleashing a lot of demand all at once. To mitigate the shock, the optional throttle argument sets the maximum number of requestors running at a time. As requestors succeed or fail, waiting requestors can be started. The throttle is optional. If provided, the throttle is a number: the maximum number of requestors to have running at any time.
Ordinarily, the number of successes must be the same as the number of requestors in the requestor_array. If you need few successes, specify your need with the need argument. The need could be 1, meaning that 1 or more successes are needed. The need could be 0, meaning that no successes are needed. If the number of successes is greater than or equal to need, then the whole operation succeeds. The need must be between 0 and requestor_array.length.
The requestor function itself returns a cancel function that cancels all of the pending requestors from the requestor_array.
race(requestor_array, throttle, need)
Race. The race function is a requestor factory that takes an array of requestor functions and returns a requestor function that starts all of the requestors in the requestor_array at once.
By default, it starts all of the requestors in the requestor_array at once, each in its own turn so that they do not interfere with each other. This can shock some systems by unleashing a lot of demand at once. To mitigate the shock, the optional throttle argument sets the maximum number of requestors running at a time. As requestors succeed or fail, waiting requestors can be started.
By default, a single result is produced. If an array of results is need, specify the needed number of results in the need parameter. When the needed number of successful results is obtained, the operation ends. The results go into a sparce array aligned with the requestor_array, and unfinished requestors are cancelled. The need argument must be between 1 and requestor_array.length.
The returned requestor function returns a cancel function that cancels all of the pending requestors from the requestor_array.
reduce(array, function, initial, reverse)
Reduce. The reduce function takes a function that takes two input values and returns a value.
function (first, second) {
return ...
}
The function is called for each element of the array, passing the result of the previous iteration to the next iteration.
The initial value is optional. If present, the function is called for every element of the array.
If initial is null:
If length(array) is 0, then it returns null.
If length(array) is 1, then it returns array[0].
If length(array) is 2, it returns function(array[0], array[1]).
If length(array) is 3, it returns function(function(array[0], array[1]), array[2]).
And so on.
If initial is not null:
If length(array) is 0, then it returns initial.
If length(array) is 1, then it returns function(initial, array[0]).
If length(array) is 2, it returns function(function(initial, array[0]), array[1]).
If length(array) is 3, it returns function(function(function(initial, array[0]), array[1]), array[2]).
And so on.
If reverse is true, then the work begins at the end of the array and works backward.
Example:
def data: [1, 2, 3, 4, 5, 6, 7, 8, 9]
def total: reduce(data, '+) # total is 45
def product: reduce(data, '*) # product is 362880
remainder(dividend, divisor)
Remainder. For fit integers, the remainder is dividend - ((dividend // divisor) * divisor).
replace(text, target, replacement, limit)
Return a new text in which the target is replaced by the replacement.
text: the source text.
target: a text or pattern that should be replaced in the source.
replacement: text to replace the matched text, or a function that takes the matched text and the starting position, and returns the replacement text or null if it should be left alone.
limit: The maximum number of replacements. The default is all possible replacements. The limit includes null matches.
reverse(array)
The reverse function makes a new array or blob with the elements or bits in the opposite order.
It returns a new, reversed array or blob.
Example:
def data: ["I", "am", "Sam"]
def result: reverse(data) # the result is ["Sam", "am", "I"]
round(number, place)
If place is 0 or null, the number is rounded to the nearest integer. If place is a small integer, then the number is rounded to that many decimal places.
Examples:
round(12.3775) # 12
round(12.3775, -2) # 12.38
round(-12.3775, 0) # -12
round(-12.3775, 1) # -10
round(-12.3775, 2) # 0
round(-12.3775, -2) # -12.38
search(text, target, from)
Search the text for the target. If the target is found, return the character position of the left-most part of the match. If the search fails, return null.
text: the source text.
target: a text or pattern that should be found in the source.
from: The starting position for the search. The default is 0, the beginning of the text. If from is negative, it is added to length(text).
sequence(requestor_array)
Sequence. The sequence requestor factory that takes a requestor_array and returns a requestor function that starts all of the requestors in the requestor_array one at a time. The result of each becomes the input to the next. The last result is the result of the sequence.
sequence returns a requestor that returns a cancel function and processes each requestor in requestor_array one at a time. Each of those requestors is passed the result of the previous requestor as its value argument. If all succeed, then the sequence succeeds, giving the result of the last of the requestors. If any fail, then the sequence fails.The sequence succeeds if every requestor in the requestor_array succeeds
sign(number)
The sign function returns -1 if the number is negative, 0 if the number is exactly 0, 1 if the number is positive, and null if it is not a number.
sort(array, select)
The sort function produces a new array in which the values are sorted. Sort keys must be either all numbers or all texts. Any other type of key or any error in the key calculation will cause the sort to fail, returning null. The sort is ascending. The sort is stable, so the relative order of equal keys is preserved.
The optional select input determines how the sort key for each element is selected.
select type Sort key for array[index] Description
null array[index] The sort key is the element itself. This is useful for sorting simple arrays of numbers or texts.
text array[index][select] The sort key is the select field of each record element. This is useful for sorting arrays of records.
number array[index][select] The sort key is the select element of each array element. This is useful for sorting arrays of arrays.
array select[index] select is an array of the same length containing the sort keys.
It returns a new, sorted array.
Examples:
def foods: ["oats", "peas", "beans", "barley"]
def result: sort(foods) # result is ["barley", "beans", "oats", "peas"]
var stooges: [
{first: "Moe", last: "Howard"}
{first: "Joe", last: "DeRita"}
{first: "Shemp", last: "Howard"}
{first: "Larry", last: "Fine"}
{first: "Joe", last: "Besser"}
{first: "Curly", last: "Howard"}
]
assign stooges: sort(sort(stooges, "first"), "last")
# stooges is now [
# {first: "Joe", last: "Besser"}
# {first: "Joe", last: "DeRita"}
# {first: "Larry", last: "Fine"}
# {first: "Curly", last: "Howard"}
# {first: "Moe", last: "Howard"}
# {first: "Shemp", last: "Howard"}
# ]
assign stooges: sort(stooges, [50, 60, 20, 40, 10, 30])
# stooges is now [
# {first: "Moe", last: "Howard"}
# {first: "Larry", last: "Fine"}
# {first: "Shemp", last: "Howard"}
# {first: "Curly", last: "Howard"}
# {first: "Joe", last: "Besser"}
# {first: "Joe", last: "DeRita"}
# ]
stone(value)
Petrify the value, turning it into stone. Its contents are preserved, but it can no longer be modified by the assign statement or the blob.write functions. This is usually performed on arrays, records, and blobs. All other types are already stone. Attempting to turn a value that is stone to stone will have no effect.
The stone operation is deep. Any mutable objects in the value will also be turned to stone.
This can not be reversed. Immutable objects can never become mutable. A mutable copy can be made of an immutable object, but the original object remains immutable.
The stone function returns the value.
trim(text, reject)
The trim function removes selected characters from the ends of a text. The default is to remove control characters and spaces.
assign result: " Hello there ".trim() # result is "Hello there"
trunc(number, place)
The number is truncated toward zero. If the number is positive, the result is the same as floor(place). If the number is negative, the result is the same as ceiling(place).
If place is a small integer, then the number is rounded down to that many decimal places. This is like discarding decimal places.
Examples:
assign result: trunc(12.3775, 0) # result is 12
assign result: trunc(12.3775, 2) # result is 12.37
assign result: trunc(-12.3775) # result is -12
assign result: trunc(-12.3775, 2) # result is -12.37
turkish_lower(text)
Similar to lower, except that I goes to ıdotless i.
turkish_upper(text)
Similar to upper, except that i goes to İI dot.
upper(text)
The upper function returns a text in which all lowercase characters are converted to uppercase.
Example:
assign result: upper("Carl Hollywood") # result is "CARL HOLLYWOOD"
whole(number)
The whole function returns the whole part of a number. It returns null for non-numbers. Also see fraction.

View File

@@ -2,7 +2,7 @@
![image](wizard.png) ![image](wizard.png)
Cell is an actor-based scripting language for building concurrent applications. It combines a familiar JavaScript-like syntax with the actor model of computation. Cell is an actor-based scripting language for building concurrent applications. It combines a familiar C-like syntax with the actor model of computation, optimized for low memory usage and simplicity.
## Key Features ## Key Features

248
docs/memory.md Normal file
View File

@@ -0,0 +1,248 @@
# Cell actor scripting language
Cell is a Misty [https://mistysystem.com](https://mistysystem.com) implementation.
## Memory
Values are 32 bit for 32 bit builds and 64 bit for 64 bit builds.
### 32 bit value
LSB = 0
payload is a 31 bit signed int
LSB = 01
payload is a 30 bit pointer
LSB = 11
next 3 bits = special tag. 27 bits of payload.
### 64 bit value
LSB = 0
payload is a 32 bit signed int, using high 32 bits
LSB = 01
payload is a 61 bit pointer
LSB = 101
Short float: a 61 bit double, with 3 less exponent bits
LSB = 11
Special tag: next 3 bits. 5 bits total. 59 bits of payload. 8 total special tags.
Special tags:
1: Bool. Payload is 0 or 1.
2: null. payload is 0.
3: exception.
4: string.
Immediate string. Next 3 low bits = length in bytes. Rest is string data. This allows for strings up to 7 ascii letters. Encoded in utf8.
## Numbers and math
Cell can be compiled with different levels of exactness for numeracy. Any number which cannot be represented exactly becomes "null". Any numeric operation which includes "null" results in "null".
Using short floats in a 64 bit system means you have doubles in the range of +- 10^38, not the full range of double. If you create a number out of that range, it's null.
You can also compile a 64 bit system with full precision doubles, but this will use more memory and may be slower.
You can also compile a 64 bit system with 32 bit floats, stored as a 32 bit int is. Again, out of the 32 bit float range = null.
You can compile without floating point support at all; 32 bit ints are then used for fixed point calculations.
Or, you can compile using Dec64, which is a 64 bit decimal floating point format, for exact precision.
## Objects
Objects are heap allocated, referenced by a pointer value. They are all preceded by an object header, the length of a word on the system.
### 64 bit build
56 bits capacity
1 bit memory reclamation flag: note that this obj has already been moved
2 bit reserved (per object)
1 bit stone: note that this obj is immutable
3 bit type: note the type of the object
1 bit: fwd: note that this obj is a forward linkage
Last bit ..1:
The forward type indicates that the object (an array, blob, pretext, or record) has grown beyond its capacity and is now residing at a new address. The remaining 63 bits contain the address of the enlarged object. Forward linkages are cleaned up by the memory reclaimer.
Type 7: C light C object
Header
Pointer
Capacity is an ID of a registered C type.
Pointer is a pointer to the opaque C object.
Type 0: Array
Header
Length
Element[]
Capacity is number of elements the array can hold. Length is number of elements in use. Number of words used by an array is capacity + 2.
Type 1: blob
Header
Length
Bit[]
Capacity is number of bits the blob can hold. Length is number of bits in use. Bits follow, from [0] to [capacity - 1], with [0] bit in the most significant position of word 2, and [63] in the least significant position of word 2. The last word is zero filled, if necessary.
Number of words used is (capacity + 63) // 64 + 2
Type 2: Text
Text has two forms, depending on if it is stone or not, which changes the meaning of its length word.
Header
Length(pretext) or Hash(text)
Character[0] and character[1]
Capacity of pretex is the number of characters it can hold. During stoning and reclamation, capacity is set to the length.
The capacity of a text is its length.
The length of a pretext is the number of characters it contains; it is not greater than the capacity.
Hash of a text is used for organizing records. If the hash is zero, it's not been computed yet. All texts in the immutable memory have hashes.
A text object contains UTF32 characters, packed two per word. If the number of characters is odd, the least significant half of the last word is zero filled.
The number of words used by a text is (capacity + 1) // 2 + 2
Type 3: Record
A record is an array of fields represented as key/value pairs. Fields are located by hashes of texts, using open addressing with linear probing and lazy deletion. The load factor is less than 0.5.
Header
Prototype
Length
Key[0]
Value[0]
Key[1]
Value[1]
...
The capacity is the number of fields the record can hold. It is a power of two minus one. It is at least twice the length.
The length is the number of fields that the record currently contains.
A field candidate number is identified by and(key.hash, capacity). In case of hash collision, advance to the next field. If this goes past the end, continue with field 1. Field 0 is reserved.
The "exception" special tag is used to mark deleted entries in the object map.
The number of words used by a record is (capacity + 1) * 2.
Prototypes are searched for for properties if one cannot be found on the record itself. Prototypes can have prototypes.
#### key[0] and value[0]
These are reserved for internal use, and skipped over during key probing.
The first 32 bits of key are used as a 32 bit integer key, if this object has ever been used as a key itself.
The last 32 bits are used as an opaque C class key. C types can be registered with the system, and each are assigned a monotonically increasing number. In the case that this object has a C type, then the bottom 32 bits of key[0] are not 0. If that is the case, then a pointer to its C object is stored in value[0].
#### Valid keys & Hashing
Keys are stored directly in object maps. There are three possibilities for a vaild key: an object text, an object record, or an immediate text.
In the case of an immediate text, the hash is computed on the fly using the fash64_hash_one function, before being used to look up the key in the object map. Direct value comparison is used to confirm the key.
For object texts (texts longer than 7 ascii chars), the hash is stored in the text object itself. When an object text is used as a key, a stone version is created and interned. Any program static texts reference this stoned, interned text. When looking up a heap text as a key, it is first discovered if it's in the interned table. If it's not, the key is not in the object (since all keys are interned). If it is, the interned version is returned to check against the object map. The hash of the interned text is used to look up the key in the object map, and then direct pointer comparison is used to confirm the key.
For record keys, these are unique; once a record is used as a key, it gets assigned a monotonically increasing 32 bit integer, stored in key[0]. When checking it in an object map, the integer is used directly as the key. If key[0] is 0, the record has not been used as a key yet. If it's not 0, fash64_hash_one is used to compute a hash of its ID, and then direct value pointer comparison is used to confirm.
### Text interning
Texts that cannot fit in an immediate, and which are used as an object key, create a stoned and interned version (the pointer which is used as the key). Any text literals are also stoned and interned.
The interning table is an open addressed hash, with a load of 0.8, using a robin hood value. Probing is done using the text hash, confirmation is done using length, and then memcmp of the text.
When the GC run, a new interned text table is created. Each text literal, and each text used as a key, is added to the new table, as the live objects are copied. This keeps the interning table from becoming a graveyard. Interned values are never deleted until a GC.
Type 4: Function
Header
Code
Outer
A function object has zero capacity and is always stone.
Code is a pointer to the code object that the function executes.
Outer is a pointer to the frame that created this function object.
Size is 3 words.
Type 5: Frame
Header
Function
Caller
Return address
The activation frame is created when a function is invoked to hold its linkages and state.
The capacity is the number of slots, including the inputs, variables, temporaries, and the four words of overhead. A frame, unlike the other types, is never stone.
The function is the address of the function object being called.
The caller is the address of the frame that is invoking the function.
The return address is the address of the instruction in the code that should be executed upon return.
Next come the input arguments, if any.
Then the variables closed over by the inner functions.
Then the variables that are not closed over, followed by the temporaries.
When a function returns, the caller is set to zero. This is a signal to the memory reclaimer that the frame can be reduced.
Type 6: Code
Header
Arity
Size
Closure size
Entry point
Disruption point
A code object exists in the actor's immutable memory. A code object never exists in mutable memory.
A code object has a zero capacity and is always stone.
The arity is the maximum number of inputs.
The size is the capacity of an activation frame that will execute this code.
The closure size is a reduced capacity for returned frames that survive memory reclamation.
The entry point is the address at which to begin execution.
The disruption point is the address of the disruption clause.
### opaque C objects
Records can have opaque C data attached to them.
A C class can register a GC clean up, and a GC trace function. The trace function is called when the record is encountered in the live object graph; and it should mark any values it wants to keep alive in that function.
The system maintains an array of live opaque C objects. When such an object is encountered, it marks it as live in the array. When the GC completes, it iterates this array and calls the GC clean up function for each C object in the array with alive=0. Alive is then cleared for the next GC cycle.
## 32 bit build
~3 bit type
1 bit stone
1 bit memory reclamation flag
27 bit capacity
Key differences here are
blob max capacity is 2**27 bits = 2**24 bytes = 16 MB [this likely needs addressed]
fwd is type ...0, and the pointer is 31 bits
other types are
111 array
101 object
011 blob
001
## Memory
Cell uses a single block of memory that it doles out as needed to the actors in its system.
Actors are given a block of memory in standard sizes using a doubling buddy memory manager. An actor is given an immutable data section on birth, as well as a mutable data section. When its mutable data becomes full, it requests a new one. Actors utilize their mutable memory with a simple bump allocation. If there is not sufficient memory available, the actor suspends and its status changes to exhausted.
The smallest block size is determined per platform, but it can be as small as 4KB on 64 bit systems.
The actor is then given a new block of memory of the same size, and it runs a garbage collector to reclaim memory. It uses the cheney copying algorithm. If a disappointing amount of memory was reclaimed, it is noted, and the actor is given a larger block of memory on the next request.

93
fash.c Normal file
View File

@@ -0,0 +1,93 @@
/*
Fash64: Douglas Crockford (2017-02-02)
64-bit hash that uses the high 64 bits of a 128-bit product for feedback.
Notes:
- Requires a way to get the high half of a 64x64->128 multiply.
- Uses __uint128_t when available; otherwise uses MSVC _umul128.
*/
#include <stdint.h>
#include <stddef.h>
typedef struct fash64_state {
uint64_t result;
uint64_t sum;
} fash64_state;
enum {
FASH64_PRIME_11 = 11111111111111111027ull,
FASH64_PRIME_8 = 8888888888888888881ull,
FASH64_PRIME_3 = 3333333333333333271ull
};
static inline void fash64_mul_hi_lo(uint64_t a, uint64_t b, uint64_t *hi, uint64_t *lo)
{
#if defined(__SIZEOF_INT128__)
__uint128_t p = (__uint128_t)a * (__uint128_t)b;
*lo = (uint64_t)p;
*hi = (uint64_t)(p >> 64);
#elif defined(_MSC_VER) && defined(_M_X64)
*lo = _umul128(a, b, hi);
#else
/* Portable fallback (no 128-bit type, no _umul128). */
uint64_t a0 = (uint32_t)a;
uint64_t a1 = a >> 32;
uint64_t b0 = (uint32_t)b;
uint64_t b1 = b >> 32;
uint64_t p00 = a0 * b0;
uint64_t p01 = a0 * b1;
uint64_t p10 = a1 * b0;
uint64_t p11 = a1 * b1;
uint64_t mid = (p00 >> 32) + (uint32_t)p01 + (uint32_t)p10;
*lo = (p00 & 0xffffffffull) | (mid << 32);
*hi = p11 + (p01 >> 32) + (p10 >> 32) + (mid >> 32);
#endif
}
static inline void fash64_begin(fash64_state *s)
{
s->result = (uint64_t)FASH64_PRIME_8;
s->sum = (uint64_t)FASH64_PRIME_3;
}
static inline void fash64_word(fash64_state *s, uint64_t word)
{
uint64_t high, low;
uint64_t mixed = s->result ^ word;
fash64_mul_hi_lo(mixed, (uint64_t)FASH64_PRIME_11, &high, &low);
s->sum += high;
s->result = low ^ s->sum;
}
static inline void fash64_block(fash64_state *s, const uint64_t *block, size_t word_count)
{
for (size_t i = 0; i < word_count; i++) fash64_word(s, block[i]);
}
static inline uint64_t fash64_end(const fash64_state *s)
{
return s->result;
}
/* Convenience one-shot helper */
static inline uint64_t fash64_hash_words(const uint64_t *words, size_t word_count, uint64_t extra_word)
{
fash64_state s;
fash64_begin(&s);
fash64_block(&s, words, word_count);
fash64_word(&s, extra_word);
return fash64_end(&s);
}
static inline uint64_t fash64_hash_one(uint64_t word)
{
uint64_t high, low;
uint64_t mixed = (uint64_t)FASH64_PRIME_8 ^ word;
fash64_mul_hi_lo(mixed, (uint64_t)FASH64_PRIME_11, &high, &low);
return low ^ ((uint64_t)FASH64_PRIME_3 + high);
}

0
fash.h Normal file
View File

8
fd.c
View File

@@ -50,7 +50,7 @@ JSC_SCALL(fd_open,
mode_t mode = 0644; mode_t mode = 0644;
// Parse optional flags argument // Parse optional flags argument
if (argc > 1 && JS_IsString(argv[1])) { if (argc > 1 && JS_IsText(argv[1])) {
const char *flag_str = JS_ToCString(js, argv[1]); const char *flag_str = JS_ToCString(js, argv[1]);
flags = 0; flags = 0;
@@ -78,7 +78,7 @@ JSC_CCALL(fd_write,
size_t len; size_t len;
ssize_t wrote; ssize_t wrote;
if (JS_IsString(argv[1])) { if (JS_IsText(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]); const char *data = JS_ToCStringLen(js, &len, argv[1]);
if (!data) return JS_EXCEPTION; if (!data) return JS_EXCEPTION;
wrote = write(fd, data, len); wrote = write(fd, data, len);
@@ -276,7 +276,7 @@ JSC_SCALL(fd_mkdir,
JSC_SCALL(fd_mv, JSC_SCALL(fd_mv,
if (argc < 2) if (argc < 2)
ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path"); ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
else if (!JS_IsString(argv[1])) else if (!JS_IsText(argv[1]))
ret = JS_ThrowTypeError(js, "second argument must be a string (new path)"); ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
else { else {
const char *new_path = JS_ToCString(js, argv[1]); const char *new_path = JS_ToCString(js, argv[1]);
@@ -289,7 +289,7 @@ JSC_SCALL(fd_mv,
JSC_SCALL(fd_symlink, JSC_SCALL(fd_symlink,
if (argc < 2) if (argc < 2)
ret = JS_ThrowTypeError(js, "fd.symlink requires 2 arguments: target and link path"); ret = JS_ThrowTypeError(js, "fd.symlink requires 2 arguments: target and link path");
else if (!JS_IsString(argv[1])) else if (!JS_IsText(argv[1]))
ret = JS_ThrowTypeError(js, "second argument must be a string (link path)"); ret = JS_ThrowTypeError(js, "second argument must be a string (link path)");
else { else {
const char *link_path = JS_ToCString(js, argv[1]); const char *link_path = JS_ToCString(js, argv[1]);

View File

@@ -39,7 +39,7 @@ JSC_SCALL(fd_open,
FileOptions flags = kFileRead; FileOptions flags = kFileRead;
// Parse optional flags argument // Parse optional flags argument
if (argc > 1 && JS_IsString(argv[1])) { if (argc > 1 && JS_IsText(argv[1])) {
const char *flag_str = JS_ToCString(js, argv[1]); const char *flag_str = JS_ToCString(js, argv[1]);
flags = 0; flags = 0;
@@ -70,7 +70,7 @@ JSC_CCALL(fd_write,
size_t len; size_t len;
int wrote; int wrote;
if (JS_IsString(argv[1])) { if (JS_IsText(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]); const char *data = JS_ToCStringLen(js, &len, argv[1]);
if (!data) return JS_EXCEPTION; if (!data) return JS_EXCEPTION;
wrote = pd_file->write(fd, data, (unsigned int)len); wrote = pd_file->write(fd, data, (unsigned int)len);
@@ -202,7 +202,7 @@ JSC_SCALL(fd_mkdir,
JSC_SCALL(fd_mv, JSC_SCALL(fd_mv,
if (argc < 2) if (argc < 2)
ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path"); ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path");
else if (!JS_IsString(argv[1])) else if (!JS_IsText(argv[1]))
ret = JS_ThrowTypeError(js, "second argument must be a string (new path)"); ret = JS_ThrowTypeError(js, "second argument must be a string (new path)");
else { else {
const char *new_path = JS_ToCString(js, argv[1]); const char *new_path = JS_ToCString(js, argv[1]);
@@ -216,7 +216,7 @@ JSC_SCALL(fd_mv,
JSC_SCALL(fd_symlink, JSC_SCALL(fd_symlink,
// Not supported // Not supported
if (argc >= 2 && JS_IsString(argv[1])) { if (argc >= 2 && JS_IsText(argv[1])) {
// consume arg // consume arg
JS_FreeCString(js, JS_ToCString(js, argv[1])); JS_FreeCString(js, JS_ToCString(js, argv[1]));
} }

403
gc_plan.md Normal file
View File

@@ -0,0 +1,403 @@
# Plan: Complete Copying GC Implementation
## Overview
Remove reference counting (DupValue/FreeValue) entirely and complete the Cheney copying garbage collector. Each JSContext will use bump allocation from a heap block, and when out of memory, request a new heap from JSRuntime's buddy allocator and copy live objects to the new heap.
## Target Architecture (from docs/memory.md)
### Object Types (simplified from current):
**Type 0 - Array**: `{ header, length, elements[] }`
**Type 1 - Blob**: `{ header, length, bits[] }`
**Type 2 - Text**: `{ header, length_or_hash, packed_chars[] }`
**Type 3 - Record**: `{ header, prototype, length, key_value_pairs[] }`
**Type 4 - Function**: `{ header, code_ptr, outer_frame_ptr }` - 3 words only, always stone
**Type 5 - Frame**: `{ header, function_ptr, caller_ptr, ret_addr, args[], closure_vars[], local_vars[], temps[] }`
**Type 6 - Code**: Lives in immutable memory only, never copied
**Type 7 - Forward**: Object has moved; cap56 contains new address
### Key Design Points:
- **JSFunction** is just a pointer to code and a pointer to the frame that created it (3 words)
- **Closure variables live in frames** - when a function returns, its frame is "reduced" to just the closure variables
- **Code objects are immutable** - stored in stone memory, never copied during GC
- **Frame reduction**: When a function returns, `caller` is set to zero, signaling the frame can be shrunk
## Current State (needs refactoring)
1. **Partial Cheney GC exists** at `source/quickjs.c:1844-2030`: `ctx_gc`, `gc_copy_value`, `gc_scan_object`
2. **744 calls to JS_DupValue/JS_FreeValue** scattered throughout (currently undefined, causing compilation errors)
3. **Current JSFunction** is bloated (has kind, name, union of cfunc/bytecode/bound) - needs simplification
4. **Current JSVarRef** is a separate object - should be eliminated, closures live in frames
5. **Bump allocator** in `js_malloc` (line 1495) with `heap_base`/`heap_free`/`heap_end`
6. **Buddy allocator** for memory blocks (lines 1727-1837)
7. **Header offset inconsistency** - some structs have header at offset 0, some at offset 8
## Implementation Steps
### Phase 1: Define No-Op DupValue/FreeValue (To Enable Compilation)
Add these near line 100 in `source/quickjs.c`:
```c
/* Copying GC - no reference counting needed */
#define JS_DupValue(ctx, v) (v)
#define JS_FreeValue(ctx, v) ((void)0)
#define JS_DupValueRT(rt, v) (v)
#define JS_FreeValueRT(rt, v) ((void)0)
```
This makes the code compile while keeping existing call sites (they become no-ops).
### Phase 2: Standardize Object Headers (offset 0)
Remove `JSGCObjectHeader` (ref counting remnant) and put `objhdr_t` at offset 0:
```c
typedef struct JSArray {
objhdr_t hdr; // offset 0
word_t length;
JSValue values[];
} JSArray;
typedef struct JSRecord {
objhdr_t hdr; // offset 0
JSRecord *proto;
word_t length;
slot slots[];
} JSRecord;
typedef struct JSText {
objhdr_t hdr; // offset 0
word_t length; // pretext: length, text: hash
word_t packed[];
} JSText;
typedef struct JSBlob {
objhdr_t hdr; // offset 0
word_t length;
uint8_t bits[];
} JSBlob;
/* Simplified JSFunction per memory.md - 3 words */
typedef struct JSFunction {
objhdr_t hdr; // offset 0, always stone
JSCode *code; // pointer to immutable code object
struct JSFrame *outer; // frame that created this function
} JSFunction;
/* JSFrame per memory.md */
typedef struct JSFrame {
objhdr_t hdr; // offset 0
JSFunction *function; // function being executed
struct JSFrame *caller; // calling frame (NULL = reduced/returned)
word_t ret_addr; // return instruction address
JSValue slots[]; // args, closure vars, locals, temps
} JSFrame;
/* JSCode - always in immutable (stone) memory */
typedef struct JSCode {
objhdr_t hdr; // offset 0, always stone
word_t arity; // max number of inputs
word_t frame_size; // capacity of activation frame
word_t closure_size; // reduced capacity for returned frames
word_t entry_point; // address to begin execution
word_t disruption_point;// address of disruption clause
uint8_t bytecode[]; // actual bytecode
} JSCode;
```
### Phase 3: Complete gc_object_size for All Types
Update `gc_object_size` (line 1850) to read header at offset 0:
```c
static size_t gc_object_size(void *ptr) {
objhdr_t hdr = *(objhdr_t*)ptr; // Header at offset 0
uint8_t type = objhdr_type(hdr);
uint64_t cap = objhdr_cap56(hdr);
switch (type) {
case OBJ_ARRAY:
return sizeof(JSArray) + cap * sizeof(JSValue);
case OBJ_BLOB:
return sizeof(JSBlob) + (cap + 7) / 8; // cap is bits
case OBJ_TEXT:
return sizeof(JSText) + ((cap + 1) / 2) * sizeof(uint64_t);
case OBJ_RECORD:
return sizeof(JSRecord) + (cap + 1) * sizeof(slot); // cap is mask
case OBJ_FUNCTION:
return sizeof(JSFunction); // 3 words
case OBJ_FRAME:
return sizeof(JSFrame) + cap * sizeof(JSValue); // cap is slot count
case OBJ_CODE:
return 0; // Code is never copied (immutable)
default:
return 64; // Conservative fallback
}
}
```
### Phase 4: Complete gc_scan_object for All Types
Update `gc_scan_object` (line 1924):
```c
static void gc_scan_object(JSContext *ctx, void *ptr, uint8_t **to_free, uint8_t *to_end) {
objhdr_t hdr = *(objhdr_t*)ptr;
uint8_t type = objhdr_type(hdr);
switch (type) {
case OBJ_ARRAY: {
JSArray *arr = (JSArray*)ptr;
for (uint32_t i = 0; i < arr->length; i++) {
arr->values[i] = gc_copy_value(ctx, arr->values[i], to_free, to_end);
}
break;
}
case OBJ_RECORD: {
JSRecord *rec = (JSRecord*)ptr;
// Copy prototype
if (rec->proto) {
JSValue proto_val = JS_MKPTR(rec->proto);
proto_val = gc_copy_value(ctx, proto_val, to_free, to_end);
rec->proto = (JSRecord*)JS_VALUE_GET_PTR(proto_val);
}
// Copy table entries
uint32_t mask = objhdr_cap56(rec->hdr);
for (uint32_t i = 1; i <= mask; i++) { // Skip slot 0
JSValue k = rec->slots[i].key;
if (!rec_key_is_empty(k) && !rec_key_is_tomb(k)) {
rec->slots[i].key = gc_copy_value(ctx, k, to_free, to_end);
rec->slots[i].value = gc_copy_value(ctx, rec->slots[i].value, to_free, to_end);
}
}
break;
}
case OBJ_FUNCTION: {
JSFunction *func = (JSFunction*)ptr;
// Code is immutable, don't copy - but outer frame needs copying
if (func->outer) {
JSValue outer_val = JS_MKPTR(func->outer);
outer_val = gc_copy_value(ctx, outer_val, to_free, to_end);
func->outer = (JSFrame*)JS_VALUE_GET_PTR(outer_val);
}
break;
}
case OBJ_FRAME: {
JSFrame *frame = (JSFrame*)ptr;
// Copy function pointer
if (frame->function) {
JSValue func_val = JS_MKPTR(frame->function);
func_val = gc_copy_value(ctx, func_val, to_free, to_end);
frame->function = (JSFunction*)JS_VALUE_GET_PTR(func_val);
}
// Copy caller (unless NULL = reduced frame)
if (frame->caller) {
JSValue caller_val = JS_MKPTR(frame->caller);
caller_val = gc_copy_value(ctx, caller_val, to_free, to_end);
frame->caller = (JSFrame*)JS_VALUE_GET_PTR(caller_val);
}
// Copy all slots (args, closure vars, locals, temps)
uint32_t slot_count = objhdr_cap56(frame->hdr);
for (uint32_t i = 0; i < slot_count; i++) {
frame->slots[i] = gc_copy_value(ctx, frame->slots[i], to_free, to_end);
}
break;
}
case OBJ_TEXT:
case OBJ_BLOB:
case OBJ_CODE:
// No internal references to scan
break;
}
}
```
### Phase 5: Fix gc_copy_value Forwarding
Update `gc_copy_value` (line 1883) for offset 0 headers:
```c
static JSValue gc_copy_value(JSContext *ctx, JSValue v, uint8_t **to_free, uint8_t *to_end) {
if (!JS_IsPtr(v)) return v; // Immediate value
void *ptr = JS_VALUE_GET_PTR(v);
// Stone memory - don't copy (includes Code objects)
objhdr_t hdr = *(objhdr_t*)ptr;
if (objhdr_s(hdr)) return v;
// Check if in current heap
if ((uint8_t*)ptr < ctx->heap_base || (uint8_t*)ptr >= ctx->heap_end)
return v; // External allocation
// Already forwarded?
if (objhdr_type(hdr) == OBJ_FORWARD) {
void *new_ptr = (void*)(uintptr_t)objhdr_cap56(hdr);
return JS_MKPTR(new_ptr);
}
// Copy object to new space
size_t size = gc_object_size(ptr);
void *new_ptr = *to_free;
*to_free += size;
memcpy(new_ptr, ptr, size);
// Leave forwarding pointer in old location
*(objhdr_t*)ptr = objhdr_make((uint64_t)(uintptr_t)new_ptr, OBJ_FORWARD, 0, 0, 0, 0);
return JS_MKPTR(new_ptr);
}
```
### Phase 6: Complete GC Root Tracing
Update `ctx_gc` (line 1966) to trace all roots including JSGCRef:
```c
static int ctx_gc(JSContext *ctx) {
// ... existing setup code ...
// Copy roots: global object, class prototypes, etc. (existing)
ctx->global_obj = gc_copy_value(ctx, ctx->global_obj, &to_free, to_end);
ctx->global_var_obj = gc_copy_value(ctx, ctx->global_var_obj, &to_free, to_end);
// ... other existing root copying ...
// Copy GC root stack (JS_PUSH_VALUE/JS_POP_VALUE)
for (JSGCRef *ref = ctx->top_gc_ref; ref; ref = ref->prev) {
ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end);
}
// Copy GC root list (JS_AddGCRef/JS_DeleteGCRef)
for (JSGCRef *ref = ctx->last_gc_ref; ref; ref = ref->prev) {
ref->val = gc_copy_value(ctx, ref->val, &to_free, to_end);
}
// Copy current exception
ctx->current_exception = gc_copy_value(ctx, ctx->current_exception, &to_free, to_end);
// Cheney scan (existing)
// ...
}
```
### Phase 7: Trigger GC on Allocation Failure
Update `js_malloc` (line 1495):
```c
void *js_malloc(JSContext *ctx, size_t size) {
size = (size + 7) & ~7; // Align to 8 bytes
if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) {
if (ctx_gc(ctx) < 0) {
JS_ThrowOutOfMemory(ctx);
return NULL;
}
// Retry after GC
if ((uint8_t*)ctx->heap_free + size > (uint8_t*)ctx->heap_end) {
JS_ThrowOutOfMemory(ctx);
return NULL;
}
}
void *ptr = ctx->heap_free;
ctx->heap_free = (uint8_t*)ctx->heap_free + size;
return ptr;
}
```
### Phase 8: Frame Reduction (for closures)
When a function returns, "reduce" its frame to just closure variables:
```c
static void reduce_frame(JSContext *ctx, JSFrame *frame) {
if (frame->caller == NULL) return; // Already reduced
JSCode *code = frame->function->code;
uint32_t closure_size = code->closure_size;
// Shrink capacity to just closure variables
frame->hdr = objhdr_make(closure_size, OBJ_FRAME, 0, 0, 0, 0);
frame->caller = NULL; // Signal: frame is reduced
}
```
### Phase 9: Remove Unused Reference Counting Code
Delete:
- `gc_decref`, `gc_decref_child` functions
- `gc_scan_incref_child`, `gc_scan_incref_child2` functions
- `JS_GCPhaseEnum`, `gc_phase` fields
- `JSGCObjectHeader` struct (merge into objhdr_t)
- `ref_count` fields from any remaining structs
- `mark_function_children_decref` function
- All `free_*` functions that rely on ref counting
## Files to Modify
1. **source/quickjs.c** - Main implementation:
- Add DupValue/FreeValue no-op macros (~line 100)
- Restructure JSArray, JSBlob, JSText, JSRecord (lines 468-499)
- Simplify JSFunction to 3-word struct (line 1205)
- Add JSFrame as heap object (new)
- Restructure JSCode/JSFunctionBytecode (line 1293)
- Fix gc_object_size (line 1850)
- Fix gc_copy_value (line 1883)
- Complete gc_scan_object (line 1924)
- Update ctx_gc for all roots (line 1966)
- Update js_malloc to trigger GC (line 1495)
- Delete ref counting code throughout
2. **source/quickjs.h** - Public API:
- Remove JSGCObjectHeader
- Update JSValue type checks if needed
- Ensure JS_IsStone works with offset 0 headers
## Execution Order
1. **First**: Add DupValue/FreeValue macros (enables compilation)
2. **Second**: Standardize struct layouts (header at offset 0)
3. **Third**: Fix gc_object_size and gc_copy_value
4. **Fourth**: Complete gc_scan_object for all types
5. **Fifth**: Update ctx_gc with complete root tracing
6. **Sixth**: Wire js_malloc to trigger GC
7. **Seventh**: Add frame reduction for closures
8. **Finally**: Remove ref counting dead code
## Verification
1. **Compile test**: `make` should succeed without errors
2. **Basic test**: Run simple scripts:
```js
var a = [1, 2, 3]
log.console(a[1])
```
3. **Stress test**: Allocate many objects to trigger GC:
```js
for (var i = 0; i < 100000; i++) {
var x = { value: i }
}
log.console("done")
```
4. **Closure test**: Test functions with closures survive GC:
```js
fn make_counter() {
var count = 0
fn inc() { count = count + 1; return count }
return inc
}
var c = make_counter()
log.console(c()) // 1
log.console(c()) // 2
```
5. **GC stress with closures**: Create many closures, trigger GC, verify they still work
## Key Design Decisions (Resolved)
1. **JSCode storage**: Lives in stone (immutable) memory, never copied during GC ✓
2. **Header offset**: Standardized to offset 0 for all heap objects ✓
3. **Closure variables**: Live in JSFrame objects; frames are "reduced" when functions return ✓
4. **JSVarRef**: Eliminated - closures reference their outer frame directly ✓

View File

@@ -1,19 +1,12 @@
(function engine() { (function engine() {
var _cell = globalThis.cell // Hidden vars (os, actorsym, init, core_path) come from env
delete globalThis.cell var ACTORDATA = actorsym
var ACTORDATA = _cell.hidden.actorsym
var SYSYM = '__SYSTEM__' var SYSYM = '__SYSTEM__'
var hidden = _cell.hidden var _cell = {}
var os = hidden.os;
_cell.os = null
var dylib_ext var dylib_ext
_cell.id ??= "newguy"
switch(os.platform()) { switch(os.platform()) {
case 'Windows': dylib_ext = '.dll'; break; case 'Windows': dylib_ext = '.dll'; break;
case 'macOS': dylib_ext = '.dylib'; break; case 'macOS': dylib_ext = '.dylib'; break;
@@ -28,8 +21,7 @@ function use_embed(name) {
return load_internal("js_" + name + "_use") return load_internal("js_" + name + "_use")
} }
globalThis.logical = function(val1) function logical(val1) {
{
if (val1 == 0 || val1 == false || val1 == "false" || val1 == null) if (val1 == 0 || val1 == false || val1 == "false" || val1 == null)
return false; return false;
if (val1 == 1 || val1 == true || val1 == "true") if (val1 == 1 || val1 == true || val1 == "true")
@@ -37,19 +29,19 @@ globalThis.logical = function(val1)
return null; return null;
} }
globalThis.some = function(arr, pred) { function some(arr, pred) {
return find(arr, pred) != null return find(arr, pred) != null
} }
globalThis.every = function(arr, pred) { function every(arr, pred) {
return find(arr, x => not(pred(x))) == null return find(arr, x => not(pred(x))) == null
} }
globalThis.starts_with = function(str, prefix) { function starts_with(str, prefix) {
return search(str, prefix) == 0 return search(str, prefix) == 0
} }
globalThis.ends_with = function(str, suffix) { function ends_with(str, suffix) {
return search(str, suffix, -length(suffix)) != null return search(str, suffix, -length(suffix)) != null
} }
@@ -99,8 +91,7 @@ function use_core(path) {
var blob = use_core('blob') var blob = use_core('blob')
globalThis.actor = function() function actor() {
{
} }
@@ -108,7 +99,7 @@ var actor_mod = use_core('actor')
var wota = use_core('wota') var wota = use_core('wota')
var nota = use_core('nota') var nota = use_core('nota')
globalThis.is_actor = function(value) { function is_actor(value) {
return is_object(value) && value[ACTORDATA] return is_object(value) && value[ACTORDATA]
} }
@@ -138,7 +129,7 @@ function console_rec(line, file, msg) {
// time: [${time.text("mb d yyyy h:nn:ss")}] // time: [${time.text("mb d yyyy h:nn:ss")}]
} }
globalThis.log = function(name, args) { function log(name, args) {
var caller = caller_data(1) var caller = caller_data(1)
var msg = args[0] var msg = args[0]
@@ -201,9 +192,8 @@ function disrupt(err)
actor_mod.on_exception(disrupt) actor_mod.on_exception(disrupt)
_cell.args = _cell.hidden.init _cell.args = init ?? {}
_cell.args ??= {} _cell.id = "newguy"
_cell.id ??= "newguy"
function create_actor(desc = {id:guid()}) { function create_actor(desc = {id:guid()}) {
var actor = {} var actor = {}
@@ -224,10 +214,30 @@ var json = use_core('json')
var time = use_core('time') var time = use_core('time')
var pronto = use_core('pronto') var pronto = use_core('pronto')
globalThis.fallback = pronto.fallback var fallback = pronto.fallback
globalThis.parallel = pronto.parallel var parallel = pronto.parallel
globalThis.race = pronto.race var race = pronto.race
globalThis.sequence = pronto.sequence var sequence = pronto.sequence
// Create runtime environment for modules
var runtime_env = {
logical: logical,
some: some,
every: every,
starts_with: starts_with,
ends_with: ends_with,
actor: actor,
is_actor: is_actor,
log: log,
send: send,
fallback: fallback,
parallel: parallel,
race: race,
sequence: sequence
}
// Pass to os for shop to access
os.runtime_env = runtime_env
$_.time_limit = function(requestor, seconds) $_.time_limit = function(requestor, seconds)
{ {
@@ -597,13 +607,13 @@ var need_stop = false
var replies = {} var replies = {}
globalThis.send = function send(actor, message, reply) { function send(actor, message, reply) {
if (!is_object(actor)) if (!is_object(actor))
throw Error(`Must send to an actor object. Provided: ${actor}`); throw Error(`Must send to an actor object. Provided: ${actor}`);
if (!is_object(message)) if (!is_object(message))
throw Error('Message must be an object') throw Error('Message must be an object')
var send = {type:"user", data: message} var send_msg = {type:"user", data: message}
if (actor[HEADER] && actor[HEADER].replycc) { if (actor[HEADER] && actor[HEADER].replycc) {
var header = actor[HEADER] var header = actor[HEADER]
@@ -611,7 +621,7 @@ globalThis.send = function send(actor, message, reply) {
throw Error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`) throw Error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
actor = header.replycc actor = header.replycc
send.return = header.reply send_msg.return = header.reply
} }
if (reply) { if (reply) {
@@ -623,12 +633,12 @@ globalThis.send = function send(actor, message, reply) {
delete replies[id] delete replies[id]
} }
}, REPLYTIMEOUT) }, REPLYTIMEOUT)
send.reply = id send_msg.reply = id
send.replycc = $_.self send_msg.replycc = $_.self
} }
// Instead of sending immediately, queue it // Instead of sending immediately, queue it
actor_prep(actor,send); actor_prep(actor, send_msg);
} }
stone(send) stone(send)
@@ -647,7 +657,6 @@ function turn(msg)
} }
//log.console(`FIXME: need to get main from config, not just set to true`) //log.console(`FIXME: need to get main from config, not just set to true`)
//log.console(`FIXME: add freeze/unfreeze at this level, so we can do it (but scripts cannot)`)
actor_mod.register_actor(_cell.id, turn, true, config.ar_timer) actor_mod.register_actor(_cell.id, turn, true, config.ar_timer)
if (config.actor_memory) if (config.actor_memory)
@@ -786,8 +795,6 @@ if (!locator) {
if (!locator) if (!locator)
throw Error(`Main program ${_cell.args.program} could not be found`) throw Error(`Main program ${_cell.args.program} could not be found`)
stone(globalThis)
$_.clock(_ => { $_.clock(_ => {
// Get capabilities for the main program // Get capabilities for the main program
var file_info = shop.file_info ? shop.file_info(locator.path) : null var file_info = shop.file_info ? shop.file_info(locator.path) : null

View File

@@ -1,386 +0,0 @@
#include "cell.h"
#include "cell_internal.h"
#define NOTA_IMPLEMENTATION
#include "nota.h"
typedef struct NotaEncodeContext {
JSContext *ctx;
JSValue visitedStack;
NotaBuffer nb;
int cycle;
JSValue replacer;
} NotaEncodeContext;
static void nota_stack_push(NotaEncodeContext *enc, JSValueConst val)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visitedStack);
JS_SetPropertyInt64(ctx, enc->visitedStack, len, JS_DupValue(ctx, val));
}
static void nota_stack_pop(NotaEncodeContext *enc)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visitedStack);
JS_SetPropertyStr(ctx, enc->visitedStack, "length", JS_NewUint32(ctx, len - 1));
}
static int nota_stack_has(NotaEncodeContext *enc, JSValueConst val)
{
JSContext *ctx = enc->ctx;
int len = JS_ArrayLength(ctx, enc->visitedStack);
for (int i = 0; i < len; i++) {
JSValue elem = JS_GetPropertyUint32(ctx, enc->visitedStack, i);
if (JS_IsObject(elem) && JS_IsObject(val)) {
if (JS_StrictEq(ctx, elem, val)) {
JS_FreeValue(ctx, elem);
return 1;
}
}
JS_FreeValue(ctx, elem);
}
return 0;
}
static JSValue apply_replacer(NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
if (JS_IsNull(enc->replacer)) return JS_DupValue(enc->ctx, val);
JSValue args[2] = { JS_DupValue(enc->ctx, key), JS_DupValue(enc->ctx, val) };
JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
JS_FreeValue(enc->ctx, args[0]);
JS_FreeValue(enc->ctx, args[1]);
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
return result;
}
char *js_do_nota_decode(JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
int type = nota_type(nota);
JSValue ret2;
long long n;
double d;
int b;
char *str;
uint8_t *blob;
switch(type) {
case NOTA_BLOB:
nota = nota_read_blob(&n, (char**)&blob, nota);
*tmp = js_new_blob_stoned_copy(js, blob, n);
free(blob);
break;
case NOTA_TEXT:
nota = nota_read_text(&str, nota);
*tmp = JS_NewString(js, str);
free(str);
break;
case NOTA_ARR:
nota = nota_read_array(&n, nota);
*tmp = JS_NewArray(js);
for (int i = 0; i < n; i++) {
nota = js_do_nota_decode(js, &ret2, nota, *tmp, JS_NewInt32(js, i), reviver);
JS_SetPropertyInt64(js, *tmp, i, ret2);
}
break;
case NOTA_REC:
nota = nota_read_record(&n, nota);
*tmp = JS_NewObject(js);
for (int i = 0; i < n; i++) {
nota = nota_read_text(&str, nota);
JSValue prop_key = JS_NewString(js, str);
nota = js_do_nota_decode(js, &ret2, nota, *tmp, prop_key, reviver);
JS_SetPropertyStr(js, *tmp, str, ret2);
JS_FreeValue(js, prop_key);
free(str);
}
break;
case NOTA_INT:
nota = nota_read_int(&n, nota);
*tmp = JS_NewInt64(js, n);
break;
case NOTA_SYM:
nota = nota_read_sym(&b, nota);
if (b == NOTA_PRIVATE) {
JSValue inner;
nota = js_do_nota_decode(js, &inner, nota, holder, JS_NULL, reviver);
JSValue obj = JS_NewObject(js);
cell_rt *crt = JS_GetContextOpaque(js);
// JS_SetProperty(js, obj, crt->actor_sym, inner);
*tmp = obj;
} else {
switch(b) {
case NOTA_NULL: *tmp = JS_NULL; break;
case NOTA_FALSE: *tmp = JS_NewBool(js, 0); break;
case NOTA_TRUE: *tmp = JS_NewBool(js, 1); break;
default: *tmp = JS_NULL; break;
}
}
break;
default:
case NOTA_FLOAT:
nota = nota_read_float(&d, nota);
*tmp = JS_NewFloat64(js, d);
break;
}
if (!JS_IsNull(reviver)) {
JSValue args[2] = { JS_DupValue(js, key), JS_DupValue(js, *tmp) };
JSValue revived = JS_Call(js, reviver, holder, 2, args);
JS_FreeValue(js, args[0]);
JS_FreeValue(js, args[1]);
if (!JS_IsException(revived)) {
JS_FreeValue(js, *tmp);
*tmp = revived;
} else {
JS_FreeValue(js, revived);
}
}
return nota;
}
static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
JSContext *ctx = enc->ctx;
JSValue replaced = apply_replacer(enc, holder, key, val);
int tag = JS_VALUE_GET_TAG(replaced);
switch (tag) {
case JS_TAG_INT:
case JS_TAG_FLOAT64: {
double d;
JS_ToFloat64(ctx, &d, replaced);
nota_write_number(&enc->nb, d);
break;
}
case JS_TAG_STRING: {
const char *str = JS_ToCString(ctx, replaced);
nota_write_text(&enc->nb, str);
JS_FreeCString(ctx, str);
break;
}
case JS_TAG_BOOL:
if (JS_VALUE_GET_BOOL(replaced)) nota_write_sym(&enc->nb, NOTA_TRUE);
else nota_write_sym(&enc->nb, NOTA_FALSE);
break;
case JS_TAG_NULL:
nota_write_sym(&enc->nb, NOTA_NULL);
break;
case JS_TAG_OBJECT: {
if (js_is_blob(ctx, replaced)) {
size_t buf_len;
void *buf_data = js_get_blob_data(ctx, &buf_len, replaced);
if (buf_data == -1) {
JS_FreeValue(ctx, replaced);
return; // JS_EXCEPTION will be handled by caller
}
nota_write_blob(&enc->nb, (unsigned long long)buf_len * 8, (const char*)buf_data);
break;
}
if (JS_IsArray(ctx, replaced)) {
if (nota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
nota_stack_push(enc, replaced);
int arr_len = JS_ArrayLength(ctx, replaced);
nota_write_array(&enc->nb, arr_len);
for (int i = 0; i < arr_len; i++) {
JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
JSValue elem_key = JS_NewInt32(ctx, i);
nota_encode_value(enc, elem_val, replaced, elem_key);
JS_FreeValue(ctx, elem_val);
JS_FreeValue(ctx, elem_key);
}
nota_stack_pop(enc);
break;
}
cell_rt *crt = JS_GetContextOpaque(ctx);
// JSValue adata = JS_GetProperty(ctx, replaced, crt->actor_sym);
JSValue adata = JS_NULL;
if (!JS_IsNull(adata)) {
nota_write_sym(&enc->nb, NOTA_PRIVATE);
nota_encode_value(enc, adata, replaced, JS_NULL);
JS_FreeValue(ctx, adata);
break;
}
JS_FreeValue(ctx, adata);
if (nota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
nota_stack_push(enc, replaced);
JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON");
if (JS_IsFunction(to_json)) {
JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL);
JS_FreeValue(ctx, to_json);
if (!JS_IsException(result)) {
nota_encode_value(enc, result, holder, key);
JS_FreeValue(ctx, result);
} else {
nota_write_sym(&enc->nb, NOTA_NULL);
}
nota_stack_pop(enc);
break;
}
JS_FreeValue(ctx, to_json);
JSPropertyEnum *ptab;
uint32_t plen;
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, replaced, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
nota_write_sym(&enc->nb, NOTA_NULL);
nota_stack_pop(enc);
break;
}
uint32_t non_function_count = 0;
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
if (!JS_IsFunction(prop_val)) non_function_count++;
JS_FreeValue(ctx, prop_val);
}
nota_write_record(&enc->nb, non_function_count);
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, replaced, ptab[i].atom);
if (!JS_IsFunction(prop_val)) {
const char *prop_name = JS_AtomToCString(ctx, ptab[i].atom);
JSValue prop_key = JS_AtomToValue(ctx, ptab[i].atom);
nota_write_text(&enc->nb, prop_name);
nota_encode_value(enc, prop_val, replaced, prop_key);
JS_FreeCString(ctx, prop_name);
JS_FreeValue(ctx, prop_key);
}
JS_FreeValue(ctx, prop_val);
JS_FreeAtom(ctx, ptab[i].atom);
}
js_free(ctx, ptab);
nota_stack_pop(enc);
break;
}
default:
nota_write_sym(&enc->nb, NOTA_NULL);
break;
}
JS_FreeValue(ctx, replaced);
}
void *value2nota(JSContext *ctx, JSValue v) {
NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visitedStack = JS_NewArray(ctx);
enc->cycle = 0;
enc->replacer = JS_NULL;
nota_buffer_init(&enc->nb, 128);
nota_encode_value(enc, v, JS_NULL, JS_NewString(ctx, ""));
if (enc->cycle) {
JS_FreeValue(ctx, enc->visitedStack);
nota_buffer_free(&enc->nb);
return NULL;
}
JS_FreeValue(ctx, enc->visitedStack);
void *data_ptr = enc->nb.data;
enc->nb.data = NULL;
nota_buffer_free(&enc->nb);
return data_ptr;
}
JSValue nota2value(JSContext *js, void *nota) {
if (!nota) return JS_NULL;
JSValue ret;
JSValue holder = JS_NewObject(js);
js_do_nota_decode(js, &ret, nota, holder, JS_NewString(js, ""), JS_NULL);
JS_FreeValue(js, holder);
return ret;
}
static JSValue js_nota_tostring(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
size_t len;
void *nota = js_get_blob_data(ctx, &len, this_val);
if (nota == (void*)-1) return JS_EXCEPTION;
if (!nota) return JS_NULL;
JSValue decoded;
JSValue holder = JS_NewObject(ctx);
js_do_nota_decode(ctx, &decoded, (char*)nota, holder, JS_NewString(ctx, ""), JS_NULL);
JS_FreeValue(ctx, holder);
JSValue global = JS_GetGlobalObject(ctx);
JSValue json = JS_GetPropertyStr(ctx, global, "JSON");
JSValue stringify = JS_GetPropertyStr(ctx, json, "stringify");
JSValue args[3];
args[0] = decoded;
args[1] = JS_NULL;
args[2] = JS_NewInt32(ctx, 1);
JSValue result = JS_Call(ctx, stringify, json, 3, args);
JS_FreeValue(ctx, stringify);
JS_FreeValue(ctx, json);
JS_FreeValue(ctx, global);
JS_FreeValue(ctx, decoded);
JS_FreeValue(ctx, args[2]);
return result;
}
static JSValue js_nota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1) return JS_ThrowTypeError(ctx, "nota.encode requires at least 1 argument");
NotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visitedStack = JS_NewArray(ctx);
enc->cycle = 0;
enc->replacer = (argc > 1 && JS_IsFunction(argv[1])) ? argv[1] : JS_NULL;
nota_buffer_init(&enc->nb, 128);
nota_encode_value(enc, argv[0], JS_NULL, JS_NewString(ctx, ""));
if (enc->cycle) {
JS_FreeValue(ctx, enc->visitedStack);
nota_buffer_free(&enc->nb);
return JS_ThrowReferenceError(ctx, "Tried to encode something to nota with a cycle.");
}
JS_FreeValue(ctx, enc->visitedStack);
size_t total_len = enc->nb.size;
void *data_ptr = enc->nb.data;
JSValue ret = js_new_blob_stoned_copy(ctx, (uint8_t*)data_ptr, total_len);
nota_buffer_free(&enc->nb);
return ret;
}
static JSValue js_nota_decode(JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
if (argc < 1) return JS_NULL;
size_t len;
unsigned char *nota = js_get_blob_data(js, &len, argv[0]);
if (nota == -1) return JS_EXCEPTION;
if (!nota) return JS_NULL;
JSValue reviver = (argc > 1 && JS_IsFunction(argv[1])) ? argv[1] : JS_NULL;
JSValue ret;
JSValue holder = JS_NewObject(js);
js_do_nota_decode(js, &ret, (char*)nota, holder, JS_NewString(js, ""), reviver);
JS_FreeValue(js, holder);
return ret;
}
static const JSCFunctionListEntry js_nota_funcs[] = {
JS_CFUNC_DEF("encode", 1, js_nota_encode),
JS_CFUNC_DEF("decode", 1, js_nota_decode),
};
JSValue js_nota_use(JSContext *js) {
JSValue export = JS_NewObject(js);
JS_SetPropertyFunctionList(js, export, js_nota_funcs, sizeof(js_nota_funcs)/sizeof(JSCFunctionListEntry));
return export;
}

View File

@@ -379,7 +379,16 @@ Shop.get_script_capabilities = function(path) {
} }
function inject_env(inject) { function inject_env(inject) {
// Start with runtime functions from engine
var env = {} var env = {}
var rt = my$_.os ? my$_.os.runtime_env : null
if (rt) {
for (var k in rt) {
env[k] = rt[k]
}
}
// Add capability injections
for (var i = 0; i < length(inject); i++) { for (var i = 0; i < length(inject); i++) {
var inj = inject[i] var inj = inject[i]
var key = trim(inj, '$') var key = trim(inj, '$')
@@ -391,6 +400,17 @@ function inject_env(inject) {
function inject_bindings_code(inject) { function inject_bindings_code(inject) {
var lines = [] var lines = []
// Runtime function bindings
var runtime_fns = ['logical', 'some', 'every', 'starts_with', 'ends_with',
'actor', 'is_actor', 'log', 'send',
'fallback', 'parallel', 'race', 'sequence']
for (var i = 0; i < length(runtime_fns); i++) {
var fn = runtime_fns[i]
push(lines, `var ${fn} = env["${fn}"];`)
}
// Capability bindings ($delay, $start, etc.)
for (var i = 0; i < length(inject); i++) { for (var i = 0; i < length(inject); i++) {
var inj = inject[i] var inj = inject[i]
var key = trim(inj, '$') var key = trim(inj, '$')
@@ -432,18 +452,17 @@ function resolve_mod_fn(path, pkg) {
var obj = pull_from_cache(stone(blob(script))) var obj = pull_from_cache(stone(blob(script)))
if (obj) { if (obj) {
var fn = js.compile_unblob(obj) var fn = js.compile_unblob(obj)
return js.eval_compile(fn) return js.integrate(fn, null)
} }
// Compile name is just for debug/stack traces // Compile name is just for debug/stack traces
// var compile_name = pkg ? pkg + ':' + path : 'local:' + path
var compile_name = path var compile_name = path
var fn = js.compile(compile_name, script) var fn = js.compile(compile_name, script)
put_into_cache(stone(blob(script)), js.compile_blob(fn)) put_into_cache(stone(blob(script)), js.compile_blob(fn))
return js.eval_compile(fn) return js.integrate(fn, null)
} }
// given a path and a package context // given a path and a package context

View File

@@ -40,9 +40,9 @@ sources = []
src += [ # core src += [ # core
'monocypher.c', 'monocypher.c',
'cell.c', 'cell.c',
'suite.c',
'wildmatch.c', 'wildmatch.c',
'qjs_actor.c', 'qjs_actor.c',
'qjs_wota.c',
'miniz.c', 'miniz.c',
'quickjs.c', 'quickjs.c',
'libregexp.c', 'libunicode.c', 'cutils.c', 'dtoa.c' 'libregexp.c', 'libunicode.c', 'cutils.c', 'dtoa.c'
@@ -51,7 +51,6 @@ src += [ # core
src += ['scheduler.c'] src += ['scheduler.c']
scripts = [ scripts = [
'internal/nota.c',
'debug/js.c', 'debug/js.c',
'qop.c', 'qop.c',
'wildstar.c', 'wildstar.c',
@@ -59,7 +58,6 @@ scripts = [
'crypto.c', 'crypto.c',
'internal/kim.c', 'internal/kim.c',
'time.c', 'time.c',
'internal/nota.c',
'debug/debug.c', 'debug/debug.c',
'internal/os.c', 'internal/os.c',
'fd.c', 'fd.c',

View File

@@ -70,7 +70,7 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
JSValue config_obj = argv[0]; JSValue config_obj = argv[0];
JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address"); JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address");
const char *addr_str = JS_IsString(addr_val) ? JS_ToCString(ctx, addr_val) : NULL; const char *addr_str = JS_IsText(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
JS_FreeValue(ctx, addr_val); JS_FreeValue(ctx, addr_val);
if (!addr_str) if (!addr_str)
@@ -245,7 +245,7 @@ static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int
size_t data_len = 0; size_t data_len = 0;
uint8_t *buf = NULL; uint8_t *buf = NULL;
if (JS_IsString(argv[0])) { if (JS_IsText(argv[0])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]); data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
if (!data_str) return JS_EXCEPTION; if (!data_str) return JS_EXCEPTION;
} else if (js_is_blob(ctx,argv[0])) { } else if (js_is_blob(ctx,argv[0])) {
@@ -310,7 +310,7 @@ static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc
size_t data_len = 0; size_t data_len = 0;
uint8_t *buf = NULL; uint8_t *buf = NULL;
if (JS_IsString(argv[0])) { if (JS_IsText(argv[0])) {
data_str = JS_ToCStringLen(ctx, &data_len, argv[0]); data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
if (!data_str) return JS_EXCEPTION; if (!data_str) return JS_EXCEPTION;
} else if (js_is_blob(ctx,argv[0])) { } else if (js_is_blob(ctx,argv[0])) {

View File

@@ -293,7 +293,7 @@ static int par_easycurl_to_memory(char const *url, par_byte **data, int *nbytes)
// Performs a blocking HTTP GET and returns a QuickJS Blob of the body // Performs a blocking HTTP GET and returns a QuickJS Blob of the body
static JSValue js_fetch_picoparser(JSContext *ctx, JSValueConst this_val, static JSValue js_fetch_picoparser(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) { int argc, JSValueConst *argv) {
if (argc < 1 || !JS_IsString(argv[0])) if (argc < 1 || !JS_IsText(argv[0]))
return JS_ThrowTypeError(ctx, "fetch: URL string required"); return JS_ThrowTypeError(ctx, "fetch: URL string required");
const char *url = JS_ToCString(ctx, argv[0]); const char *url = JS_ToCString(ctx, argv[0]);

View File

@@ -135,7 +135,7 @@ static int parse_url(const char *url, char **host, int *port, char **path, int *
// Performs a blocking HTTP GET and returns a QuickJS Blob of the body // Performs a blocking HTTP GET and returns a QuickJS Blob of the body
static JSValue js_fetch_playdate(JSContext *ctx, JSValueConst this_val, static JSValue js_fetch_playdate(JSContext *ctx, JSValueConst this_val,
int argc, JSValueConst *argv) { int argc, JSValueConst *argv) {
if (argc < 1 || !JS_IsString(argv[0])) if (argc < 1 || !JS_IsText(argv[0]))
return JS_ThrowTypeError(ctx, "fetch: URL string required"); return JS_ThrowTypeError(ctx, "fetch: URL string required");
if (!pd_network || !pd_network->http) { if (!pd_network || !pd_network->http) {

View File

@@ -144,7 +144,7 @@ JSC_CCALL(socket_socket,
int protocol = 0; int protocol = 0;
// Parse domain // Parse domain
if (JS_IsString(argv[0])) { if (JS_IsText(argv[0])) {
const char *domain_str = JS_ToCString(js, argv[0]); const char *domain_str = JS_ToCString(js, argv[0]);
if (strcmp(domain_str, "AF_INET") == 0) domain = AF_INET; if (strcmp(domain_str, "AF_INET") == 0) domain = AF_INET;
else if (strcmp(domain_str, "AF_INET6") == 0) domain = AF_INET6; else if (strcmp(domain_str, "AF_INET6") == 0) domain = AF_INET6;
@@ -156,7 +156,7 @@ JSC_CCALL(socket_socket,
// Parse type // Parse type
if (argc > 1) { if (argc > 1) {
if (JS_IsString(argv[1])) { if (JS_IsText(argv[1])) {
const char *type_str = JS_ToCString(js, argv[1]); const char *type_str = JS_ToCString(js, argv[1]);
if (strcmp(type_str, "SOCK_STREAM") == 0) type = SOCK_STREAM; if (strcmp(type_str, "SOCK_STREAM") == 0) type = SOCK_STREAM;
else if (strcmp(type_str, "SOCK_DGRAM") == 0) type = SOCK_DGRAM; else if (strcmp(type_str, "SOCK_DGRAM") == 0) type = SOCK_DGRAM;
@@ -297,7 +297,7 @@ JSC_CCALL(socket_send,
flags = js2number(js, argv[2]); flags = js2number(js, argv[2]);
} }
if (JS_IsString(argv[1])) { if (JS_IsText(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]); const char *data = JS_ToCStringLen(js, &len, argv[1]);
sent = send(sockfd, data, len, flags); sent = send(sockfd, data, len, flags);
JS_FreeCString(js, data); JS_FreeCString(js, data);
@@ -385,7 +385,7 @@ JSC_CCALL(socket_sendto,
to_len = sizeof(addr); to_len = sizeof(addr);
} }
if (JS_IsString(argv[1])) { if (JS_IsText(argv[1])) {
const char *data = JS_ToCStringLen(js, &len, argv[1]); const char *data = JS_ToCStringLen(js, &len, argv[1]);
sent = sendto(sockfd, data, len, flags, to_addr, to_len); sent = sendto(sockfd, data, len, flags, to_addr, to_len);
JS_FreeCString(js, data); JS_FreeCString(js, data);
@@ -525,7 +525,7 @@ JSC_CCALL(socket_setsockopt,
int optname = 0; int optname = 0;
// Parse level // Parse level
if (JS_IsString(argv[1])) { if (JS_IsText(argv[1])) {
const char *level_str = JS_ToCString(js, argv[1]); const char *level_str = JS_ToCString(js, argv[1]);
if (strcmp(level_str, "SOL_SOCKET") == 0) level = SOL_SOCKET; if (strcmp(level_str, "SOL_SOCKET") == 0) level = SOL_SOCKET;
else if (strcmp(level_str, "IPPROTO_TCP") == 0) level = IPPROTO_TCP; else if (strcmp(level_str, "IPPROTO_TCP") == 0) level = IPPROTO_TCP;
@@ -537,7 +537,7 @@ JSC_CCALL(socket_setsockopt,
} }
// Parse option name // Parse option name
if (JS_IsString(argv[2])) { if (JS_IsText(argv[2])) {
const char *opt_str = JS_ToCString(js, argv[2]); const char *opt_str = JS_ToCString(js, argv[2]);
if (strcmp(opt_str, "SO_REUSEADDR") == 0) optname = SO_REUSEADDR; if (strcmp(opt_str, "SO_REUSEADDR") == 0) optname = SO_REUSEADDR;
else if (strcmp(opt_str, "SO_KEEPALIVE") == 0) optname = SO_KEEPALIVE; else if (strcmp(opt_str, "SO_KEEPALIVE") == 0) optname = SO_KEEPALIVE;

338
plan.md Normal file
View File

@@ -0,0 +1,338 @@
# Cell/QuickJS Refactoring Plan: Remove Atoms, Shapes, and Dual-Encoding
## Overview
Refactor `source/quickjs.c` to match `docs/memory.md` specification:
- Remove JSAtom system (171 references → ~41 remaining)
- Remove JSShape system (94 references) ✓
- Remove IC caches (shape-based inline caches) ✓
- Remove `is_wide_char` dual-encoding (18 locations) ✓
- Use JSValue texts directly as property keys
- Reference: `mquickjs.c` shows the target pattern
## Completed Phases
### Phase 1: Remove is_wide_char Remnants ✓
### Phase 2: Remove IC Caches ✓
### Phase 3: Remove JSShape System ✓
### Phase 4: Complete Property Access with JSValue Keys ✓
Completed:
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_get_field
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_put_field
- Removed JS_GC_OBJ_TYPE_JS_OBJECT fallbacks from OP_define_field
- Created emit_key() function that adds JSValue to cpool and emits index
---
## Phase 5: Convert JSAtom to JSValue Text (IN PROGRESS)
This is the core transformation. All identifier handling moves from atoms to JSValue.
### Completed Items
**Token and Parser Infrastructure:**
- [x] Change JSToken.u.ident.atom to JSToken.u.ident.str (JSValue)
- [x] Change parse_ident() to return JSValue
- [x] Create emit_key() function (cpool-based)
- [x] Create JS_KEY_* macros for common names (lines ~279-335 in quickjs.c)
- [x] Update all token.u.ident.atom references to .str
- [x] Create keyword lookup table (js_keywords[]) with string comparison
- [x] Rewrite update_token_ident() to use js_keyword_lookup()
- [x] Rewrite is_strict_future_keyword() to use JSValue
- [x] Update token_is_pseudo_keyword() to use JSValue and js_key_equal()
**Function Declaration Parsing:**
- [x] Update js_parse_function_decl() signature to use JSValue func_name
- [x] Update js_parse_function_decl2() to use JSValue func_name throughout
- [x] Update js_parse_function_check_names() to use JSValue
- [x] Convert JS_DupAtom/JS_FreeAtom to JS_DupValue/JS_FreeValue in function parsing
**Variable Definition and Lookup:**
- [x] Update find_global_var() to use JSValue and js_key_equal()
- [x] Update find_lexical_global_var() to use JSValue
- [x] Update find_lexical_decl() to use JSValue and js_key_equal()
- [x] Update js_define_var() to use JSValue
- [x] Update js_parse_check_duplicate_parameter() to use JSValue and js_key_equal()
- [x] Update js_parse_destructuring_var() to return JSValue
- [x] Update js_parse_var() to use JSValue for variable names
**Comparison Helpers:**
- [x] Create js_key_equal_str() for comparing JSValue with C string literals
- [x] Update is_var_in_arg_scope() to use js_key_equal/js_key_equal_str
- [x] Update has_with_scope() to use js_key_equal_str
- [x] Update closure variable comparisons (cv->var_name) to use js_key_equal_str
**Property Access:**
- [x] Fix JS_GetPropertyStr to create proper JSValue keys
- [x] Fix JS_SetPropertyInternal callers to use JS_KEY_* instead of JS_ATOM_*
### JS_KEY_* Macros Added
Compile-time immediate ASCII string constants (≤7 chars):
```c
JS_KEY_empty, JS_KEY_name, JS_KEY_message, JS_KEY_stack,
JS_KEY_errors, JS_KEY_Error, JS_KEY_cause, JS_KEY_length,
JS_KEY_value, JS_KEY_get, JS_KEY_set, JS_KEY_raw,
JS_KEY_flags, JS_KEY_source, JS_KEY_exec, JS_KEY_toJSON,
JS_KEY_eval, JS_KEY_this, JS_KEY_true, JS_KEY_false,
JS_KEY_null, JS_KEY_NaN, JS_KEY_default, JS_KEY_index,
JS_KEY_input, JS_KEY_groups, JS_KEY_indices, JS_KEY_let,
JS_KEY_var, JS_KEY_new, JS_KEY_of, JS_KEY_yield,
JS_KEY_async, JS_KEY_target, JS_KEY_from, JS_KEY_meta,
JS_KEY_as, JS_KEY_with
```
Runtime macro for strings >7 chars:
```c
#define JS_KEY_STR(ctx, str) JS_NewStringLen((ctx), (str), sizeof(str) - 1)
```
Helper function for comparing JSValue with C string literals:
```c
static JS_BOOL js_key_equal_str(JSValue a, const char *str);
```
### Remaining Work
#### 5.3 Update js_parse_property_name() ✓
- [x] Change return type from JSAtom* to JSValue*
- [x] Update all callers (js_parse_object_literal, etc.)
- [x] Updated get_lvalue(), put_lvalue(), js_parse_destructuring_element()
#### 5.4 Replace remaining emit_atom() calls with emit_key() ✓
- [x] Removed emit_atom wrapper function
- [x] Changed last emit_atom(JS_ATOM_this) to emit_key(JS_KEY_this)
#### 5.5 Update Variable Opcode Format in quickjs-opcode.h
- [ ] Change `atom` format opcodes to `key` format
- [ ] Change `atom_u8` and `atom_u16` to `key_u8` and `key_u16`
#### 5.6 Update VM Opcode Handlers ✓
These now read cpool indices and look up JSValue:
- [x] OP_check_var, OP_get_var_undef, OP_get_var
- [x] OP_put_var, OP_put_var_init, OP_put_var_strict
- [x] OP_set_name, OP_make_var_ref, OP_delete_var
- [x] OP_define_var, OP_define_func, OP_throw_error
- [x] OP_make_loc_ref, OP_make_arg_ref
- [x] OP_define_method, OP_define_method_computed
#### 5.7 Update resolve_scope_var() ✓
- [x] Changed signature to use JSValue var_name
- [x] Updated all comparisons to use js_key_equal()/js_key_equal_str()
- [x] Updated var_object_test() to use JSValue
- [x] Updated optimize_scope_make_global_ref() to use JSValue
- [x] Updated resolve_variables() callers to read from cpool
#### 5.8 Convert Remaining JS_ATOM_* Usages
Categories remaining:
- Some debug/print functions still use JSAtom
- Some function signatures not yet converted
- Will be addressed in Phase 7 cleanup
---
## Phase 6: Update Bytecode Serialization ✓
### 6.1 JS_WriteObjectTag Changes ✓
- [x] Changed JS_WriteObjectTag to use bc_put_key() directly for property keys
- [x] Removed JS_ValueToAtom/bc_put_atom path (was broken anyway)
- [x] cpool values serialized via JS_WriteObjectRec()
### 6.2 JS_ReadObject Changes ✓
- [x] Changed JS_ReadObjectTag to use bc_get_key() for property keys
- [x] Uses JS_SetPropertyInternal with JSValue keys
### 6.3 Opcode Format Updates ✓
- [x] Added OP_FMT_key_u8, OP_FMT_key_u16, OP_FMT_key_label_u16 formats
- [x] Updated variable opcodes to use key formats instead of atom formats
- [x] Updated bc_byte_swap() to handle new key formats
- [x] Updated JS_WriteFunctionBytecode() to skip key format opcodes
- [x] Updated JS_ReadFunctionBytecode() to skip key format opcodes
### 6.4 Version Bump ✓
- [x] Incremented BC_VERSION from 5 to 6
---
## Phase 7: Final Cleanup ✓
### 7.1 Additional Parser/Compiler Fixes ✓
- [x] Fixed TOK_IDENT case to use JSValue name, JS_DupValue, emit_key
- [x] Fixed TOK_TRY catch clause to use JSValue name
- [x] Fixed js_parse_statement_or_decl label_name to use JSValue
- [x] Fixed OP_scope_get_var "eval" check to use js_key_equal_str
- [x] Fixed js_parse_delete to use JSValue for name comparison
- [x] Fixed JSON parsing to use js_key_from_string for property names
- [x] Added js_key_from_string() helper function
- [x] Added JS_KEY__ret_, JS_KEY__eval_, JS_KEY__var_ for internal names
- [x] Updated add_closure_var, get_closure_var2, get_closure_var to use JSValue var_name
- [x] Updated set_closure_from_var to use JS_DupValue
- [x] Updated add_closure_variables to use JS_DupValue
- [x] Updated instantiate_hoisted_definitions to use fd_cpool_add for keys
- [x] Updated resolve_variables to use js_key_equal and fd_cpool_add
### 7.1.1 Property Access and Runtime Fixes ✓
- [x] Fixed JS_GetPropertyValue to use js_key_from_string instead of JS_ValueToAtom
- [x] Fixed JS_GetPropertyKey to use js_key_from_string for string keys
- [x] Fixed JS_SetPropertyKey to use js_key_from_string for string keys
- [x] Fixed JS_HasPropertyKey to use js_key_from_string for string keys
- [x] Fixed JS_DeletePropertyKey to use js_key_from_string for string keys
- [x] Updated JS_HasProperty signature to take JSValue prop
- [x] Fixed OP_get_ref_value handler to use JSValue key
- [x] Fixed OP_put_ref_value handler to use JSValue key
- [x] Updated free_func_def to use JS_FreeValue for JSValue fields
### 7.2 Remove JSAtom Type and Functions ✓
- [x] Removed most JS_ATOM_* constants (kept JS_ATOM_NULL, JS_ATOM_END for BC compat)
- [x] JS_NewAtomString now returns JSValue using js_key_new
- [x] JS_FreeAtom, JS_DupAtom are stubs (no-op for backward compat)
- [x] JS_AtomToValue, JS_ValueToAtom are stubs (minimal BC compat)
- [x] Replaced JS_ATOM_* usages with JS_KEY_* or JS_GetPropertyStr
### 7.3 Additional Runtime Fixes ✓
- [x] Fixed free_function_bytecode to use JS_FreeValueRT for JSValue fields
- [x] Fixed JS_SetPropertyFunctionList to use JSValue keys via find_key()
- [x] Fixed JS_InstantiateFunctionListItem to use JSValue keys
- [x] Fixed internalize_json_property to use JSValue names
- [x] Fixed emit_break and push_break_entry to use JSValue label_name
- [x] Implemented JS_Invoke to use JSValue method key
### 7.4 Remaining Stubs (kept for bytecode backward compatibility)
- JSAtom typedef (uint32_t) - used in BC serialization
- JS_ATOM_NULL, JS_ATOM_END - bytecode format markers
- JS_FreeAtom, JS_DupAtom - no-op stubs
- JS_FreeAtomRT, JS_DupAtomRT - no-op stubs
- Legacy BC reader (idx_to_atom array) - for reading old bytecode
---
## Current Build Status
**Build: SUCCEEDS** with warnings (unused variables, labels)
**Statistics:**
- JS_ATOM_* usages: Minimal (only BC serialization compat)
- Property access uses JS_KEY_* macros or JS_GetPropertyStr
- BC_VERSION: 6 (updated for new key-based format)
**What Works:**
- All property access via JSValue keys
- Keyword detection via string comparison
- Function declaration parsing with JSValue names
- Variable definition with JSValue names
- Closure variable tracking with JSValue names
- VM opcode handlers read cpool indices and look up JSValue
- resolve_scope_var() uses JSValue throughout
- js_parse_property_name() returns JSValue
- Bytecode serialization uses bc_put_key/bc_get_key for property keys
- Variable opcodes use key format (cpool indices)
- JSON parsing uses JSValue keys
- Internal variable names use JS_KEY__ret_, JS_KEY__eval_, JS_KEY__var_
- JS_SetPropertyFunctionList uses JSValue keys
- JS_Invoke uses JSValue method keys
- break/continue labels use JSValue
---
## Phase 8: Migrate to New Tagging System (IN PROGRESS)
**Problem**: `JS_VALUE_GET_TAG` returns `JS_TAG_PTR` for all pointers, but ~200 places check for obsolete tags like `JS_TAG_OBJECT`, `JS_TAG_STRING`, `JS_TAG_FUNCTION`, etc., which are never returned. This causes crashes.
**Target Design** (from memory.md):
- JSValue tags: Only `JS_TAG_INT`, `JS_TAG_PTR`, `JS_TAG_SHORT_FLOAT`, `JS_TAG_SPECIAL`
- Pointer types determined by `objhdr_t` header at offset 8 in heap objects
- mist_obj_type: `OBJ_ARRAY(0)`, `OBJ_BLOB(1)`, `OBJ_TEXT(2)`, `OBJ_RECORD(3)`, `OBJ_FUNCTION(4)`, etc.
### 8.1 Unified Heap Object Layout ✓
- [x] Updated mist_text structure to have objhdr_t at offset 8:
```c
typedef struct mist_text {
JSRefCountHeader _dummy_header; /* unused, for offset alignment */
uint32_t _pad; /* padding to align objhdr_t to offset 8 */
objhdr_t hdr; /* NOW at offset 8, like JSString */
word_t length;
word_t packed[];
} mist_text;
```
- [x] JSString already has objhdr_t at offset 8
### 8.2 Type-Checking Helper Functions ✓
Added lowercase internal helpers (to avoid conflict with quickjs.h declarations):
```c
static inline JS_BOOL js_is_gc_object(JSValue v) { return JS_IsPtr(v); }
static inline JSGCObjectTypeEnum js_get_gc_type(JSValue v) {
return ((JSGCObjectHeader *)JS_VALUE_GET_PTR(v))->gc_obj_type;
}
static inline JS_BOOL js_is_record(JSValue v) {
if (!JS_IsPtr(v)) return FALSE;
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_RECORD;
}
static inline JS_BOOL js_is_array(JSValue v) {
if (!JS_IsPtr(v)) return FALSE;
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_ARRAY;
}
static inline JS_BOOL js_is_function(JSValue v) {
if (!JS_IsPtr(v)) return FALSE;
return js_get_gc_type(v) == JS_GC_OBJ_TYPE_FUNCTION;
}
static inline JS_BOOL js_is_object(JSValue v) {
if (!JS_IsPtr(v)) return FALSE;
JSGCObjectTypeEnum t = js_get_gc_type(v);
return t == JS_GC_OBJ_TYPE_RECORD || t == JS_GC_OBJ_TYPE_ARRAY;
}
```
### 8.3 Updated Core Functions ✓
- [x] Updated JS_IsString to read objhdr_t from offset 8
- [x] Updated js_key_hash to read objhdr_t from offset 8
- [x] Updated js_key_equal to read objhdr_t from offset 8
- [x] Updated __JS_FreeValueRT to use objhdr_type for type dispatch
- [x] Updated JS_MarkValue, JS_MarkValueEdgeEx for GC
- [x] Added JS_SetPropertyValue function
- [x] Changed quickjs.h JS_IsFunction/JS_IsObject from inline to extern declarations
### 8.4 Tag Check Migration (PARTIAL)
Updated some critical tag checks:
- [x] Some JS_TAG_OBJECT checks → js_is_object() or js_is_record()
- [ ] Many more JS_TAG_OBJECT checks remain (~200 total)
- [ ] JS_TAG_FUNCTION checks → js_is_function()
- [ ] JS_TAG_STRING checks (some already use JS_IsString)
### 8.5 Remaining Work
- [ ] Fix ASAN memory corruption error (attempting free on address not malloc'd)
- Crash occurs in js_def_realloc during js_realloc_array
- Address is 112 bytes inside a JSFunctionDef allocation
- [ ] Complete remaining ~200 tag check migrations
- [ ] Add mist_hdr to JSFunction (optional, gc_obj_type already works)
- [ ] Remove obsolete tag definitions from quickjs.h:
- JS_TAG_STRING = -8
- JS_TAG_ARRAY = -6
- JS_TAG_FUNCTION = -5
- JS_TAG_FUNCTION_BYTECODE = -2
- JS_TAG_OBJECT = -1
### Current Status
**Build: SUCCEEDS** with warnings
**Runtime: CRASHES** with ASAN error:
```
==16122==ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed
```
The crash occurs during test execution in `js_def_realloc` called from `js_realloc_array`.
Root cause not yet identified - likely a pointer being passed to realloc that wasn't allocated with malloc.
---
## Notes
- JSVarDef.var_name is JSValue
- JSClosureVar.var_name is JSValue
- JSGlobalVar.var_name is JSValue
- JSFunctionDef.func_name is JSValue
- BlockEnv.label_name is JSValue
- OP_get_field/put_field/define_field already use cpool index format
- JSRecord with open addressing is fully implemented
- js_key_hash and js_key_equal work with both immediate and heap text
- js_key_equal_str enables comparison with C string literals for internal names

View File

@@ -145,12 +145,12 @@ static void encode_js_value(json_encoder *enc, JSContext *js, JSValue val) {
double d = js2number(js, val); double d = js2number(js, val);
if (d == (int)d) enc->writeInt(enc, (int)d); if (d == (int)d) enc->writeInt(enc, (int)d);
else enc->writeDouble(enc, d); else enc->writeDouble(enc, d);
} else if (JS_IsString(val)) { } else if (JS_IsText(val)) {
size_t len; size_t len;
const char *str = JS_ToCStringLen(js, &len, val); const char *str = JS_ToCStringLen(js, &len, val);
enc->writeString(enc, str, len); enc->writeString(enc, str, len);
JS_FreeCString(js, str); JS_FreeCString(js, str);
} else if (JS_IsArray(js, val)) { } else if (JS_IsArray(val)) {
encode_js_array(enc, js, val); encode_js_array(enc, js, val);
} else if (JS_IsObject(val)) { } else if (JS_IsObject(val)) {
encode_js_object(enc, js, val); encode_js_object(enc, js, val);

8
qop.c
View File

@@ -8,10 +8,10 @@ static void js_qop_archive_finalizer(JSRuntime *rt, JSValue val) {
qop_desc *qop = JS_GetOpaque(val, js_qop_archive_class_id); qop_desc *qop = JS_GetOpaque(val, js_qop_archive_class_id);
if (qop) { if (qop) {
if (qop->hashmap) { if (qop->hashmap) {
js_free_rt(rt, qop->hashmap); js_free_rt(qop->hashmap);
} }
qop_close(qop); qop_close(qop);
js_free_rt(rt, qop); js_free_rt(qop);
} }
} }
@@ -34,8 +34,8 @@ static void js_qop_writer_finalizer(JSRuntime *rt, JSValue val) {
qop_writer *w = JS_GetOpaque(val, js_qop_writer_class_id); qop_writer *w = JS_GetOpaque(val, js_qop_writer_class_id);
if (w) { if (w) {
if (w->fh) fclose(w->fh); if (w->fh) fclose(w->fh);
if (w->files) js_free_rt(rt, w->files); if (w->files) js_free_rt(w->files);
js_free_rt(rt, w); js_free_rt(w);
} }
} }

View File

@@ -2,7 +2,6 @@
#include <windows.h> #include <windows.h>
#endif #endif
#define WOTA_IMPLEMENTATION
#include "wota.h" #include "wota.h"
#define STB_DS_IMPLEMENTATION #define STB_DS_IMPLEMENTATION
@@ -15,10 +14,15 @@
#define CELL_SHOP_DIR ".cell" #define CELL_SHOP_DIR ".cell"
#define CELL_CORE_DIR "packages/core" #define CELL_CORE_DIR "packages/core"
#include <math.h>
#include <signal.h> #include <signal.h>
#include <unistd.h> #include <unistd.h>
#include <sys/stat.h> #include <sys/stat.h>
/* Test suite declarations */
int run_c_test_suite(JSContext *ctx);
static int run_test_suite(size_t heap_size);
cell_rt *root_cell = NULL; cell_rt *root_cell = NULL;
static char *core_path = NULL; static char *core_path = NULL;
@@ -114,20 +118,15 @@ void actor_disrupt(cell_rt *crt)
JSValue js_os_use(JSContext *js); JSValue js_os_use(JSContext *js);
JSValue js_math_use(JSContext *js); JSValue js_math_use(JSContext *js);
JSValue js_json_use(JSContext *js);
JSValue js_nota_use(JSContext *js);
JSValue js_wota_use(JSContext *js);
void script_startup(cell_rt *prt) void script_startup(cell_rt *prt)
{ {
JSRuntime *rt; JSRuntime *rt = JS_NewRuntime();
JS_SetInterruptHandler(rt, (JSInterruptHandler *)actor_interrupt_cb, prt);
rt = JS_NewRuntime(); JSContext *js = JS_NewContext(rt);
JSContext *js = JS_NewContextRaw(rt);
JS_SetInterruptHandler(rt, actor_interrupt_cb, prt);
JS_AddIntrinsicBaseObjects(js);
JS_AddIntrinsicEval(js);
JS_AddIntrinsicRegExp(js);
JS_AddIntrinsicJSON(js);
JS_SetContextOpaque(js, prt); JS_SetContextOpaque(js, prt);
prt->context = js; prt->context = js;
@@ -135,38 +134,7 @@ void script_startup(cell_rt *prt)
cell_rt *crt = JS_GetContextOpaque(js); cell_rt *crt = JS_GetContextOpaque(js);
JS_FreeValue(js, js_blob_use(js)); JS_FreeValue(js, js_blob_use(js));
JSValue globalThis = JS_GetGlobalObject(js); // Load and compile engine.cm
JSValue cell = JS_NewObject(js);
JS_SetPropertyStr(js,globalThis,"cell", cell);
JSValue hidden_fn = JS_NewObject(js);
JS_SetPropertyStr(js, cell, "hidden", hidden_fn);
JS_SetPropertyStr(js, hidden_fn, "os", js_os_use(js));
crt->actor_sym = JS_NewObject(js);
JS_SetPropertyStr(js, hidden_fn, "actorsym", JS_DupValue(js,crt->actor_sym));
if (crt->init_wota) {
JS_SetPropertyStr(js, hidden_fn, "init", wota2value(js, crt->init_wota));
// init wota can now be freed
free(crt->init_wota);
crt->init_wota = NULL;
}
// Store the core path for scripts to use
JSValue js_cell = JS_GetPropertyStr(js, globalThis, "cell");
JSValue hidden = JS_GetPropertyStr(js, js_cell, "hidden");
if (core_path) {
JS_SetPropertyStr(js, hidden, "core_path", JS_NewString(js, core_path));
}
JS_FreeValue(js, hidden);
JS_FreeValue(js, js_cell);
JS_FreeValue(js, globalThis);
// Load engine.cm from the core directory
size_t engine_size; size_t engine_size;
char *data = load_core_file(ENGINE, &engine_size); char *data = load_core_file(ENGINE, &engine_size);
if (!data) { if (!data) {
@@ -174,9 +142,42 @@ void script_startup(cell_rt *prt)
return; return;
} }
crt->state = ACTOR_RUNNING; JSValue bytecode = JS_Compile(js, data, engine_size, ENGINE);
JSValue v = JS_Eval(js, data, engine_size, ENGINE, 0);
free(data); free(data);
if (JS_IsException(bytecode)) {
uncaught_exception(js, bytecode);
return;
}
// Create hidden environment
JSValue hidden_env = JS_NewObject(js);
JS_SetPropertyStr(js, hidden_env, "os", js_os_use(js));
JS_SetPropertyStr(js, hidden_env, "json", js_json_use(js));
JS_SetPropertyStr(js, hidden_env, "nota", js_nota_use(js));
JS_SetPropertyStr(js, hidden_env, "wota", js_wota_use(js));
crt->actor_sym = JS_NewObject(js);
JS_SetPropertyStr(js, hidden_env, "actorsym", JS_DupValue(js, crt->actor_sym));
// Always set init (even if null)
if (crt->init_wota) {
JS_SetPropertyStr(js, hidden_env, "init", wota2value(js, crt->init_wota));
free(crt->init_wota);
crt->init_wota = NULL;
} else {
JS_SetPropertyStr(js, hidden_env, "init", JS_NULL);
}
if (core_path) {
JS_SetPropertyStr(js, hidden_env, "core_path", JS_NewString(js, core_path));
}
// Stone the environment
hidden_env = JS_Stone(js, hidden_env);
// Integrate and run
crt->state = ACTOR_RUNNING;
JSValue v = JS_Integrate(js, bytecode, hidden_env);
uncaught_exception(js, v); uncaught_exception(js, v);
crt->state = ACTOR_IDLE; crt->state = ACTOR_IDLE;
set_actor_state(crt); set_actor_state(crt);
@@ -200,8 +201,158 @@ static void signal_handler(int sig)
exit_handler(); exit_handler();
} }
/* Run the C test suite with minimal runtime setup */
static int run_test_suite(size_t heap_size)
{
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
return 1;
}
JSContext *ctx = JS_NewContextWithHeapSize(rt, heap_size);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
return 1;
}
int result = run_c_test_suite(ctx);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return result;
}
/* Run an immediate script string */
static int run_eval(const char *script_or_file, int print_bytecode, int use_bootstrap_env)
{
if (!find_cell_shop()) return 1;
/* Check if argument is a file path */
struct stat st;
char *script = NULL;
char *allocated_script = NULL;
const char *filename = "<eval>";
if (stat(script_or_file, &st) == 0 && S_ISREG(st.st_mode)) {
/* It's a file, read its contents */
FILE *f = fopen(script_or_file, "r");
if (!f) {
printf("Failed to open file: %s\n", script_or_file);
return 1;
}
allocated_script = malloc(st.st_size + 1);
if (!allocated_script) {
fclose(f);
printf("Failed to allocate memory for script\n");
return 1;
}
size_t read_size = fread(allocated_script, 1, st.st_size, f);
fclose(f);
allocated_script[read_size] = '\0';
script = allocated_script;
filename = script_or_file;
} else {
/* Treat as inline script */
script = (char *)script_or_file;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
printf("Failed to create JS runtime\n");
free(allocated_script);
return 1;
}
JSContext *ctx = JS_NewContext(rt);
if (!ctx) {
printf("Failed to create JS context\n");
JS_FreeRuntime(rt);
free(allocated_script);
return 1;
}
int result = 0;
JSGCRef bytecode_ref;
JS_PushGCRef(ctx, &bytecode_ref);
bytecode_ref.val = JS_Compile(ctx, script, strlen(script), filename);
if (JS_IsException(bytecode_ref.val)) {
uncaught_exception(ctx, bytecode_ref.val);
JS_PopGCRef(ctx, &bytecode_ref);
result = 1;
} else {
if (print_bytecode) {
printf("=== Compiled Bytecode ===\n");
JS_DumpFunctionBytecode(ctx, bytecode_ref.val);
}
JSValue env = JS_NULL;
if (use_bootstrap_env) {
JSGCRef env_ref, json_ref, nota_ref, wota_ref;
JS_PushGCRef(ctx, &env_ref);
JS_PushGCRef(ctx, &json_ref);
JS_PushGCRef(ctx, &nota_ref);
JS_PushGCRef(ctx, &wota_ref);
env_ref.val = JS_NewObject(ctx);
/* Create modules with GC rooting, then stone them */
json_ref.val = js_json_use(ctx);
nota_ref.val = js_nota_use(ctx);
wota_ref.val = js_wota_use(ctx);
JS_SetPropertyStr(ctx, env_ref.val, "json", JS_Stone(ctx, json_ref.val));
JS_SetPropertyStr(ctx, env_ref.val, "nota", JS_Stone(ctx, nota_ref.val));
JS_SetPropertyStr(ctx, env_ref.val, "wota", JS_Stone(ctx, wota_ref.val));
env = JS_Stone(ctx, env_ref.val);
JS_PopGCRef(ctx, &wota_ref);
JS_PopGCRef(ctx, &nota_ref);
JS_PopGCRef(ctx, &json_ref);
JS_PopGCRef(ctx, &env_ref);
}
JSValue v = JS_Integrate(ctx, bytecode_ref.val, env);
JS_PopGCRef(ctx, &bytecode_ref);
if (JS_IsException(v)) {
uncaught_exception(ctx, v);
result = 1;
} else {
JS_FreeValue(ctx, v);
}
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
free(allocated_script);
return result;
}
int cell_init(int argc, char **argv) int cell_init(int argc, char **argv)
{ {
/* Check for --test flag to run C test suite */
if (argc >= 2 && strcmp(argv[1], "--test") == 0) {
size_t heap_size = 64 * 1024; /* 64KB default */
if (argc >= 3) {
heap_size = strtoull(argv[2], NULL, 0);
/* Round up to power of 2 for buddy allocator */
size_t p = 1;
while (p < heap_size) p <<= 1;
heap_size = p;
}
return run_test_suite(heap_size);
}
/* Check for -e or --eval flag to run immediate script */
/* Also check for -p flag to print bytecode */
/* -s / --serializers flag provides json, nota, wota in env */
if (argc >= 3 && (strcmp(argv[1], "-e") == 0 || strcmp(argv[1], "--eval") == 0)) {
return run_eval(argv[2], 0, 0);
}
if (argc >= 3 && (strcmp(argv[1], "-p") == 0 || strcmp(argv[1], "--print-bytecode") == 0)) {
return run_eval(argv[2], 1, 0);
}
if (argc >= 3 && (strcmp(argv[1], "-s") == 0 || strcmp(argv[1], "--serializers") == 0)) {
return run_eval(argv[2], 0, 1);
}
int script_start = 1; int script_start = 1;
/* Find the cell shop at ~/.cell */ /* Find the cell shop at ~/.cell */

View File

@@ -28,6 +28,13 @@ JSValue number2js(JSContext *js, double g);
JSValue wota2value(JSContext *js, void *v); JSValue wota2value(JSContext *js, void *v);
void *value2wota(JSContext *js, JSValue v, JSValue replacer, size_t *bytes); void *value2wota(JSContext *js, JSValue v, JSValue replacer, size_t *bytes);
JSValue nota2value(JSContext *js, void *nota);
void *value2nota(JSContext *js, JSValue v);
JSValue js_json_use(JSContext *js);
JSValue js_nota_use(JSContext *js);
JSValue js_wota_use(JSContext *js);
#define CELL_HOOK_ENTER 1 #define CELL_HOOK_ENTER 1
#define CELL_HOOK_EXIT 2 #define CELL_HOOK_EXIT 2
typedef void (*cell_hook)(const char *name, int type); typedef void (*cell_hook)(const char *name, int type);

View File

@@ -1,396 +0,0 @@
#include "cell.h"
#include "cell_internal.h"
#include "wota.h"
#include <stdlib.h>
typedef struct ObjectRef {
void *ptr;
struct ObjectRef *next;
} ObjectRef;
typedef struct WotaEncodeContext {
JSContext *ctx;
ObjectRef *visited_stack;
WotaBuffer wb;
int cycle;
JSValue replacer;
} WotaEncodeContext;
static void wota_stack_push(WotaEncodeContext *enc, JSValueConst val)
{
if (!JS_IsObject(val)) return;
ObjectRef *ref = malloc(sizeof(ObjectRef));
if (!ref) return;
ref->ptr = JS_VALUE_GET_PTR(val);
ref->next = enc->visited_stack;
enc->visited_stack = ref;
}
static void wota_stack_pop(WotaEncodeContext *enc)
{
if (!enc->visited_stack) return;
ObjectRef *top = enc->visited_stack;
enc->visited_stack = top->next;
free(top);
}
static int wota_stack_has(WotaEncodeContext *enc, JSValueConst val)
{
if (!JS_IsObject(val)) return 0;
void *ptr = JS_VALUE_GET_PTR(val);
ObjectRef *current = enc->visited_stack;
while (current) {
if (current->ptr == ptr) return 1;
current = current->next;
}
return 0;
}
static void wota_stack_free(WotaEncodeContext *enc)
{
while (enc->visited_stack) {
wota_stack_pop(enc);
}
}
static JSValue apply_replacer(WotaEncodeContext *enc, JSValueConst holder, JSAtom key, JSValueConst val)
{
if (JS_IsNull(enc->replacer)) return JS_DupValue(enc->ctx, val);
JSValue key_val = (key == JS_ATOM_NULL) ? JS_NULL : JS_AtomToValue(enc->ctx, key);
JSValue args[2] = { key_val, JS_DupValue(enc->ctx, val) };
JSValue result = JS_Call(enc->ctx, enc->replacer, holder, 2, args);
JS_FreeValue(enc->ctx, args[0]);
JS_FreeValue(enc->ctx, args[1]);
if (JS_IsException(result)) return JS_DupValue(enc->ctx, val);
return result;
}
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSAtom key);
static void encode_object_properties(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder)
{
JSContext *ctx = enc->ctx;
JSPropertyEnum *ptab;
uint32_t plen;
if (JS_GetOwnPropertyNames(ctx, &ptab, &plen, val, JS_GPN_ENUM_ONLY | JS_GPN_STRING_MASK) < 0) {
wota_write_sym(&enc->wb, WOTA_NULL);
return;
}
uint32_t non_function_count = 0;
JSValue props[plen];
JSAtom atoms[plen];
for (uint32_t i = 0; i < plen; i++) {
JSValue prop_val = JS_GetProperty(ctx, val, ptab[i].atom);
if (!JS_IsFunction(prop_val)) {
atoms[non_function_count] = ptab[i].atom;
props[non_function_count++] = prop_val;
} else
JS_FreeValue(ctx, prop_val);
}
wota_write_record(&enc->wb, non_function_count);
for (uint32_t i = 0; i < non_function_count; i++) {
size_t plen;
const char *prop_name = JS_AtomToCStringLen(ctx, &plen, atoms[i]);
JSValue prop_val = props[i];
wota_write_text_len(&enc->wb, prop_name, plen);
wota_encode_value(enc, prop_val, val, atoms[i]);
JS_FreeCString(ctx, prop_name);
JS_FreeValue(ctx, prop_val);
}
for (int i = 0; i < plen; i++)
JS_FreeAtom(ctx, ptab[i].atom);
js_free(ctx, ptab);
}
static void wota_encode_value(WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSAtom key)
{
JSContext *ctx = enc->ctx;
JSValue replaced;
if (!JS_IsNull(enc->replacer) && key != JS_ATOM_NULL)
replaced = apply_replacer(enc, holder, key, val);
else
replaced = JS_DupValue(enc->ctx, val);
int tag = JS_VALUE_GET_TAG(replaced);
switch (tag) {
case JS_TAG_INT: {
int32_t d;
JS_ToInt32(ctx, &d, replaced);
wota_write_int_word(&enc->wb, d);
break;
}
case JS_TAG_FLOAT64: {
double d;
if (JS_ToFloat64(ctx, &d, replaced) < 0) {
wota_write_sym(&enc->wb, WOTA_NULL);
break;
}
wota_write_float_word(&enc->wb, d);
break;
}
case JS_TAG_STRING: {
size_t plen;
const char *str = JS_ToCStringLen(ctx, &plen, replaced);
wota_write_text_len(&enc->wb, str ? str : "", str ? plen : 0);
JS_FreeCString(ctx, str);
break;
}
case JS_TAG_BOOL:
wota_write_sym(&enc->wb, JS_VALUE_GET_BOOL(replaced) ? WOTA_TRUE : WOTA_FALSE);
break;
case JS_TAG_NULL:
wota_write_sym(&enc->wb, WOTA_NULL);
break;
case JS_TAG_OBJECT: {
if (js_is_blob(ctx, replaced)) {
size_t buf_len;
void *buf_data = js_get_blob_data(ctx, &buf_len, replaced);
if (buf_data == (void *)-1) {
JS_FreeValue(ctx, replaced);
return; // JS_EXCEPTION will be handled by caller
}
if (buf_len == 0) {
wota_write_blob(&enc->wb, 0, "");
} else {
wota_write_blob(&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data);
}
break;
}
if (JS_IsArray(ctx, replaced)) {
if (wota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
wota_stack_push(enc, replaced);
int64_t arr_len;
JS_GetLength(ctx, replaced, &arr_len);
wota_write_array(&enc->wb, arr_len);
for (int64_t i = 0; i < arr_len; i++) {
JSValue elem_val = JS_GetPropertyUint32(ctx, replaced, i);
JSAtom idx_atom = JS_NewAtomUInt32(ctx, (uint32_t)i);
wota_encode_value(enc, elem_val, replaced, idx_atom);
JS_FreeAtom(ctx, idx_atom);
JS_FreeValue(ctx, elem_val);
}
wota_stack_pop(enc);
break;
}
cell_rt *crt = JS_GetContextOpaque(ctx);
// JSValue adata = JS_GetProperty(ctx, replaced, crt->actor_sym);
JSValue adata = JS_NULL;
if (!JS_IsNull(adata)) {
wota_write_sym(&enc->wb, WOTA_PRIVATE);
wota_encode_value(enc, adata, replaced, JS_ATOM_NULL);
JS_FreeValue(ctx, adata);
break;
}
JS_FreeValue(ctx, adata);
if (wota_stack_has(enc, replaced)) {
enc->cycle = 1;
break;
}
wota_stack_push(enc, replaced);
JSValue to_json = JS_GetPropertyStr(ctx, replaced, "toJSON");
if (JS_IsFunction(to_json)) {
JSValue result = JS_Call(ctx, to_json, replaced, 0, NULL);
JS_FreeValue(ctx, to_json);
if (!JS_IsException(result)) {
wota_encode_value(enc, result, holder, key);
JS_FreeValue(ctx, result);
} else
wota_write_sym(&enc->wb, WOTA_NULL);
wota_stack_pop(enc);
break;
}
JS_FreeValue(ctx, to_json);
encode_object_properties(enc, replaced, holder);
wota_stack_pop(enc);
break;
}
default:
wota_write_sym(&enc->wb, WOTA_NULL);
break;
}
JS_FreeValue(ctx, replaced);
}
static char *decode_wota_value(JSContext *ctx, char *data_ptr, JSValue *out_val, JSValue holder, JSAtom key, JSValue reviver)
{
uint64_t first_word = *(uint64_t *)data_ptr;
int type = (int)(first_word & 0xffU);
switch (type) {
case WOTA_INT: {
long long val;
data_ptr = wota_read_int(&val, data_ptr);
*out_val = JS_NewInt64(ctx, val);
break;
}
case WOTA_FLOAT: {
double d;
data_ptr = wota_read_float(&d, data_ptr);
*out_val = JS_NewFloat64(ctx, d);
break;
}
case WOTA_SYM: {
int scode;
data_ptr = wota_read_sym(&scode, data_ptr);
if (scode == WOTA_PRIVATE) {
JSValue inner = JS_NULL;
data_ptr = decode_wota_value(ctx, data_ptr, &inner, holder, JS_ATOM_NULL, reviver);
JSValue obj = JS_NewObject(ctx);
cell_rt *crt = JS_GetContextOpaque(ctx);
// JS_SetProperty(ctx, obj, crt->actor_sym, inner);
*out_val = obj;
} else if (scode == WOTA_NULL) *out_val = JS_NULL;
else if (scode == WOTA_FALSE) *out_val = JS_NewBool(ctx, 0);
else if (scode == WOTA_TRUE) *out_val = JS_NewBool(ctx, 1);
else *out_val = JS_NULL;
break;
}
case WOTA_BLOB: {
long long blen;
char *bdata = NULL;
data_ptr = wota_read_blob(&blen, &bdata, data_ptr);
*out_val = bdata ? js_new_blob_stoned_copy(ctx, (uint8_t *)bdata, (size_t)blen) : js_new_blob_stoned_copy(ctx, NULL, 0);
if (bdata) free(bdata);
break;
}
case WOTA_TEXT: {
char *utf8 = NULL;
data_ptr = wota_read_text(&utf8, data_ptr);
*out_val = JS_NewString(ctx, utf8 ? utf8 : "");
if (utf8) free(utf8);
break;
}
case WOTA_ARR: {
long long c;
data_ptr = wota_read_array(&c, data_ptr);
JSValue arr = JS_NewArrayLen(ctx, c);
for (long long i = 0; i < c; i++) {
JSValue elem_val = JS_NULL;
JSAtom idx_atom = JS_NewAtomUInt32(ctx, (uint32_t)i);
data_ptr = decode_wota_value(ctx, data_ptr, &elem_val, arr, idx_atom, reviver);
JS_SetPropertyUint32(ctx, arr, i, elem_val);
JS_FreeAtom(ctx, idx_atom);
}
*out_val = arr;
break;
}
case WOTA_REC: {
long long c;
data_ptr = wota_read_record(&c, data_ptr);
JSValue obj = JS_NewObject(ctx);
for (long long i = 0; i < c; i++) {
char *tkey = NULL;
size_t key_len;
data_ptr = wota_read_text_len(&key_len, &tkey, data_ptr);
if (!tkey) continue; // invalid key
JSAtom prop_key = JS_NewAtomLen(ctx, tkey, key_len);
JSValue sub_val = JS_NULL;
data_ptr = decode_wota_value(ctx, data_ptr, &sub_val, obj, prop_key, reviver);
JS_SetProperty(ctx, obj, prop_key, sub_val);
JS_FreeAtom(ctx, prop_key);
free(tkey);
}
*out_val = obj;
break;
}
default:
data_ptr += 8;
*out_val = JS_NULL;
break;
}
if (!JS_IsNull(reviver)) {
JSValue key_val = (key == JS_ATOM_NULL) ? JS_NULL : JS_AtomToValue(ctx, key);
JSValue args[2] = { key_val, JS_DupValue(ctx, *out_val) };
JSValue revived = JS_Call(ctx, reviver, holder, 2, args);
JS_FreeValue(ctx, args[0]);
JS_FreeValue(ctx, args[1]);
if (!JS_IsException(revived)) {
JS_FreeValue(ctx, *out_val);
*out_val = revived;
} else
JS_FreeValue(ctx, revived);
}
return data_ptr;
}
void *value2wota(JSContext *ctx, JSValue v, JSValue replacer, size_t *bytes)
{
WotaEncodeContext enc_s, *enc = &enc_s;
enc->ctx = ctx;
enc->visited_stack = NULL;
enc->cycle = 0;
enc->replacer = replacer;
wota_buffer_init(&enc->wb, 16);
wota_encode_value(enc, v, JS_NULL, JS_ATOM_NULL);
if (enc->cycle) {
wota_stack_free(enc);
wota_buffer_free(&enc->wb);
return NULL;
}
wota_stack_free(enc);
size_t total_bytes = enc->wb.size * sizeof(uint64_t);
void *wota = realloc(enc->wb.data, total_bytes);
if (bytes) *bytes = total_bytes;
return wota;
}
JSValue wota2value(JSContext *ctx, void *wota)
{
JSValue result = JS_NULL;
JSValue holder = JS_NewObject(ctx);
decode_wota_value(ctx, wota, &result, holder, JS_ATOM_NULL, JS_NULL);
JS_FreeValue(ctx, holder);
return result;
}
static JSValue js_wota_encode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_ThrowTypeError(ctx, "wota.encode requires at least 1 argument");
size_t total_bytes;
void *wota = value2wota(ctx, argv[0], JS_IsFunction(argv[1]) ? argv[1] : JS_NULL, &total_bytes);
JSValue ret = js_new_blob_stoned_copy(ctx, wota, total_bytes);
free(wota);
return ret;
}
static JSValue js_wota_decode(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
if (argc < 1) return JS_NULL;
size_t len;
uint8_t *buf = js_get_blob_data(ctx, &len, argv[0]);
if (buf == (uint8_t *)-1) return JS_EXCEPTION;
if (!buf || len == 0) return JS_ThrowTypeError(ctx, "No blob data present");
JSValue reviver = (argc > 1 && JS_IsFunction(argv[1])) ? argv[1] : JS_NULL;
char *data_ptr = (char *)buf;
JSValue result = JS_NULL;
JSValue holder = JS_NewObject(ctx);
JSAtom empty_atom = JS_NewAtom(ctx, "");
decode_wota_value(ctx, data_ptr, &result, holder, empty_atom, reviver);
JS_FreeAtom(ctx, empty_atom);
JS_FreeValue(ctx, holder);
return result;
}
static const JSCFunctionListEntry js_wota_funcs[] = {
JS_CFUNC_DEF("encode", 2, js_wota_encode),
JS_CFUNC_DEF("decode", 2, js_wota_decode),
};
JSValue js_wota_use(JSContext *ctx)
{
JSValue exports = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, exports, js_wota_funcs, sizeof(js_wota_funcs)/sizeof(js_wota_funcs[0]));
return exports;
}

View File

@@ -28,7 +28,6 @@ FMT(none)
FMT(none_int) FMT(none_int)
FMT(none_loc) FMT(none_loc)
FMT(none_arg) FMT(none_arg)
FMT(none_var_ref)
FMT(u8) FMT(u8)
FMT(i8) FMT(i8)
FMT(loc8) FMT(loc8)
@@ -42,17 +41,16 @@ FMT(npopx)
FMT(npop_u16) FMT(npop_u16)
FMT(loc) FMT(loc)
FMT(arg) FMT(arg)
FMT(var_ref)
FMT(u32) FMT(u32)
FMT(i32) FMT(i32)
FMT(const) FMT(const)
FMT(label) FMT(label)
FMT(atom)
FMT(atom_u8)
FMT(atom_u16)
FMT(atom_label_u8)
FMT(atom_label_u16)
FMT(label_u16) FMT(label_u16)
FMT(key)
FMT(key_u8)
FMT(key_u16)
FMT(key_label_u16)
FMT(u8_u16) /* 1 byte + 2 bytes for upvalue access */
#undef FMT #undef FMT
#endif /* FMT */ #endif /* FMT */
@@ -68,7 +66,6 @@ DEF(invalid, 1, 0, 0, none) /* never emitted */
DEF( push_i32, 5, 0, 1, i32) DEF( push_i32, 5, 0, 1, i32)
DEF( push_const, 5, 0, 1, const) DEF( push_const, 5, 0, 1, const)
DEF( fclosure, 5, 0, 1, const) /* must follow push_const */ DEF( fclosure, 5, 0, 1, const) /* must follow push_const */
DEF(push_atom_value, 5, 0, 1, atom)
DEF( null, 1, 0, 1, none) DEF( null, 1, 0, 1, none)
DEF( push_this, 1, 0, 1, none) /* only used at the start of a function */ DEF( push_this, 1, 0, 1, none) /* only used at the start of a function */
DEF( push_false, 1, 0, 1, none) DEF( push_false, 1, 0, 1, none)
@@ -104,40 +101,38 @@ DEF( array_from, 3, 0, 1, npop) /* arguments are not counted in n_pop */
DEF( return, 1, 1, 0, none) DEF( return, 1, 1, 0, none)
DEF( return_undef, 1, 0, 0, none) DEF( return_undef, 1, 0, 0, none)
DEF( throw, 1, 1, 0, none) DEF( throw, 1, 1, 0, none)
DEF( throw_error, 6, 0, 0, atom_u8) DEF( throw_error, 6, 0, 0, key_u8)
DEF( eval, 5, 1, 1, npop_u16) /* func args... -> ret_val */
DEF( regexp, 1, 2, 1, none) /* create a RegExp object from the pattern and a DEF( regexp, 1, 2, 1, none) /* create a RegExp object from the pattern and a
bytecode string */ bytecode string */
DEF( check_var, 5, 0, 1, atom) /* check if a variable exists */ /* Global variable access - resolved by linker to get/set_global_slot */
DEF( get_var_undef, 5, 0, 1, atom) /* push undefined if the variable does not exist */ DEF( check_var, 5, 0, 1, key) /* check if a variable exists - resolved by linker */
DEF( get_var, 5, 0, 1, atom) /* throw an exception if the variable does not exist */ DEF( get_var_undef, 5, 0, 1, key) /* resolved by linker to get_global_slot */
DEF( put_var, 5, 1, 0, atom) /* must come after get_var */ DEF( get_var, 5, 0, 1, key) /* resolved by linker to get_global_slot */
DEF( put_var_init, 5, 1, 0, atom) /* must come after put_var. Used to initialize a global lexical variable */ DEF( put_var, 5, 1, 0, key) /* resolved by linker to set_global_slot */
DEF( put_var_strict, 5, 2, 0, atom) /* for strict mode variable write */ DEF( put_var_init, 5, 1, 0, key) /* resolved by linker to set_global_slot */
DEF( put_var_strict, 5, 2, 0, key) /* resolved by linker to set_global_slot */
DEF( get_ref_value, 1, 2, 3, none) /* Global variable opcodes - resolved by linker to get/set_global_slot */
DEF( put_ref_value, 1, 3, 0, none) DEF( define_var, 6, 0, 0, key_u8)
DEF(check_define_var, 6, 0, 0, key_u8)
DEF( define_var, 6, 0, 0, atom_u8) DEF( define_func, 6, 1, 0, key_u8)
DEF(check_define_var, 6, 0, 0, atom_u8) DEF( get_field, 5, 1, 1, key)
DEF( define_func, 6, 1, 0, atom_u8) DEF( get_field2, 5, 1, 2, key)
DEF( get_field, 5, 1, 1, atom) DEF( put_field, 5, 2, 0, key)
DEF( get_field2, 5, 1, 2, atom)
DEF( put_field, 5, 2, 0, atom)
DEF( get_array_el, 1, 2, 1, none) DEF( get_array_el, 1, 2, 1, none)
DEF( get_array_el2, 1, 2, 2, none) /* obj prop -> obj value */ DEF( get_array_el2, 1, 2, 2, none) /* obj prop -> obj value */
DEF( get_array_el3, 1, 2, 3, none) /* obj prop -> obj prop1 value */ DEF( get_array_el3, 1, 2, 3, none) /* obj prop -> obj prop1 value */
DEF( put_array_el, 1, 3, 0, none) DEF( put_array_el, 1, 3, 0, none)
DEF( define_field, 5, 2, 1, atom) DEF( define_field, 5, 2, 1, key)
DEF( set_name, 5, 1, 1, atom) DEF( set_name, 5, 1, 1, key)
DEF(set_name_computed, 1, 2, 2, none) DEF(set_name_computed, 1, 2, 2, none)
DEF(define_array_el, 1, 3, 2, none) DEF(define_array_el, 1, 3, 2, none)
DEF(copy_data_properties, 2, 3, 3, u8) DEF(copy_data_properties, 2, 3, 3, u8)
DEF( define_method, 6, 2, 1, atom_u8) DEF( define_method, 6, 2, 1, key_u8)
DEF(define_method_computed, 2, 3, 1, u8) /* must come after define_method */ DEF(define_method_computed, 2, 3, 1, u8) /* must come after define_method */
DEF( define_class, 6, 2, 2, atom_u8) /* parent ctor -> ctor proto */ DEF( define_class, 6, 2, 2, key_u8) /* parent ctor -> ctor proto */
DEF( define_class_computed, 6, 3, 3, atom_u8) /* field_name parent ctor -> field_name ctor proto (class with computed name) */ DEF( define_class_computed, 6, 3, 3, key_u8) /* field_name parent ctor -> field_name ctor proto (class with computed name) */
DEF( get_loc, 3, 0, 1, loc) DEF( get_loc, 3, 0, 1, loc)
DEF( put_loc, 3, 1, 0, loc) /* must come after get_loc */ DEF( put_loc, 3, 1, 0, loc) /* must come after get_loc */
@@ -145,18 +140,11 @@ DEF( set_loc, 3, 1, 1, loc) /* must come after put_loc */
DEF( get_arg, 3, 0, 1, arg) DEF( get_arg, 3, 0, 1, arg)
DEF( put_arg, 3, 1, 0, arg) /* must come after get_arg */ DEF( put_arg, 3, 1, 0, arg) /* must come after get_arg */
DEF( set_arg, 3, 1, 1, arg) /* must come after put_arg */ DEF( set_arg, 3, 1, 1, arg) /* must come after put_arg */
DEF( get_var_ref, 3, 0, 1, var_ref)
DEF( put_var_ref, 3, 1, 0, var_ref) /* must come after get_var_ref */
DEF( set_var_ref, 3, 1, 1, var_ref) /* must come after put_var_ref */
DEF(set_loc_uninitialized, 3, 0, 0, loc) DEF(set_loc_uninitialized, 3, 0, 0, loc)
DEF( get_loc_check, 3, 0, 1, loc) DEF( get_loc_check, 3, 0, 1, loc)
DEF( put_loc_check, 3, 1, 0, loc) /* must come after get_loc_check */ DEF( put_loc_check, 3, 1, 0, loc) /* must come after get_loc_check */
DEF( put_loc_check_init, 3, 1, 0, loc) DEF( put_loc_check_init, 3, 1, 0, loc)
DEF(get_loc_checkthis, 3, 0, 1, loc) DEF(get_loc_checkthis, 3, 0, 1, loc)
DEF(get_var_ref_check, 3, 0, 1, var_ref)
DEF(put_var_ref_check, 3, 1, 0, var_ref) /* must come after get_var_ref_check */
DEF(put_var_ref_check_init, 3, 1, 0, var_ref)
DEF( close_loc, 3, 0, 0, loc)
DEF( if_false, 5, 1, 0, label) DEF( if_false, 5, 1, 0, label)
DEF( if_true, 5, 1, 0, label) /* must come after if_false */ DEF( if_true, 5, 1, 0, label) /* must come after if_false */
DEF( goto, 5, 0, 0, label) /* must come after if_true */ DEF( goto, 5, 0, 0, label) /* must come after if_true */
@@ -165,15 +153,8 @@ DEF( gosub, 5, 0, 0, label) /* used to execute the finally block */
DEF( ret, 1, 1, 0, none) /* used to return from the finally block */ DEF( ret, 1, 1, 0, none) /* used to return from the finally block */
DEF( nip_catch, 1, 2, 1, none) /* catch ... a -> a */ DEF( nip_catch, 1, 2, 1, none) /* catch ... a -> a */
DEF( to_object, 1, 1, 1, none)
//DEF( to_string, 1, 1, 1, none)
DEF( to_propkey, 1, 1, 1, none) DEF( to_propkey, 1, 1, 1, none)
DEF( make_loc_ref, 7, 0, 2, atom_u16)
DEF( make_arg_ref, 7, 0, 2, atom_u16)
DEF(make_var_ref_ref, 7, 0, 2, atom_u16)
DEF( make_var_ref, 5, 0, 2, atom)
/* arithmetic/logic operations */ /* arithmetic/logic operations */
DEF( neg, 1, 1, 1, none) DEF( neg, 1, 1, 1, none)
DEF( plus, 1, 1, 1, none) DEF( plus, 1, 1, 1, none)
@@ -187,7 +168,7 @@ DEF( add_loc, 2, 1, 0, loc8)
DEF( not, 1, 1, 1, none) DEF( not, 1, 1, 1, none)
DEF( lnot, 1, 1, 1, none) DEF( lnot, 1, 1, 1, none)
DEF( delete, 1, 2, 1, none) DEF( delete, 1, 2, 1, none)
DEF( delete_var, 5, 0, 1, atom) DEF( delete_var, 5, 0, 1, key) /* deprecated - global object is immutable */
DEF( mul, 1, 2, 1, none) DEF( mul, 1, 2, 1, none)
DEF( mul_float, 1, 2, 1, none) DEF( mul_float, 1, 2, 1, none)
@@ -212,8 +193,20 @@ DEF( strict_neq, 1, 2, 1, none)
DEF( and, 1, 2, 1, none) DEF( and, 1, 2, 1, none)
DEF( xor, 1, 2, 1, none) DEF( xor, 1, 2, 1, none)
DEF( or, 1, 2, 1, none) DEF( or, 1, 2, 1, none)
/* template literal concatenation - pops N parts, pushes concatenated string */ /* format template - format_string_cpool_idx(u32), expr_count(u16)
DEF(template_concat, 3, 0, 1, npop_u16) Note: n_push=2 ensures stack has room for temp [format_str, arr] pair,
even though we only leave 1 value (the result) on the stack. */
DEF(format_template, 7, 0, 1, npop_u16)
/* Upvalue access (closures via outer_frame chain) */
DEF( get_up, 4, 0, 1, u8_u16) /* depth:u8, slot:u16 -> value */
DEF( set_up, 4, 1, 0, u8_u16) /* value, depth:u8, slot:u16 -> */
/* Name resolution with bytecode patching */
DEF( get_name, 5, 0, 1, const) /* cpool_idx -> value, patches itself */
DEF( get_env_slot, 3, 0, 1, u16) /* slot -> value (patched from get_name) */
DEF(get_global_slot, 3, 0, 1, u16) /* slot -> value (patched from get_var) */
DEF(set_global_slot, 3, 1, 0, u16) /* value -> slot (patched from put_var) */
/* must be the last non short and non temporary opcode */ /* must be the last non short and non temporary opcode */
DEF( nop, 1, 0, 0, none) DEF( nop, 1, 0, 0, none)
@@ -227,15 +220,13 @@ def( label, 5, 0, 0, label) /* emitted in phase 1, removed in phase 3 *
/* the following opcodes must be in the same order as the 'with_x' and /* the following opcodes must be in the same order as the 'with_x' and
get_var_undef, get_var and put_var opcodes */ get_var_undef, get_var and put_var opcodes */
def(scope_get_var_undef, 7, 0, 1, atom_u16) /* emitted in phase 1, removed in phase 2 */ def(scope_get_var_undef, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_get_var, 7, 0, 1, atom_u16) /* emitted in phase 1, removed in phase 2 */ def( scope_get_var, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_put_var, 7, 1, 0, atom_u16) /* emitted in phase 1, removed in phase 2 */ def( scope_put_var, 7, 1, 0, key_u16) /* emitted in phase 1, removed in phase 2 */
def(scope_delete_var, 7, 0, 1, atom_u16) /* emitted in phase 1, removed in phase 2 */ def(scope_delete_var, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_make_ref, 11, 0, 2, atom_label_u16) /* emitted in phase 1, removed in phase 2 */ def(scope_put_var_init, 7, 0, 2, key_u16) /* emitted in phase 1, removed in phase 2 */
def( scope_get_ref, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase 2 */ def(scope_get_var_checkthis, 7, 0, 1, key_u16) /* emitted in phase 1, removed in phase 2, only used to return 'this' in derived class constructors */
def(scope_put_var_init, 7, 0, 2, atom_u16) /* emitted in phase 1, removed in phase 2 */ def(get_field_opt_chain, 5, 1, 1, key) /* emitted in phase 1, removed in phase 2 */
def(scope_get_var_checkthis, 7, 0, 1, atom_u16) /* emitted in phase 1, removed in phase 2, only used to return 'this' in derived class constructors */
def(get_field_opt_chain, 5, 1, 1, atom) /* emitted in phase 1, removed in phase 2 */
def(get_array_el_opt_chain, 1, 2, 1, none) /* emitted in phase 1, removed in phase 2 */ def(get_array_el_opt_chain, 1, 2, 1, none) /* emitted in phase 1, removed in phase 2 */
def( set_class_name, 5, 1, 1, u32) /* emitted in phase 1, removed in phase 2 */ def( set_class_name, 5, 1, 1, u32) /* emitted in phase 1, removed in phase 2 */
@@ -285,18 +276,6 @@ DEF( set_arg0, 1, 1, 1, none_arg)
DEF( set_arg1, 1, 1, 1, none_arg) DEF( set_arg1, 1, 1, 1, none_arg)
DEF( set_arg2, 1, 1, 1, none_arg) DEF( set_arg2, 1, 1, 1, none_arg)
DEF( set_arg3, 1, 1, 1, none_arg) DEF( set_arg3, 1, 1, 1, none_arg)
DEF( get_var_ref0, 1, 0, 1, none_var_ref)
DEF( get_var_ref1, 1, 0, 1, none_var_ref)
DEF( get_var_ref2, 1, 0, 1, none_var_ref)
DEF( get_var_ref3, 1, 0, 1, none_var_ref)
DEF( put_var_ref0, 1, 1, 0, none_var_ref)
DEF( put_var_ref1, 1, 1, 0, none_var_ref)
DEF( put_var_ref2, 1, 1, 0, none_var_ref)
DEF( put_var_ref3, 1, 1, 0, none_var_ref)
DEF( set_var_ref0, 1, 1, 1, none_var_ref)
DEF( set_var_ref1, 1, 1, 1, none_var_ref)
DEF( set_var_ref2, 1, 1, 1, none_var_ref)
DEF( set_var_ref3, 1, 1, 1, none_var_ref)
DEF( if_false8, 2, 1, 0, label8) DEF( if_false8, 2, 1, 0, label8)
DEF( if_true8, 2, 1, 0, label8) /* must come after if_false8 */ DEF( if_true8, 2, 1, 0, label8) /* must come after if_false8 */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -122,7 +122,7 @@ void actor_free(cell_rt *actor)
JS_FreeValue(js, actor->message_handle); JS_FreeValue(js, actor->message_handle);
JS_FreeValue(js, actor->on_exception); JS_FreeValue(js, actor->on_exception);
JS_FreeValue(js, actor->unneeded); JS_FreeValue(js, actor->unneeded);
JS_FreeAtom(js, actor->actor_sym); JS_FreeValue(js, actor->actor_sym);
/* Free timer callbacks stored in actor */ /* Free timer callbacks stored in actor */
for (int i = 0; i < hmlen(actor->timers); i++) { for (int i = 0; i < hmlen(actor->timers); i++) {
@@ -332,7 +332,7 @@ cell_rt *create_actor(void *wota)
actor->message_handle = JS_NULL; actor->message_handle = JS_NULL;
actor->unneeded = JS_NULL; actor->unneeded = JS_NULL;
actor->on_exception = JS_NULL; actor->on_exception = JS_NULL;
actor->actor_sym = JS_ATOM_NULL; actor->actor_sym = JS_NULL;
arrsetcap(actor->letters, 5); arrsetcap(actor->letters, 5);

2217
source/suite.c Normal file

File diff suppressed because it is too large Load Diff

94
status.md Normal file
View File

@@ -0,0 +1,94 @@
QuickJS Mist Memory Format Refactoring
Current Status
The codebase is partially refactored but doesn't compile due to missing KeyId type definitions.
Incremental Refactoring Tasks
Phase 0: Fix Compilation (Prerequisite)
Define missing KeyId type as transitional typedef (will be replaced by JSValue later)
Define K_EMPTY, K_TOMB,
key_text()
,
key_is_text()
,
key_payload()
macros/functions
Verify build compiles and tests pass
Phase 1: New JSValue Encoding in quickjs.h
Add new LSB-based tag constants alongside existing tags
Add JS_TAG_SHORT_FLOAT for 61-bit truncated double
Add JS_TAG_STRING_ASCII for immediate 7-byte ASCII strings
Add new value extraction/creation macros
Add type check inline functions
Keep existing NaN-boxing code active (compile-time switch)
Phase 2: Short Float Implementation
Implement JS_NewFloat64_ShortFloat() with range checking
Implement JS_VALUE_GET_FLOAT64_ShortFloat() for decoding
Out-of-range values return JS_NULL
Prefer integer encoding when exact
Phase 3: Immediate ASCII String
Phase 3: Immediate ASCII String
Implement JS_TryNewImmediateASCII() for strings up to 7 chars
Implement JS_IsImmediateASCII() type check
Implement JS_GetImmediateASCIILen() and JS_GetImmediateASCIIChar()
Integrate with
JS_NewStringLen()
to try immediate first
Phase 4: Remove JSStringRope
Delete JSStringRope structure
Remove JS_TAG_STRING_ROPE handling
Update string concatenation to create immediate mist_text objects
Remove rope-related iterator functions
Phase 5: Refactor JSString to UTF-32 (mist_text)
Modify struct JSString to store UTF-32 characters only
Remove is_wide_char flag and 8.16 unions
Update
js_alloc_string
to allocate UTF-32 buffer
Update string creation functions (
js_new_string8
, etc.)
Update all string accessors to use UTF-32
Implement immediate-to-UTF32 conversion helper
Update string operations (
concat
,
compare
) to work on UTF-32
Phase 6: Replace KeyId with JSValue in Records
Change JSRecordEntry.key from KeyId to JSValue
Update
rec_hash_key()
to hash JSValue keys directly
Update
rec_find_slot()
for JSValue key comparison
Update
rec_get_own()
,
rec_get()
,
rec_set_own()
for JSValue keys
Remove KeyId typedef and related functions
Phase 7: Consolidate JSObject → JSRecord
Remove JSShape and JSShapeProperty structures
Remove shape hash table from JSRuntime
Update all property access to use JSRecord
Migrate JSObject users to JSRecord
Remove JSObject structure
Phase 8: Update GC for New Format
Update mark_children for JSRecord with JSValue keys
Update free_record for JSValue keys
Handle immediate values correctly (no marking needed)
Test for cycles and correct collection
Phase 9: C Class Storage in Slot 0
Implement slot 0 reservation for class_id and opaque pointer
Update JS_SetOpaque() / JS_GetOpaque()
Migrate existing class storage
Verification Checklist
Build compiles without errors
Existing tests pass
Property access works correctly
GC correctly handles cycles
Short float encoding/decoding verified
Immediate ASCII strings work

View File

@@ -1244,15 +1244,16 @@ return {
// EDGE CASES AND SPECIAL VALUES // EDGE CASES AND SPECIAL VALUES
// ============================================================================ // ============================================================================
test_infinity: function() { test_division_by_zero_is_null: function() {
var inf = 1 / 0 var inf = 1 / 0
if (!(inf > 1000000)) throw "infinity failed" if (inf != null) throw "division by zero should be null"
if (!(-inf < -1000000)) throw "negative infinity failed" var ninf = -1 / 0
if (ninf != null) throw "negative division by zero should be null"
}, },
test_nan: function() { test_zero_div_zero_is_null: function() {
var nan = 0 / 0 var nan = 0 / 0
if (nan == nan) throw "NaN should not equal itself" if (nan != null) throw "0/0 should be null"
}, },
test_max_safe_integer: function() { test_max_safe_integer: function() {
@@ -1403,17 +1404,36 @@ return {
test_number_division_by_zero: function() { test_number_division_by_zero: function() {
var result = 1 / 0 var result = 1 / 0
if (!(result > 1000000)) throw "division by zero should give infinity" if (result != null) throw "division by zero should give null"
}, },
test_number_negative_division_by_zero: function() { test_number_negative_division_by_zero: function() {
var result = -1 / 0 var result = -1 / 0
if (!(result < -1000000)) throw "negative division by zero should give -infinity" if (result != null) throw "negative division by zero should give null"
}, },
test_zero_division_by_zero: function() { test_zero_division_by_zero: function() {
var result = 0 / 0 var result = 0 / 0
if (result == result) throw "0/0 should give NaN" if (result != null) throw "0/0 should give null"
},
test_negative_zero_normalized: function() {
var nz = -0
if (nz != 0) throw "-0 should equal 0"
var mul_nz = 0 * -1
if (mul_nz != 0) throw "0 * -1 should be 0"
var neg_zero = -(0)
if (neg_zero != 0) throw "-(0) should be 0"
},
test_overflow_is_null: function() {
var result = 1e38 * 1e38
if (result != null) throw "overflow should give null"
},
test_modulo_by_zero_is_null: function() {
var result = 5 % 0
if (result != null) throw "modulo by zero should give null"
}, },
// ============================================================================ // ============================================================================
@@ -3506,4 +3526,158 @@ return {
if (obj.beta[1] != obj) throw "text key cycle failed" if (obj.beta[1] != obj) throw "text key cycle failed"
}, },
// ============================================================================
// OBJECT INTRINSIC TESTS
// ============================================================================
test_object_shallow_copy: function() {
var orig = {a: 1, b: 2, c: 3}
var copy = object(orig)
if (copy.a != 1) throw "object copy a failed"
if (copy.b != 2) throw "object copy b failed"
if (copy.c != 3) throw "object copy c failed"
copy.a = 99
if (orig.a != 1) throw "object copy should not mutate original"
},
test_object_combine: function() {
var obj1 = {a: 1, b: 2}
var obj2 = {c: 3, d: 4}
var combined = object(obj1, obj2)
if (combined.a != 1) throw "object combine a failed"
if (combined.b != 2) throw "object combine b failed"
if (combined.c != 3) throw "object combine c failed"
if (combined.d != 4) throw "object combine d failed"
},
test_object_combine_override: function() {
var obj1 = {a: 1, b: 2}
var obj2 = {b: 99, c: 3}
var combined = object(obj1, obj2)
if (combined.a != 1) throw "object combine override a failed"
if (combined.b != 99) throw "object combine should override with second arg"
if (combined.c != 3) throw "object combine override c failed"
},
test_object_select_keys: function() {
var orig = {a: 1, b: 2, c: 3, d: 4}
var selected = object(orig, ["a", "c"])
if (selected.a != 1) throw "object select a failed"
if (selected.c != 3) throw "object select c failed"
if (selected.b != null) throw "object select should not include b"
if (selected.d != null) throw "object select should not include d"
},
test_object_from_keys_true: function() {
var keys = ["x", "y", "z"]
var obj = object(keys)
if (obj.x != true) throw "object from keys x failed"
if (obj.y != true) throw "object from keys y failed"
if (obj.z != true) throw "object from keys z failed"
},
test_object_from_keys_function: function() {
var keys = ["a", "b", "c"]
var obj = object(keys, function(k) { return k + "_val" })
if (obj.a != "a_val") throw "object from keys func a failed"
if (obj.b != "b_val") throw "object from keys func b failed"
if (obj.c != "c_val") throw "object from keys func c failed"
},
// ============================================================================
// SPLAT INTRINSIC TESTS
// ============================================================================
test_splat_prototype_flattening: function() {
var proto = {x: 10, y: 20}
var obj = {z: 30}
obj.__proto__ = proto
var flat = splat(obj)
if (flat.x != 10) throw "splat x failed"
if (flat.y != 20) throw "splat y failed"
if (flat.z != 30) throw "splat z failed"
},
// ============================================================================
// REVERSE INTRINSIC TESTS
// ============================================================================
test_reverse_array: function() {
var arr = [1, 2, 3, 4, 5]
var rev = reverse(arr)
if (rev[0] != 5) throw "reverse[0] failed"
if (rev[1] != 4) throw "reverse[1] failed"
if (rev[2] != 3) throw "reverse[2] failed"
if (rev[3] != 2) throw "reverse[3] failed"
if (rev[4] != 1) throw "reverse[4] failed"
if (arr[0] != 1) throw "reverse should not mutate original"
},
// ============================================================================
// APPLY INTRINSIC TESTS
// ============================================================================
test_apply_with_array_args: function() {
def sum = function(a, b, c) { return a + b + c }
var result = fn.apply(sum, [1, 2, 3])
if (result != 6) throw "apply with array args failed"
},
test_apply_with_no_args: function() {
def ret42 = function() { return 42 }
var result = fn.apply(ret42)
if (result != 42) throw "apply with no args failed"
},
test_apply_with_single_value: function() {
def double = function(x) { return x * 2 }
var result = fn.apply(double, 10)
if (result != 20) throw "apply with single value failed"
},
// ============================================================================
// GC STRESS TESTS FOR FIXED INTRINSICS
// ============================================================================
test_gc_reverse_under_pressure: function() {
// Create GC pressure by making many arrays, then reverse
var arrays = []
for (var i = 0; i < 100; i = i + 1) {
arrays[i] = [i, i+1, i+2, i+3, i+4]
}
// Now reverse each one - this tests re-chase after allocation
for (var i = 0; i < 100; i = i + 1) {
var rev = reverse(arrays[i])
if (rev[0] != i+4) throw "gc reverse stress failed at " + text(i)
}
},
test_gc_object_select_under_pressure: function() {
// Create GC pressure
var objs = []
for (var i = 0; i < 100; i = i + 1) {
objs[i] = {a: i, b: i+1, c: i+2, d: i+3}
}
// Select keys - tests re-chase in loop
for (var i = 0; i < 100; i = i + 1) {
var selected = object(objs[i], ["a", "c"])
if (selected.a != i) throw "gc object select stress failed at " + text(i)
if (selected.c != i+2) throw "gc object select stress c failed at " + text(i)
}
},
test_gc_object_from_keys_function_under_pressure: function() {
// Create GC pressure
var keysets = []
for (var i = 0; i < 50; i = i + 1) {
keysets[i] = ["k" + text(i), "j" + text(i), "m" + text(i)]
}
// Create objects with function - tests JS_PUSH/POP and re-chase
for (var i = 0; i < 50; i = i + 1) {
var obj = object(keysets[i], function(k) { return k + "_value" })
var expected = "k" + text(i) + "_value"
if (obj["k" + text(i)] != expected) throw "gc object from keys func stress failed at " + text(i)
}
},
} }