52 Commits

Author SHA1 Message Date
John Alanbrook
b771b2b5d8 suite passes now with mcode->mach lowering 2026-02-12 09:40:24 -06:00
John Alanbrook
68fb440502 Merge branch 'mach' into bytecode_cleanup 2026-02-12 07:50:09 -06:00
John Alanbrook
e7a2f16004 mcode to mach 2026-02-12 05:23:33 -06:00
John Alanbrook
3a8a17ab60 mcode->mach 2026-02-12 04:28:14 -06:00
John Alanbrook
8a84be65e1 new path 2026-02-11 14:41:37 -06:00
John Alanbrook
c1910ee1db Merge branch 'mcode2' into mach 2026-02-11 13:16:07 -06:00
John Alanbrook
7036cdf2d1 Merge branch 'mach' into bytecode_cleanup 2026-02-11 13:15:20 -06:00
John Alanbrook
fbeec17ce5 simplifications 2026-02-11 13:15:04 -06:00
John Alanbrook
2c55ae8cb2 quiesence exit 2026-02-11 11:50:29 -06:00
John Alanbrook
259bc139fc rm stack usage 2026-02-11 10:17:55 -06:00
John Alanbrook
a252412eca removal of old code 2026-02-11 09:47:30 -06:00
John Alanbrook
b327e16463 rm unused functions 2026-02-11 09:09:40 -06:00
John Alanbrook
da6f096a56 qbe rt 2026-02-10 20:28:51 -06:00
John Alanbrook
1320ef9f47 Merge branch 'mcode2' into mach 2026-02-10 19:04:35 -06:00
John Alanbrook
ed4a5474d5 Merge branch 'mach' into mcode2 2026-02-10 19:04:22 -06:00
John Alanbrook
f52dd80d52 fix compile error 2026-02-10 19:02:42 -06:00
John Alanbrook
504e268b9d run native modules 2026-02-10 18:52:11 -06:00
John Alanbrook
0d47002167 add compile script 2026-02-10 18:35:18 -06:00
John Alanbrook
b65db63447 remove vm_test, update test harness 2026-02-10 17:52:57 -06:00
John Alanbrook
c1ccff5437 fix >256 object literal error 2026-02-10 17:42:58 -06:00
John Alanbrook
2f681fa366 output for parser stages and c runtime doc 2026-02-10 17:38:15 -06:00
John Alanbrook
682b1cf9cf Merge branch 'pitweb' into mcode2 2026-02-10 17:29:03 -06:00
John Alanbrook
ddf3fc1c77 add object literal test 2026-02-10 17:28:59 -06:00
John Alanbrook
f1a5072ff2 fix increment operators on objects 2026-02-10 17:17:36 -06:00
John Alanbrook
f44fb502be string literal error 2026-02-10 17:02:22 -06:00
John Alanbrook
d75ce916d7 compile optimization 2026-02-10 16:37:11 -06:00
John Alanbrook
fe5dc6ecc9 fix fd.c bugs 2026-02-10 14:21:49 -06:00
John Alanbrook
54673e4a04 better disrupt logging; actor exit on crash 2026-02-10 12:38:06 -06:00
John Alanbrook
0d8b5cfb04 bootstrap loads engine 2026-02-10 12:13:18 -06:00
John Alanbrook
3d71f4a363 Merge branch 'mach' into pitweb 2026-02-10 11:15:44 -06:00
John Alanbrook
4deb0e2577 new syntax for internals 2026-02-10 11:03:01 -06:00
John Alanbrook
67b96e1627 add test for multiple declaration 2026-02-10 10:39:23 -06:00
John Alanbrook
4e5f1d8faa fix labeled loops, do-while, shorthand property syntax, and added more tests 2026-02-10 10:32:54 -06:00
John Alanbrook
bd577712d9 fix function shorthand default params 2026-02-10 10:13:46 -06:00
John Alanbrook
6df3b741cf add runtime warnings for stale files 2026-02-10 10:05:27 -06:00
John Alanbrook
178837b88d bootstrap 2026-02-10 09:53:41 -06:00
John Alanbrook
120ce9d30c Merge branch 'mcode2' into pitweb 2026-02-10 09:23:30 -06:00
John Alanbrook
58f185b379 fix merge 2026-02-10 09:21:33 -06:00
John Alanbrook
f7b5252044 core flag 2026-02-10 09:21:21 -06:00
John Alanbrook
ded5f7d74b cell shop env var 2026-02-10 09:13:10 -06:00
John Alanbrook
fe6033d6cb deploy website script 2026-02-10 08:12:51 -06:00
John Alanbrook
60e61eef76 scheduler starts 2026-02-10 08:12:42 -06:00
John Alanbrook
ad863fb89b postfix/prefix operators handled correctly 2026-02-10 08:12:27 -06:00
John Alanbrook
96f8157039 Merge branch 'mach' into mcode2 2026-02-10 07:38:35 -06:00
John Alanbrook
c4ff0bc109 intrinsics rewritten without ++, --, etc 2026-02-10 07:19:45 -06:00
John Alanbrook
877250b1d8 decomposed mcode 2026-02-10 07:12:27 -06:00
John Alanbrook
747227de40 better parse errors 2026-02-10 06:51:26 -06:00
John Alanbrook
3f7e34cd7a more useful parse errors 2026-02-10 06:08:15 -06:00
John Alanbrook
cef5c50169 add is_letter intrinsic 2026-02-10 06:00:47 -06:00
John Alanbrook
0428424ec7 Merge branch 'mach' into mcode2 2026-02-10 05:53:51 -06:00
John Alanbrook
78e64c5067 optimize parse 2026-02-10 05:53:49 -06:00
John Alanbrook
ff11c49c39 optimize tokenize 2026-02-10 05:52:19 -06:00
183 changed files with 10030 additions and 8435 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.mach binary merge=ours

View File

@@ -5,10 +5,16 @@
# or manually build with meson once.
#
# The cell shop is at ~/.cell and core scripts are installed to ~/.cell/core
#
# See BUILDING.md for details on the bootstrap process and .mach files.
CELL_SHOP = $(HOME)/.cell
CELL_CORE_PACKAGE = $(CELL_SHOP)/packages/core
# .cm sources that compile to .mach bytecode
MACH_SOURCES = tokenize.cm parse.cm fold.cm mcode.cm \
internal/bootstrap.cm internal/engine.cm
maker: install
makecell:
@@ -16,7 +22,7 @@ makecell:
cp cell /opt/homebrew/bin/
# Install core: symlink this directory to ~/.cell/core
install: bootstrap $(CELL_SHOP)
install: bootstrap .mach.stamp $(CELL_SHOP)
@echo "Linking cell core to $(CELL_CORE_PACKAGE)"
rm -rf $(CELL_CORE_PACKAGE)
ln -s $(PWD) $(CELL_CORE_PACKAGE)
@@ -40,6 +46,16 @@ libcell_runtime.dylib: $(CELL_SHOP)/build/dynamic
cell_main: source/main.c libcell_runtime.dylib
cc -o cell_main source/main.c -L. -lcell_runtime -Wl,-rpath,@loader_path -Wl,-rpath,/opt/homebrew/lib
# Regenerate .mach bytecode when any .cm source changes
.mach.stamp: $(MACH_SOURCES)
./cell --core . regen.cm
@touch $@
# Force-regenerate all .mach bytecode files
regen:
./cell --core . regen.cm
@touch .mach.stamp
# Create the cell shop directories
$(CELL_SHOP):
mkdir -p $(CELL_SHOP)
@@ -57,7 +73,7 @@ static:
# Bootstrap: build cell from scratch using meson (only needed once)
# Also installs core scripts to ~/.cell/core
bootstrap:
bootstrap:
meson setup build_bootstrap -Dbuildtype=debugoptimized
meson compile -C build_bootstrap
cp build_bootstrap/cell .
@@ -68,7 +84,7 @@ bootstrap:
# Clean build artifacts
clean:
rm -rf $(CELL_SHOP)/build build_bootstrap
rm -f cell cell_main libcell_runtime.dylib
rm -f cell cell_main libcell_runtime.dylib .mach.stamp
# Ensure dynamic build directory exists
$(CELL_SHOP)/build/dynamic: $(CELL_SHOP)
@@ -79,4 +95,4 @@ meson:
meson setup build_dbg -Dbuildtype=debugoptimized
meson install -C build_dbg
.PHONY: cell static bootstrap clean meson install
.PHONY: cell static bootstrap clean meson install regen

View File

@@ -319,7 +319,7 @@ JSValue js_reader_list(JSContext *js, JSValue self, int argc, JSValue *argv)
JS_FreeValue(js, arr);
return filename;
}
JS_SetPropertyUint32(js, arr, arr_index++, filename);
JS_SetPropertyNumber(js, arr, arr_index++, filename);
}
return arr;

View File

@@ -12,7 +12,8 @@ var files = [
{src: "parse.cm", name: "parse", out: "parse.mach"},
{src: "fold.cm", name: "fold", out: "fold.mach"},
{src: "mcode.cm", name: "mcode", out: "mcode.mach"},
{src: "internal/bootstrap.cm", name: "bootstrap", out: "internal/bootstrap.mach"}
{src: "internal/bootstrap.cm", name: "bootstrap", out: "internal/bootstrap.mach"},
{src: "internal/engine.cm", name: "engine", out: "internal/engine.mach"}
]
var i = 0

102
compile.ce Normal file
View File

@@ -0,0 +1,102 @@
// compile.ce — compile a .cm module to native .dylib via QBE
//
// Usage:
// cell --core . compile.ce <file.cm>
//
// Produces <file>.dylib in the current directory.
var fd = use('fd')
var os = use('os')
if (length(args) < 1) {
print('usage: cell --core . compile.ce <file.cm>')
return
}
var file = args[0]
var base = file
if (ends_with(base, '.cm')) {
base = text(base, 0, length(base) - 3)
}
var safe = replace(replace(base, '/', '_'), '-', '_')
var symbol = 'js_' + safe + '_use'
var tmp = '/tmp/qbe_' + safe
var ssa_path = tmp + '.ssa'
var s_path = tmp + '.s'
var o_path = tmp + '.o'
var rt_o_path = '/tmp/qbe_rt.o'
var dylib_path = base + '.dylib'
var cwd = fd.getcwd()
var rc = 0
// Step 1: emit QBE IL
print('emit qbe...')
rc = os.system('cd ' + cwd + ' && ./cell --core . --emit-qbe ' + file + ' > ' + ssa_path)
if (rc != 0) {
print('failed to emit qbe il')
return
}
// Step 2: post-process — insert dead labels after ret/jmp, append wrapper
// Use awk via shell to avoid blob/slurpwrite issues with long strings
print('post-process...')
var awk_cmd = `awk '
need_label && /^[[:space:]]*[^@}]/ && NF > 0 {
print "@_dead_" dead_id; dead_id++; need_label=0
}
/^@/ || /^}/ || NF==0 { need_label=0 }
/^[[:space:]]*ret / || /^[[:space:]]*jmp / { need_label=1; print; next }
{ print }
' ` + ssa_path + ` > ` + tmp + `_fixed.ssa`
rc = os.system(awk_cmd)
if (rc != 0) {
print('post-process failed')
return
}
// Append wrapper function — called as symbol(ctx) by os.dylib_symbol.
// Delegates to cell_rt_module_entry which heap-allocates a frame
// (so closures survive) and calls cell_main.
var wrapper_cmd = `printf '\nexport function l $` + symbol + `(l %%ctx) {\n@entry\n %%result =l call $cell_rt_module_entry(l %%ctx)\n ret %%result\n}\n' >> ` + tmp + `_fixed.ssa`
rc = os.system(wrapper_cmd)
if (rc != 0) {
print('wrapper append failed')
return
}
// Step 3: compile QBE IL to assembly
print('qbe compile...')
rc = os.system('~/.local/bin/qbe -o ' + s_path + ' ' + tmp + '_fixed.ssa')
if (rc != 0) {
print('qbe compilation failed')
return
}
// Step 4: assemble
print('assemble...')
rc = os.system('cc -c ' + s_path + ' -o ' + o_path)
if (rc != 0) {
print('assembly failed')
return
}
// Step 5: compile runtime stubs (cached — skip if already built)
if (!fd.is_file(rt_o_path)) {
print('compile runtime stubs...')
rc = os.system('cc -c ' + cwd + '/qbe_rt.c -o ' + rt_o_path + ' -fPIC')
if (rc != 0) {
print('runtime stubs compilation failed')
return
}
}
// Step 6: link dylib
print('link...')
rc = os.system('cc -shared -fPIC -undefined dynamic_lookup ' + o_path + ' ' + rt_o_path + ' -o ' + cwd + '/' + dylib_path)
if (rc != 0) {
print('linking failed')
return
}
print('built: ' + dylib_path)

View File

@@ -4,7 +4,7 @@
JSC_CCALL(debug_stack_depth, return number2js(js,js_debugger_stack_depth(js)))
// Return a backtrace of the current call stack.
JSC_CCALL(debug_build_backtrace, return js_debugger_build_backtrace(js,NULL))
JSC_CCALL(debug_build_backtrace, return js_debugger_build_backtrace(js))
// Return the closure variables for a given function.
JSC_CCALL(debug_closure_vars, return js_debugger_closure_variables(js,argv[0]))
@@ -21,7 +21,7 @@ JSC_CCALL(debug_local_vars, return js_debugger_local_variables(js, js2number(js,
JSC_CCALL(debug_fn_info, return js_debugger_fn_info(js, argv[0]))
// Return an array of functions in the current backtrace.
JSC_CCALL(debug_backtrace_fns, return js_debugger_backtrace_fns(js,NULL))
JSC_CCALL(debug_backtrace_fns, return js_debugger_backtrace_fns(js))
static const JSCFunctionListEntry js_debug_funcs[] = {
MIST_FUNC_DEF(debug, stack_depth, 0),

View File

@@ -11,7 +11,7 @@ type: "docs"
### Variables and Constants
Variables are declared with `var`, constants with `def`. All declarations must be initialized and must appear at the function body level — not inside `if`, `while`, `for`, or bare `{}` blocks.
Variables are declared with `var`, constants with `def`. All declarations must be initialized and must appear at the function body level — not inside `if`, `while`, `for`, or `do` blocks.
```javascript
var x = 10

296
docs/spec/c-runtime.md Normal file
View File

@@ -0,0 +1,296 @@
---
title: "C Runtime for Native Code"
description: "Minimum C runtime surface for QBE-generated native code"
---
## Overview
QBE-generated native code calls into a C runtime for anything that touches the heap, dispatches dynamically, or requires GC awareness. The design principle: **native code handles control flow and integer math directly; everything else is a runtime call.**
This document defines the runtime boundary — what must be in C, what QBE handles inline, and how to organize the C code to serve both the mcode interpreter and native code cleanly.
## The Boundary
### What native code does inline (no C calls)
These operations compile to straight QBE instructions with no runtime involvement:
- **Integer arithmetic**: `add`, `sub`, `mul` on NaN-boxed ints (shift right 1, operate, shift left 1)
- **Integer comparisons**: extract int with shift, compare, produce tagged bool
- **Control flow**: jumps, branches, labels, function entry/exit
- **Slot access**: load/store to frame slots via `%fp` + offset
- **NaN-box tagging**: integer tagging (`n << 1`), bool constants (`0x03`/`0x23`), null (`0x07`)
- **Type tests**: `JS_IsInt` (LSB check), `JS_IsNumber`, `JS_IsText`, `JS_IsNull` — these are bit tests on the value, no heap access needed
### What requires a C call
Anything that:
1. **Allocates** (arrays, records, strings, frames, function objects)
2. **Touches the heap** (property get/set, array indexing, closure access)
3. **Dispatches on type at runtime** (dynamic load/store, polymorphic arithmetic)
4. **Calls user functions** (frame setup, argument passing, invocation)
5. **Does string operations** (concatenation, comparison, conversion)
## Runtime Functions
### Tier 1: Essential (must exist for any program to run)
These are called by virtually every QBE program.
#### Intrinsic Lookup
```c
// Look up a built-in function by name. Called once per intrinsic per callsite.
JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name);
```
Maps name → C function pointer wrapped in JSValue. This is the primary entry point for all built-in functions (`print`, `text`, `length`, `is_array`, etc). The native code never calls intrinsics directly — it always goes through `get_intrinsic``frame``invoke`.
#### Function Calls
```c
// Allocate a call frame with space for nr_args arguments.
JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int nr_args);
// Set argument idx in the frame.
void cell_rt_setarg(JSValue frame, int idx, JSValue val);
// Execute the function. Returns the result.
JSValue cell_rt_invoke(JSContext *ctx, JSValue frame);
```
This is the universal calling convention. Every function call — user functions, intrinsics, methods — goes through frame/setarg/invoke. The frame allocates a `JSFrameRegister` on the GC heap, setarg fills slots, invoke dispatches.
**Tail call variants:**
```c
JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int nr_args);
void cell_rt_goinvoke(JSContext *ctx, JSValue frame);
```
Same as frame/invoke but reuse the caller's stack position.
### Tier 2: Property Access (needed by any program using records or arrays)
```c
// Record field by constant name.
JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name);
void cell_rt_store_field(JSContext *ctx, JSValue obj, JSValue val, const char *name);
// Array element by integer index.
JSValue cell_rt_load_index(JSContext *ctx, JSValue obj, JSValue idx);
void cell_rt_store_index(JSContext *ctx, JSValue obj, JSValue idx, JSValue val);
// Dynamic — type of key unknown at compile time.
JSValue cell_rt_load_dynamic(JSContext *ctx, JSValue obj, JSValue key);
void cell_rt_store_dynamic(JSContext *ctx, JSValue obj, JSValue key, JSValue val);
```
The typed variants (`load_field`/`load_index`) skip the key-type dispatch that `load_dynamic` must do. When parse and fold provide type information, QBE emit selects the typed variant and the streamline optimizer can narrow dynamic → typed.
**Implementation**: These are thin wrappers around existing `JS_GetPropertyStr`/`JS_GetPropertyNumber`/`JS_GetProperty` and their `Set` counterparts.
### Tier 3: Closures (needed by programs with nested functions)
```c
// Walk depth levels up the frame chain, read slot.
JSValue cell_rt_get_closure(JSContext *ctx, JSValue fp, int depth, int slot);
// Walk depth levels up, write slot.
void cell_rt_put_closure(JSContext *ctx, JSValue fp, JSValue val, int depth, int slot);
```
Closure variables live in outer frames. `depth` is how many `caller` links to follow; `slot` is the register index in that frame.
### Tier 4: Object Construction (needed by programs creating arrays/records/functions)
```c
// Create a function object from a compiled function index.
// The native code loader must maintain a function table.
JSValue cell_rt_make_function(JSContext *ctx, int fn_id);
```
Array and record literals are currently compiled as intrinsic calls (`array(...)`, direct `{...}` construction) which go through the frame/invoke path. A future optimization could add:
```c
// Fast paths (optional, not yet needed)
JSValue cell_rt_new_array(JSContext *ctx, int len);
JSValue cell_rt_new_record(JSContext *ctx);
```
### Tier 5: Collection Operations
```c
// a[] = val (push) and var v = a[] (pop)
void cell_rt_push(JSContext *ctx, JSValue arr, JSValue val);
JSValue cell_rt_pop(JSContext *ctx, JSValue arr);
```
### Tier 6: Error Handling
```c
// Trigger disruption. Jumps to the disrupt handler or unwinds.
void cell_rt_disrupt(JSContext *ctx);
```
### Tier 7: Miscellaneous
```c
JSValue cell_rt_delete(JSContext *ctx, JSValue obj, JSValue key);
JSValue cell_rt_typeof(JSContext *ctx, JSValue val);
```
### Tier 8: String and Float Helpers (called from QBE inline code, not from qbe_emit)
These are called from the QBE IL that `qbe.cm` generates inline for arithmetic and comparison operations. They're not `cell_rt_` prefixed — they're lower-level:
```c
// Float arithmetic (when operands aren't both ints)
JSValue qbe_float_add(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_sub(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_mul(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_div(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_mod(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_pow(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_float_neg(JSContext *ctx, JSValue v);
JSValue qbe_float_inc(JSContext *ctx, JSValue v);
JSValue qbe_float_dec(JSContext *ctx, JSValue v);
// Float comparison (returns C int 0/1 for QBE branching)
int qbe_float_cmp(JSContext *ctx, int op, JSValue a, JSValue b);
// Bitwise ops on non-int values (convert to int32 first)
JSValue qbe_bnot(JSContext *ctx, JSValue v);
JSValue qbe_bitwise_and(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_bitwise_or(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_bitwise_xor(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_shift_shl(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_shift_sar(JSContext *ctx, JSValue a, JSValue b);
JSValue qbe_shift_shr(JSContext *ctx, JSValue a, JSValue b);
// String operations
JSValue JS_ConcatString(JSContext *ctx, JSValue a, JSValue b);
int js_string_compare_value(JSContext *ctx, JSValue a, JSValue b, int eq_only);
JSValue JS_NewString(JSContext *ctx, const char *str);
JSValue __JS_NewFloat64(JSContext *ctx, double d);
int JS_ToBool(JSContext *ctx, JSValue v);
// String/number type tests (inline-able but currently calls)
int JS_IsText(JSValue v);
int JS_IsNumber(JSValue v);
// Tolerant equality (== on mixed types)
JSValue cell_rt_eq_tol(JSContext *ctx, JSValue a, JSValue b);
JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b);
// Text ordering comparisons
JSValue cell_rt_lt_text(JSContext *ctx, JSValue a, JSValue b);
JSValue cell_rt_le_text(JSContext *ctx, JSValue a, JSValue b);
JSValue cell_rt_gt_text(JSContext *ctx, JSValue a, JSValue b);
JSValue cell_rt_ge_text(JSContext *ctx, JSValue a, JSValue b);
```
## What Exists vs What Needs Writing
### Already exists (in qbe_helpers.c)
All `qbe_float_*`, `qbe_bnot`, `qbe_bitwise_*`, `qbe_shift_*`, `qbe_to_bool` — these are implemented and working.
### Already exists (in runtime.c / quickjs.c) but not yet wrapped
The underlying operations exist but aren't exposed with the `cell_rt_` names:
| Runtime function | Underlying implementation |
|---|---|
| `cell_rt_load_field` | `JS_GetPropertyStr(ctx, obj, name)` |
| `cell_rt_load_index` | `JS_GetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(idx))` |
| `cell_rt_load_dynamic` | `JS_GetProperty(ctx, obj, key)` |
| `cell_rt_store_field` | `JS_SetPropertyStr(ctx, obj, name, val)` |
| `cell_rt_store_index` | `JS_SetPropertyNumber(ctx, obj, JS_VALUE_GET_INT(idx), val)` |
| `cell_rt_store_dynamic` | `JS_SetProperty(ctx, obj, key, val)` |
| `cell_rt_delete` | `JS_DeleteProperty(ctx, obj, key)` |
| `cell_rt_push` | `JS_ArrayPush(ctx, &arr, val)` |
| `cell_rt_pop` | `JS_ArrayPop(ctx, arr)` |
| `cell_rt_typeof` | type tag switch → `JS_NewString` |
| `cell_rt_disrupt` | `JS_Throw(ctx, ...)` |
| `cell_rt_eq_tol` / `cell_rt_ne_tol` | comparison logic in mcode.c `eq_tol`/`ne_tol` handler |
| `cell_rt_lt_text` etc. | `js_string_compare_value` + wrap result |
### Needs new code
| Runtime function | What's needed |
|---|---|
| `cell_rt_get_intrinsic` | Look up intrinsic by name string, return JSValue function. Currently scattered across `js_cell_intrinsic_get` and the mcode handler. Needs a clean single entry point. |
| `cell_rt_frame` | Allocate `JSFrameRegister`, set function slot, set argc. Exists in mcode.c `frame` handler but not as a callable function. |
| `cell_rt_setarg` | Write to frame slot. Trivial: `frame->slots[idx + 1] = val` (slot 0 is `this`). |
| `cell_rt_invoke` | Call the function in the frame. Needs to dispatch: native C function vs mach bytecode vs mcode. This is the critical piece — it must handle all function types. |
| `cell_rt_goframe` / `cell_rt_goinvoke` | Tail call variants. Similar to frame/invoke but reuse caller frame. |
| `cell_rt_make_function` | Create function object from index. Needs a function table (populated by the native loader). |
| `cell_rt_get_closure` / `cell_rt_put_closure` | Walk frame chain. Exists inline in mcode.c `get`/`put` handlers. |
## Recommended C File Organization
```
source/
cell_runtime.c — NEW: all cell_rt_* functions (the native code API)
qbe_helpers.c — existing: float/bitwise/shift helpers for inline QBE
runtime.c — existing: JS_GetProperty, JS_SetProperty, etc.
quickjs.c — existing: core VM, GC, value representation
mcode.c — existing: mcode interpreter (can delegate to cell_runtime.c)
```
**`cell_runtime.c`** is the single file that defines the native code contract. It should:
1. Include `quickjs-internal.h` for access to value representation and heap types
2. Export all `cell_rt_*` functions with C linkage (no `static`)
3. Keep each function thin — delegate to existing `JS_*` functions where possible
4. Handle GC safety: after any allocation (frame, string, array), callers' frames may have moved
### Implementation Priority
**Phase 1** — Get "hello world" running natively:
- `cell_rt_get_intrinsic` (to find `print` and `text`)
- `cell_rt_frame`, `cell_rt_setarg`, `cell_rt_invoke` (to call them)
- A loader that takes QBE output → assembles → links → calls `cell_main`
**Phase 2** — Variables and arithmetic:
- All property access (`load_field`, `load_index`, `store_*`, `load_dynamic`)
- `cell_rt_make_function`, `cell_rt_get_closure`, `cell_rt_put_closure`
**Phase 3** — Full language:
- `cell_rt_push`, `cell_rt_pop`, `cell_rt_delete`, `cell_rt_typeof`
- `cell_rt_disrupt`
- `cell_rt_goframe`, `cell_rt_goinvoke`
- Text comparison wrappers (`cell_rt_lt_text`, etc.)
- Tolerant equality (`cell_rt_eq_tol`, `cell_rt_ne_tol`)
## Calling Convention
All `cell_rt_*` functions follow the same pattern:
- First argument is always `JSContext *ctx`
- Values are passed/returned as `JSValue` (64-bit, by value)
- Frame pointers are `JSValue` (tagged pointer to `JSFrameRegister`)
- String names are `const char *` (pointer to data section label)
- Integer constants (slot indices, arg counts) are `int` / `long`
Native code maintains `%ctx` (JSContext) and `%fp` (current frame pointer) as persistent values across the function body. All slot reads/writes go through `%fp` + offset.
## What Should NOT Be in the C Runtime
These are handled entirely by QBE-generated code:
- **Integer arithmetic and comparisons** — bit operations on NaN-boxed values
- **Control flow** — branches, loops, labels, jumps
- **Boolean logic** — `and`/`or`/`not` on tagged values
- **Constant loading** — integer constants are immediate, strings are data labels
- **Type guard branches** — the `is_int`/`is_text`/`is_null` checks are inline bit tests; the branch to the float or text path is just a QBE `jnz`
The `qbe.cm` macros already handle all of this. The arithmetic path looks like:
```
check both ints? → yes → inline int add → done
→ no → call qbe_float_add (or JS_ConcatString for text)
```
The C runtime is only called on the slow paths (float, text, dynamic dispatch). The fast path (integer arithmetic, comparisons, branching) is fully native.

View File

@@ -154,3 +154,12 @@ struct JSCodeRegister {
```
The constant pool holds all non-immediate values referenced by `LOADK` instructions: strings, large numbers, and other constants.
### Constant Pool Index Overflow
Named property instructions (`LOAD_FIELD`, `STORE_FIELD`, `DELETE`) use the iABC format where the constant pool key index occupies an 8-bit field (max 255). When a function references more than 256 unique property names, the serializer automatically falls back to a two-instruction sequence:
1. `LOADK tmp, key_index` — load the key string into a temporary register (iABx, 16-bit index)
2. `LOAD_DYNAMIC` / `STORE_DYNAMIC` / `DELETEINDEX` — use the register-based variant
This is transparent to the mcode compiler and streamline optimizer.

View File

@@ -10,12 +10,44 @@ Mcode is a JSON-based intermediate representation that can be interpreted direct
## Pipeline
```
Source → Tokenize → Parse (AST) → Mcode (JSON) → Interpret
→ Compile to Mach (planned)
→ Compile to native (planned)
Source → Tokenize → Parse (AST) → Fold → Mcode (JSON) → Streamline → Mach VM (default)
→ Mcode Interpreter
→ QBE → Native
```
Mcode is produced by the `JS_Mcode` compiler pass, which emits a cJSON tree. The mcode interpreter walks this tree directly, dispatching on instruction name strings.
Mcode is produced by `mcode.cm`, which lowers the folded AST to JSON instruction arrays. The streamline optimizer (`streamline.cm`) then eliminates redundant operations. The result is serialized to binary bytecode by the Mach compiler (`mach.c`), interpreted directly by `mcode.c`, or lowered to QBE IL by `qbe_emit.cm` for native compilation. See [Compilation Pipeline](pipeline.md) for the full overview.
### Function Proxy Decomposition
When the compiler encounters a method call `obj.method(args)`, it emits a branching pattern to handle ƿit's function proxy protocol. An arity-2 function used as a proxy target receives the method name and argument array instead of a normal method call:
```json
["is_proxy", check, obj]
["jump_false", check, "record_path"]
// Proxy path: call obj(name, [args...]) with this=null
["access", name_slot, "method"]
["array", args_arr, N, arg0, arg1, ...]
["null", null_slot]
["frame", f, obj, 2]
["setarg", f, 0, null_slot]
["setarg", f, 1, name_slot]
["setarg", f, 2, args_arr]
["invoke", f, dest]
["jump", "done"]
["LABEL", "record_path"]
["load_field", method, obj, "method"]
["frame", f2, method, N]
["setarg", f2, 0, obj]
["setarg", f2, 1, arg0]
...
["invoke", f2, dest]
["LABEL", "done"]
```
The streamline optimizer can eliminate the dead branch when the type of `obj` is statically known.
## JSMCode Structure
@@ -44,16 +76,37 @@ struct JSMCode {
## Instruction Format
Each instruction is a JSON array. The first element is the instruction name (string), followed by operands:
Each instruction is a JSON array. The first element is the instruction name (string), followed by operands (typically `[op, dest, ...args, line, col]`):
```json
["LOADK", 0, 42]
["ADD", 2, 0, 1]
["JMPFALSE", 3, "else_label"]
["CALL", 0, 2, 1]
["access", 3, 5, 1, 9]
["load_index", 10, 4, 9, 5, 11]
["store_dynamic", 4, 11, 12, 6, 3]
["frame", 15, 14, 1, 7, 7]
["setarg", 15, 0, 16, 7, 7]
["invoke", 15, 13, 7, 7]
```
The instruction set mirrors the Mach VM opcodes — same operations, same register semantics, but with string dispatch instead of numeric opcodes.
### Typed Load/Store
Memory operations come in typed variants for optimization:
- `load_index dest, obj, idx` — array element by integer index
- `load_field dest, obj, key` — record property by string key
- `load_dynamic dest, obj, key` — unknown; dispatches at runtime
- `store_index obj, val, idx` — array element store
- `store_field obj, val, key` — record property store
- `store_dynamic obj, val, key` — unknown; dispatches at runtime
The compiler selects the appropriate variant based on `type_tag` and `access_kind` annotations from parse and fold.
### Decomposed Calls
Function calls are split into separate instructions:
- `frame dest, fn, argc` — allocate call frame
- `setarg frame, idx, val` — set argument
- `invoke frame, result` — execute the call
## Labels

124
docs/spec/pipeline.md Normal file
View File

@@ -0,0 +1,124 @@
---
title: "Compilation Pipeline"
description: "Overview of the compilation stages and optimizations"
---
## Overview
The compilation pipeline transforms source code through several stages, each adding information or lowering the representation toward execution. All backends share the same path through mcode and streamline. There are three execution backends: the Mach register VM (default), the Mcode interpreter (debug), and native code via QBE (experimental).
```
Source → Tokenize → Parse → Fold → Mcode → Streamline → Mach VM (default)
→ Mcode Interpreter
→ QBE → Native
```
## Stages
### Tokenize (`tokenize.cm`)
Splits source text into tokens. Handles string interpolation by re-tokenizing template literal contents. Produces a token array with position information (line, column).
### Parse (`parse.cm`)
Converts tokens into an AST. Also performs semantic analysis:
- **Scope records**: For each scope (global, function), builds a record mapping variable names to their metadata: `make` (var/def/function/input), `function_nr`, `nr_uses`, `closure` flag, and `level`.
- **Type tags**: When the right-hand side of a `def` is a syntactically obvious type, stamps `type_tag` on the scope record entry. Derivable types: `"integer"`, `"number"`, `"text"`, `"array"`, `"record"`, `"function"`, `"logical"`, `"null"`.
- **Intrinsic resolution**: Names used but not locally bound are recorded in `ast.intrinsics`. Name nodes referencing intrinsics get `intrinsic: true`.
- **Access kind**: Subscript (`[`) nodes get `access_kind`: `"index"` for numeric subscripts, `"field"` for string subscripts, omitted otherwise.
- **Tail position**: Return statements where the expression is a call get `tail: true`.
### Fold (`fold.cm`)
Operates on the AST. Performs constant folding and type analysis:
- **Constant folding**: Evaluates arithmetic on known constants at compile time (e.g., `5 + 10` becomes `15`).
- **Constant propagation**: Tracks `def` bindings whose values are known constants.
- **Type propagation**: Extends `type_tag` through operations. When both operands of an arithmetic op have known types, the result type is known. Propagates type tags to reference sites.
- **Intrinsic specialization**: When an intrinsic call's argument types are known, stamps a `hint` on the call node. For example, `length(x)` where x is a known array gets `hint: "array_length"`. Type checks like `is_array(known_array)` are folded to `true`.
- **Purity marking**: Stamps `pure: true` on expressions with no side effects (literals, name references, arithmetic on pure operands).
- **Dead code elimination**: Removes unreachable branches when conditions are known constants.
### Mcode (`mcode.cm`)
Lowers the AST to a JSON-based intermediate representation with explicit operations. Key design principle: **every type check is an explicit instruction** so downstream optimizers can see and eliminate them.
- **Typed load/store**: Emits `load_index` (array by integer), `load_field` (record by string), or `load_dynamic` (unknown) based on type information from fold.
- **Decomposed calls**: Function calls are split into `frame` (create call frame) + `setarg` (set arguments) + `invoke` (execute call).
- **Intrinsic access**: Intrinsic functions are loaded via `access` with an intrinsic marker rather than global lookup.
See [Mcode IR](mcode.md) for instruction format details.
### Streamline (`streamline.cm`)
Optimizes the Mcode IR. Operates per-function:
- **Redundant instruction elimination**: Removes no-op patterns and redundant moves.
- **Dead code removal**: Eliminates instructions whose results are never used.
- **Type-based narrowing**: When type information is available, narrows `load_dynamic`/`store_dynamic` to typed variants.
### QBE Emit (`qbe_emit.cm`)
Lowers optimized Mcode IR to QBE intermediate language for native code compilation. Each Mcode function becomes a QBE function that calls into the cell runtime (`cell_rt_*` functions) for operations that require the runtime (allocation, intrinsic dispatch, etc.).
String constants are interned in a data section. Integer constants are NaN-boxed inline.
### QBE Macros (`qbe.cm`)
Provides operation implementations as QBE IL templates. Each arithmetic, comparison, and type operation is defined as a function that emits the corresponding QBE instructions, handling type dispatch (integer, float, text paths) with proper guard checks.
## Execution Backends
### Mach VM (default)
Binary 32-bit register VM. The Mach serializer (`mach.c`) converts streamlined mcode JSON into compact 32-bit bytecode with a constant pool. Used for production execution and bootstrapping.
```
./cell script.ce
```
Debug the mach bytecode output:
```
./cell --core . --dump-mach script.ce
```
### Mcode Interpreter
JSON-based interpreter. Used for debugging the compilation pipeline.
```
./cell --mcode script.ce
```
### QBE Native (experimental)
Generates QBE IL that can be compiled to native code.
```
./cell --emit-qbe script.ce > output.ssa
```
## Files
| File | Role |
|------|------|
| `tokenize.cm` | Lexer |
| `parse.cm` | Parser + semantic analysis |
| `fold.cm` | Constant folding + type analysis |
| `mcode.cm` | AST → Mcode IR lowering |
| `streamline.cm` | Mcode IR optimizer |
| `qbe_emit.cm` | Mcode IR → QBE IL emitter |
| `qbe.cm` | QBE IL operation templates |
| `internal/bootstrap.cm` | Pipeline orchestrator |
## Test Files
| File | Tests |
|------|-------|
| `parse_test.ce` | Type tags, access_kind, intrinsic resolution |
| `fold_test.ce` | Type propagation, purity, intrinsic hints |
| `mcode_test.ce` | Typed load/store, decomposed calls |
| `streamline_test.ce` | Optimization counts, IR before/after |
| `qbe_test.ce` | End-to-end QBE IL generation |

20
dump_mcode.cm Normal file
View File

@@ -0,0 +1,20 @@
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
var name = args[0]
var src = text(fd.slurp(name))
var tok = tokenize(src, name)
var ast = parse(tok.tokens, src, name, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
var optimized = streamline(compiled)
var out = json.encode(optimized)
var f = fd.open("/tmp/mcode_dump.json", "w")
fd.write(f, out)
fd.close(f)
print("wrote /tmp/mcode_dump.json")

34
fd.c
View File

@@ -504,7 +504,7 @@ JSC_SCALL(fd_readdir,
ret = JS_NewArray(js);
do {
if (strcmp(ffd.cFileName, ".") == 0 || strcmp(ffd.cFileName, "..") == 0) continue;
JS_ArrayPush(js, ret,JS_NewString(js, ffd.cFileName));
JS_ArrayPush(js, &ret, JS_NewString(js, ffd.cFileName));
} while (FindNextFile(hFind, &ffd) != 0);
FindClose(hFind);
}
@@ -516,7 +516,7 @@ JSC_SCALL(fd_readdir,
ret = JS_NewArray(js);
while ((dir = readdir(d)) != NULL) {
if (strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) continue;
JS_ArrayPush(js, ret, JS_NewString(js, dir->d_name));
JS_ArrayPush(js, &ret, JS_NewString(js, dir->d_name));
}
closedir(d);
} else {
@@ -565,18 +565,22 @@ JSC_CCALL(fd_slurpwrite,
if (!str) return JS_EXCEPTION;
int fd = open(str, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
ret = JS_ThrowInternalError(js, "open failed for %s: %s", str, strerror(errno));
JS_FreeCString(js, str);
return JS_ThrowInternalError(js, "open failed for %s: %s", str, strerror(errno));
return ret;
}
ssize_t written = write(fd, data, len);
close(fd);
if (written != (ssize_t)len) {
ret = JS_ThrowInternalError(js, "write failed for %s: %s", str, strerror(errno));
JS_FreeCString(js, str);
return ret;
}
JS_FreeCString(js, str);
if (written != (ssize_t)len)
return JS_ThrowInternalError(js, "write failed for %s: %s", str, strerror(errno));
return JS_NULL;
)
@@ -598,7 +602,7 @@ static void visit_directory(JSContext *js, JSValue results, int *result_count, c
} else {
strcpy(item_rel, ffd.cFileName);
}
JS_SetPropertyUint32(js, results, (*result_count)++, JS_NewString(js, item_rel));
JS_SetPropertyNumber(js, results, (*result_count)++, JS_NewString(js, item_rel));
if (recurse) {
struct stat st;
@@ -623,7 +627,7 @@ static void visit_directory(JSContext *js, JSValue results, int *result_count, c
} else {
strcpy(item_rel, dir->d_name);
}
JS_SetPropertyUint32(js, results, (*result_count)++, JS_NewString(js, item_rel));
JS_SetPropertyNumber(js, results, (*result_count)++, JS_NewString(js, item_rel));
if (recurse) {
struct stat st;
@@ -664,17 +668,21 @@ JSC_CCALL(fd_realpath,
#ifdef _WIN32
char resolved[PATH_MAX];
DWORD len = GetFullPathNameA(path, PATH_MAX, resolved, NULL);
JS_FreeCString(js, path);
if (len == 0 || len >= PATH_MAX) {
return JS_ThrowInternalError(js, "realpath failed for %s: %s", path, strerror(errno));
JSValue err = JS_ThrowInternalError(js, "realpath failed for %s: %s", path, strerror(errno));
JS_FreeCString(js, path);
return err;
}
JS_FreeCString(js, path);
return JS_NewString(js, resolved);
#else
char *resolved = realpath(path, NULL);
JS_FreeCString(js, path);
if (!resolved) {
return JS_ThrowInternalError(js, "realpath failed for %s: %s", path, strerror(errno));
JSValue err = JS_ThrowInternalError(js, "realpath failed for %s: %s", path, strerror(errno));
JS_FreeCString(js, path);
return err;
}
JS_FreeCString(js, path);
JSValue result = JS_NewString(js, resolved);
free(resolved);
return result;

19
fd.cm
View File

@@ -1,4 +1,4 @@
var fd = this
var fd = native
var wildstar = use('wildstar')
function last_pos(str, sep) {
@@ -12,11 +12,11 @@ function last_pos(str, sep) {
// Helper to join paths
function join_paths(base, rel) {
base = replace(base, /\/+$/, "")
rel = replace(rel, /^\/+/, "")
if (!base) return rel
if (!rel) return base
return base + "/" + rel
var b = replace(base, /\/+$/, "")
var r = replace(rel, /^\/+/, "")
if (!b) return r
if (!r) return b
return b + "/" + r
}
fd.join_paths = join_paths
@@ -39,7 +39,8 @@ fd.stem = function stem(path) {
}
fd.globfs = function(globs, dir) {
if (dir == null) dir = "."
var _dir = dir
if (_dir == null) _dir = "."
var results = []
function check_neg(path) {
@@ -88,9 +89,9 @@ fd.globfs = function(globs, dir) {
});
}
var st = fd.stat(dir)
var st = fd.stat(_dir)
if (st && st.isDirectory) {
visit(dir, "")
visit(_dir, "")
}
return results

View File

@@ -324,7 +324,7 @@ static void listfiles_cb(const char *path, void *userdata) {
// Playdate listfiles returns just the name, but sometimes with slash for dir?
// Docs say "names of files".
JS_SetPropertyUint32(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path));
JS_SetPropertyNumber(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path));
}
JSC_SCALL(fd_readdir,
@@ -427,7 +427,7 @@ static void enum_cb(const char *name, void *userdata) {
strcpy(item_rel, name);
}
JS_SetPropertyUint32(ctx->js, ctx->results, (*ctx->count)++, JS_NewString(ctx->js, item_rel));
JS_SetPropertyNumber(ctx->js, ctx->results, (*ctx->count)++, JS_NewString(ctx->js, item_rel));
if (ctx->recurse) {
// Check if directory

47
fold.cm
View File

@@ -158,6 +158,7 @@ var fold = function(ast) {
var name = null
var sv = null
var item = null
var rhs_target = null
while (i < length(stmts)) {
stmt = stmts[i]
kind = stmt.kind
@@ -169,6 +170,19 @@ var fold = function(ast) {
register_const(fn_nr, name, stmt.right)
}
}
if (name != null && stmt.right != null && stmt.right.kind == "(") {
rhs_target = stmt.right.expression
if (rhs_target != null && rhs_target.intrinsic == true) {
sv = scope_var(fn_nr, name)
if (sv != null && sv.type_tag == null) {
if (rhs_target.name == "array") sv.type_tag = "array"
else if (rhs_target.name == "record") sv.type_tag = "record"
else if (rhs_target.name == "text") sv.type_tag = "text"
else if (rhs_target.name == "number") sv.type_tag = "number"
else if (rhs_target.name == "blob") sv.type_tag = "blob"
}
}
}
} else if (kind == "function") {
name = stmt.name
if (name != null && stmt.arity != null) {
@@ -320,6 +334,8 @@ var fold = function(ast) {
var ar = null
var akey = null
var tv = null
var att = null
var arg = null
// Recurse into children first (bottom-up)
if (k == "+" || k == "-" || k == "*" || k == "/" || k == "%" ||
@@ -385,6 +401,10 @@ var fold = function(ast) {
return copy_loc(expr, {kind: lit.kind, value: lit.value, number: lit.number})
}
}
sv = scope_var(fn_nr, expr.name)
if (sv != null && sv.type_tag != null) {
expr.type_tag = sv.type_tag
}
return expr
}
@@ -497,7 +517,7 @@ var fold = function(ast) {
return expr
}
// Call: stamp arity
// Call: stamp arity and fold intrinsic type checks
if (k == "(") {
target = expr.expression
if (target != null && target.kind == "name" && target.level == 0) {
@@ -506,6 +526,30 @@ var fold = function(ast) {
if (fn_arities[akey] != null) ar = fn_arities[akey][target.name]
if (ar != null) expr.arity = ar
}
if (target != null && target.intrinsic == true && length(expr.list) == 1) {
arg = expr.list[0]
att = null
if (arg.type_tag != null) {
att = arg.type_tag
} else if (arg.kind == "name" && arg.level == 0) {
sv = scope_var(fn_nr, arg.name)
if (sv != null) att = sv.type_tag
}
if (att != null) {
if (target.name == "is_array") return make_bool(att == "array", expr)
if (target.name == "is_text") return make_bool(att == "text", expr)
if (target.name == "is_number") return make_bool(att == "number" || att == "integer", expr)
if (target.name == "is_integer") return make_bool(att == "integer", expr)
if (target.name == "is_function") return make_bool(att == "function", expr)
if (target.name == "is_logical") return make_bool(att == "logical", expr)
if (target.name == "is_null") return make_bool(att == "null", expr)
if (target.name == "is_object") return make_bool(att == "record", expr)
if (target.name == "length") {
if (att == "array") expr.hint = "array_length"
else if (att == "text") expr.hint = "text_length"
}
}
}
return expr
}
@@ -525,6 +569,7 @@ var fold = function(ast) {
if (k == "var" || k == "def") {
stmt.right = fold_expr(stmt.right, fn_nr)
if (is_pure(stmt.right)) stmt.pure = true
return stmt
}
if (k == "var_list") {

BIN
fold.mach

Binary file not shown.

BIN
fold_new.mach Normal file

Binary file not shown.

View File

@@ -1,4 +1,6 @@
// Hidden vars (os, args, core_path, use_mcode) come from env
// Hidden vars come from env:
// CLI mode (cell_init): os, args, core_path, shop_path, emit_qbe, dump_mach
// Actor spawn (script_startup): os, json, nota, wota, actorsym, init, core_path, shop_path
// args[0] = script name, args[1..] = user args
var load_internal = os.load_internal
function use_embed(name) {
@@ -22,28 +24,69 @@ function use_basic(path) {
return result
}
// Load a module from .mach bytecode, falling back to .ast.json
// Load a module from .mach bytecode (bootstrap modules have no source fallback)
function boot_load(name, env) {
var mach_path = name + ".mach"
var mach_path = core_path + '/' + name + ".mach"
var data = null
if (fd.is_file(mach_path)) {
data = fd.slurp(mach_path)
return mach_load(data, env)
}
data = text(fd.slurp(name + ".ast.json"))
return mach_eval_ast(name, data, env)
print("error: missing bootstrap bytecode: " + mach_path + "\n")
disrupt
}
var boot_env = {use: use_basic}
var tokenize_mod = boot_load("tokenize", boot_env)
var parse_mod = boot_load("parse", boot_env)
var fold_mod = boot_load("fold", boot_env)
use_cache['tokenize'] = tokenize_mod
use_cache['parse'] = parse_mod
use_cache['fold'] = fold_mod
// Optionally load mcode compiler module
var mcode_mod = null
if (use_mcode) {
mcode_mod = boot_load("mcode", boot_env)
// Always load mcode compiler module
var mcode_mod = boot_load("mcode", boot_env)
use_cache['mcode'] = mcode_mod
var streamline_mod = null
var qbe_emit_mod = null
// Warn if any .cm source is newer than its .mach bytecode
function check_mach_stale() {
var pairs = [
["tokenize.cm", "tokenize.mach"],
["parse.cm", "parse.mach"],
["fold.cm", "fold.mach"],
["mcode.cm", "mcode.mach"],
["streamline.cm", "streamline.mach"],
["qbe.cm", "qbe.mach"],
["qbe_emit.cm", "qbe_emit.mach"],
["internal/bootstrap.cm", "internal/bootstrap.mach"],
["internal/engine.cm", "internal/engine.mach"]
]
var stale = []
var _i = 0
var cm_path = null
var mach_path = null
var cm_stat = null
var mach_stat = null
while (_i < length(pairs)) {
cm_path = core_path + '/' + pairs[_i][0]
mach_path = core_path + '/' + pairs[_i][1]
if (fd.is_file(cm_path) && fd.is_file(mach_path)) {
cm_stat = fd.stat(cm_path)
mach_stat = fd.stat(mach_path)
if (cm_stat.mtime > mach_stat.mtime) {
push(stale, pairs[_i][0])
}
}
_i = _i + 1
}
if (length(stale) > 0) {
print("warning: bytecode is stale for: " + text(stale, ", ") + "\n")
print("run 'make regen' or './cell --core . regen.cm' to update\n")
}
}
check_mach_stale()
// analyze: tokenize + parse, check for errors
function analyze(src, filename) {
@@ -80,33 +123,86 @@ function analyze(src, filename) {
return ast
}
// Run AST through either mcode or mach pipeline
function run_ast(name, ast, env) {
// Load a module from .mach bytecode, falling back to source compilation
function load_module(name, env) {
var mach_path = core_path + '/' + name + ".mach"
var data = null
var src_path = null
var src = null
var ast = null
var compiled = null
if (use_mcode) {
compiled = mcode_mod(ast)
return mcode_run(name, json.encode(compiled), env)
var optimized = null
if (fd.is_file(mach_path)) {
data = fd.slurp(mach_path)
return mach_load(data, env)
}
return mach_eval_ast(name, json.encode(ast), env)
src_path = core_path + '/' + name + ".cm"
src = text(fd.slurp(src_path))
ast = analyze(src, src_path)
compiled = mcode_mod(ast)
optimized = streamline_mod(compiled)
return mach_eval_mcode(name, json.encode(optimized), env)
}
// Load optimization pipeline modules (needs analyze to be defined)
var qbe_macros = null
streamline_mod = load_module("streamline", boot_env)
use_cache['streamline'] = streamline_mod
if (emit_qbe) {
qbe_macros = load_module("qbe", boot_env)
qbe_emit_mod = load_module("qbe_emit", boot_env)
use_cache['qbe'] = qbe_macros
use_cache['qbe_emit'] = qbe_emit_mod
}
// Run AST through mcode pipeline → register VM
function run_ast(name, ast, env) {
var compiled = mcode_mod(ast)
var optimized = streamline_mod(compiled)
var qbe_il = null
if (emit_qbe) {
qbe_il = qbe_emit_mod(optimized, qbe_macros)
print(qbe_il)
return null
}
if (dump_mach) {
mach_dump_mcode(name, json.encode(optimized), env)
return null
}
return mach_eval_mcode(name, json.encode(optimized), env)
}
// use() with ƿit pipeline for .cm modules
function use(path) {
var file_path = path + '.cm'
function use_fn(path) {
var file_path = null
var mach_path = null
var data = null
var script = null
var ast = null
var result = null
if (use_cache[path])
return use_cache[path]
// Check CWD first, then core_path
// Try .mach bytecode first (CWD then core_path)
mach_path = path + '.mach'
if (!fd.is_file(mach_path))
mach_path = core_path + '/' + path + '.mach'
if (fd.is_file(mach_path)) {
data = fd.slurp(mach_path)
result = mach_load(data, {use: use_fn})
use_cache[path] = result
return result
}
// Try .cm source (CWD then core_path)
file_path = path + '.cm'
if (!fd.is_file(file_path))
file_path = core_path + '/' + path + '.cm'
if (fd.is_file(file_path)) {
script = text(fd.slurp(file_path))
ast = analyze(script, file_path)
result = run_ast(path, ast, {use: use})
result = run_ast(path, ast, {use: use_fn})
use_cache[path] = result
return result
}
@@ -117,21 +213,71 @@ function use(path) {
return result
}
// Load and run the user's program
var program = args[0]
var script_file = program
// Add .ce extension if not already present
if (!ends_with(script_file, '.ce') && !ends_with(script_file, '.cm'))
script_file = program + '.ce'
var user_args = []
var _j = 1
while (_j < length(args)) {
push(user_args, args[_j])
_j = _j + 1
// Helper to load engine.cm and run it with given env
function load_engine(env) {
var engine_path = core_path + '/internal/engine.mach'
var data = null
var engine_src = null
var engine_ast = null
if (fd.is_file(engine_path)) {
data = fd.slurp(engine_path)
return mach_load(data, env)
}
engine_path = core_path + '/internal/engine.cm'
engine_src = text(fd.slurp(engine_path))
engine_ast = analyze(engine_src, engine_path)
return run_ast('engine', engine_ast, env)
}
var script = text(fd.slurp(script_file))
var ast = analyze(script, script_file)
run_ast(program, ast, {use: use, args: user_args, json: json})
// Detect mode and route
// CLI mode has 'args'; actor spawn mode has 'init'
var program = null
var user_args = []
var _j = 0
var script_file = null
var script = null
var ast = null
if (args != null) {
// CLI mode — parse args
program = args[0]
_j = 1
while (_j < length(args)) {
push(user_args, args[_j])
_j = _j + 1
}
// Resolve script file: try .cm then .ce in CWD then core_path
script_file = program
if (!ends_with(script_file, '.ce') && !ends_with(script_file, '.cm'))
script_file = program + '.cm'
if (!fd.is_file(script_file))
script_file = core_path + '/' + program + '.cm'
if (!fd.is_file(script_file))
script_file = program + '.ce'
if (!fd.is_file(script_file))
script_file = core_path + '/' + program + '.ce'
if (ends_with(script_file, '.ce')) {
// Actor script — delegate to engine
load_engine({
os: os, actorsym: actorsym,
init: {program: program, arg: user_args},
core_path: core_path, shop_path: shop_path, json: json,
analyze: analyze, run_ast_fn: run_ast
})
} else {
// Module script — run directly
script = text(fd.slurp(script_file))
ast = analyze(script, script_file)
run_ast(program, ast, {use: use_fn, args: user_args, json: json})
}
} else {
// Actor spawn mode — load engine.cm with full actor env
load_engine({
os: os, actorsym: actorsym, init: init,
core_path: core_path, shop_path: shop_path, json: json, nota: nota, wota: wota,
analyze: analyze, run_ast_fn: run_ast
})
}

Binary file not shown.

View File

@@ -1,21 +1,18 @@
// Hidden vars (os, actorsym, init, core_path) come from env
// Hidden vars (os, actorsym, init, core_path, shop_path, analyze, run_ast_fn, json) come from env
// In actor spawn mode, also: nota, wota
var ACTORDATA = actorsym
var SYSYM = '__SYSTEM__'
var _cell = {}
var need_stop = false
var dylib_ext
var cases = {
Windows: '.dll',
macOS: '.dylib',
Linux: '.so'
}
print(os.platform())
dylib_ext = cases[os.platform()]
var dylib_ext = cases[os.platform()]
var MOD_EXT = '.cm'
var ACTOR_EXT = '.ce'
@@ -49,52 +46,62 @@ function ends_with(str, suffix) {
return search(str, suffix, -length(suffix)) != null
}
var js = use_embed('js')
var fd = use_embed('fd')
var js = use_embed('js')
// Get the shop path from HOME environment
var home = os.getenv('HOME') || os.getenv('USERPROFILE')
if (!home) {
os.print('Could not determine home directory\n')
os.exit(1)
}
var shop_path = home + '/.cell'
var packages_path = shop_path + '/packages'
var core_path = packages_path + '/core'
if (!fd.is_dir(core_path)) {
os.print('Cell shop not found at ' + shop_path + '. Run "cell install" to set up.\n')
os.exit(1)
}
// core_path and shop_path come from env (bootstrap.cm passes them through)
// shop_path may be null if --core was used without --shop
var packages_path = shop_path ? shop_path + '/packages' : null
var use_cache = {}
use_cache['core/os'] = os
// Extra env properties added as engine initializes (log, runtime fns, etc.)
var core_extras = {}
// Load a core module from the file system
function use_core(path) {
var cache_key = 'core/' + path
var env = null
if (use_cache[cache_key])
return use_cache[cache_key];
return use_cache[cache_key]
var sym = use_embed(replace(path, '/', '_'))
var result = null
var script = null
var ast = null
// Core scripts are in packages/core/
var file_path = core_path + '/' + path + MOD_EXT
// Build env: merge core_extras, include C embed as 'native' if available
env = {use: use_core}
arrfor(array(core_extras), function(k) { env[k] = core_extras[k] })
if (sym) env.native = sym
if (fd.is_file(file_path)) {
var script_blob = fd.slurp(file_path)
var script = text(script_blob)
var mod = `(function setup_module(use){${script}})`
var fn = mach_eval('core:' + path, mod)
var result = call(fn,sym, [use_core])
use_cache[cache_key] = result;
return result;
// Check for pre-compiled .mach file first
var mach_path = core_path + '/' + path + '.mach'
if (fd.is_file(mach_path)) {
result = mach_load(fd.slurp(mach_path), env)
use_cache[cache_key] = result
return result
}
use_cache[cache_key] = sym;
return sym;
// Fall back to source .cm file — compile at runtime
var file_path = core_path + '/' + path + MOD_EXT
if (fd.is_file(file_path)) {
script = text(fd.slurp(file_path))
ast = analyze(script, file_path)
result = run_ast_fn('core:' + path, ast, env)
use_cache[cache_key] = result
return result
}
// Embedded C module only
use_cache[cache_key] = sym
return sym
}
// Load full modules via use_core (extends C embeds with .cm additions, and caches)
fd = use_core('fd')
use_core('js')
var blob = use_core('blob')
function actor() {
@@ -112,22 +119,9 @@ function is_actor(value) {
var ENETSERVICE = 0.1
var REPLYTIMEOUT = 60 // seconds before replies are ignored
function caller_data(depth = 0)
function caller_data(depth)
{
var file = "nofile"
var line = 0
var caller = array(Error().stack, "\n")[1+depth]
if (caller) {
var md = extract(caller, /\((.*)\:/)
var m = md ? md[1] : "SCRIPT"
if (m) file = m
md = extract(caller, /\:(\d*)\)/)
m = md ? md[1] : 0
if (m) line = m
}
return {file,line}
return {file: "nofile", line: 0}
}
function console_rec(line, file, msg) {
@@ -142,9 +136,7 @@ function log(name, args) {
if (name == 'console') {
os.print(console_rec(caller.line, caller.file, msg))
} else if (name == 'error') {
if (msg == null) msg = Error()
if (is_proto(msg, Error))
msg = msg.name + ": " + msg.message + "\n" + msg.stack
if (msg == null) msg = "error"
os.print(console_rec(caller.line, caller.file, msg))
} else if (name == 'system') {
msg = "[SYSTEM] " + msg
@@ -156,6 +148,9 @@ function log(name, args) {
function actor_die(err)
{
var reason = null
var unders = null
if (err && is_function(err.toString)) {
os.print(err.toString())
os.print("\n")
@@ -165,14 +160,14 @@ function actor_die(err)
if (overling) {
if (err) {
// with an err, this is a forceful disrupt
var reason = (is_proto(err, Error)) ? err.stack : err
reason = err
report_to_overling({type:'disrupt', reason})
} else
report_to_overling({type:'stop'})
}
if (underlings) {
var unders = array(underlings)
unders = array(underlings)
arrfor(unders, function(id, index) {
log.console(`calling on ${id} to disrupt too`)
$_.stop(create_actor({id}))
@@ -191,14 +186,15 @@ function actor_die(err)
actor_mod.on_exception(actor_die)
//actor_mod.on_exception(actor_die)
_cell.args = init != null ? init : {}
_cell.id = "newguy"
function create_actor(desc = {id:guid()}) {
function create_actor(desc) {
var _desc = desc == null ? {id:guid()} : desc
var actor = {}
actor[ACTORDATA] = desc
actor[ACTORDATA] = _desc
return actor
}
@@ -208,10 +204,12 @@ $_.self = create_actor()
os.use_cache = use_cache
os.global_shop_path = shop_path
os.$_ = $_
os.analyze = analyze
os.run_ast_fn = run_ast_fn
os.json = json
use_cache['core/json'] = json
var shop = use_core('internal/shop')
var json = use_core('json')
var time = use_core('time')
var pronto = use_core('pronto')
@@ -237,6 +235,9 @@ var runtime_env = {
sequence: sequence
}
// Make runtime functions available to modules loaded via use_core
arrfor(array(runtime_env), function(k) { core_extras[k] = runtime_env[k] })
// Pass to os for shop to access
os.runtime_env = runtime_env
@@ -296,8 +297,8 @@ $_.time_limit = function(requestor, seconds)
callback(val, reason)
}, value)
} disruption {
cancel(Error('requestor failed'))
callback(null, Error('requestor failed'))
cancel('requestor failed')
callback(null, 'requestor failed')
}
do_request()
@@ -345,9 +346,10 @@ REPLYTIMEOUT = config.reply_timeout
}
*/
function guid(bits = 256)
function guid(bits)
{
var guid = blob(bits, os.random)
var _bits = bits == null ? 256 : bits
var guid = blob(_bits, os.random)
stone(guid)
return text(guid,'h')
}
@@ -429,13 +431,16 @@ $_.portal = function(fn, port) {
}
function handle_host(e) {
var queue = null
var data = null
if (e.type == "connect") {
log.system(`connected a new peer: ${e.peer.address}:${e.peer.port}`)
peers[`${e.peer.address}:${e.peer.port}`] = e.peer
var queue = peer_queue.get(e.peer)
queue = peer_queue.get(e.peer)
if (queue) {
arrfor(queue, (msg, index) => e.peer.send(nota.encode(msg)))
log.system(`sent ${msg} out of queue`)
log.system(`sent queue out of queue`)
peer_queue.delete(e.peer)
}
} else if (e.type == "disconnect") {
@@ -445,27 +450,28 @@ function handle_host(e) {
})
log.system('portal got disconnect from ' + e.peer.address + ":" + e.peer.port)
} else if (e.type == "receive") {
var data = nota.decode(e.data)
data = nota.decode(e.data)
if (data.replycc && !data.replycc.address) {
data.replycc[ACTORDATA].address = e.peer.address
data.replycc[ACTORDATA].port = e.peer.port
}
function populate_actor_addresses(obj) {
if (!is_object(obj)) return
if (obj[ACTORDATA] && !obj[ACTORDATA].address) {
obj[ACTORDATA].address = e.peer.address
obj[ACTORDATA].port = e.peer.port
}
arrfor(array(obj), function(key, index) {
if (key in obj)
populate_actor_addresses(obj[key])
})
}
if (data.data) populate_actor_addresses(data.data)
if (data.data) populate_actor_addresses(data.data, e)
turn(data)
}
}
function populate_actor_addresses(obj, e) {
if (!is_object(obj)) return
if (obj[ACTORDATA] && !obj[ACTORDATA].address) {
obj[ACTORDATA].address = e.peer.address
obj[ACTORDATA].port = e.peer.port
}
arrfor(array(obj), function(key, index) {
if (key in obj)
populate_actor_addresses(obj[key], e)
})
}
// takes a callback function, an actor object, and a configuration record for getting information about the status of a connection to the actor. The configuration record is used to request the sort of information that needs to be communicated. This can include latency, bandwidth, activity, congestion, cost, partitions. The callback is given a record containing the requested information.
$_.contact = function(callback, record) {
send(create_actor(record), record, callback)
@@ -514,12 +520,13 @@ $_.unneeded = function unneeded(fn, seconds) {
}
// schedules the invocation of a function after a specified amount of time.
$_.delay = function delay(fn, seconds = 0) {
$_.delay = function delay(fn, seconds) {
var _seconds = seconds == null ? 0 : seconds
function delay_turn() {
fn()
send_messages()
}
var id = actor_mod.delay(delay_turn, seconds)
var id = actor_mod.delay(delay_turn, _seconds)
return function() { actor_mod.removetimer(id) }
}
@@ -544,6 +551,9 @@ function actor_send_immediate(actor, send) {
}
function actor_send(actor, message) {
var wota_blob = null
var peer = null
if (actor[HEADER] && !actor[HEADER].replycc) // attempting to respond to a message but sender is not expecting; silently drop
return
@@ -562,22 +572,21 @@ function actor_send(actor, message) {
if (receive_fn) receive_fn(message.data)
return
}
// message to actor in same flock
if (actor[ACTORDATA].id && actor_mod.mailbox_exist(actor[ACTORDATA].id)) {
var wota_blob = wota.encode(message)
// log.console(`sending wota blob of ${length(wota_blob)/8} bytes`)
wota_blob = wota.encode(message)
actor_mod.mailbox_push(actor[ACTORDATA].id, wota_blob)
return
}
if (actor[ACTORDATA].address) {
if (actor[ACTORDATA].id)
message.target = actor[ACTORDATA].id
else
message.type = "contact"
var peer = peers[actor[ACTORDATA].address + ":" + actor[ACTORDATA].port]
peer = peers[actor[ACTORDATA].address + ":" + actor[ACTORDATA].port]
if (!peer) {
if (!portal) {
log.system(`creating a contactor ...`)
@@ -621,6 +630,11 @@ function send_messages() {
var replies = {}
function send(actor, message, reply) {
var send_msg = null
var target = null
var header = null
var id = null
if (!is_object(actor)) {
log.error(`Must send to an actor object. Provided: ${actor}`)
disrupt
@@ -630,11 +644,11 @@ function send(actor, message, reply) {
log.error('Message must be an object')
disrupt
}
var send_msg = {type:"user", data: message}
var target = actor
send_msg = {type:"user", data: message}
target = actor
if (actor[HEADER] && actor[HEADER].replycc) {
var header = actor[HEADER]
header = actor[HEADER]
if (!header.replycc || !is_actor(header.replycc)) {
log.error(`Supplied actor had a return, but it's not a valid actor! ${actor[HEADER]}`)
disrupt
@@ -645,7 +659,7 @@ function send(actor, message, reply) {
}
if (reply) {
var id = guid()
id = guid()
replies[id] = reply
$_.delay(_ => {
if (replies[id]) {
@@ -730,18 +744,21 @@ function handle_actor_disconnect(id) {
function handle_sysym(msg)
{
var from
var from = null
var greeter = null
var letter2 = null
if (msg.kind == 'stop') {
actor_die("got stop message")
} else if (msg.kind == 'underling') {
from = msg.from
var greeter = greeters[from[ACTORDATA].id]
greeter = greeters[from[ACTORDATA].id]
if (greeter) greeter(msg.message)
if (msg.message.type == 'disrupt')
delete underlings[from[ACTORDATA].id]
} else if (msg.kind == 'contact') {
if (portal_fn) {
var letter2 = msg.data
letter2 = msg.data
letter2[HEADER] = msg
delete msg.data
portal_fn(letter2)
@@ -758,13 +775,16 @@ function handle_sysym(msg)
}
function handle_message(msg) {
var letter = null
var fn = null
if (msg[SYSYM]) {
handle_sysym(msg[SYSYM], msg.from)
return
}
if (msg.type == "user") {
var letter = msg.data // what the sender really sent
letter = msg.data // what the sender really sent
_ObjectDefineProperty(letter, HEADER, {
value: msg, enumerable: false
})
@@ -773,7 +793,7 @@ function handle_message(msg) {
})
if (msg.return) {
var fn = replies[msg.return]
fn = replies[msg.return]
if (fn) fn(letter)
delete replies[msg.return]
return
@@ -798,44 +818,62 @@ function enet_check()
actor_mod.setname(_cell.args.program)
var prog = _cell.args.program
if (ends_with(prog, '.ce')) prog = text(prog, 0, -3)
var package = use_core('package')
var locator = shop.resolve_locator(_cell.args.program + ".ce", null)
if (!locator) {
var pkg = package.find_package_dir(_cell.args.program + ".ce")
locator = shop.resolve_locator(_cell.args.program + ".ce", pkg)
// Find the .ce file
var prog_path = prog + ".ce"
var pkg_dir = null
var core_dir = null
if (!fd.is_file(prog_path)) {
pkg_dir = package.find_package_dir(prog_path)
if (pkg_dir)
prog_path = pkg_dir + '/' + prog + '.ce'
}
if (!locator) {
os.print(`Main program ${_cell.args.program} could not be found\n`)
if (!fd.is_file(prog_path)) {
// Check core packages
core_dir = core_path
prog_path = core_dir + '/' + prog + '.ce'
}
if (!fd.is_file(prog_path)) {
os.print(`Main program ${prog} could not be found\n`)
os.exit(1)
}
$_.clock(_ => {
// 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(prog_path) : null
var inject = shop.script_inject_for ? shop.script_inject_for(file_info) : []
// Build env object for injection
// Build env with runtime functions + capability injections
var env = {}
for (var i = 0; i < length(inject); i++) {
var key = inject[i]
arrfor(array(runtime_env), function(k) { env[k] = runtime_env[k] })
var _ki = 0
var inj = null
var key = null
while (_ki < length(inject)) {
inj = inject[_ki]
key = inj
if (key && key[0] == '$') key = text(key, 1)
if (key == 'fd') env[key] = fd
else env[key] = $_[key]
if (key == 'fd') env['$fd'] = fd
else env['$' + key] = $_[key]
_ki = _ki + 1
}
// Create use function bound to the program's package
var pkg = file_info ? file_info.package : null
var use_fn = function(path) { return shop.use(path, pkg) }
env.use = function(path) {
var ck = 'core/' + path
if (use_cache[ck]) return use_cache[ck]
return shop.use(path, pkg)
}
env.args = _cell.args.arg
env.log = log
// Call with signature: setup_module(args, use, env)
// The script wrapper binds $delay, $start, etc. from env
var val = call(locator.symbol, null, [_cell.args.arg, use_fn, env])
if (val)
var script = text(fd.slurp(prog_path))
var ast = analyze(script, prog_path)
var val = run_ast_fn(prog, ast, env)
if (val) {
log.error('Program must not return anything')
disrupt
}
})

BIN
internal/engine.mach Normal file

Binary file not shown.

View File

@@ -5,7 +5,6 @@ var fd = use('fd')
var http = use('http')
var miniz = use('miniz')
var time = use('time')
var js = use('js')
var crypto = use('crypto')
var blob = use('blob')
@@ -13,6 +12,10 @@ var pkg_tools = use('package')
var os = use('os')
var link = use('link')
var analyze = os.analyze
var run_ast_fn = os.run_ast_fn
var shop_json = os.json
var core = "core"
function pull_from_cache(content)
@@ -32,9 +35,10 @@ function ensure_dir(path) {
if (fd.stat(path).isDirectory) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
current = current + parts[i] + '/'
if (!fd.stat(current).isDirectory) {
fd.mkdir(current)
}
@@ -76,12 +80,12 @@ function get_packages_dir() {
}
// Get the core directory (in the global shop)
var core_package = 'core'
Shop.get_core_dir = function() {
return get_packages_dir() + '/' + core_package
}
var core_package = 'core'
// Get the links file path (in the global shop)
function get_links_path() {
return global_shop_path + '/link.toml'
@@ -116,12 +120,16 @@ function split_explicit_package_import(path)
if (!looks_explicit) return null
// Find the longest prefix that is an installed package
for (var i = length(parts) - 1; i >= 1; i--) {
var pkg_candidate = text(array(parts, 0, i), '/')
var mod_path = text(array(parts, i), '/')
var i = 0
var pkg_candidate = null
var mod_path = null
var candidate_dir = null
for (i = length(parts) - 1; i >= 1; i--) {
pkg_candidate = text(array(parts, 0, i), '/')
mod_path = text(array(parts, i), '/')
if (!mod_path || length(mod_path) == 0) continue
var candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate)
candidate_dir = get_packages_dir() + '/' + safe_package_path(pkg_candidate)
if (fd.is_file(candidate_dir + '/cell.toml'))
return {package: pkg_candidate, path: mod_path}
@@ -142,8 +150,10 @@ function package_in_shop(package) {
function abs_path_to_package(package_dir)
{
if (!fd.is_file(package_dir + '/cell.toml'))
throw Error('Not a valid package directory (no cell.toml): ' + package_dir)
if (!fd.is_file(package_dir + '/cell.toml')) {
print('Not a valid package directory (no cell.toml): ' + package_dir)
disrupt
}
var packages_prefix = get_packages_dir() + '/'
var core_dir = packages_prefix + core_package
@@ -153,8 +163,9 @@ function abs_path_to_package(package_dir)
return 'core'
}
// Also check if core_dir is a symlink pointing to package_dir
var core_target = null
if (fd.is_link(core_dir)) {
var core_target = fd.readlink(core_dir)
core_target = fd.readlink(core_dir)
if (core_target == package_dir || fd.realpath(core_dir) == package_dir) {
return 'core'
}
@@ -175,13 +186,14 @@ function abs_path_to_package(package_dir)
return package_dir
// For local directories (e.g., linked targets), read the package name from cell.toml
try {
var content = text(fd.slurp(package_dir + '/cell.toml'))
var cfg = toml.decode(content)
var _toml_path = package_dir + '/cell.toml'
var content = null
var cfg = null
if (fd.is_file(_toml_path)) {
content = text(fd.slurp(_toml_path))
cfg = toml.decode(content)
if (cfg.package)
return cfg.package
} catch (e) {
// Fall through
}
return null
@@ -299,23 +311,29 @@ Shop.resolve_package_info = function(pkg) {
// Verify if a package name is valid and return status
Shop.verify_package_name = function(pkg) {
if (!pkg) throw Error("Empty package name")
if (pkg == 'local') throw Error("local is not a valid package name")
if (pkg == 'core') throw Error("core is not a valid package name")
if (search(pkg, '://') != null)
throw Error(`Invalid package name: ${pkg}; did you mean ${array(pkg, '://')[1]}?`)
if (!pkg) { print("Empty package name"); disrupt }
if (pkg == 'local') { print("local is not a valid package name"); disrupt }
if (pkg == 'core') { print("core is not a valid package name"); disrupt }
if (search(pkg, '://') != null) {
print(`Invalid package name: ${pkg}; did you mean ${array(pkg, '://')[1]}?`)
disrupt
}
}
// Convert module package to download URL
Shop.get_download_url = function(pkg, commit_hash) {
var info = Shop.resolve_package_info(pkg)
var parts = null
var host = null
var user = null
var repo = null
if (info == 'gitea') {
var parts = array(pkg, '/')
var host = parts[0]
var user = parts[1]
var repo = parts[2]
parts = array(pkg, '/')
host = parts[0]
user = parts[1]
repo = parts[2]
return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip'
}
@@ -326,12 +344,16 @@ Shop.get_download_url = function(pkg, commit_hash) {
// Get the API URL for checking remote git commits
Shop.get_api_url = function(pkg) {
var info = Shop.resolve_package_info(pkg)
var parts = null
var host = null
var user = null
var repo = null
if (info == 'gitea') {
var parts = array(pkg, '/')
var host = parts[0]
var user = parts[1]
var repo = parts[2]
parts = array(pkg, '/')
host = parts[0]
user = parts[1]
repo = parts[2]
return 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/'
}
@@ -378,116 +400,105 @@ Shop.get_script_capabilities = function(path) {
return Shop.script_inject_for(file_info)
}
// Build the env object for a module, with runtime fns and $-prefixed capabilities.
// Matches engine.cm's approach: env properties become free variables in the module.
function inject_env(inject) {
// Start with runtime functions from engine
var env = {}
var rt = my$_.os ? my$_.os.runtime_env : null
if (rt) {
for (var k in rt) {
env[k] = rt[k]
}
arrfor(array(rt), function(k) { env[k] = rt[k] })
}
// Add capability injections
for (var i = 0; i < length(inject); i++) {
var inj = inject[i]
var key = trim(inj, '$')
if (key == 'fd') env[key] = fd
else env[key] = my$_[key]
// Add capability injections with $ prefix
var i = 0
var inj = null
var key = null
for (i = 0; i < length(inject); i++) {
inj = inject[i]
key = inj
if (key && key[0] == '$') key = text(key, 1)
if (key == 'fd') env['$fd'] = fd
else env['$' + key] = my$_[key]
}
return env
}
function inject_bindings_code(inject) {
var lines = []
// Lazy-loaded compiler modules for on-the-fly compilation
var _mcode_mod = null
var _streamline_mod = null
// 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++) {
var inj = inject[i]
var key = trim(inj, '$')
push(lines, `var $${key} = env["${key}"];`)
}
return text(lines, '\n')
}
// Build the use function for a specific package context
function make_use_fn_code(pkg_arg) {
return `function(path) { return globalThis.use(path, ${pkg_arg}); }`
}
// for script forms, path is the canonical path of the module
var script_form = function(path, script, pkg, inject) {
var pkg_arg = pkg ? `'${pkg}'` : 'null'
var binds = inject_bindings_code(inject)
var fn = `(function setup_module(args, use, env){
def arg = args;
def PACKAGE = ${pkg_arg};
${binds}
${script}
})`
return fn
}
// Resolve module function, hashing it in the process
// path is the exact path to the script file
// Compile a module and return its bytecode blob.
// The bytecode is cached on disk by content hash.
function resolve_mod_fn(path, pkg) {
if (!fd.is_file(path)) throw Error(`path ${path} is not a file`)
if (!fd.is_file(path)) { print(`path ${path} is not a file`); disrupt }
var file_info = Shop.file_info(path)
var file_pkg = file_info.package
var inject = Shop.script_inject_for(file_info)
var content = text(fd.slurp(path))
var script = script_form(path, content, file_pkg, inject);
var cached = pull_from_cache(stone(blob(content)))
var ast = null
var compiled = null
var mach_path = null
var mach_blob = null
var ir = null
var optimized = null
var obj = pull_from_cache(stone(blob(script)))
if (obj) {
var fn = js.compile_unblob(obj)
return js.integrate(fn, null)
// Check cache for pre-compiled .mach blob
if (cached) {
return cached
}
// Compile name is just for debug/stack traces
var compile_name = path
// Check for pre-compiled .mach file alongside .cm source
if (ends_with(path, '.cm')) {
mach_path = text(path, 0, length(path) - 3) + '.mach'
if (fd.is_file(mach_path)) {
mach_blob = fd.slurp(mach_path)
put_into_cache(stone(blob(content)), mach_blob)
return mach_blob
}
}
var fn = js.compile(compile_name, script)
// Compile via full pipeline: analyze → mcode → streamline → serialize
if (!_mcode_mod) _mcode_mod = Shop.use("mcode", null)
if (!_streamline_mod) _streamline_mod = Shop.use("streamline", null)
ast = analyze(content, path)
ir = _mcode_mod(ast)
optimized = _streamline_mod(ir)
compiled = mach_compile_mcode_bin(path, shop_json.encode(optimized))
put_into_cache(stone(blob(content)), compiled)
put_into_cache(stone(blob(script)), js.compile_blob(fn))
return js.integrate(fn, null)
return compiled
}
// given a path and a package context
// return module info about where it was found
function resolve_locator(path, ctx)
{
{
var explicit = split_explicit_package_import(path)
var explicit_path = null
var fn = null
var core_dir = null
var core_file_path = null
var is_core = null
var scope = null
var alias_path = null
if (explicit) {
if (is_internal_path(explicit.path) && ctx && explicit.package != ctx)
explicit = null
}
if (explicit) {
var explicit_path = get_packages_dir() + '/' + safe_package_path(explicit.package) + '/' + explicit.path
explicit_path = get_packages_dir() + '/' + safe_package_path(explicit.package) + '/' + explicit.path
if (fd.is_file(explicit_path)) {
var fn = resolve_mod_fn(explicit_path, explicit.package)
fn = resolve_mod_fn(explicit_path, explicit.package)
return {path: explicit_path, scope: SCOPE_PACKAGE, symbol: fn}
}
}
// 1. If no context, resolve from core only
if (!ctx) {
var core_dir = Shop.get_core_dir()
var core_file_path = core_dir + '/' + path
core_dir = Shop.get_core_dir()
core_file_path = core_dir + '/' + path
if (fd.is_file(core_file_path)) {
var fn = resolve_mod_fn(core_file_path, 'core')
fn = resolve_mod_fn(core_file_path, 'core')
return {path: core_file_path, scope: SCOPE_CORE, symbol: fn}
}
return null
@@ -496,7 +507,7 @@ function resolve_locator(path, ctx)
// check in ctx package
// If ctx is an absolute path (starts with /), use it directly
// Otherwise, look it up in the packages directory
var ctx_dir
var ctx_dir = null
if (starts_with(ctx, '/')) {
ctx_dir = ctx
} else {
@@ -505,10 +516,10 @@ function resolve_locator(path, ctx)
var ctx_path = ctx_dir + '/' + path
if (fd.is_file(ctx_path)) {
var fn = resolve_mod_fn(ctx_path, ctx)
fn = resolve_mod_fn(ctx_path, ctx)
// Check if ctx is the core package (either by name or by path)
var is_core = (ctx == 'core') || (ctx_dir == Shop.get_core_dir())
var scope = is_core ? SCOPE_CORE : SCOPE_LOCAL
is_core = (ctx == 'core') || (ctx_dir == Shop.get_core_dir())
scope = is_core ? SCOPE_CORE : SCOPE_LOCAL
return {path: ctx_path, scope: scope, symbol: fn}
}
@@ -518,24 +529,24 @@ function resolve_locator(path, ctx)
// check for aliased dependency
var alias = pkg_tools.split_alias(ctx, path)
if (alias) {
var alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path
alias_path = get_packages_dir() + '/' + safe_package_path(alias.package) + '/' + alias.path
if (fd.is_file(alias_path)) {
var fn = resolve_mod_fn(alias_path, ctx)
fn = resolve_mod_fn(alias_path, ctx)
return {path: alias_path, scope:SCOPE_PACKAGE, symbol:fn}
}
}
var package_path = get_packages_dir() + '/' + safe_package_path(path)
var package_path = get_packages_dir() + '/' + safe_package_path(path)
if (fd.is_file(package_path)) {
var fn = resolve_mod_fn(package_path, ctx)
fn = resolve_mod_fn(package_path, ctx)
return {path: package_path, scope: SCOPE_PACKAGE, symbol: fn}
}
// 4. Check core as fallback
var core_dir = Shop.get_core_dir()
var core_file_path = core_dir + '/' + path
core_dir = Shop.get_core_dir()
core_file_path = core_dir + '/' + path
if (fd.is_file(core_file_path)) {
var fn = resolve_mod_fn(core_file_path, 'core')
fn = resolve_mod_fn(core_file_path, 'core')
return {path: core_file_path, scope: SCOPE_CORE, symbol: fn}
}
@@ -567,7 +578,7 @@ Shop.open_package_dylib = function(pkg) {
var link_target = link.get_target(pkg)
var resolved_pkg = link_target ? link_target : pkg
var pkg_dir;
var pkg_dir = null
if (starts_with(resolved_pkg, '/')) {
pkg_dir = resolved_pkg
} else {
@@ -575,34 +586,23 @@ Shop.open_package_dylib = function(pkg) {
}
var toml_path = pkg_dir + '/cell.toml'
var content = null
var cfg = null
if (fd.is_file(toml_path)) {
try {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
if (cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias, i) {
var dep_pkg = cfg.dependencies[alias]
try {
Shop.open_package_dylib(dep_pkg)
} catch (dep_e) {
// Dependency dylib load failed, continue with others
}
})
}
} catch (e) {
// Error reading toml, continue
content = text(fd.slurp(toml_path))
cfg = toml.decode(content)
if (cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias, i) {
var dep_pkg = cfg.dependencies[alias]
Shop.open_package_dylib(dep_pkg)
})
}
}
var dl_path = get_lib_path(pkg)
if (fd.is_file(dl_path)) {
if (!open_dls[dl_path]) {
try {
open_dls[dl_path] = os.dylib_open(dl_path)
} catch (e) {
dylib_visited[pkg] = false
throw e
}
open_dls[dl_path] = os.dylib_open(dl_path)
}
}
}
@@ -613,12 +613,19 @@ Shop.open_package_dylib = function(pkg) {
// Core is never loaded as a dynamic library via dlopen
function resolve_c_symbol(path, package_context) {
var explicit = split_explicit_package_import(path)
var sym = null
var dl_path = null
var _path = null
var core_sym = null
var canon_pkg = null
var mod_name = null
if (explicit) {
if (is_internal_path(explicit.path) && package_context && explicit.package != package_context)
explicit = null
}
if (explicit) {
var sym = make_c_symbol(explicit.package, explicit.path)
sym = make_c_symbol(explicit.package, explicit.path)
if (os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
@@ -629,7 +636,7 @@ function resolve_c_symbol(path, package_context) {
}
Shop.open_package_dylib(explicit.package)
var dl_path = get_lib_path(explicit.package)
dl_path = get_lib_path(explicit.package)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
@@ -642,8 +649,8 @@ function resolve_c_symbol(path, package_context) {
// If no package context, only check core internal symbols
if (!package_context || package_context == 'core') {
path = replace(path, '/', '_')
var core_sym = `js_${path}_use`
_path = replace(path, '/', '_')
core_sym = `js_${_path}_use`
if (os.internal_exists(core_sym)) {
return {
symbol: function() { return os.load_internal(core_sym) },
@@ -655,7 +662,7 @@ function resolve_c_symbol(path, package_context) {
}
// 1. Check own package first (internal, then dylib)
var sym = make_c_symbol(package_context, path)
sym = make_c_symbol(package_context, path)
if (os.internal_exists(sym)) {
return {
symbol: function() { return os.load_internal(sym) },
@@ -665,7 +672,7 @@ function resolve_c_symbol(path, package_context) {
}
Shop.open_package_dylib(package_context)
var dl_path = get_lib_path(package_context)
dl_path = get_lib_path(package_context)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return {
@@ -681,10 +688,10 @@ function resolve_c_symbol(path, package_context) {
// 2. Check aliased package imports (e.g. 'prosperon/sprite')
var pkg_alias = get_import_package(path)
if (pkg_alias) {
var canon_pkg = get_aliased_package(path, package_context)
canon_pkg = get_aliased_package(path, package_context)
if (canon_pkg) {
var mod_name = get_import_name(path)
var sym = make_c_symbol(canon_pkg, mod_name)
mod_name = get_import_name(path)
sym = make_c_symbol(canon_pkg, mod_name)
// Check internal first
if (os.internal_exists(sym)) {
@@ -698,7 +705,7 @@ function resolve_c_symbol(path, package_context) {
// Then check dylib
Shop.open_package_dylib(canon_pkg)
var dl_path = get_lib_path(canon_pkg)
dl_path = get_lib_path(canon_pkg)
if (open_dls[dl_path] && os.dylib_has_symbol(open_dls[dl_path], sym)) {
return {
symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym) },
@@ -711,7 +718,7 @@ function resolve_c_symbol(path, package_context) {
}
// 3. Check core internal symbols (core is never a dynamic library)
var core_sym = `js_${replace(path, '/', '_')}_use`
core_sym = `js_${replace(path, '/', '_')}_use`
if (os.internal_exists(core_sym)) {
return {
symbol: function() { return os.load_internal(core_sym) },
@@ -739,31 +746,37 @@ function resolve_module_info(path, package_context) {
if (min_scope == 999)
return null
var cache_key
var cache_key = null
var real_path = null
var real_info = null
var pkg_alias = null
var canon_pkg = null
var mod_name = null
if (mod_resolve.scope == SCOPE_CORE) {
cache_key = 'core/' + path
} else if (mod_resolve.scope < 900 && mod_resolve.path) {
var real_path = fd.realpath(mod_resolve.path)
real_path = fd.realpath(mod_resolve.path)
if (real_path) {
var real_info = Shop.file_info(real_path)
real_info = Shop.file_info(real_path)
if (real_info.package && real_info.name)
cache_key = real_info.package + '/' + real_info.name
else
cache_key = real_path
}
}
if (!cache_key) {
if (min_scope == SCOPE_CORE)
cache_key = 'core/' + path
else if (min_scope == SCOPE_LOCAL && package_context)
cache_key = package_context + '/' + path
else if (min_scope == SCOPE_PACKAGE) {
var pkg_alias = get_import_package(path)
pkg_alias = get_import_package(path)
if (pkg_alias) {
var canon_pkg = get_canonical_package(pkg_alias, package_context)
canon_pkg = get_canonical_package(pkg_alias, package_context)
if (canon_pkg) {
var mod_name = get_import_name(path)
mod_name = get_import_name(path)
cache_key = canon_pkg + '/' + mod_name
} else
cache_key = path
@@ -814,54 +827,50 @@ function execute_module(info)
var c_resolve = info.c_resolve
var mod_resolve = info.mod_resolve
var used
var used = null
var file_info = null
var inject = null
var env = null
var pkg = null
if (mod_resolve.scope < 900) {
var context = null
// Build env with runtime fns, capabilities, and use function
file_info = Shop.file_info(mod_resolve.path)
inject = Shop.script_inject_for(file_info)
env = inject_env(inject)
pkg = file_info.package
env.use = make_use_fn(pkg)
// Add C module as native context if available
if (c_resolve.scope < 900) {
context = call_c_module(c_resolve)
env.native = call_c_module(c_resolve)
}
// Get file info to determine inject list
var file_info = Shop.file_info(mod_resolve.path)
var inject = Shop.script_inject_for(file_info)
var env = inject_env(inject)
var pkg = file_info.package
var use_fn = make_use_fn(pkg)
// Call with signature: setup_module(args, use, env)
// args is null for module loading
used = call(mod_resolve.symbol, context, [null, use_fn, env])
// Load compiled bytecode with env
used = mach_load(mod_resolve.symbol, env)
} else if (c_resolve.scope < 900) {
// C only
used = call_c_module(c_resolve)
} else {
throw Error(`Module ${info.path} could not be found`)
print(`Module ${info.path} could not be found`); disrupt
}
// if (is_function(used))
// throw Error('C module loader returned a function; did you forget to call it?')
if (!used) { print(`Module ${info} returned null`); disrupt }
if (!used)
throw Error(`Module ${info} returned null`)
// stone(used)
return used
}
function get_module(path, package_context) {
var info = resolve_module_info(path, package_context)
if (!info)
throw Error(`Module ${path} could not be found in ${package_context}`)
if (!info) { print(`Module ${path} could not be found in ${package_context}`); disrupt }
return execute_module(info)
}
Shop.use = function use(path, package_context) {
var info = resolve_module_info(path, package_context)
if (!info)
throw Error(`Module ${path} could not be found in ${package_context}`)
if (!info) { print(`Module ${path} could not be found in ${package_context}`); disrupt }
if (use_cache[info.cache_key])
return use_cache[info.cache_key]
@@ -889,13 +898,13 @@ function fetch_remote_hash(pkg) {
if (!api_url) return null
try {
var _fetch_hash = function() {
var resp = http.fetch(api_url)
return Shop.extract_commit_hash(pkg, text(resp))
} catch (e) {
log.console("Warning: Could not check for updates for " + pkg)
} disruption {
return null
}
return _fetch_hash()
}
// Download a zip for a package at a specific commit and cache it
@@ -909,14 +918,14 @@ function download_zip(pkg, commit_hash) {
return null
}
try {
var _download = function() {
var zip_blob = http.fetch(download_url)
fd.slurpwrite(cache_path, zip_blob)
return zip_blob
} catch (e) {
log.error("Download failed for " + pkg + ": " + e)
} disruption {
return null
}
return _download()
}
// Get zip from cache, returns null if not cached
@@ -952,17 +961,18 @@ Shop.fetch = function(pkg) {
// Check if we have the zip cached
var zip_blob = get_cached_zip(pkg, commit)
var actual_hash = null
if (zip_blob) {
// If we have a hash on record, verify it
if (expected_hash) {
var actual_hash = text(crypto.blake2(zip_blob), 'h')
actual_hash = text(crypto.blake2(zip_blob), 'h')
if (actual_hash == expected_hash) {
return { status: 'cached' }
}
log.console("Zip hash mismatch for " + pkg + ", re-fetching...")
} else {
// No hash stored yet - compute and store it
var actual_hash = text(crypto.blake2(zip_blob), 'h')
actual_hash = text(crypto.blake2(zip_blob), 'h')
lock_entry.zip_hash = actual_hash
Shop.save_lock(lock)
return { status: 'cached' }
@@ -1014,10 +1024,12 @@ Shop.extract = function(pkg) {
// Check if already extracted at correct commit
var lock = Shop.load_lock()
var lock_entry = lock[pkg]
var extracted_commit_file = null
var extracted_commit = null
if (lock_entry && lock_entry.commit) {
var extracted_commit_file = target_dir + '/.cell_commit'
extracted_commit_file = target_dir + '/.cell_commit'
if (fd.is_file(extracted_commit_file)) {
var extracted_commit = trim(text(fd.slurp(extracted_commit_file)))
extracted_commit = trim(text(fd.slurp(extracted_commit_file)))
if (extracted_commit == lock_entry.commit) {
// Already extracted at this commit, skip
return true
@@ -1028,7 +1040,7 @@ Shop.extract = function(pkg) {
var zip_blob = get_package_zip(pkg)
if (!zip_blob)
throw Error("No zip blob available for " + pkg)
print("No zip blob available for " + pkg); disrupt
// Extract zip for remote package
install_zip(zip_blob, target_dir)
@@ -1069,6 +1081,7 @@ Shop.update = function(pkg) {
log.console(`checking ${pkg}`)
var new_entry = null
if (info == 'local') {
// Check if local path exists
if (!fd.is_dir(pkg)) {
@@ -1076,7 +1089,7 @@ Shop.update = function(pkg) {
return null
}
// Local packages always get a lock entry
var new_entry = {
new_entry = {
type: 'local',
updated: time.number()
}
@@ -1099,7 +1112,7 @@ Shop.update = function(pkg) {
if (local_commit == remote_commit)
return null
var new_entry = {
new_entry = {
type: info,
commit: remote_commit,
updated: time.number()
@@ -1113,7 +1126,7 @@ Shop.update = function(pkg) {
function install_zip(zip_blob, target_dir) {
var zip = miniz.read(zip_blob)
if (!zip) throw Error("Failed to read zip archive")
if (!zip) { print("Failed to read zip archive"); disrupt }
if (fd.is_link(target_dir)) fd.unlink(target_dir)
if (fd.is_dir(target_dir)) fd.rmdir(target_dir, 1)
@@ -1124,21 +1137,28 @@ function install_zip(zip_blob, target_dir) {
var count = zip.count()
var created_dirs = {}
for (var i = 0; i < count; i++) {
var i = 0
var filename = null
var slash_pos = null
var rel_path = null
var full_path = null
var dir_path = null
var file_data = null
for (i = 0; i < count; i++) {
if (zip.is_directory(i)) continue
var filename = zip.get_filename(i)
var slash_pos = search(filename, '/')
filename = zip.get_filename(i)
slash_pos = search(filename, '/')
if (slash_pos == null) continue
if (slash_pos + 1 >= length(filename)) continue
var rel_path = text(filename, slash_pos + 1)
var full_path = target_dir + '/' + rel_path
var dir_path = fd.dirname(full_path)
rel_path = text(filename, slash_pos + 1)
full_path = target_dir + '/' + rel_path
dir_path = fd.dirname(full_path)
if (!created_dirs[dir_path]) {
ensure_dir(dir_path)
created_dirs[dir_path] = true
}
var file_data = zip.slurp(filename)
file_data = zip.slurp(filename)
stone(file_data)
@@ -1161,18 +1181,20 @@ Shop.remove = function(pkg) {
Shop.get = function(pkg) {
var lock = Shop.load_lock()
var info = null
var commit = null
if (!lock[pkg]) {
var info = Shop.resolve_package_info(pkg)
info = Shop.resolve_package_info(pkg)
if (!info) {
throw Error("Invalid package: " + pkg)
print("Invalid package: " + pkg); disrupt
}
var commit = null
commit = null
if (info != 'local') {
commit = fetch_remote_hash(pkg)
if (!commit) {
throw Error("Could not resolve commit for " + pkg)
print("Could not resolve commit for " + pkg); disrupt
}
}
@@ -1188,8 +1210,6 @@ Shop.get = function(pkg) {
// Compile a module
// List all files in a package
var debug = use('debug')
Shop.file_reload = function(file)
{
var info = Shop.file_info(file)
@@ -1228,9 +1248,11 @@ function get_package_scripts(package)
{
var files = pkg_tools.list_files(package)
var scripts = []
for (var i = 0; i < length(files); i++) {
var file = files[i]
var i = 0
var file = null
for (i = 0; i < length(files); i++) {
file = files[i]
if (ends_with(file, '.cm') || ends_with(file, '.ce')) {
push(scripts, file)
}

View File

@@ -4,19 +4,21 @@ var pkg = use('package')
// Check if current directory is a valid cell package
function is_valid_package(dir) {
if (!dir) dir = '.'
return fd.is_file(dir + '/cell.toml')
var _dir = dir == null ? '.' : dir
if (!_dir) _dir = '.'
return fd.is_file(_dir + '/cell.toml')
}
// Get current package name from cell.toml or null
function get_current_package_name() {
if (!is_valid_package('.')) return null
try {
var _load = function() {
var config = pkg.load_config(null)
return config.package || 'local'
} catch (e) {
} disruption {
return 'local'
}
return _load()
}
// Get the directory for a package
@@ -37,9 +39,10 @@ function ensure_dir(path) {
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
current = current + parts[i] + '/'
if (!fd.is_dir(current)) {
fd.mkdir(current)
}

74
link.cm
View File

@@ -34,9 +34,10 @@ function ensure_dir(path) {
if (fd.stat(path).isDirectory) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
current = current + parts[i] + '/'
if (!fd.stat(current).isDirectory) {
fd.mkdir(current)
}
@@ -66,14 +67,16 @@ Link.load = function() {
return link_cache
}
try {
var _load = function() {
var content = text(fd.slurp(path))
var cfg = toml.decode(content)
link_cache = cfg.links || {}
} catch (e) {
log.console("Warning: Failed to load link.toml: " + e)
if (cfg && cfg.links) link_cache = cfg.links
else link_cache = {}
} disruption {
print("Warning: Failed to load link.toml\n")
link_cache = {}
}
_load()
return link_cache
}
@@ -90,14 +93,16 @@ Link.add = function(canonical, target, shop) {
// Validate canonical package exists in shop
var lock = shop.load_lock()
if (!lock[canonical]) {
throw Error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical)
print('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical + '\n')
disrupt
}
// Validate target is a valid package
if (starts_with(target, '/')) {
// Local path - must have cell.toml
if (!fd.is_file(target + '/cell.toml')) {
throw Error('Target ' + target + ' is not a valid package (no cell.toml)')
print('Target ' + target + ' is not a valid package (no cell.toml)\n')
disrupt
}
} else {
// Remote package target - ensure it's installed
@@ -115,34 +120,36 @@ Link.add = function(canonical, target, shop) {
// Read the target's cell.toml to find its dependencies
var target_path = starts_with(target, '/') ? target : get_package_abs_dir(target)
var toml_path = target_path + '/cell.toml'
var _install_deps = null
if (fd.is_file(toml_path)) {
try {
_install_deps = function() {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
if (cfg.dependencies) {
if (cfg && cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias) {
var dep_locator = cfg.dependencies[alias]
// Skip local dependencies that don't exist
if (starts_with(dep_locator, '/') && !fd.is_dir(dep_locator)) {
log.console(" Skipping missing local dependency: " + dep_locator)
print(" Skipping missing local dependency: " + dep_locator + "\n")
return
}
// Install the dependency if not already in shop
try {
var _get_dep = function() {
shop.get(dep_locator)
shop.extract(dep_locator)
} catch (e) {
log.console(` Warning: Could not install dependency ${dep_locator}: ${e.message}`)
log.error(e)
} disruption {
print(` Warning: Could not install dependency ${dep_locator}\n`)
}
_get_dep()
})
}
} catch (e) {
log.console(` Warning: Could not read dependencies from ${toml_path}`)
} disruption {
print(` Warning: Could not read dependencies from ${toml_path}\n`)
}
_install_deps()
}
log.console("Linked " + canonical + " -> " + target)
print("Linked " + canonical + " -> " + target + "\n")
return true
}
@@ -154,12 +161,12 @@ Link.remove = function(canonical) {
var target_dir = get_package_abs_dir(canonical)
if (fd.is_link(target_dir)) {
fd.unlink(target_dir)
log.console("Removed symlink at " + target_dir)
print("Removed symlink at " + target_dir + "\n")
}
delete links[canonical]
Link.save(links)
log.console("Unlinked " + canonical)
print("Unlinked " + canonical + "\n")
return true
}
@@ -174,7 +181,7 @@ Link.clear = function() {
})
Link.save({})
log.console("Cleared all links")
print("Cleared all links\n")
return true
}
@@ -218,7 +225,7 @@ Link.sync_all = function(shop) {
arrfor(array(links), function(canonical) {
var target = links[canonical]
try {
var _sync = function() {
// Validate target exists
var link_target = resolve_link_target(target)
if (!fd.is_dir(link_target)) {
@@ -234,10 +241,10 @@ Link.sync_all = function(shop) {
// Install dependencies of the linked package
var toml_path = link_target + '/cell.toml'
try {
var _install = function() {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
if (cfg.dependencies) {
if (cfg && cfg.dependencies) {
arrfor(array(cfg.dependencies), function(alias) {
var dep_locator = cfg.dependencies[alias]
// Skip local dependencies that don't exist
@@ -245,22 +252,25 @@ Link.sync_all = function(shop) {
return
}
// Install the dependency if not already in shop
try {
var _get = function() {
shop.get(dep_locator)
shop.extract(dep_locator)
} catch (e) {
} disruption {
// Silently continue - dependency may already be installed
}
_get()
})
}
} catch (e) {
} disruption {
// Could not read dependencies - continue anyway
}
_install()
count++
} catch (e) {
push(errors, canonical + ': ' + e.message)
count = count + 1
} disruption {
push(errors, canonical + ': sync failed')
}
_sync()
})
return { synced: count, errors: errors }
@@ -269,7 +279,7 @@ Link.sync_all = function(shop) {
// Check if a package is currently linked
Link.is_linked = function(canonical) {
var links = Link.load()
return canonical in links
return links[canonical] != null
}
// Get the link target for a package (or null if not linked)

View File

@@ -2,10 +2,12 @@ var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var filename = args[0]
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename)
var compiled = mcode(ast)
var ast = parse(result.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
print(json.encode(compiled))

704
mcode.cm
View File

@@ -43,6 +43,8 @@ var mcode = function(ast) {
var s_func_counter = 0
var s_loop_break = null
var s_loop_continue = null
var s_label_map = {}
var s_pending_label = null
var s_is_arrow = false
var s_function_nr = 0
var s_scopes = null
@@ -51,6 +53,13 @@ var mcode = function(ast) {
var s_cur_col = 0
var s_filename = null
// Shared closure vars for binop helpers (avoids >4 param functions)
var _bp_dest = 0
var _bp_left = 0
var _bp_right = 0
var _bp_ln = null
var _bp_rn = null
// State save/restore for nested function compilation
var save_state = function() {
return {
@@ -64,6 +73,7 @@ var mcode = function(ast) {
max_slot: s_max_slot,
loop_break: s_loop_break,
loop_continue: s_loop_continue,
label_map: s_label_map,
is_arrow: s_is_arrow,
function_nr: s_function_nr,
intrinsic_cache: s_intrinsic_cache,
@@ -83,6 +93,7 @@ var mcode = function(ast) {
s_max_slot = saved.max_slot
s_loop_break = saved.loop_break
s_loop_continue = saved.loop_continue
s_label_map = saved.label_map
s_is_arrow = saved.is_arrow
s_function_nr = saved.function_nr
s_intrinsic_cache = saved.intrinsic_cache
@@ -227,20 +238,513 @@ var mcode = function(ast) {
add_instr([op, slot, label])
}
// ---- Decomposed op emission helpers ----
// Helper: check if an AST node is a known-int literal
var is_known_int = function(node) {
if (node == null) { return false }
return node.kind == "number" && is_integer(node.number)
}
// Helper: check if an AST node is a known-text literal
var is_known_text = function(node) {
if (node == null) { return false }
return node.kind == "text" || node.kind == "text literal"
}
// Helper: check if an AST node is a known-number literal (int or float)
var is_known_number = function(node) {
if (node == null) { return false }
return node.kind == "number"
}
// Helper: check if an AST node is a known-bool literal
var is_known_bool = function(node) {
if (node == null) { return false }
return node.kind == "true" || node.kind == "false"
}
// Helper: check if an AST node is a known-null literal
var is_known_null = function(node) {
if (node == null) { return false }
return node.kind == "null"
}
// emit_add_decomposed: int path -> text path -> float path -> disrupt
// reads _bp_dest, _bp_left, _bp_right, _bp_ln, _bp_rn from closure
var emit_add_decomposed = function() {
var dest = _bp_dest
var left = _bp_left
var right = _bp_right
var t0 = 0
var t1 = 0
var left_is_int = is_known_int(_bp_ln)
var left_is_text = is_known_text(_bp_ln)
var left_is_num = is_known_number(_bp_ln)
var right_is_int = is_known_int(_bp_rn)
var right_is_text = is_known_text(_bp_rn)
var right_is_num = is_known_number(_bp_rn)
var not_int = null
var not_text = null
var done = null
var err = null
// Both sides known int
if (left_is_int && right_is_int) {
emit_3("add_int", dest, left, right)
return null
}
// Both sides known text
if (left_is_text && right_is_text) {
emit_3("concat", dest, left, right)
return null
}
// Both sides known number (but not both int)
if (left_is_num && right_is_num) {
if (left_is_int && right_is_int) {
emit_3("add_int", dest, left, right)
} else {
emit_3("add_float", dest, left, right)
}
return null
}
not_int = gen_label("add_ni")
not_text = gen_label("add_nt")
done = gen_label("add_done")
err = gen_label("add_err")
// Int path
t0 = alloc_slot()
if (!left_is_int) {
emit_2("is_int", t0, left)
emit_jump_cond("jump_false", t0, not_int)
}
t1 = alloc_slot()
if (!right_is_int) {
emit_2("is_int", t1, right)
emit_jump_cond("jump_false", t1, not_int)
}
emit_3("add_int", dest, left, right)
emit_jump(done)
// Text path
emit_label(not_int)
if (!left_is_text) {
emit_2("is_text", t0, left)
emit_jump_cond("jump_false", t0, not_text)
}
if (!right_is_text) {
emit_2("is_text", t1, right)
emit_jump_cond("jump_false", t1, not_text)
}
emit_3("concat", dest, left, right)
emit_jump(done)
// Float path
emit_label(not_text)
if (!left_is_num) {
emit_2("is_num", t0, left)
emit_jump_cond("jump_false", t0, err)
}
if (!right_is_num) {
emit_2("is_num", t1, right)
emit_jump_cond("jump_false", t1, err)
}
emit_3("add_float", dest, left, right)
emit_jump(done)
emit_label(err)
emit_0("disrupt")
emit_label(done)
return null
}
// emit_numeric_binop: int path -> float path -> disrupt
// reads _bp_dest, _bp_left, _bp_right, _bp_ln, _bp_rn from closure
var emit_numeric_binop = function(int_op, float_op) {
var dest = _bp_dest
var left = _bp_left
var right = _bp_right
var t0 = 0
var t1 = 0
var left_is_int = is_known_int(_bp_ln)
var left_is_num = is_known_number(_bp_ln)
var right_is_int = is_known_int(_bp_rn)
var right_is_num = is_known_number(_bp_rn)
var not_int = null
var done = null
var err = null
// Both sides known int
if (left_is_int && right_is_int) {
emit_3(int_op, dest, left, right)
return null
}
// Both sides known number (but not both int)
if (left_is_num && right_is_num) {
emit_3(float_op, dest, left, right)
return null
}
not_int = gen_label("num_ni")
done = gen_label("num_done")
err = gen_label("num_err")
t0 = alloc_slot()
if (!left_is_int) {
emit_2("is_int", t0, left)
emit_jump_cond("jump_false", t0, not_int)
}
t1 = alloc_slot()
if (!right_is_int) {
emit_2("is_int", t1, right)
emit_jump_cond("jump_false", t1, not_int)
}
emit_3(int_op, dest, left, right)
emit_jump(done)
emit_label(not_int)
if (!left_is_num) {
emit_2("is_num", t0, left)
emit_jump_cond("jump_false", t0, err)
}
if (!right_is_num) {
emit_2("is_num", t1, right)
emit_jump_cond("jump_false", t1, err)
}
emit_3(float_op, dest, left, right)
emit_jump(done)
emit_label(err)
emit_0("disrupt")
emit_label(done)
return null
}
// emit_eq_decomposed: identical -> int -> float -> text -> null -> bool -> mismatch(false)
// reads _bp_dest, _bp_left, _bp_right from closure
var emit_eq_decomposed = function() {
var dest = _bp_dest
var left = _bp_left
var right = _bp_right
var t0 = 0
var t1 = 0
var done = gen_label("eq_done")
var not_int = gen_label("eq_ni")
var not_num = gen_label("eq_nn")
var not_text = gen_label("eq_nt")
var not_null = gen_label("eq_nnl")
var not_bool = gen_label("eq_nb")
// Identical check
emit_3("is_identical", dest, left, right)
emit_jump_cond("jump_true", dest, done)
// Int path
t0 = alloc_slot()
emit_2("is_int", t0, left)
emit_jump_cond("jump_false", t0, not_int)
t1 = alloc_slot()
emit_2("is_int", t1, right)
emit_jump_cond("jump_false", t1, not_int)
emit_3("eq_int", dest, left, right)
emit_jump(done)
// Float path
emit_label(not_int)
emit_2("is_num", t0, left)
emit_jump_cond("jump_false", t0, not_num)
emit_2("is_num", t1, right)
emit_jump_cond("jump_false", t1, not_num)
emit_3("eq_float", dest, left, right)
emit_jump(done)
// Text path
emit_label(not_num)
emit_2("is_text", t0, left)
emit_jump_cond("jump_false", t0, not_text)
emit_2("is_text", t1, right)
emit_jump_cond("jump_false", t1, not_text)
emit_3("eq_text", dest, left, right)
emit_jump(done)
// Null path
emit_label(not_text)
emit_2("is_null", t0, left)
emit_jump_cond("jump_false", t0, not_null)
emit_2("is_null", t1, right)
emit_jump_cond("jump_false", t1, not_null)
emit_1("true", dest)
emit_jump(done)
// Bool path
emit_label(not_null)
emit_2("is_bool", t0, left)
emit_jump_cond("jump_false", t0, not_bool)
emit_2("is_bool", t1, right)
emit_jump_cond("jump_false", t1, not_bool)
emit_3("eq_bool", dest, left, right)
emit_jump(done)
// Mismatch -> false
emit_label(not_bool)
emit_1("false", dest)
emit_label(done)
return null
}
// emit_ne_decomposed: identical -> int -> float -> text -> null -> bool -> mismatch(true)
// reads _bp_dest, _bp_left, _bp_right from closure
var emit_ne_decomposed = function() {
var dest = _bp_dest
var left = _bp_left
var right = _bp_right
var t0 = 0
var t1 = 0
var done = gen_label("ne_done")
var not_ident = gen_label("ne_nid")
var not_int = gen_label("ne_ni")
var not_num = gen_label("ne_nn")
var not_text = gen_label("ne_nt")
var not_null = gen_label("ne_nnl")
var not_bool = gen_label("ne_nb")
// Identical -> false
emit_3("is_identical", dest, left, right)
emit_jump_cond("jump_true", dest, not_ident)
// If jump_true doesn't fire, dest already holds false, continue to checks
emit_jump(not_int)
emit_label(not_ident)
emit_1("false", dest)
emit_jump(done)
// Int path
emit_label(not_int)
t0 = alloc_slot()
emit_2("is_int", t0, left)
emit_jump_cond("jump_false", t0, not_num)
t1 = alloc_slot()
emit_2("is_int", t1, right)
emit_jump_cond("jump_false", t1, not_num)
emit_3("ne_int", dest, left, right)
emit_jump(done)
// Float path
emit_label(not_num)
emit_2("is_num", t0, left)
emit_jump_cond("jump_false", t0, not_text)
emit_2("is_num", t1, right)
emit_jump_cond("jump_false", t1, not_text)
emit_3("ne_float", dest, left, right)
emit_jump(done)
// Text path
emit_label(not_text)
emit_2("is_text", t0, left)
emit_jump_cond("jump_false", t0, not_null)
emit_2("is_text", t1, right)
emit_jump_cond("jump_false", t1, not_null)
emit_3("ne_text", dest, left, right)
emit_jump(done)
// Null path
emit_label(not_null)
emit_2("is_null", t0, left)
emit_jump_cond("jump_false", t0, not_bool)
emit_2("is_null", t1, right)
emit_jump_cond("jump_false", t1, not_bool)
emit_1("false", dest)
emit_jump(done)
// Bool path
var mismatch = gen_label("ne_mis")
emit_label(not_bool)
emit_2("is_bool", t0, left)
emit_jump_cond("jump_false", t0, mismatch)
emit_2("is_bool", t1, right)
emit_jump_cond("jump_false", t1, mismatch)
emit_3("ne_bool", dest, left, right)
emit_jump(done)
// Mismatch -> true (ne of different types is true)
emit_label(mismatch)
emit_1("true", dest)
emit_label(done)
return null
}
// emit_relational: int -> float -> text -> disrupt
// reads _bp_dest, _bp_left, _bp_right, _bp_ln, _bp_rn from closure
var emit_relational = function(int_op, float_op, text_op) {
var dest = _bp_dest
var left = _bp_left
var right = _bp_right
var t0 = 0
var t1 = 0
var left_is_int = is_known_int(_bp_ln)
var left_is_num = is_known_number(_bp_ln)
var left_is_text = is_known_text(_bp_ln)
var right_is_int = is_known_int(_bp_rn)
var right_is_num = is_known_number(_bp_rn)
var right_is_text = is_known_text(_bp_rn)
var not_int = null
var not_num = null
var done = null
var err = null
// Both known int
if (left_is_int && right_is_int) {
emit_3(int_op, dest, left, right)
return null
}
// Both known number
if (left_is_num && right_is_num) {
emit_3(float_op, dest, left, right)
return null
}
// Both known text
if (left_is_text && right_is_text) {
emit_3(text_op, dest, left, right)
return null
}
not_int = gen_label("rel_ni")
not_num = gen_label("rel_nn")
done = gen_label("rel_done")
err = gen_label("rel_err")
t0 = alloc_slot()
emit_2("is_int", t0, left)
emit_jump_cond("jump_false", t0, not_int)
t1 = alloc_slot()
emit_2("is_int", t1, right)
emit_jump_cond("jump_false", t1, not_int)
emit_3(int_op, dest, left, right)
emit_jump(done)
emit_label(not_int)
emit_2("is_num", t0, left)
emit_jump_cond("jump_false", t0, not_num)
emit_2("is_num", t1, right)
emit_jump_cond("jump_false", t1, not_num)
emit_3(float_op, dest, left, right)
emit_jump(done)
emit_label(not_num)
emit_2("is_text", t0, left)
emit_jump_cond("jump_false", t0, err)
emit_2("is_text", t1, right)
emit_jump_cond("jump_false", t1, err)
emit_3(text_op, dest, left, right)
emit_jump(done)
emit_label(err)
emit_0("disrupt")
emit_label(done)
return null
}
// emit_neg_decomposed: int path -> float path -> disrupt
var emit_neg_decomposed = function(dest, src, src_node) {
var t0 = 0
var not_int = null
var done = null
var err = null
if (is_known_int(src_node)) {
emit_2("neg_int", dest, src)
return null
}
if (is_known_number(src_node)) {
emit_2("neg_float", dest, src)
return null
}
not_int = gen_label("neg_ni")
done = gen_label("neg_done")
err = gen_label("neg_err")
t0 = alloc_slot()
emit_2("is_int", t0, src)
emit_jump_cond("jump_false", t0, not_int)
emit_2("neg_int", dest, src)
emit_jump(done)
emit_label(not_int)
emit_2("is_num", t0, src)
emit_jump_cond("jump_false", t0, err)
emit_2("neg_float", dest, src)
emit_jump(done)
emit_label(err)
emit_0("disrupt")
emit_label(done)
return null
}
// Central router: maps op string to decomposition helper
// Sets _bp_* closure vars then calls helper with reduced args
var emit_binop = function(op_str, dest, left, right) {
_bp_dest = dest
_bp_left = left
_bp_right = right
if (op_str == "add") {
emit_add_decomposed()
} else if (op_str == "subtract") {
emit_numeric_binop("sub_int", "sub_float")
} else if (op_str == "multiply") {
emit_numeric_binop("mul_int", "mul_float")
} else if (op_str == "divide") {
emit_numeric_binop("div_int", "div_float")
} else if (op_str == "modulo") {
emit_numeric_binop("mod_int", "mod_float")
} else if (op_str == "eq") {
emit_eq_decomposed()
} else if (op_str == "ne") {
emit_ne_decomposed()
} else if (op_str == "lt") {
emit_relational("lt_int", "lt_float", "lt_text")
} else if (op_str == "le") {
emit_relational("le_int", "le_float", "le_text")
} else if (op_str == "gt") {
emit_relational("gt_int", "gt_float", "gt_text")
} else if (op_str == "ge") {
emit_relational("ge_int", "ge_float", "ge_text")
} else {
// Passthrough for bitwise, pow, in, etc.
emit_3(op_str, dest, left, right)
}
return null
}
var emit_get_prop = function(dest, obj, prop) {
add_instr(["load", dest, obj, prop])
add_instr(["load_field", dest, obj, prop])
}
var emit_set_prop = function(obj, prop, val) {
add_instr(["store", obj, val, prop])
add_instr(["store_field", obj, val, prop])
}
var emit_get_elem = function(dest, obj, idx) {
emit_3("load", dest, obj, idx)
var emit_get_elem = function(dest, obj, idx, access_kind) {
if (access_kind == "index") {
emit_3("load_index", dest, obj, idx)
} else if (access_kind == "field") {
emit_3("load_field", dest, obj, idx)
} else {
emit_3("load_dynamic", dest, obj, idx)
}
}
var emit_set_elem = function(obj, idx, val) {
emit_3("store", obj, val, idx)
var emit_set_elem = function(obj, idx, val, access_kind) {
if (access_kind == "index") {
emit_3("store_index", obj, val, idx)
} else if (access_kind == "field") {
emit_3("store_field", obj, val, idx)
} else {
emit_3("store_dynamic", obj, val, idx)
}
}
var emit_call = function(dest, func_slot, args) {
@@ -261,23 +765,114 @@ var mcode = function(ast) {
}
var emit_call_method = function(dest, obj, prop, args) {
var instr = ["callmethod", dest, obj, prop]
var argc = length(args)
var check = alloc_slot()
var record_path = gen_label("record_path")
var done_label = gen_label("call_done")
var _i = 0
while (_i < length(args)) {
push(instr, args[_i])
var arg_idx = 0
// Check if obj is a proxy function (arity 2)
emit_2("is_proxy", check, obj)
emit_jump_cond("jump_false", check, record_path)
// Function proxy path: call obj(prop_name, [args...]) with this=null
var null_slot = alloc_slot()
emit_const_null(null_slot)
var name_str = alloc_slot()
emit_const_str(name_str, prop)
var args_arr = alloc_slot()
var arr_instr = ["array", args_arr, argc]
_i = 0
while (_i < argc) {
push(arr_instr, args[_i])
_i = _i + 1
}
add_instr(instr)
add_instr(arr_instr)
var pf = alloc_slot()
emit_3("frame", pf, obj, 2)
emit_3("setarg", pf, 0, null_slot)
emit_3("setarg", pf, 1, name_str)
emit_3("setarg", pf, 2, args_arr)
emit_2("invoke", pf, dest)
emit_jump(done_label)
// Record path: load method, call with this=obj
emit_label(record_path)
var method_slot = alloc_slot()
add_instr(["load_field", method_slot, obj, prop])
var frame_slot = alloc_slot()
emit_3("frame", frame_slot, method_slot, argc)
emit_3("setarg", frame_slot, 0, obj)
arg_idx = 1
_i = 0
while (_i < argc) {
emit_3("setarg", frame_slot, arg_idx, args[_i])
arg_idx = arg_idx + 1
_i = _i + 1
}
emit_2("invoke", frame_slot, dest)
emit_label(done_label)
}
var emit_call_method_dyn = function(dest, obj, key_reg, args) {
var instr = ["callmethod_dyn", dest, obj, key_reg]
var argc = length(args)
var check = alloc_slot()
var record_path = gen_label("dyn_record_path")
var done_label = gen_label("dyn_call_done")
var _i = 0
while (_i < length(args)) {
push(instr, args[_i])
var arg_idx = 0
// Check if obj is a proxy function (arity 2)
emit_2("is_proxy", check, obj)
emit_jump_cond("jump_false", check, record_path)
// Function proxy path (dynamic key): must be text
var key_ok = alloc_slot()
var error_path = gen_label("dyn_error")
emit_2("is_text", key_ok, key_reg)
emit_jump_cond("jump_false", key_ok, error_path)
var null_slot = alloc_slot()
emit_const_null(null_slot)
var args_arr = alloc_slot()
var arr_instr = ["array", args_arr, argc]
_i = 0
while (_i < argc) {
push(arr_instr, args[_i])
_i = _i + 1
}
add_instr(instr)
add_instr(arr_instr)
var pf = alloc_slot()
emit_3("frame", pf, obj, 2)
emit_3("setarg", pf, 0, null_slot)
emit_3("setarg", pf, 1, key_reg)
emit_3("setarg", pf, 2, args_arr)
emit_2("invoke", pf, dest)
emit_jump(done_label)
// Error path: non-text key on function disrupts
emit_label(error_path)
emit_0("disrupt")
emit_jump(done_label)
// Record path: load method dynamically, call with this=obj
emit_label(record_path)
var method_slot = alloc_slot()
emit_3("load_dynamic", method_slot, obj, key_reg)
var frame_slot = alloc_slot()
emit_3("frame", frame_slot, method_slot, argc)
emit_3("setarg", frame_slot, 0, obj)
arg_idx = 1
_i = 0
while (_i < argc) {
emit_3("setarg", frame_slot, arg_idx, args[_i])
arg_idx = arg_idx + 1
_i = _i + 1
}
emit_2("invoke", frame_slot, dest)
emit_label(done_label)
}
var emit_go_call = function(func_slot, args) {
@@ -463,7 +1058,9 @@ var mcode = function(ast) {
if (op == null) {
op = "add"
}
emit_3(op, dest, left_slot, right_slot)
_bp_ln = left
_bp_rn = right
emit_binop(op, dest, left_slot, right_slot)
return dest
}
@@ -515,7 +1112,9 @@ var mcode = function(ast) {
}
right_slot = gen_expr(right, -1)
dest = alloc_slot()
emit_3(op, dest, left_slot, right_slot)
_bp_ln = null
_bp_rn = right
emit_binop(op, dest, left_slot, right_slot)
if (level == 0) {
local = find_var(name)
if (local >= 0) {
@@ -538,7 +1137,9 @@ var mcode = function(ast) {
emit_get_prop(old_val, obj_slot, prop)
right_slot = gen_expr(right, -1)
dest = alloc_slot()
emit_3(op, dest, old_val, right_slot)
_bp_ln = null
_bp_rn = right
emit_binop(op, dest, old_val, right_slot)
emit_set_prop(obj_slot, prop, dest)
return dest
} else if (left_kind == "[") {
@@ -547,11 +1148,13 @@ var mcode = function(ast) {
obj_slot = gen_expr(obj, -1)
idx_slot = gen_expr(idx_expr, -1)
old_val = alloc_slot()
emit_get_elem(old_val, obj_slot, idx_slot)
emit_get_elem(old_val, obj_slot, idx_slot, left.access_kind)
right_slot = gen_expr(right, -1)
dest = alloc_slot()
emit_3(op, dest, old_val, right_slot)
emit_set_elem(obj_slot, idx_slot, dest)
_bp_ln = null
_bp_rn = right
emit_binop(op, dest, old_val, right_slot)
emit_set_elem(obj_slot, idx_slot, dest, left.access_kind)
return dest
}
return -1
@@ -624,7 +1227,7 @@ var mcode = function(ast) {
idx_expr = left.right
obj_slot = gen_expr(obj, -1)
idx_slot = gen_expr(idx_expr, -1)
emit_set_elem(obj_slot, idx_slot, val_slot)
emit_set_elem(obj_slot, idx_slot, val_slot, left.access_kind)
}
return val_slot
}
@@ -679,6 +1282,7 @@ var mcode = function(ast) {
var arith_op = null
var operand_kind = null
var one_slot = 0
var one_node = null
var old_slot = 0
var local = 0
var new_slot = 0
@@ -843,7 +1447,7 @@ var mcode = function(ast) {
obj_slot = gen_expr(obj, -1)
idx_slot = gen_expr(idx, -1)
slot = alloc_slot()
emit_get_elem(slot, obj_slot, idx_slot)
emit_get_elem(slot, obj_slot, idx_slot, expr.access_kind)
return slot
}
@@ -899,7 +1503,9 @@ var mcode = function(ast) {
a0 = gen_expr(args_list[0], -1)
a1 = gen_expr(args_list[1], -1)
d = alloc_slot()
emit_3(mop, d, a0, a1)
_bp_ln = args_list[0]
_bp_rn = args_list[1]
emit_binop(mop, d, a0, a1)
return d
}
@@ -946,7 +1552,7 @@ var mcode = function(ast) {
if (kind == "-unary") {
operand_slot = gen_expr(expr.expression, -1)
slot = alloc_slot()
emit_2("neg", slot, operand_slot)
emit_neg_decomposed(slot, operand_slot, expr.expression)
return slot
}
if (kind == "+unary") {
@@ -961,6 +1567,7 @@ var mcode = function(ast) {
operand_kind = operand.kind
one_slot = alloc_slot()
emit_2("int", one_slot, 1)
one_node = {kind: "number", number: 1}
if (operand_kind == "name") {
name = operand.name
@@ -983,7 +1590,9 @@ var mcode = function(ast) {
emit_access_intrinsic(old_slot, name)
}
new_slot = alloc_slot()
emit_3(arith_op, new_slot, old_slot, one_slot)
_bp_ln = null
_bp_rn = one_node
emit_binop(arith_op, new_slot, old_slot, one_slot)
if (level == 0) {
local = find_var(name)
if (local >= 0) {
@@ -1003,7 +1612,9 @@ var mcode = function(ast) {
old_slot = alloc_slot()
emit_get_prop(old_slot, obj_slot, prop)
new_slot = alloc_slot()
emit_3(arith_op, new_slot, old_slot, one_slot)
_bp_ln = null
_bp_rn = one_node
emit_binop(arith_op, new_slot, old_slot, one_slot)
emit_set_prop(obj_slot, prop, new_slot)
return postfix ? old_slot : new_slot
} else if (operand_kind == "[") {
@@ -1012,10 +1623,12 @@ var mcode = function(ast) {
obj_slot = gen_expr(obj, -1)
idx_slot = gen_expr(idx_expr, -1)
old_slot = alloc_slot()
emit_get_elem(old_slot, obj_slot, idx_slot)
emit_get_elem(old_slot, obj_slot, idx_slot, operand.access_kind)
new_slot = alloc_slot()
emit_3(arith_op, new_slot, old_slot, one_slot)
emit_set_elem(obj_slot, idx_slot, new_slot)
_bp_ln = null
_bp_rn = one_node
emit_binop(arith_op, new_slot, old_slot, one_slot)
emit_set_elem(obj_slot, idx_slot, new_slot, operand.access_kind)
return postfix ? old_slot : new_slot
}
}
@@ -1281,6 +1894,13 @@ var mcode = function(ast) {
return null
}
if (kind == "label") {
s_pending_label = stmt.name
gen_statement(stmt.statement)
s_pending_label = null
return null
}
if (kind == "while") {
cond = stmt.expression
stmts = stmt.statements
@@ -1290,6 +1910,10 @@ var mcode = function(ast) {
old_continue = s_loop_continue
s_loop_break = end_label
s_loop_continue = start_label
if (s_pending_label != null) {
s_label_map[s_pending_label] = {break_target: end_label, continue_target: start_label}
s_pending_label = null
}
emit_label(start_label)
cond_slot = gen_expr(cond, -1)
emit_jump_cond("jump_false", cond_slot, end_label)
@@ -1315,6 +1939,10 @@ var mcode = function(ast) {
old_continue = s_loop_continue
s_loop_break = end_label
s_loop_continue = cond_label
if (s_pending_label != null) {
s_label_map[s_pending_label] = {break_target: end_label, continue_target: cond_label}
s_pending_label = null
}
emit_label(start_label)
_i = 0
while (_i < length(stmts)) {
@@ -1342,6 +1970,10 @@ var mcode = function(ast) {
old_continue = s_loop_continue
s_loop_break = end_label
s_loop_continue = update_label
if (s_pending_label != null) {
s_label_map[s_pending_label] = {break_target: end_label, continue_target: update_label}
s_pending_label = null
}
if (init != null) {
init_kind = init.kind
if (init_kind == "var" || init_kind == "def") {
@@ -1417,14 +2049,18 @@ var mcode = function(ast) {
}
if (kind == "break") {
if (s_loop_break != null) {
if (stmt.name != null && s_label_map[stmt.name] != null) {
emit_jump(s_label_map[stmt.name].break_target)
} else if (s_loop_break != null) {
emit_jump(s_loop_break)
}
return null
}
if (kind == "continue") {
if (s_loop_continue != null) {
if (stmt.name != null && s_label_map[stmt.name] != null) {
emit_jump(s_label_map[stmt.name].continue_target)
} else if (s_loop_continue != null) {
emit_jump(s_loop_continue)
}
return null
@@ -1452,7 +2088,9 @@ var mcode = function(ast) {
case_expr = case_node.expression
case_val = gen_expr(case_expr, -1)
cmp_slot = alloc_slot()
emit_3("eq", cmp_slot, switch_val, case_val)
_bp_ln = null
_bp_rn = case_expr
emit_binop("eq", cmp_slot, switch_val, case_val)
emit_jump_cond("jump_true", cmp_slot, case_label)
push(case_labels, case_label)
}
@@ -1548,6 +2186,7 @@ var mcode = function(ast) {
s_intrinsic_cache = []
s_loop_break = null
s_loop_continue = null
s_label_map = {}
s_is_arrow = is_arrow
@@ -1741,6 +2380,7 @@ var mcode = function(ast) {
s_func_counter = 0
s_loop_break = null
s_loop_continue = null
s_label_map = {}
s_function_nr = 0
// Scan scope

Binary file not shown.

View File

@@ -46,7 +46,6 @@ src += [ # core
'miniz.c',
'runtime.c',
'mach.c',
'mcode.c',
'libregexp.c', 'libunicode.c', 'cutils.c', 'dtoa.c'
]

View File

@@ -14,11 +14,11 @@ static void js_enet_host_finalizer(JSRuntime *rt, JSValue val)
if (host) enet_host_destroy(host);
}
static void js_enet_peer_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func)
{
ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
JS_MarkValue(rt, *(JSValue*)peer->data, mark_func);
}
//static void js_enet_peer_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func)
//{
// ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
// JS_MarkValue(rt, *(JSValue*)peer->data, mark_func);
//}
static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val)
{
@@ -62,7 +62,7 @@ static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int ar
enet_uint32 outgoing_bandwidth = 0;
JSValue obj;
if (argc < 1 || !JS_IsObject(argv[0])) {
if (argc < 1 || !JS_IsRecord(argv[0])) {
host = enet_host_create(NULL, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet client host");
goto wrap;
@@ -414,7 +414,7 @@ static JSClassDef enet_host = {
static JSClassDef enet_peer_class = {
"ENetPeer",
.finalizer = js_enet_peer_finalizer,
.gc_mark = js_enet_peer_mark
// .gc_mark = js_enet_peer_mark
};
JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv)

View File

@@ -132,7 +132,7 @@ JSC_CCALL(socket_getaddrinfo,
// Store the addrinfo pointer as an internal property
// We'll need to handle this differently since we can't wrap it
// For now, we'll skip storing the raw addrinfo
JS_SetPropertyUint32(js, ret, idx++, info);
JS_SetPropertyNumber(js, ret, idx++, info);
}
freeaddrinfo(res);

31
num_torture.cm Normal file
View File

@@ -0,0 +1,31 @@
// num_torture.cm — integer math torture test
// Pure integer arithmetic so it stays on the fast int path.
// Returns the final checksum so the caller can verify correctness.
var n = 5000000
var sum = 0
var i = 0
var a = 0
var b = 0
while (i < n) {
a = (i * 7 + 13) % 10007
b = (a * a) % 10007
sum = (sum + b) % 1000000007
i = i + 1
}
return function(n) {
var i = 0
var a = 0
var b = 0
var sum = 0
while (i < n) {
a = (i * 7 + 13) % 10007
b = (a * a) % 10007
sum = (sum + b) % 1000000007
i = i + 1
}
return sum
}

View File

@@ -51,7 +51,7 @@ package.load_config = function(name)
return config_cache[config_path]
if (!fd.is_file(config_path)) {
throw Error(`${config_path} does not exist`)
print(`${config_path} does not exist`); disrupt
}
var content = text(fd.slurp(config_path))
@@ -101,11 +101,12 @@ package.alias_to_package = function(name, alias)
}
// alias is optional
package.add_dependency = function(name, locator, alias = locator)
package.add_dependency = function(name, locator, alias)
{
var _alias = alias == null ? locator : alias
var config = package.load_config(name)
if (!config.dependencies) config.dependencies = {}
config.dependencies[alias] = locator
config.dependencies[_alias] = locator
package.save_config(name, config)
}
@@ -115,10 +116,11 @@ package.remove_dependency = function(name, locator)
var config = package.load_config(name)
if (!config.dependencies) return
var alias = null
if (config.dependencies[locator])
delete config.dependencies[locator]
else {
var alias = package.find_alias(name, locator)
alias = package.find_alias(name, locator)
if (alias)
delete config.dependencies[alias]
}
@@ -133,8 +135,9 @@ package.find_package_dir = function(file)
if (fd.is_file(dir))
dir = fd.dirname(dir)
var toml_path = null
while (dir && length(dir) > 0) {
var toml_path = dir + '/cell.toml'
toml_path = dir + '/cell.toml'
if (fd.is_file(toml_path)) {
return dir
}
@@ -158,21 +161,23 @@ package.split_alias = function(name, path)
var parts = array(path, '/')
var first_part = parts[0]
try {
var _split = function() {
var config = package.load_config(name)
if (!config) return null
var deps = config.dependencies
var dep_locator = null
var remaining_path = null
if (deps && deps[first_part]) {
var dep_locator = deps[first_part]
var remaining_path = text(array(parts, 1), '/')
dep_locator = deps[first_part]
remaining_path = text(array(parts, 1), '/')
return { package: dep_locator, path: remaining_path }
}
} catch (e) {
// Config doesn't exist or couldn't be loaded
return null
} disruption {
return null
}
return null
return _split()
}
package.gather_dependencies = function(name)
@@ -208,18 +213,23 @@ package.list_files = function(pkg) {
var walk = function(current_dir, current_prefix) {
var list = fd.readdir(current_dir)
if (!list) return
for (var i = 0; i < length(list); i++) {
var item = list[i]
var i = 0
var item = null
var full_path = null
var rel_path = null
var st = null
for (i = 0; i < length(list); i++) {
item = list[i]
if (item == '.' || item == '..') continue
if (starts_with(item, '.')) continue
if (starts_with(item, '.')) continue
// Skip build directories in root
var full_path = current_dir + "/" + item
var rel_path = current_prefix ? current_prefix + "/" + item : item
var st = fd.stat(full_path)
full_path = current_dir + "/" + item
rel_path = current_prefix ? current_prefix + "/" + item : item
st = fd.stat(full_path)
if (st.isDirectory) {
walk(full_path, rel_path)
} else {
@@ -237,7 +247,8 @@ package.list_files = function(pkg) {
package.list_modules = function(name) {
var files = package.list_files(name)
var modules = []
for (var i = 0; i < length(files); i++) {
var i = 0
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.cm')) {
push(modules, text(files[i], 0, -3))
}
@@ -248,7 +259,8 @@ package.list_modules = function(name) {
package.list_programs = function(name) {
var files = package.list_files(name)
var programs = []
for (var i = 0; i < length(files); i++) {
var i = 0
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.ce')) {
push(programs, text(files[i], 0, -3))
}
@@ -265,14 +277,16 @@ package.get_flags = function(name, flag_type, target) {
var flags = []
// Base flags
var base = null
var target_flags = null
if (config.compilation && config.compilation[flag_type]) {
var base = config.compilation[flag_type]
base = config.compilation[flag_type]
flags = array(flags, filter(array(base, /\s+/), function(f) { return length(f) > 0 }))
}
// Target-specific flags
if (target && config.compilation && config.compilation[target] && config.compilation[target][flag_type]) {
var target_flags = config.compilation[target][flag_type]
target_flags = config.compilation[target][flag_type]
flags = array(flags, filter(array(target_flags, /\s+/), function(f) { return length(f) > 0 }))
}
@@ -290,23 +304,36 @@ package.get_c_files = function(name, target, exclude_main) {
// Group files by their base name (without target suffix)
var groups = {} // base_key -> { generic: file, variants: { target: file } }
for (var i = 0; i < length(files); i++) {
var file = files[i]
var i = 0
var file = null
var ext = null
var base = null
var name_part = null
var dir_part = null
var dir = null
var is_variant = null
var variant_target = null
var generic_name = null
var t = 0
var suffix = null
var group_key = null
for (i = 0; i < length(files); i++) {
file = files[i]
if (!ends_with(file, '.c') && !ends_with(file, '.cpp')) continue
var ext = ends_with(file, '.cpp') ? '.cpp' : '.c'
var base = text(file, 0, -length(ext))
var name_part = fd.basename(base)
var dir_part = fd.dirname(base)
var dir = (dir_part && dir_part != '.') ? dir_part + '/' : ''
ext = ends_with(file, '.cpp') ? '.cpp' : '.c'
base = text(file, 0, -length(ext))
name_part = fd.basename(base)
dir_part = fd.dirname(base)
dir = (dir_part && dir_part != '.') ? dir_part + '/' : ''
// Check for target suffix
var is_variant = false
var variant_target = null
var generic_name = name_part
for (var t = 0; t < length(known_targets); t++) {
var suffix = '_' + known_targets[t]
is_variant = false
variant_target = null
generic_name = name_part
for (t = 0; t < length(known_targets); t++) {
suffix = '_' + known_targets[t]
if (ends_with(name_part, suffix)) {
is_variant = true
variant_target = known_targets[t]
@@ -315,7 +342,7 @@ package.get_c_files = function(name, target, exclude_main) {
}
}
var group_key = dir + generic_name + ext
group_key = dir + generic_name + ext
if (!groups[group_key]) {
groups[group_key] = { generic: null, variants: {} }
}
@@ -332,18 +359,19 @@ package.get_c_files = function(name, target, exclude_main) {
arrfor(array(groups), function(key) {
var group = groups[key]
var selected = null
var basename = null
// Prefer target-specific variant if available
if (target && group.variants[target]) {
selected = group.variants[target]
} else if (group.generic) {
selected = group.generic
}
if (selected) {
// Skip main.c if requested
if (exclude_main) {
var basename = fd.basename(selected)
basename = fd.basename(selected)
if (basename == 'main.c' || starts_with(basename, 'main_')) return
}
push(result, selected)

View File

@@ -1,8 +1,9 @@
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var filename = args[0]
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
print(json.encode(ast))

401
parse.cm
View File

@@ -1,18 +1,5 @@
def CP_SLASH = 47
def CP_BSLASH = 92
var is_alpha = function(c) {
return (c >= 65 && c <= 90) || (c >= 97 && c <= 122)
}
var parse = function(tokens, src, filename, tokenizer) {
var _src_len = length(src)
var cp = []
var _i = 0
while (_i < _src_len) {
push(cp, codepoint(src[_i]))
_i = _i + 1
}
// ============================================================
// Parser Cursor
@@ -22,6 +9,9 @@ var parse = function(tokens, src, filename, tokenizer) {
var tok = null
var got_lf = false
var prev_tok = null
var _control_depth = 0
var _control_type = null
var _expecting_body = false
var advance = function() {
var t = null
@@ -75,7 +65,7 @@ var parse = function(tokens, src, filename, tokenizer) {
var errors = []
var error_count = 0
var function_nr = 1
var fn_counter = 1
var ast_node = function(kind, token) {
return {
@@ -103,14 +93,18 @@ var parse = function(tokens, src, filename, tokenizer) {
})
}
var _keywords = {
"if": true, in: true, "do": true, go: true,
"var": true, def: true, "for": true,
"else": true, "this": true, "null": true, "true": true,
"false": true, "while": true, "break": true,
"return": true, "delete": true,
disrupt: true, "function": true, "continue": true,
disruption: true
}
var is_keyword = function(kind) {
return kind == "if" || kind == "in" || kind == "do" || kind == "go" ||
kind == "var" || kind == "def" || kind == "for" ||
kind == "else" || kind == "this" || kind == "null" || kind == "true" ||
kind == "false" || kind == "while" || kind == "break" ||
kind == "return" || kind == "delete" ||
kind == "disrupt" || kind == "function" || kind == "continue" ||
kind == "disruption"
return _keywords[kind] == true
}
// ============================================================
@@ -165,17 +159,18 @@ var parse = function(tokens, src, filename, tokenizer) {
var params = null
var param = null
var rpos = 0
var pattern_str = ""
var flags = ""
var pattern_parts = null
var flags_parts = null
var tv = null
var has_interp = false
var ti = 0
var tpl_list = null
var fmt = null
var fmt_parts = null
var idx = 0
var tvi = 0
var tvlen = 0
var depth = 0
var expr_parts = null
var expr_str = null
var tc = null
var tq = null
@@ -184,6 +179,9 @@ var parse = function(tokens, src, filename, tokenizer) {
var sub_ast = null
var sub_stmt = null
var sub_expr = null
var meth_old_cd = 0
var meth_old_ct = null
var meth_old_eb = false
if (k == "number") {
node = ast_node("number", start)
@@ -218,52 +216,53 @@ var parse = function(tokens, src, filename, tokenizer) {
node = ast_node("text literal", start)
tpl_list = []
node.list = tpl_list
fmt = ""
fmt_parts = []
idx = 0
tvi = 0
tvlen = length(tv)
while (tvi < tvlen) {
if (tv[tvi] == "\\" && tvi + 1 < tvlen) {
esc_ch = tv[tvi + 1]
if (esc_ch == "n") { fmt = fmt + "\n" }
else if (esc_ch == "t") { fmt = fmt + "\t" }
else if (esc_ch == "r") { fmt = fmt + "\r" }
else if (esc_ch == "\\") { fmt = fmt + "\\" }
else if (esc_ch == "`") { fmt = fmt + "`" }
else if (esc_ch == "$") { fmt = fmt + "$" }
else if (esc_ch == "0") { fmt = fmt + character(0) }
else { fmt = fmt + esc_ch }
if (esc_ch == "n") { push(fmt_parts, "\n") }
else if (esc_ch == "t") { push(fmt_parts, "\t") }
else if (esc_ch == "r") { push(fmt_parts, "\r") }
else if (esc_ch == "\\") { push(fmt_parts, "\\") }
else if (esc_ch == "`") { push(fmt_parts, "`") }
else if (esc_ch == "$") { push(fmt_parts, "$") }
else if (esc_ch == "0") { push(fmt_parts, character(0)) }
else { push(fmt_parts, esc_ch) }
tvi = tvi + 2
} else if (tv[tvi] == "$" && tvi + 1 < tvlen && tv[tvi + 1] == "{") {
tvi = tvi + 2
depth = 1
expr_str = ""
expr_parts = []
while (tvi < tvlen && depth > 0) {
tc = tv[tvi]
if (tc == "{") { depth = depth + 1; expr_str = expr_str + tc; tvi = tvi + 1 }
if (tc == "{") { depth = depth + 1; push(expr_parts, tc); tvi = tvi + 1 }
else if (tc == "}") {
depth = depth - 1
if (depth > 0) { expr_str = expr_str + tc }
if (depth > 0) { push(expr_parts, tc) }
tvi = tvi + 1
}
else if (tc == "'" || tc == "\"" || tc == "`") {
tq = tc
expr_str = expr_str + tc
push(expr_parts, tc)
tvi = tvi + 1
while (tvi < tvlen && tv[tvi] != tq) {
if (tv[tvi] == "\\" && tvi + 1 < tvlen) {
expr_str = expr_str + tv[tvi]
push(expr_parts, tv[tvi])
tvi = tvi + 1
}
expr_str = expr_str + tv[tvi]
push(expr_parts, tv[tvi])
tvi = tvi + 1
}
if (tvi < tvlen) { expr_str = expr_str + tv[tvi]; tvi = tvi + 1 }
if (tvi < tvlen) { push(expr_parts, tv[tvi]); tvi = tvi + 1 }
} else {
expr_str = expr_str + tc
push(expr_parts, tc)
tvi = tvi + 1
}
}
expr_str = text(expr_parts)
expr_tokens = tokenizer(expr_str, "<template>").tokens
sub_ast = parse(expr_tokens, expr_str, "<template>", tokenizer)
if (sub_ast != null && sub_ast.statements != null && length(sub_ast.statements) > 0) {
@@ -276,14 +275,16 @@ var parse = function(tokens, src, filename, tokenizer) {
}
push(tpl_list, sub_expr)
}
fmt = fmt + "{" + text(idx) + "}"
push(fmt_parts, "{")
push(fmt_parts, text(idx))
push(fmt_parts, "}")
idx = idx + 1
} else {
fmt = fmt + tv[tvi]
push(fmt_parts, tv[tvi])
tvi = tvi + 1
}
}
node.value = fmt
node.value = text(fmt_parts)
advance()
ast_node_end(node)
return node
@@ -390,7 +391,7 @@ var parse = function(tokens, src, filename, tokenizer) {
ast_node_end(param)
if (tok.kind == "=" || tok.kind == "|") {
advance()
param.expression = parse_expr()
param.expression = parse_assign_expr()
}
push(params, param)
} else {
@@ -404,6 +405,12 @@ var parse = function(tokens, src, filename, tokenizer) {
else if (tok.kind == "eof") parse_error(tok, "unterminated method parameter list")
if (length(params) > 4) parse_error(tok, "functions cannot have more than 4 parameters")
fn.arity = length(params)
meth_old_cd = _control_depth
meth_old_ct = _control_type
meth_old_eb = _expecting_body
_control_depth = 0
_control_type = null
_expecting_body = false
if (tok.kind == "{") {
advance()
fn.statements = parse_block_statements()
@@ -412,11 +419,19 @@ var parse = function(tokens, src, filename, tokenizer) {
} else {
parse_error(tok, "expected '{' for method body")
}
fn.function_nr = function_nr
function_nr = function_nr + 1
_control_depth = meth_old_cd
_control_type = meth_old_ct
_expecting_body = meth_old_eb
fn.function_nr = fn_counter
fn_counter = fn_counter + 1
ast_node_end(fn)
pair.right = fn
} else if (!(is_ident && (tok.kind == "," || tok.kind == "}"))) {
} else if (is_ident && (tok.kind == "," || tok.kind == "}")) {
right = ast_node("name", pair.left)
right.name = pair.left.name
ast_node_end(right)
pair.right = right
} else {
parse_error(tok, "expected ':' after property name")
}
push(list, pair)
@@ -445,24 +460,25 @@ var parse = function(tokens, src, filename, tokenizer) {
if (k == "/") {
node = ast_node("regexp", start)
rpos = tok.at + 1
pattern_str = ""
flags = ""
while (rpos < _src_len && cp[rpos] != CP_SLASH) {
if (cp[rpos] == CP_BSLASH && rpos + 1 < _src_len) {
pattern_str = pattern_str + character(cp[rpos]) + character(cp[rpos + 1])
pattern_parts = []
flags_parts = []
while (rpos < _src_len && src[rpos] != "/") {
if (src[rpos] == "\\" && rpos + 1 < _src_len) {
push(pattern_parts, src[rpos])
push(pattern_parts, src[rpos + 1])
rpos = rpos + 2
} else {
pattern_str = pattern_str + character(cp[rpos])
push(pattern_parts, src[rpos])
rpos = rpos + 1
}
}
if (rpos < _src_len) rpos = rpos + 1
while (rpos < _src_len && is_alpha(cp[rpos])) {
flags = flags + character(cp[rpos])
while (rpos < _src_len && is_letter(src[rpos])) {
push(flags_parts, src[rpos])
rpos = rpos + 1
}
node.pattern = pattern_str
if (length(flags) > 0) node.flags = flags
node.pattern = text(pattern_parts)
if (length(flags_parts) > 0) node.flags = text(flags_parts)
// Skip all tokens consumed by the regex re-scan
while (true) {
advance()
@@ -488,6 +504,9 @@ var parse = function(tokens, src, filename, tokenizer) {
var index = null
var arg = null
var args_list = null
var one_node = null
var binop_node = null
var op = null
if (node == null) return null
while (true) {
start = tok
@@ -563,6 +582,10 @@ var parse = function(tokens, src, filename, tokenizer) {
var node = null
var expr = null
var k = tok.kind
var operand = null
var one_node = null
var binop_node = null
var op = null
if (k == "!") {
advance()
node = ast_node("!", start)
@@ -591,19 +614,22 @@ var parse = function(tokens, src, filename, tokenizer) {
ast_node_end(node)
return node
}
if (k == "++") {
if (k == "++" || k == "--") {
advance()
node = ast_node("++", start)
node.expression = parse_unary()
node.postfix = false
ast_node_end(node)
return node
}
if (k == "--") {
advance()
node = ast_node("--", start)
node.expression = parse_unary()
node.postfix = false
operand = parse_unary()
one_node = ast_node("number", start)
one_node.number = 1
one_node.value = "1"
ast_node_end(one_node)
op = "+"
if (k == "--") op = "-"
binop_node = ast_node(op, start)
binop_node.left = operand
binop_node.right = one_node
ast_node_end(binop_node)
node = ast_node("assign", start)
node.left = operand
node.right = binop_node
ast_node_end(node)
return node
}
@@ -688,6 +714,13 @@ var parse = function(tokens, src, filename, tokenizer) {
"&&=": "&&=", "||=": "||="
}
var compound_binop = {
"+=": "+", "-=": "-", "*=": "*", "/=": "/", "%=": "%",
"<<=": "<<", ">>=": ">>", ">>>=": ">>>",
"&=": "&", "^=": "^", "|=": "|", "**=": "**",
"&&=": "&&", "||=": "||"
}
parse_assign = function(unused) {
var left_node = parse_ternary()
var start = null
@@ -696,6 +729,8 @@ var parse = function(tokens, src, filename, tokenizer) {
var node = null
var left_kind = null
var right_kind = null
var binop = null
var binop_node = null
if (left_node == null) return null
start = tok
kind = assign_ops[tok.kind]
@@ -708,12 +743,23 @@ var parse = function(tokens, src, filename, tokenizer) {
advance()
right_node = parse_assign()
node = ast_node(kind, start)
node.left = left_node
node.right = right_node
if (left_node.kind == "[" && left_node.right == null) node.push = true
if (right_node != null && right_node.kind == "[" && right_node.right == null) node.pop = true
binop = compound_binop[kind]
if (binop != null) {
binop_node = ast_node(binop, start)
binop_node.left = left_node
binop_node.right = right_node
ast_node_end(binop_node)
node = ast_node("assign", start)
node.left = left_node
node.right = binop_node
} else {
node = ast_node(kind, start)
node.left = left_node
node.right = right_node
if (left_node.kind == "[" && left_node.right == null) node.push = true
if (right_node != null && right_node.kind == "[" && right_node.right == null) node.pop = true
}
ast_node_end(node)
return node
@@ -791,9 +837,10 @@ var parse = function(tokens, src, filename, tokenizer) {
var param = null
var prev_names = null
var pname = null
var dup = false
var j = 0
var old_dis = 0
var old_cd = _control_depth
var old_ct = _control_type
var old_eb = _expecting_body
if (in_disruption) {
parse_error(tok, "cannot define function inside disruption clause")
@@ -815,13 +862,7 @@ var parse = function(tokens, src, filename, tokenizer) {
param = ast_node("name", tok)
param.name = tok.value
pname = tok.value
dup = false
j = 0
while (j < length(prev_names)) {
if (prev_names[j] == pname) { dup = true; break }
j = j + 1
}
if (dup) parse_error(tok, "duplicate parameter name '" + pname + "'")
if (find(prev_names, pname) != null) parse_error(tok, "duplicate parameter name '" + pname + "'")
push(prev_names, pname)
advance()
ast_node_end(param)
@@ -846,6 +887,9 @@ var parse = function(tokens, src, filename, tokenizer) {
if (length(params) > 4) parse_error(tok, "functions cannot have more than 4 parameters")
node.arity = length(params)
_control_depth = 0
_control_type = null
_expecting_body = false
if (tok.kind == "{") {
advance()
stmts = parse_block_statements()
@@ -871,8 +915,11 @@ var parse = function(tokens, src, filename, tokenizer) {
}
}
node.function_nr = function_nr
function_nr = function_nr + 1
_control_depth = old_cd
_control_type = old_ct
_expecting_body = old_eb
node.function_nr = fn_counter
fn_counter = fn_counter + 1
ast_node_end(node)
return node
}
@@ -887,8 +934,9 @@ var parse = function(tokens, src, filename, tokenizer) {
var expr = null
var prev_names = null
var pname = null
var dup = false
var j = 0
var old_cd = _control_depth
var old_ct = _control_type
var old_eb = _expecting_body
node.arrow = true
if (in_disruption) {
@@ -911,13 +959,7 @@ var parse = function(tokens, src, filename, tokenizer) {
param = ast_node("name", tok)
param.name = tok.value
pname = tok.value
dup = false
j = 0
while (j < length(prev_names)) {
if (prev_names[j] == pname) { dup = true; break }
j = j + 1
}
if (dup) parse_error(tok, "duplicate parameter name '" + pname + "'")
if (find(prev_names, pname) != null) parse_error(tok, "duplicate parameter name '" + pname + "'")
push(prev_names, pname)
advance()
ast_node_end(param)
@@ -945,6 +987,9 @@ var parse = function(tokens, src, filename, tokenizer) {
advance()
}
_control_depth = 0
_control_type = null
_expecting_body = false
if (tok.kind == "{") {
advance()
stmts = parse_block_statements()
@@ -960,8 +1005,11 @@ var parse = function(tokens, src, filename, tokenizer) {
node.statements = stmts
}
node.function_nr = function_nr
function_nr = function_nr + 1
_control_depth = old_cd
_control_type = old_ct
_expecting_body = old_eb
node.function_nr = fn_counter
fn_counter = fn_counter + 1
ast_node_end(node)
return node
}
@@ -991,8 +1039,25 @@ var parse = function(tokens, src, filename, tokenizer) {
var elif = null
var p1_tok = null
var labeled_stmt = null
var depth = 0
var saved_ct = null
var saved_cd = 0
var saved_eb = false
if (k == "{") {
if (!_expecting_body) {
parse_error(start, "bare block '{ ... }' is not a valid statement; use a function, if, while, or for instead")
advance()
depth = 1
while (tok.kind != "eof" && depth > 0) {
if (tok.kind == "{") depth = depth + 1
else if (tok.kind == "}") depth = depth - 1
if (depth > 0) advance()
}
if (tok.kind == "}") advance()
return null
}
_expecting_body = false
node = ast_node("block", start)
advance()
stmts = parse_block_statements()
@@ -1003,6 +1068,9 @@ var parse = function(tokens, src, filename, tokenizer) {
}
if (k == "var" || k == "def") {
if (_control_depth > 0) {
parse_error(start, "'" + k + "' declarations must appear at function body level, not inside '" + _control_type + "'; move this declaration before the '" + _control_type + "' statement")
}
kind_name = k
is_def = (k == "def")
advance()
@@ -1029,6 +1097,8 @@ var parse = function(tokens, src, filename, tokenizer) {
}
} else if (is_def) {
parse_error(start, "missing initializer for constant '" + var_name + "'")
} else {
parse_error(start, "'var' declarations must be initialized; use 'var " + var_name + " = null' if no value is needed")
}
ast_node_end(node)
push(decls, node)
@@ -1057,6 +1127,11 @@ var parse = function(tokens, src, filename, tokenizer) {
else parse_error(tok, "expected ')' after if condition")
then_stmts = []
node.then = then_stmts
saved_ct = _control_type
saved_cd = _control_depth
_control_type = "if"
_control_depth = _control_depth + 1
_expecting_body = true
body = parse_statement()
if (body != null) push(then_stmts, body)
else_ifs = []
@@ -1064,15 +1139,22 @@ var parse = function(tokens, src, filename, tokenizer) {
if (tok.kind == "else") {
advance()
if (tok.kind == "if") {
_control_depth = saved_cd
_control_type = saved_ct
elif = parse_statement()
if (elif != null) push(else_ifs, elif)
ast_node_end(node)
return node
} else {
else_stmts = []
node.else = else_stmts
_expecting_body = true
body = parse_statement()
if (body != null) push(else_stmts, body)
}
}
_control_depth = saved_cd
_control_type = saved_ct
ast_node_end(node)
return node
}
@@ -1088,8 +1170,15 @@ var parse = function(tokens, src, filename, tokenizer) {
else parse_error(tok, "expected ')' after while condition")
stmts = []
node.statements = stmts
saved_ct = _control_type
saved_cd = _control_depth
_control_type = "while"
_control_depth = _control_depth + 1
_expecting_body = true
body = parse_statement()
if (body != null) push(stmts, body)
_control_depth = saved_cd
_control_type = saved_ct
ast_node_end(node)
return node
}
@@ -1099,8 +1188,15 @@ var parse = function(tokens, src, filename, tokenizer) {
advance()
stmts = []
node.statements = stmts
saved_ct = _control_type
saved_cd = _control_depth
_control_type = "do"
_control_depth = _control_depth + 1
_expecting_body = true
body = parse_statement()
if (body != null) push(stmts, body)
_control_depth = saved_cd
_control_type = saved_ct
if (tok.kind == "while") advance()
else parse_error(tok, "expected 'while' after do body")
if (tok.kind == "(") advance()
@@ -1121,6 +1217,7 @@ var parse = function(tokens, src, filename, tokenizer) {
else parse_error(tok, "expected '(' after for")
if (tok.kind != ";") {
if (tok.kind == "var" || tok.kind == "def") {
parse_error(tok, "'" + tok.kind + "' declarations cannot appear in the for initializer; declare variables before the for loop")
init = parse_statement()
node.init = init
} else {
@@ -1144,8 +1241,15 @@ var parse = function(tokens, src, filename, tokenizer) {
else parse_error(tok, "expected ')' after for clauses")
stmts = []
node.statements = stmts
saved_ct = _control_type
saved_cd = _control_depth
_control_type = "for"
_control_depth = _control_depth + 1
_expecting_body = true
body = parse_statement()
if (body != null) push(stmts, body)
_control_depth = saved_cd
_control_type = saved_ct
ast_node_end(node)
return node
}
@@ -1216,6 +1320,36 @@ var parse = function(tokens, src, filename, tokenizer) {
}
if (k == "name") {
if (tok.value == "try" || tok.value == "catch" || tok.value == "finally") {
parse_error(start, "'" + tok.value + "' is not supported; use disrupt/disruption instead")
sync_to_statement()
return null
}
if (tok.value == "throw") {
parse_error(start, "'throw' is not supported; use disrupt instead")
sync_to_statement()
return null
}
if (tok.value == "class") {
parse_error(start, "'class' is not supported; use meme()/proto() instead")
sync_to_statement()
return null
}
if (tok.value == "new") {
parse_error(start, "'new' is not supported; use meme()/proto() instead")
sync_to_statement()
return null
}
if (tok.value == "switch" || tok.value == "case") {
parse_error(start, "'" + tok.value + "' is not supported; use if/else instead")
sync_to_statement()
return null
}
if (tok.value == "let" || tok.value == "const") {
parse_error(start, "'" + tok.value + "' is not supported; use var/def instead")
sync_to_statement()
return null
}
p1_tok = peek_ahead(1)
if (p1_tok.kind == ":") {
node = ast_node("label", start)
@@ -1345,12 +1479,7 @@ var parse = function(tokens, src, filename, tokenizer) {
}
var sem_add_intrinsic = function(name) {
var i = 0
while (i < length(intrinsics)) {
if (intrinsics[i] == name) return null
i = i + 1
}
push(intrinsics, name)
if (find(intrinsics, name) == null) push(intrinsics, name)
}
var functino_names = {
@@ -1364,12 +1493,31 @@ var parse = function(tokens, src, filename, tokenizer) {
return functino_names[name] == true
}
var sem_propagate_vars = function(parent, child) {
var i = 0
while (i < length(child.vars)) {
push(parent.vars, child.vars[i])
i = i + 1
var derive_type_tag = function(expr) {
if (expr == null) return null
var k = expr.kind
if (k == "array") return "array"
if (k == "record") return "record"
if (k == "function") return "function"
if (k == "text" || k == "text literal") return "text"
if (k == "number") {
if (is_integer(expr.number)) return "integer"
return "number"
}
if (k == "true" || k == "false") return "logical"
if (k == "null") return "null"
return null
}
var _assign_kinds = {
assign: true, "+=": true, "-=": true, "*=": true, "/=": true, "%=": true,
"<<=": true, ">>=": true, ">>>=": true,
"&=": true, "^=": true, "|=": true, "**=": true,
"&&=": true, "||=": true
}
var sem_propagate_vars = function(parent, child) {
parent.vars = array(parent.vars, child.vars)
}
var sem_build_scope_record = function(scope) {
@@ -1385,7 +1533,8 @@ var parse = function(tokens, src, filename, tokenizer) {
function_nr: v.function_nr,
nr_uses: v.nr_uses,
closure: v.closure == 1,
level: 0
level: 0,
type_tag: v.type_tag
}
slots = slots + 1
if (v.closure) close_slots = close_slots + 1
@@ -1487,10 +1636,7 @@ var parse = function(tokens, src, filename, tokenizer) {
var def_val = null
var sr = null
if (kind == "assign" || kind == "+=" || kind == "-=" || kind == "*=" ||
kind == "/=" || kind == "%=" || kind == "<<=" || kind == ">>=" ||
kind == ">>>=" || kind == "&=" || kind == "^=" || kind == "|=" ||
kind == "**=" || kind == "&&=" || kind == "||=") {
if (_assign_kinds[kind] == true) {
sem_check_assign_target(scope, expr.left)
sem_check_expr(scope, expr.right)
return null
@@ -1515,6 +1661,21 @@ var parse = function(tokens, src, filename, tokenizer) {
operand.level = -1
}
}
} else if (operand != null) {
sem_check_assign_target(scope, operand)
}
return null
}
if (kind == "[") {
sem_check_expr(scope, expr.left)
sem_check_expr(scope, expr.right)
if (expr.right != null) {
if (expr.right.kind == "number" && is_integer(expr.right.number)) {
expr.access_kind = "index"
} else if (expr.right.kind == "text") {
expr.access_kind = "field"
}
}
return null
}
@@ -1525,7 +1686,7 @@ var parse = function(tokens, src, filename, tokenizer) {
kind == "&&" || kind == "||" || kind == "&" ||
kind == "|" || kind == "^" || kind == "<<" || kind == ">>" ||
kind == ">>>" || kind == "**" || kind == "in" ||
kind == "." || kind == "[") {
kind == ".") {
sem_check_expr(scope, expr.left)
sem_check_expr(scope, expr.right)
return null
@@ -1634,6 +1795,7 @@ var parse = function(tokens, src, filename, tokenizer) {
if (r.level > 0) r.v.closure = 1
} else {
expr.level = -1
expr.intrinsic = true
sem_add_intrinsic(name)
}
}
@@ -1657,6 +1819,7 @@ var parse = function(tokens, src, filename, tokenizer) {
var pname = null
var def_val = null
var sr = null
var tt = null
if (kind == "var_list") {
i = 0
@@ -1696,6 +1859,13 @@ var parse = function(tokens, src, filename, tokenizer) {
}
}
sem_check_expr(scope, stmt.right)
if (name != null) {
tt = derive_type_tag(stmt.right)
if (tt != null) {
existing = sem_find_var(scope, name)
if (existing != null) existing.type_tag = tt
}
}
return null
}
@@ -1773,6 +1943,9 @@ var parse = function(tokens, src, filename, tokenizer) {
if (kind == "return" || kind == "go") {
sem_check_expr(scope, stmt.expression)
if (stmt.expression != null && stmt.expression.kind == "(") {
stmt.tail = true
}
return null
}

Binary file not shown.

View File

@@ -128,7 +128,7 @@ struct listfiles_ctx {
static void listfiles_cb(const char *path, void *userdata) {
struct listfiles_ctx *ctx = (struct listfiles_ctx*)userdata;
JS_SetPropertyUint32(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path));
JS_SetPropertyNumber(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path));
}
JSC_SCALL(file_listfiles,

View File

@@ -79,7 +79,7 @@ static void json_decode_array_value(json_decoder *decoder, int pos, json_value v
case kJSONString: jsval = JS_NewString(ctx->js, value.data.stringval); break;
default: jsval = JS_NULL; break;
}
JS_SetPropertyUint32(ctx->js, container, pos, jsval);
JS_SetPropertyNumber(ctx->js, container, pos, jsval);
}
// --- JSON Encoder Context ---
@@ -128,7 +128,7 @@ static void encode_js_array(json_encoder *enc, JSContext *js, JSValue arr) {
JS_FreeValue(js, lenVal);
for (int i = 0; i < len; i++) {
enc->addArrayMember(enc);
JSValue val = JS_GetPropertyUint32(js, arr, i);
JSValue val = JS_GetPropertyNumber(js, arr, i);
encode_js_value(enc, js, val);
JS_FreeValue(js, val);
}

View File

@@ -57,7 +57,7 @@ static void boards_list_cb(PDBoardsList *boards, const char *errorMessage) {
boards->boards[i].boardID ? JS_NewString(g_scoreboard_js, boards->boards[i].boardID) : JS_NULL);
JS_SetPropertyStr(g_scoreboard_js, board, "name",
boards->boards[i].name ? JS_NewString(g_scoreboard_js, boards->boards[i].name) : JS_NULL);
JS_SetPropertyUint32(g_scoreboard_js, arr, i, board);
JS_SetPropertyNumber(g_scoreboard_js, arr, i, board);
}
args[0] = arr;
} else {
@@ -83,7 +83,7 @@ static void scores_cb(PDScoresList *scores, const char *errorMessage) {
JS_SetPropertyStr(g_scoreboard_js, obj, "limit", JS_NewInt32(g_scoreboard_js, scores->limit));
JSValue arr = JS_NewArray(g_scoreboard_js);
for (unsigned int i = 0; i < scores->count; i++) {
JS_SetPropertyUint32(g_scoreboard_js, arr, i, score_to_js(g_scoreboard_js, &scores->scores[i]));
JS_SetPropertyNumber(g_scoreboard_js, arr, i, score_to_js(g_scoreboard_js, &scores->scores[i]));
}
JS_SetPropertyStr(g_scoreboard_js, obj, "scores", arr);
args[0] = obj;

206
pronto.cm
View File

@@ -4,9 +4,9 @@
// Time is in seconds.
function make_reason(factory, excuse, evidence) {
def reason = Error(`pronto.${factory}${excuse ? ': ' + excuse : ''}`)
reason.evidence = evidence
return reason
var msg = 'pronto.' + factory
if (excuse) msg = msg + ': ' + excuse
return { message: msg, evidence: evidence }
}
function is_requestor(fn) {
@@ -14,21 +14,27 @@ function is_requestor(fn) {
}
function check_requestors(list, factory) {
if (!is_array(list) || some(list, r => !is_requestor(r)))
throw make_reason(factory, 'Bad requestor array.', list)
if (!is_array(list) || some(list, r => !is_requestor(r))) {
print(make_reason(factory, 'Bad requestor array.', list).message + '\n')
disrupt
}
}
function check_callback(cb, factory) {
if (!is_function(cb) || length(cb) != 2)
throw make_reason(factory, 'Not a callback.', cb)
if (!is_function(cb) || length(cb) != 2) {
print(make_reason(factory, 'Not a callback.', cb).message + '\n')
disrupt
}
}
// fallback(requestor_array)
// Tries each requestor in order until one succeeds.
function fallback(requestor_array) {
def factory = 'fallback'
if (!is_array(requestor_array) || length(requestor_array) == 0)
throw make_reason(factory, 'Empty requestor array.')
if (!is_array(requestor_array) || length(requestor_array) == 0) {
print(make_reason(factory, 'Empty requestor array.').message + '\n')
disrupt
}
check_requestors(requestor_array, factory)
return function fallback_requestor(callback, value) {
@@ -38,9 +44,11 @@ function fallback(requestor_array) {
var cancelled = false
function cancel(reason) {
var _c = null
cancelled = true
if (current_cancel) {
try { current_cancel(reason) } catch (_) {}
_c = function() { current_cancel(reason) } disruption {}
_c()
current_cancel = null
}
}
@@ -53,9 +61,9 @@ function fallback(requestor_array) {
}
def requestor = requestor_array[index]
index += 1
index = index + 1
try {
var _run = function() {
current_cancel = requestor(function(val, reason) {
if (cancelled) return
current_cancel = null
@@ -65,9 +73,10 @@ function fallback(requestor_array) {
try_next()
}
}, value)
} catch (ex) {
} disruption {
try_next()
}
_run()
}
try_next()
@@ -79,25 +88,32 @@ function fallback(requestor_array) {
// Runs requestors in parallel, collecting all results.
function parallel(requestor_array, throttle, need) {
def factory = 'parallel'
if (!is_array(requestor_array))
throw make_reason(factory, 'Not an array.', requestor_array)
if (!is_array(requestor_array)) {
print(make_reason(factory, 'Not an array.', requestor_array).message + '\n')
disrupt
}
check_requestors(requestor_array, factory)
def length = length(requestor_array)
if (length == 0)
def len = length(requestor_array)
if (len == 0)
return function(callback, value) { callback([]) }
if (need == null) need = length
if (!is_number(need) || need < 0 || need > length)
throw make_reason(factory, 'Bad need.', need)
var _need = need
if (_need == null) _need = len
if (!is_number(_need) || _need < 0 || _need > len) {
print(make_reason(factory, 'Bad need.', _need).message + '\n')
disrupt
}
if (throttle != null && (!is_number(throttle) || throttle < 1))
throw make_reason(factory, 'Bad throttle.', throttle)
if (throttle != null && (!is_number(throttle) || throttle < 1)) {
print(make_reason(factory, 'Bad throttle.', throttle).message + '\n')
disrupt
}
return function parallel_requestor(callback, value) {
check_callback(callback, factory)
def results = array(length)
def cancel_list = array(length)
def results = array(len)
def cancel_list = array(len)
var next_index = 0
var successes = 0
var failures = 0
@@ -107,33 +123,34 @@ function parallel(requestor_array, throttle, need) {
if (finished) return
finished = true
arrfor(cancel_list, c => {
try { if (is_function(c)) c(reason) } catch (_) {}
var _c = function() { if (is_function(c)) c(reason) } disruption {}
_c()
})
}
function start_one() {
if (finished || next_index >= length) return
if (finished || next_index >= len) return
def idx = next_index
next_index += 1
next_index = next_index + 1
def requestor = requestor_array[idx]
try {
var _run = function() {
cancel_list[idx] = requestor(function(val, reason) {
if (finished) return
cancel_list[idx] = null
if (val != null) {
results[idx] = val
successes += 1
if (successes >= need) {
successes = successes + 1
if (successes >= _need) {
finished = true
cancel(make_reason(factory, 'Finished.'))
callback(results)
return
}
} else {
failures += 1
if (failures > length - need) {
failures = failures + 1
if (failures > len - _need) {
cancel(reason)
callback(null, reason || make_reason(factory, 'Too many failures.'))
return
@@ -142,20 +159,21 @@ function parallel(requestor_array, throttle, need) {
start_one()
}, value)
} catch (ex) {
failures += 1
if (failures > length - need) {
cancel(ex)
callback(null, ex)
} disruption {
failures = failures + 1
if (failures > len - _need) {
cancel(make_reason(factory, 'Requestor threw.'))
callback(null, make_reason(factory, 'Requestor threw.'))
return
}
start_one()
}
_run()
}
def concurrent = throttle ? min(throttle, length) : length
for (var i = 0; i < concurrent; i++) start_one()
def concurrent = throttle ? min(throttle, len) : len
var i = 0
while (i < concurrent) { start_one(); i = i + 1 }
return cancel
}
@@ -165,22 +183,29 @@ function parallel(requestor_array, throttle, need) {
// Runs requestors in parallel, returns first success(es).
function race(requestor_array, throttle, need) {
def factory = 'race'
if (!is_array(requestor_array) || length(requestor_array) == 0)
throw make_reason(factory, 'Empty requestor array.')
if (!is_array(requestor_array) || length(requestor_array) == 0) {
print(make_reason(factory, 'Empty requestor array.').message + '\n')
disrupt
}
check_requestors(requestor_array, factory)
def length = length(requestor_array)
if (need == null) need = 1
if (!is_number(need) || need < 1 || need > length)
throw make_reason(factory, 'Bad need.', need)
def len = length(requestor_array)
var _need = need
if (_need == null) _need = 1
if (!is_number(_need) || _need < 1 || _need > len) {
print(make_reason(factory, 'Bad need.', _need).message + '\n')
disrupt
}
if (throttle != null && (!is_number(throttle) || throttle < 1))
throw make_reason(factory, 'Bad throttle.', throttle)
if (throttle != null && (!is_number(throttle) || throttle < 1)) {
print(make_reason(factory, 'Bad throttle.', throttle).message + '\n')
disrupt
}
return function race_requestor(callback, value) {
check_callback(callback, factory)
def results = array(length)
def cancel_list = array(length)
def results = array(len)
def cancel_list = array(len)
var next_index = 0
var successes = 0
var failures = 0
@@ -190,27 +215,28 @@ function race(requestor_array, throttle, need) {
if (finished) return
finished = true
arrfor(cancel_list, c => {
try { if (is_function(c)) c(reason) } catch (_) {}
var _c = function() { if (is_function(c)) c(reason) } disruption {}
_c()
})
}
function start_one() {
if (finished || next_index >= length) return
if (finished || next_index >= len) return
def idx = next_index
next_index += 1
next_index = next_index + 1
def requestor = requestor_array[idx]
try {
var _run = function() {
cancel_list[idx] = requestor(function(val, reason) {
if (finished) return
cancel_list[idx] = null
if (val != null) {
results[idx] = val
successes += 1
if (successes >= need) {
successes = successes + 1
if (successes >= _need) {
cancel(make_reason(factory, 'Winner.'))
if (need == 1) {
if (_need == 1) {
callback(val)
} else {
callback(results)
@@ -218,8 +244,8 @@ function race(requestor_array, throttle, need) {
return
}
} else {
failures += 1
if (failures > length - need) {
failures = failures + 1
if (failures > len - _need) {
cancel(reason)
callback(null, reason || make_reason(factory, 'All failed.'))
return
@@ -228,19 +254,21 @@ function race(requestor_array, throttle, need) {
start_one()
}, value)
} catch (ex) {
failures += 1
if (failures > length - need) {
cancel(ex)
callback(null, ex)
} disruption {
failures = failures + 1
if (failures > len - _need) {
cancel(make_reason(factory, 'Requestor threw.'))
callback(null, make_reason(factory, 'Requestor threw.'))
return
}
start_one()
}
_run()
}
def concurrent = throttle ? min(throttle, length) : length
for (var i = 0; i < concurrent; i++) start_one()
def concurrent = throttle ? min(throttle, len) : len
var i = 0
while (i < concurrent) { start_one(); i = i + 1 }
return cancel
}
@@ -250,8 +278,10 @@ function race(requestor_array, throttle, need) {
// Runs requestors one at a time, passing result to next.
function sequence(requestor_array) {
def factory = 'sequence'
if (!is_array(requestor_array))
throw make_reason(factory, 'Not an array.', requestor_array)
if (!is_array(requestor_array)) {
print(make_reason(factory, 'Not an array.', requestor_array).message + '\n')
disrupt
}
check_requestors(requestor_array, factory)
if (length(requestor_array) == 0)
@@ -264,9 +294,11 @@ function sequence(requestor_array) {
var cancelled = false
function cancel(reason) {
var _c = null
cancelled = true
if (current_cancel) {
try { current_cancel(reason) } catch (_) {}
_c = function() { current_cancel(reason) } disruption {}
_c()
current_cancel = null
}
}
@@ -279,9 +311,9 @@ function sequence(requestor_array) {
}
def requestor = requestor_array[index]
index += 1
index = index + 1
try {
var _run = function() {
current_cancel = requestor(function(result, reason) {
if (cancelled) return
current_cancel = null
@@ -291,9 +323,10 @@ function sequence(requestor_array) {
run_next(result)
}
}, val)
} catch (ex) {
callback(null, ex)
} disruption {
callback(null, make_reason(factory, 'Requestor threw.'))
}
_run()
}
run_next(value)
@@ -305,26 +338,29 @@ function sequence(requestor_array) {
// Converts a unary function into a requestor.
function requestorize(unary) {
def factory = 'requestorize'
if (!is_function(unary))
throw make_reason(factory, 'Not a function.', unary)
if (!is_function(unary)) {
print(make_reason(factory, 'Not a function.', unary).message + '\n')
disrupt
}
return function requestorized(callback, value) {
check_callback(callback, factory)
try {
var _run = function() {
def result = unary(value)
callback(result == null ? true : result)
} catch (ex) {
callback(null, ex)
} disruption {
callback(null, make_reason(factory, 'Function threw.'))
}
_run()
}
}
return {
fallback,
parallel,
race,
sequence,
requestorize,
is_requestor,
check_callback
fallback: fallback,
parallel: parallel,
race: race,
sequence: sequence,
requestorize: requestorize,
is_requestor: is_requestor,
check_callback: check_callback
}

18
qbe.ce Normal file
View File

@@ -0,0 +1,18 @@
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
var qbe_macros = use("qbe")
var qbe_emit = use("qbe_emit")
var filename = args[0]
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
var optimized = streamline(compiled)
var il = qbe_emit(optimized, qbe_macros)
print(il)

364
qbe.cm
View File

@@ -13,6 +13,11 @@ def js_true = 35
def js_exception = 15
def js_empty_text = 27
// Shared closure vars for functions with >4 params
var _qop = null
var _qop2 = null
var _qflags = null
def int32_min = -2147483648
def int32_max = 2147483647
def mantissa_mask = 4503599627370495
@@ -398,18 +403,20 @@ var mod = function(p, ctx, a, b) {
// ============================================================
// Helper: generate comparison for a given op string and int comparison QBE op
// null_true: whether null==null returns true (eq, le, ge) or false (ne, lt, gt)
var cmp = function(p, ctx, a, b, int_cmp_op, float_cmp_op_id, is_eq, is_ne, null_true) {
// reads _qflags = {int_cmp_op, float_id, is_eq, is_ne, null_true} from closure
var cmp = function(p, ctx, a, b) {
var int_cmp_op = _qflags.int_cmp_op
var float_cmp_op_id = _qflags.float_id
var eq_only = 0
if (is_eq || is_ne) {
var mismatch_val = js_false
var null_val = js_false
if (_qflags.is_eq || _qflags.is_ne) {
eq_only = 1
}
var mismatch_val = js_false
if (is_ne) {
if (_qflags.is_ne) {
mismatch_val = js_true
}
var null_val = js_false
if (null_true) {
if (_qflags.null_true) {
null_val = js_true
}
return `@${p}.start
@@ -485,27 +492,32 @@ var cmp = function(p, ctx, a, b, int_cmp_op, float_cmp_op_id, is_eq, is_ne, null
// MACH_EQ=0, NEQ=1, LT=2, LE=3, GT=4, GE=5
// null_true: eq, le, ge return true for null==null; ne, lt, gt return false
var eq = function(p, ctx, a, b) {
return cmp(p, ctx, a, b, "ceqw", 0, true, false, true)
_qflags = {int_cmp_op: "ceqw", float_id: 0, is_eq: true, is_ne: false, null_true: true}
return cmp(p, ctx, a, b)
}
var ne = function(p, ctx, a, b) {
return cmp(p, ctx, a, b, "cnew", 1, false, true, false)
_qflags = {int_cmp_op: "cnew", float_id: 1, is_eq: false, is_ne: true, null_true: false}
return cmp(p, ctx, a, b)
}
var lt = function(p, ctx, a, b) {
return cmp(p, ctx, a, b, "csltw", 2, false, false, false)
_qflags = {int_cmp_op: "csltw", float_id: 2, is_eq: false, is_ne: false, null_true: false}
return cmp(p, ctx, a, b)
}
var le = function(p, ctx, a, b) {
return cmp(p, ctx, a, b, "cslew", 3, false, false, true)
_qflags = {int_cmp_op: "cslew", float_id: 3, is_eq: false, is_ne: false, null_true: true}
return cmp(p, ctx, a, b)
}
var gt = function(p, ctx, a, b) {
return cmp(p, ctx, a, b, "csgtw", 4, false, false, false)
_qflags = {int_cmp_op: "csgtw", float_id: 4, is_eq: false, is_ne: false, null_true: false}
return cmp(p, ctx, a, b)
}
var ge = function(p, ctx, a, b) {
return cmp(p, ctx, a, b, "csgew", 5, false, false, true)
_qflags = {int_cmp_op: "csgew", float_id: 5, is_eq: false, is_ne: false, null_true: true}
}
// ============================================================
@@ -627,7 +639,9 @@ var bnot = function(p, ctx, v) {
// Both operands must be numeric. Int fast path, float -> convert to int32.
// ============================================================
var bitwise_op = function(p, ctx, a, b, qbe_op) {
// reads _qop from closure
var bitwise_op = function(p, ctx, a, b) {
var qbe_op = _qop
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
@@ -654,19 +668,24 @@ var bitwise_op = function(p, ctx, a, b, qbe_op) {
}
var band = function(p, ctx, a, b) {
return bitwise_op(p, ctx, a, b, "and")
_qop = "and"
return bitwise_op(p, ctx, a, b)
}
var bor = function(p, ctx, a, b) {
return bitwise_op(p, ctx, a, b, "or")
_qop = "or"
return bitwise_op(p, ctx, a, b)
}
var bxor = function(p, ctx, a, b) {
return bitwise_op(p, ctx, a, b, "xor")
_qop = "xor"
return bitwise_op(p, ctx, a, b)
}
// Shift ops: mask shift amount to 5 bits (& 31)
var shift_op = function(p, ctx, a, b, qbe_op) {
// reads _qop from closure
var shift_op = function(p, ctx, a, b) {
var qbe_op = _qop
return `@${p}.start
%${p}.at =l and ${a}, 1
%${p}.bt =l and ${b}, 1
@@ -694,15 +713,274 @@ var shift_op = function(p, ctx, a, b, qbe_op) {
}
var shl = function(p, ctx, a, b) {
return shift_op(p, ctx, a, b, "shl")
_qop = "shl"
return shift_op(p, ctx, a, b)
}
var shr = function(p, ctx, a, b) {
return shift_op(p, ctx, a, b, "sar")
_qop = "sar"
return shift_op(p, ctx, a, b)
}
var ushr = function(p, ctx, a, b) {
return shift_op(p, ctx, a, b, "shr")
_qop = "shr"
return shift_op(p, ctx, a, b)
}
// ============================================================
// Decomposed per-type-path operations
// These map directly to the new IR ops emitted by mcode.cm.
// ============================================================
// --- Arithmetic (int path) ---
// add_int: assume both operands are tagged ints. Overflow -> float.
var add_int = function(p, ctx, a, b) {
return ` %${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.sum =l add %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.sum, ${int32_min}
%${p}.hi =w csgtl %${p}.sum, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.ov, @${p}.ok
@${p}.ok
%${p}.rw =w copy %${p}.sum
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.ov
%${p}.fd =d sltof %${p}.sum
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
@${p}.done
`
}
var sub_int = function(p, ctx, a, b) {
return ` %${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.diff =l sub %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.diff, ${int32_min}
%${p}.hi =w csgtl %${p}.diff, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.ov, @${p}.ok
@${p}.ok
%${p}.rw =w copy %${p}.diff
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.ov
%${p}.fd =d sltof %${p}.diff
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
@${p}.done
`
}
var mul_int = function(p, ctx, a, b) {
return ` %${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.prod =l mul %${p}.ia, %${p}.ib
%${p}.lo =w csltl %${p}.prod, ${int32_min}
%${p}.hi =w csgtl %${p}.prod, ${int32_max}
%${p}.ov =w or %${p}.lo, %${p}.hi
jnz %${p}.ov, @${p}.ov, @${p}.ok
@${p}.ok
%${p}.rw =w copy %${p}.prod
%${p}.rext =l extuw %${p}.rw
%${p} =l shl %${p}.rext, 1
jmp @${p}.done
@${p}.ov
%${p}.fd =d sltof %${p}.prod
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fd)
@${p}.done
`
}
var div_int = function(p, ctx, a, b) {
return ` %${p}.ia =w copy 0
%${p}.tmp =l sar ${a}, 1
%${p}.ia =w copy %${p}.tmp
%${p}.ib =w copy 0
%${p}.tmp2 =l sar ${b}, 1
%${p}.ib =w copy %${p}.tmp2
%${p}.div0 =w ceqw %${p}.ib, 0
jnz %${p}.div0, @${p}.null, @${p}.chk
@${p}.null
%${p} =l copy ${js_null}
jmp @${p}.done
@${p}.chk
%${p}.rem =w rem %${p}.ia, %${p}.ib
%${p}.exact =w ceqw %${p}.rem, 0
jnz %${p}.exact, @${p}.idiv, @${p}.fdiv
@${p}.idiv
%${p}.q =w div %${p}.ia, %${p}.ib
%${p}.qext =l extuw %${p}.q
%${p} =l shl %${p}.qext, 1
jmp @${p}.done
@${p}.fdiv
%${p}.da =d swtof %${p}.ia
%${p}.db =d swtof %${p}.ib
%${p}.dr =d div %${p}.da, %${p}.db
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.dr)
@${p}.done
`
}
var mod_int = function(p, ctx, a, b) {
return ` %${p}.ia =w copy 0
%${p}.tmp =l sar ${a}, 1
%${p}.ia =w copy %${p}.tmp
%${p}.ib =w copy 0
%${p}.tmp2 =l sar ${b}, 1
%${p}.ib =w copy %${p}.tmp2
%${p}.div0 =w ceqw %${p}.ib, 0
jnz %${p}.div0, @${p}.null, @${p}.do_mod
@${p}.null
%${p} =l copy ${js_null}
jmp @${p}.done
@${p}.do_mod
%${p}.r =w rem %${p}.ia, %${p}.ib
%${p}.rext =l extuw %${p}.r
%${p} =l shl %${p}.rext, 1
@${p}.done
`
}
var neg_int = function(p, ctx, v) {
return ` %${p}.sl =l sar ${v}, 1
%${p}.iw =w copy %${p}.sl
%${p}.is_min =w ceqw %${p}.iw, ${int32_min}
jnz %${p}.is_min, @${p}.ov, @${p}.ok
@${p}.ov
%${p}.fd =d swtof %${p}.iw
%${p}.fdn =d neg %${p}.fd
%${p} =l call $__JS_NewFloat64(l ${ctx}, d %${p}.fdn)
jmp @${p}.done
@${p}.ok
%${p}.ni =w sub 0, %${p}.iw
%${p}.niext =l extuw %${p}.ni
%${p} =l shl %${p}.niext, 1
@${p}.done
`
}
// --- Arithmetic (float path) ---
var add_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_add(l ${ctx}, l ${a}, l ${b})
`
}
var sub_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_sub(l ${ctx}, l ${a}, l ${b})
`
}
var mul_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_mul(l ${ctx}, l ${a}, l ${b})
`
}
var div_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_div(l ${ctx}, l ${a}, l ${b})
`
}
var mod_float = function(p, ctx, a, b) {
return ` %${p} =l call $qbe_float_mod(l ${ctx}, l ${a}, l ${b})
`
}
var neg_float = function(p, ctx, v) {
return ` %${p} =l call $qbe_float_neg(l ${ctx}, l ${v})
`
}
// --- Text concat ---
var concat = function(p, ctx, a, b) {
return ` %${p} =l call $JS_ConcatString(l ${ctx}, l ${a}, l ${b})
`
}
// --- Comparisons (int path) ---
var cmp_int = function(p, a, b, qbe_op) {
return ` %${p}.ia =l sar ${a}, 1
%${p}.ib =l sar ${b}, 1
%${p}.iaw =w copy %${p}.ia
%${p}.ibw =w copy %${p}.ib
%${p}.cr =w ${qbe_op} %${p}.iaw, %${p}.ibw
%${p}.crext =l extuw %${p}.cr
%${p}.sh =l shl %${p}.crext, 5
%${p} =l or %${p}.sh, 3
`
}
var eq_int = function(p, ctx, a, b) { return cmp_int(p, a, b, "ceqw") }
var ne_int = function(p, ctx, a, b) { return cmp_int(p, a, b, "cnew") }
var lt_int = function(p, ctx, a, b) { return cmp_int(p, a, b, "csltw") }
var le_int = function(p, ctx, a, b) { return cmp_int(p, a, b, "cslew") }
var gt_int = function(p, ctx, a, b) { return cmp_int(p, a, b, "csgtw") }
var ge_int = function(p, ctx, a, b) { return cmp_int(p, a, b, "csgew") }
// --- Comparisons (float path) ---
// reads _qop from closure (op_id)
var cmp_float = function(p, ctx, a, b) {
var op_id = _qop
return ` %${p}.fcr =w call $qbe_float_cmp(l ${ctx}, w ${op_id}, l ${a}, l ${b})
%${p}.fcrext =l extuw %${p}.fcr
%${p}.fsh =l shl %${p}.fcrext, 5
%${p} =l or %${p}.fsh, 3
`
}
var eq_float = function(p, ctx, a, b) { _qop = 0; return cmp_float(p, ctx, a, b) }
var ne_float = function(p, ctx, a, b) { _qop = 1; return cmp_float(p, ctx, a, b) }
var lt_float = function(p, ctx, a, b) { _qop = 2; return cmp_float(p, ctx, a, b) }
var le_float = function(p, ctx, a, b) { _qop = 3; return cmp_float(p, ctx, a, b) }
var gt_float = function(p, ctx, a, b) { _qop = 4; return cmp_float(p, ctx, a, b) }
var ge_float = function(p, ctx, a, b) { _qop = 5; return cmp_float(p, ctx, a, b) }
// --- Comparisons (text path) ---
// reads _qop (qbe_op) and _qop2 (eq_only) from closure
var cmp_text = function(p, ctx, a, b) {
var qbe_op = _qop
var eq_only = _qop2
return ` %${p}.scmp =w call $js_string_compare_value(l ${ctx}, l ${a}, l ${b}, w ${eq_only})
%${p}.tcr =w ${qbe_op} %${p}.scmp, 0
%${p}.tcrext =l extuw %${p}.tcr
%${p}.tsh =l shl %${p}.tcrext, 5
%${p} =l or %${p}.tsh, 3
`
}
var eq_text = function(p, ctx, a, b) { _qop = "ceqw"; _qop2 = 1; return cmp_text(p, ctx, a, b) }
var ne_text = function(p, ctx, a, b) { _qop = "cnew"; _qop2 = 1; return cmp_text(p, ctx, a, b) }
var lt_text = function(p, ctx, a, b) { _qop = "csltw"; _qop2 = 0; return cmp_text(p, ctx, a, b) }
var le_text = function(p, ctx, a, b) { _qop = "cslew"; _qop2 = 0; return cmp_text(p, ctx, a, b) }
var gt_text = function(p, ctx, a, b) { _qop = "csgtw"; _qop2 = 0; return cmp_text(p, ctx, a, b) }
var ge_text = function(p, ctx, a, b) { _qop = "csgew"; _qop2 = 0; return cmp_text(p, ctx, a, b) }
// --- Comparisons (bool path) ---
var eq_bool = function(p, a, b) {
return ` %${p}.cr =w ceql ${a}, ${b}
%${p}.crext =l extuw %${p}.cr
%${p}.sh =l shl %${p}.crext, 5
%${p} =l or %${p}.sh, 3
`
}
var ne_bool = function(p, a, b) {
return ` %${p}.cr =w cnel ${a}, ${b}
%${p}.crext =l extuw %${p}.cr
%${p}.sh =l shl %${p}.crext, 5
%${p} =l or %${p}.sh, 3
`
}
// --- Type guard: is_identical ---
var is_identical = function(p, a, b) {
return ` %${p}.cr =w ceql ${a}, ${b}
%${p}.crext =l extuw %${p}.cr
%${p}.sh =l shl %${p}.crext, 5
%${p} =l or %${p}.sh, 3
`
}
// ============================================================
@@ -760,5 +1038,47 @@ return {
bxor: bxor,
shl: shl,
shr: shr,
ushr: ushr
ushr: ushr,
// decomposed arithmetic (int path)
add_int: add_int,
sub_int: sub_int,
mul_int: mul_int,
div_int: div_int,
mod_int: mod_int,
neg_int: neg_int,
// decomposed arithmetic (float path)
add_float: add_float,
sub_float: sub_float,
mul_float: mul_float,
div_float: div_float,
mod_float: mod_float,
neg_float: neg_float,
// text concat
concat: concat,
// decomposed comparisons (int)
eq_int: eq_int,
ne_int: ne_int,
lt_int: lt_int,
le_int: le_int,
gt_int: gt_int,
ge_int: ge_int,
// decomposed comparisons (float)
eq_float: eq_float,
ne_float: ne_float,
lt_float: lt_float,
le_float: le_float,
gt_float: gt_float,
ge_float: ge_float,
// decomposed comparisons (text)
eq_text: eq_text,
ne_text: ne_text,
lt_text: lt_text,
le_text: le_text,
gt_text: gt_text,
ge_text: ge_text,
// decomposed comparisons (bool)
eq_bool: eq_bool,
ne_bool: ne_bool,
// type guard
is_identical: is_identical
}

BIN
qbe.mach Normal file

Binary file not shown.

757
qbe_emit.cm Normal file
View File

@@ -0,0 +1,757 @@
// qbe_emit.cm — mcode IR → QBE IL compiler
// Takes mcode IR (from mcode.cm) and uses qbe.cm macros to produce
// a complete QBE IL program ready for the qbe compiler.
// qbe module is passed via env as 'qbe'
var qbe_emit = function(ir, qbe) {
var out = []
var data_out = []
var str_table = {}
var str_id = 0
var uid = 0
// ============================================================
// Output helpers
// ============================================================
var emit = function(s) {
push(out, s)
}
var fresh = function() {
uid = uid + 1
return "u" + text(uid)
}
var s = function(n) {
return "%s" + text(n)
}
var sanitize = function(lbl) {
var r = replace(lbl, ".", "_")
r = replace(r, "-", "_")
r = replace(r, " ", "_")
r = replace(r, "/", "_")
r = replace(r, "<", "")
r = replace(r, ">", "")
r = replace(r, "(", "")
r = replace(r, ")", "")
return r
}
// ============================================================
// String interning — emit data section entries
// ============================================================
var intern_str = function(val) {
if (str_table[val] != null) return str_table[val]
var label = "$d_str_" + text(str_id)
str_id = str_id + 1
var escaped = replace(val, "\\", "\\\\")
escaped = replace(escaped, "\"", "\\\"")
var line = "data " + label + ' = ' + '{ b "' + escaped + '", b 0 }'
push(data_out, line)
str_table[val] = label
return label
}
// ============================================================
// Extract property name from mcode operand
// ============================================================
var prop_name = function(a) {
if (is_text(a)) return a
if (is_object(a)) {
if (a.name != null) return a.name
if (a.value != null) return a.value
}
return null
}
// ============================================================
// Compile one function's instructions
// ============================================================
var compile_fn = function(fn, fn_idx, is_main) {
var instrs = fn.instructions
var nr_slots = fn.nr_slots
var nr_args = fn.nr_args
var name = is_main ? "cell_main" : "cell_fn_" + text(fn_idx)
name = sanitize(name)
var i = 0
var instr = null
var op = null
var a1 = null
var a2 = null
var a3 = null
var a4 = null
var p = null
var pn = null
var sl = null
var fop_id = 0
var nr_elems = 0
var ei = 0
var elem_slot = 0
// Function signature: (ctx, frame_ptr) → JSValue
emit(`export function l $${name}(l %ctx, l %fp) {`)
emit("@entry")
// Load all slots from frame into SSA variables
// Each slot is a JSValue (8 bytes) at fp + slot*8
var off = 0
i = 0
while (i < nr_slots) {
off = i * 8
emit(` %p${text(i)} =l add %fp, ${text(off)}`)
emit(` ${s(i)} =l loadl %p${text(i)}`)
i = i + 1
}
// Write-back: store SSA var to frame slot so closures see updates
var wb = function(slot) {
emit(` storel ${s(slot)}, %p${text(slot)}`)
}
// Walk instructions
i = 0
while (i < length(instrs)) {
instr = instrs[i]
i = i + 1
// Labels are plain strings
if (is_text(instr)) {
emit("@" + sanitize(instr))
continue
}
op = instr[0]
a1 = instr[1]
a2 = instr[2]
a3 = instr[3]
// --- Constants ---
if (op == "int") {
emit(` ${s(a1)} =l copy ${text(a2 * 2)}`)
wb(a1)
continue
}
if (op == "null") {
emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`)
wb(a1)
continue
}
if (op == "true") {
emit(` ${s(a1)} =l copy ${text(qbe.js_true)}`)
wb(a1)
continue
}
if (op == "false") {
emit(` ${s(a1)} =l copy ${text(qbe.js_false)}`)
wb(a1)
continue
}
if (op == "access") {
if (is_number(a2)) {
if (is_integer(a2)) {
emit(` ${s(a1)} =l copy ${text(a2 * 2)}`)
} else {
emit(` ${s(a1)} =l call $__JS_NewFloat64(l %ctx, d d_${text(a2)})`)
}
} else if (is_text(a2)) {
sl = intern_str(a2)
emit(` ${s(a1)} =l call $JS_NewString(l %ctx, l ${sl})`)
} else if (is_object(a2)) {
if (a2.make == "intrinsic") {
sl = intern_str(a2.name)
emit(` ${s(a1)} =l call $cell_rt_get_intrinsic(l %ctx, l ${sl})`)
} else if (a2.kind == "number") {
if (a2.number != null && is_integer(a2.number)) {
emit(` ${s(a1)} =l copy ${text(a2.number * 2)}`)
} else if (a2.number != null) {
emit(` ${s(a1)} =l call $__JS_NewFloat64(l %ctx, d d_${text(a2.number)})`)
} else {
emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`)
}
} else if (a2.kind == "text") {
sl = intern_str(a2.value)
emit(` ${s(a1)} =l call $JS_NewString(l %ctx, l ${sl})`)
} else if (a2.kind == "true") {
emit(` ${s(a1)} =l copy ${text(qbe.js_true)}`)
} else if (a2.kind == "false") {
emit(` ${s(a1)} =l copy ${text(qbe.js_false)}`)
} else if (a2.kind == "null") {
emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`)
} else {
emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`)
}
} else {
emit(` ${s(a1)} =l copy ${text(qbe.js_null)}`)
}
wb(a1)
continue
}
// --- Movement ---
if (op == "move") {
emit(` ${s(a1)} =l copy ${s(a2)}`)
wb(a1)
continue
}
// --- Arithmetic (int path) — use qbe.cm macros ---
if (op == "add_int") {
p = fresh()
emit(qbe.add_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "sub_int") {
p = fresh()
emit(qbe.sub_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "mul_int") {
p = fresh()
emit(qbe.mul_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "div_int") {
p = fresh()
emit(qbe.div_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "mod_int") {
p = fresh()
emit(qbe.mod_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
// --- Arithmetic (float path) ---
if (op == "add_float") {
p = fresh()
emit(qbe.add_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "sub_float") {
p = fresh()
emit(qbe.sub_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "mul_float") {
p = fresh()
emit(qbe.mul_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "div_float") {
p = fresh()
emit(qbe.div_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "mod_float") {
p = fresh()
emit(qbe.mod_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
// --- String concat ---
if (op == "concat") {
p = fresh()
emit(qbe.concat(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
// --- Type checks — use qbe.cm macros ---
if (op == "is_int") {
p = fresh()
emit(qbe.is_int(p, s(a2)))
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_text") {
p = fresh()
emit(qbe.is_imm_text(p, s(a2)))
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_num") {
p = fresh()
emit(qbe.is_number(p, s(a2)))
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_bool") {
p = fresh()
emit(qbe.is_bool(p, s(a2)))
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_null") {
p = fresh()
emit(qbe.is_null(p, s(a2)))
emit(qbe.new_bool(p + ".r", "%" + p))
emit(` ${s(a1)} =l copy %${p}.r`)
wb(a1)
continue
}
if (op == "is_identical") {
p = fresh()
emit(qbe.is_identical(p, s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
// --- Comparisons (int path) ---
if (op == "eq_int") {
p = fresh()
emit(qbe.eq_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "ne_int") {
p = fresh()
emit(qbe.ne_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "lt_int") {
p = fresh()
emit(qbe.lt_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "gt_int") {
p = fresh()
emit(qbe.gt_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "le_int") {
p = fresh()
emit(qbe.le_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "ge_int") {
p = fresh()
emit(qbe.ge_int(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
// --- Comparisons (float/text/bool) ---
if (op == "eq_float") {
p = fresh()
emit(qbe.eq_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "ne_float") {
p = fresh()
emit(qbe.ne_float(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "lt_float" || op == "gt_float" || op == "le_float" || op == "ge_float") {
p = fresh()
fop_id = 0
if (op == "lt_float") fop_id = 2
else if (op == "le_float") fop_id = 3
else if (op == "gt_float") fop_id = 4
else if (op == "ge_float") fop_id = 5
emit(qbe.cmp_float != null ? qbe.cmp_float(p, "%ctx", s(a2), s(a3), fop_id) : ` %${p} =l call $qbe_float_cmp(l %ctx, w ${text(fop_id)}, l ${s(a2)}, l ${s(a3)})`)
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "eq_text") {
p = fresh()
emit(qbe.eq_text(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "ne_text") {
p = fresh()
emit(qbe.ne_text(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "lt_text" || op == "gt_text" || op == "le_text" || op == "ge_text") {
p = fresh()
emit(` ${s(a1)} =l call $cell_rt_${op}(l %ctx, l ${s(a2)}, l ${s(a3)})`)
wb(a1)
continue
}
if (op == "eq_bool") {
p = fresh()
emit(qbe.eq_bool(p, s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "ne_bool") {
p = fresh()
emit(qbe.ne_bool(p, s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "eq_tol" || op == "ne_tol") {
emit(` ${s(a1)} =l call $cell_rt_${op}(l %ctx, l ${s(a2)}, l ${s(a3)})`)
wb(a1)
continue
}
// --- Boolean ops ---
if (op == "not") {
p = fresh()
emit(qbe.lnot(p, "%ctx", s(a2)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "and") {
emit(` ${s(a1)} =l and ${s(a2)}, ${s(a3)}`)
wb(a1)
continue
}
if (op == "or") {
emit(` ${s(a1)} =l or ${s(a2)}, ${s(a3)}`)
wb(a1)
continue
}
// --- Bitwise ops — use qbe.cm macros ---
if (op == "bitnot") {
p = fresh()
emit(qbe.bnot(p, "%ctx", s(a2)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "bitand") {
p = fresh()
emit(qbe.band(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "bitor") {
p = fresh()
emit(qbe.bor(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "bitxor") {
p = fresh()
emit(qbe.bxor(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "shl") {
p = fresh()
emit(qbe.shl(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "shr") {
p = fresh()
emit(qbe.shr(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
if (op == "ushr") {
p = fresh()
emit(qbe.ushr(p, "%ctx", s(a2), s(a3)))
emit(` ${s(a1)} =l copy %${p}`)
wb(a1)
continue
}
// --- Property access — runtime calls ---
if (op == "load_field") {
pn = prop_name(a3)
if (pn != null) {
sl = intern_str(pn)
emit(` ${s(a1)} =l call $cell_rt_load_field(l %ctx, l ${s(a2)}, l ${sl})`)
} else {
emit(` ${s(a1)} =l call $cell_rt_load_dynamic(l %ctx, l ${s(a2)}, l ${s(a3)})`)
}
wb(a1)
continue
}
if (op == "load_index") {
emit(` ${s(a1)} =l call $cell_rt_load_index(l %ctx, l ${s(a2)}, l ${s(a3)})`)
wb(a1)
continue
}
if (op == "load_dynamic") {
emit(` ${s(a1)} =l call $cell_rt_load_dynamic(l %ctx, l ${s(a2)}, l ${s(a3)})`)
wb(a1)
continue
}
if (op == "store_field") {
// IR: ["store_field", obj, val, prop] → C: (ctx, val, obj, name)
pn = prop_name(a3)
if (pn != null) {
sl = intern_str(pn)
emit(` call $cell_rt_store_field(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${sl})`)
} else {
emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`)
}
continue
}
if (op == "store_index") {
// IR: ["store_index", obj, val, idx] → C: (ctx, val, obj, idx)
emit(` call $cell_rt_store_index(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`)
continue
}
if (op == "store_dynamic") {
// IR: ["store_dynamic", obj, val, key] → C: (ctx, val, obj, key)
emit(` call $cell_rt_store_dynamic(l %ctx, l ${s(a2)}, l ${s(a1)}, l ${s(a3)})`)
continue
}
// --- Closure access ---
if (op == "get") {
emit(` ${s(a1)} =l call $cell_rt_get_closure(l %ctx, l %fp, l ${text(a2)}, l ${text(a3)})`)
wb(a1)
continue
}
if (op == "put") {
emit(` call $cell_rt_put_closure(l %ctx, l %fp, l ${s(a1)}, l ${text(a2)}, l ${text(a3)})`)
continue
}
// --- Control flow ---
if (op == "jump") {
emit(` jmp @${sanitize(a1)}`)
continue
}
if (op == "jump_true") {
p = fresh()
emit(` %${p} =w call $JS_ToBool(l %ctx, l ${s(a1)})`)
emit(` jnz %${p}, @${sanitize(a2)}, @${p}_f`)
emit(`@${p}_f`)
continue
}
if (op == "jump_false") {
p = fresh()
emit(` %${p} =w call $JS_ToBool(l %ctx, l ${s(a1)})`)
emit(` jnz %${p}, @${p}_t, @${sanitize(a2)}`)
emit(`@${p}_t`)
continue
}
if (op == "jump_null") {
p = fresh()
emit(` %${p} =w ceql ${s(a1)}, ${text(qbe.js_null)}`)
emit(` jnz %${p}, @${sanitize(a2)}, @${p}_nn`)
emit(`@${p}_nn`)
continue
}
if (op == "jump_not_null") {
p = fresh()
emit(` %${p} =w cnel ${s(a1)}, ${text(qbe.js_null)}`)
emit(` jnz %${p}, @${sanitize(a2)}, @${p}_n`)
emit(`@${p}_n`)
continue
}
if (op == "wary_true") {
p = fresh()
emit(` %${p} =w call $JS_ToBool(l %ctx, l ${s(a1)})`)
emit(` jnz %${p}, @${sanitize(a2)}, @${p}_f`)
emit(`@${p}_f`)
continue
}
if (op == "wary_false") {
p = fresh()
emit(` %${p} =w call $JS_ToBool(l %ctx, l ${s(a1)})`)
emit(` jnz %${p}, @${p}_t, @${sanitize(a2)}`)
emit(`@${p}_t`)
continue
}
// --- Function calls ---
if (op == "frame") {
emit(` ${s(a1)} =l call $cell_rt_frame(l %ctx, l ${s(a2)}, l ${text(a3)})`)
wb(a1)
continue
}
if (op == "setarg") {
emit(` call $cell_rt_setarg(l ${s(a1)}, l ${text(a2)}, l ${s(a3)})`)
continue
}
if (op == "invoke") {
emit(` ${s(a2)} =l call $cell_rt_invoke(l %ctx, l ${s(a1)})`)
wb(a2)
continue
}
if (op == "goframe") {
emit(` ${s(a1)} =l call $cell_rt_goframe(l %ctx, l ${s(a2)}, l ${text(a3)})`)
wb(a1)
continue
}
if (op == "goinvoke") {
emit(` call $cell_rt_goinvoke(l %ctx, l ${s(a1)})`)
continue
}
// --- Function object creation ---
if (op == "function") {
emit(` ${s(a1)} =l call $cell_rt_make_function(l %ctx, l ${text(a2)}, l %fp)`)
wb(a1)
continue
}
// --- Record/Array creation ---
if (op == "record") {
emit(` ${s(a1)} =l call $JS_NewObject(l %ctx)`)
wb(a1)
continue
}
if (op == "array") {
nr_elems = a2 != null ? a2 : 0
emit(` ${s(a1)} =l call $JS_NewArray(l %ctx)`)
ei = 0
while (ei < nr_elems) {
elem_slot = instr[3 + ei]
emit(` call $JS_SetPropertyNumber(l %ctx, l ${s(a1)}, l ${text(ei)}, l ${s(elem_slot)})`)
ei = ei + 1
}
wb(a1)
continue
}
// --- Array push/pop ---
if (op == "push") {
emit(` call $cell_rt_push(l %ctx, l ${s(a1)}, l ${s(a2)})`)
continue
}
if (op == "pop") {
emit(` ${s(a1)} =l call $cell_rt_pop(l %ctx, l ${s(a2)})`)
wb(a1)
continue
}
// --- Misc ---
if (op == "return") {
emit(` ret ${s(a1)}`)
continue
}
if (op == "disrupt") {
emit(` call $cell_rt_disrupt(l %ctx)`)
emit(` ret ${text(qbe.js_null)}`)
continue
}
if (op == "delete") {
emit(` ${s(a1)} =l call $cell_rt_delete(l %ctx, l ${s(a2)}, l ${s(a3)})`)
wb(a1)
continue
}
if (op == "typeof") {
emit(` ${s(a1)} =l call $cell_rt_typeof(l %ctx, l ${s(a2)})`)
wb(a1)
continue
}
// --- Unknown opcode ---
emit(` # unknown: ${op}`)
}
emit("}")
emit("")
}
// ============================================================
// Main: compile all functions then main
// ============================================================
var fi = 0
while (fi < length(ir.functions)) {
compile_fn(ir.functions[fi], fi, false)
fi = fi + 1
}
compile_fn(ir.main, -1, true)
// Assemble: data section first, then function bodies
var result = []
var di = 0
while (di < length(data_out)) {
push(result, data_out[di])
di = di + 1
}
if (length(data_out) > 0) push(result, "")
di = 0
while (di < length(out)) {
push(result, out[di])
di = di + 1
}
return text(result, "\n")
}
return qbe_emit

BIN
qbe_emit.mach Normal file

Binary file not shown.

82
qbe_rt.c Normal file
View File

@@ -0,0 +1,82 @@
/*
* qbe_rt.c - Non-inline wrappers for QBE-compiled ƿit modules
*
* Provides non-inline versions of static-inline quickjs functions
* (which QBE-generated code calls as external symbols).
*
* All cell_rt_* runtime functions are implemented in source/qbe_helpers.c
* (compiled into the cell binary) and resolved via -undefined dynamic_lookup.
*/
#include <stdint.h>
#include <string.h>
#include <math.h>
typedef uint64_t JSValue;
typedef struct JSContext JSContext;
#define JS_TAG_SHORT_FLOAT 5
#define JS_TAG_NULL 7
#define JS_VAL_NULL 7
/* ============================================================
Non-inline wrappers for static-inline quickjs functions
============================================================ */
/*
* __JS_NewFloat64 — encode double as tagged JSValue
* Short float: [sign:1][exp:8][mantissa:52][tag:3]
* Returns tagged int if value is an exact integer in int32 range
*/
JSValue __JS_NewFloat64(JSContext *ctx, double d) {
union { double d; uint64_t u; } u;
u.d = d;
uint64_t sign = u.u >> 63;
int exp = (u.u >> 52) & 0x7FF;
uint64_t mantissa = u.u & ((1ULL << 52) - 1);
/* Zero */
if (exp == 0 && mantissa == 0)
return JS_TAG_SHORT_FLOAT;
/* NaN/Inf → null */
if (exp == 0x7FF)
return JS_VAL_NULL;
/* Subnormals → zero */
if (exp == 0)
return (sign << 63) | JS_TAG_SHORT_FLOAT;
int short_exp = exp - 1023 + 127;
if (short_exp < 1 || short_exp > 254)
return JS_VAL_NULL;
/* Prefer integer if exact */
if (d >= (double)(-2147483647 - 1) && d <= (double)2147483647) {
int32_t i = (int32_t)d;
if ((double)i == d)
return (uint64_t)(uint32_t)i << 1;
}
return (sign << 63)
| ((uint64_t)short_exp << 55)
| (mantissa << 3)
| JS_TAG_SHORT_FLOAT;
}
/*
* JS_IsNumber — check if value is tagged int or short float
*/
int JS_IsNumber(JSValue v) {
int is_int = (v & 1) == 0;
int is_float = (v & 7) == JS_TAG_SHORT_FLOAT;
return is_int || is_float;
}
/*
* JS_NewString — create string from C string (wraps JS_NewStringLen)
*/
extern JSValue JS_NewStringLen(JSContext *ctx, const char *str, size_t len);
JSValue JS_NewString(JSContext *ctx, const char *str) {
return JS_NewStringLen(ctx, str, strlen(str));
}

2
qop.c
View File

@@ -267,7 +267,7 @@ static JSValue js_qop_list(JSContext *js, JSValue self, int argc, JSValue *argv)
JSValue str = JS_NewStringLen(js, path, len - 1); // -1 for null terminator
js_free(js, path);
JS_SetPropertyUint32(js, arr, count++, str);
JS_SetPropertyNumber(js, arr, count++, str);
}
return arr;

75
regen.cm Normal file
View File

@@ -0,0 +1,75 @@
// regen.cm — regenerate .mach bytecode files
// Run with: ./cell --core . regen.cm
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
var files = [
{src: "tokenize.cm", name: "tokenize", out: "tokenize.mach"},
{src: "parse.cm", name: "parse", out: "parse.mach"},
{src: "fold.cm", name: "fold", out: "fold.mach"},
{src: "mcode.cm", name: "mcode", out: "mcode.mach"},
{src: "streamline.cm", name: "streamline", out: "streamline.mach"},
{src: "qbe.cm", name: "qbe", out: "qbe.mach"},
{src: "qbe_emit.cm", name: "qbe_emit", out: "qbe_emit.mach"},
{src: "internal/bootstrap.cm", name: "bootstrap", out: "internal/bootstrap.mach"},
{src: "internal/engine.cm", name: "engine", out: "internal/engine.mach"}
]
var i = 0
var entry = null
var src = null
var tok_result = null
var ast = null
var folded = null
var compiled = null
var optimized = null
var mcode_json = null
var bytecode = null
var f = null
var errs = null
var ei = 0
var e = null
var had_errors = false
while (i < length(files)) {
entry = files[i]
src = text(fd.slurp(entry.src))
tok_result = tokenize(src, entry.src)
ast = parse(tok_result.tokens, src, entry.src, tokenize)
// Check for parse/semantic errors
errs = ast.errors
if (errs != null && length(errs) > 0) {
ei = 0
while (ei < length(errs)) {
e = errs[ei]
if (e.line != null) {
print(`${entry.src}:${text(e.line)}:${text(e.column)}: error: ${e.message}`)
} else {
print(`${entry.src}: error: ${e.message}`)
}
ei = ei + 1
}
had_errors = true
i = i + 1
continue
}
folded = fold(ast)
compiled = mcode(folded)
optimized = streamline(compiled)
mcode_json = json.encode(optimized)
bytecode = mach_compile_mcode_bin(entry.name, mcode_json)
f = fd.open(entry.out, "w")
fd.write(f, bytecode)
fd.close(f)
print(`wrote ${entry.out}`)
i = i + 1
}
if (had_errors) {
print("regen aborted: fix errors above")
}

91
run_native.ce Normal file
View File

@@ -0,0 +1,91 @@
// run_native.ce — load a module both interpreted and native, compare speed
//
// Usage:
// cell --core . run_native.ce <module>
//
// Loads <module>.cm via use() (interpreted) and <module>.dylib (native),
// runs both and compares results and timing.
var os = use('os')
if (length(args) < 1) {
print('usage: cell --core . run_native.ce <module>')
print(' e.g. cell --core . run_native.ce num_torture')
return
}
var name = args[0]
if (ends_with(name, '.cm')) {
name = text(name, 0, length(name) - 3)
}
var safe = replace(replace(name, '/', '_'), '-', '_')
var symbol = 'js_' + safe + '_use'
var dylib_path = './' + name + '.dylib'
var fd = use('fd')
// --- Test argument for function-returning modules ---
var test_arg = 5000000
if (length(args) > 1) {
test_arg = number(args[1])
}
// --- Interpreted run ---
print('--- interpreted ---')
var t1 = os.now()
var mod_interp = use(name)
var t2 = os.now()
var result_interp = null
if (is_function(mod_interp)) {
print('module returns a function, calling with ' + text(test_arg))
t1 = os.now()
result_interp = mod_interp(test_arg)
t2 = os.now()
}
result_interp = result_interp != null ? result_interp : mod_interp
var ms_interp = (t2 - t1) / 1000000
print('result: ' + text(result_interp))
print('time: ' + text(ms_interp) + ' ms')
// --- Native run ---
if (!fd.is_file(dylib_path)) {
print('\nno ' + dylib_path + ' found — run compile.ce first')
return
}
print('\n--- native ---')
var t3 = os.now()
var lib = os.dylib_open(dylib_path)
var t4 = os.now()
var mod_native = os.dylib_symbol(lib, symbol)
var t5 = os.now()
var result_native = null
if (is_function(mod_native)) {
print('module returns a function, calling with ' + text(test_arg))
t4 = os.now()
result_native = mod_native(test_arg)
t5 = os.now()
}
result_native = result_native != null ? result_native : mod_native
var ms_load = (t4 - t3) / 1000000
var ms_exec = (t5 - t4) / 1000000
var ms_native = (t5 - t3) / 1000000
print('result: ' + text(result_native))
print('load: ' + text(ms_load) + ' ms')
print('exec: ' + text(ms_exec) + ' ms')
print('total: ' + text(ms_native) + ' ms')
// --- Comparison ---
print('\n--- comparison ---')
var match = result_interp == result_native
var speedup = 0
var speedup_exec = 0
print('match: ' + text(match))
if (ms_native > 0) {
speedup = ms_interp / ms_native
print('speedup: ' + text(speedup) + 'x (total)')
}
if (ms_exec > 0) {
speedup_exec = ms_interp / ms_exec
print('speedup: ' + text(speedup_exec) + 'x (exec only)')
}

View File

@@ -12,7 +12,7 @@
#include "cJSON.h"
#define BOOTSTRAP_MACH "internal/bootstrap.mach"
#define BOOTSTRAP_AST "internal/bootstrap.ast.json"
#define BOOTSTRAP_SRC "internal/bootstrap.cm"
#define CELL_SHOP_DIR ".cell"
#define CELL_CORE_DIR "packages/core"
@@ -26,6 +26,7 @@ int run_c_test_suite(JSContext *ctx);
static int run_test_suite(size_t heap_size);
cell_rt *root_cell = NULL;
static char *shop_path = NULL;
static char *core_path = NULL;
static JSRuntime *g_runtime = NULL;
@@ -38,31 +39,57 @@ static const char* get_home_dir(void) {
return home;
}
// Find and verify the cell shop at ~/.cell
int find_cell_shop(void)
// Resolve shop_path and core_path
// core: --core flag > CELL_CORE env > derived from shop
// shop: --shop flag > CELL_SHOP env > ~/.cell
int find_cell_shop(const char *shop_override, const char *core_override)
{
const char *home = get_home_dir();
if (!home) {
printf("ERROR: Could not determine home directory. Set HOME environment variable.\n");
return 0;
// Resolve shop_path
if (shop_override) {
shop_path = strdup(shop_override);
} else {
const char *env = getenv("CELL_SHOP");
if (env) {
shop_path = strdup(env);
} else {
const char *home = get_home_dir();
if (!home && !core_override && !getenv("CELL_CORE")) {
printf("ERROR: Could not determine home directory. Set HOME environment variable.\n");
return 0;
}
if (home) {
size_t path_len = strlen(home) + strlen("/" CELL_SHOP_DIR) + 1;
shop_path = malloc(path_len);
if (shop_path)
snprintf(shop_path, path_len, "%s/" CELL_SHOP_DIR, home);
}
}
}
// Resolve core_path
if (core_override) {
core_path = strdup(core_override);
} else {
const char *env = getenv("CELL_CORE");
if (env) {
core_path = strdup(env);
} else if (shop_path) {
size_t core_len = strlen(shop_path) + strlen("/" CELL_CORE_DIR) + 1;
core_path = malloc(core_len);
if (core_path)
snprintf(core_path, core_len, "%s/" CELL_CORE_DIR, shop_path);
}
}
// Build path to ~/.cell/core
size_t path_len = strlen(home) + strlen("/" CELL_SHOP_DIR "/" CELL_CORE_DIR) + 1;
core_path = malloc(path_len);
if (!core_path) {
printf("ERROR: Could not allocate memory for core path\n");
printf("ERROR: No core path. Use --core <path> or set CELL_CORE.\n");
return 0;
}
snprintf(core_path, path_len, "%s/" CELL_SHOP_DIR "/" CELL_CORE_DIR, home);
// Check if the core directory exists
struct stat st;
if (stat(core_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
printf("ERROR: Cell shop not found at %s/" CELL_SHOP_DIR "\n", home);
printf("Run 'cell install' to set up the cell environment.\n");
free(core_path);
core_path = NULL;
printf("ERROR: Core not found at %s\n", core_path);
return 0;
}
@@ -151,14 +178,9 @@ void script_startup(cell_rt *prt)
cell_rt *crt = JS_GetContextOpaque(js);
JS_FreeValue(js, js_blob_use(js));
// Load pre-compiled bootstrap bytecode (.mach), fall back to AST JSON
// Load pre-compiled bootstrap bytecode (.mach)
size_t boot_size;
int boot_is_bin = 1;
char *boot_data = load_core_file(BOOTSTRAP_MACH, &boot_size);
if (!boot_data) {
boot_is_bin = 0;
boot_data = load_core_file(BOOTSTRAP_AST, &boot_size);
}
if (!boot_data) {
printf("ERROR: Could not load bootstrap from %s!\n", core_path);
return;
@@ -183,26 +205,21 @@ void script_startup(cell_rt *prt)
JS_SetPropertyStr(js, hidden_env, "init", JS_NULL);
}
if (core_path) {
// Set args to null for actor spawn (not CLI mode)
JS_SetPropertyStr(js, hidden_env, "args", JS_NULL);
if (core_path)
JS_SetPropertyStr(js, hidden_env, "core_path", JS_NewString(js, core_path));
}
JS_SetPropertyStr(js, hidden_env, "shop_path",
shop_path ? JS_NewString(js, shop_path) : JS_NULL);
// Stone the environment
hidden_env = JS_Stone(js, hidden_env);
// Run through MACH VM
crt->state = ACTOR_RUNNING;
JSValue v;
if (boot_is_bin) {
v = JS_RunMachBin(js, (const uint8_t *)boot_data, boot_size, hidden_env);
free(boot_data);
} else {
cJSON *ast = cJSON_Parse(boot_data);
free(boot_data);
if (!ast) { printf("ERROR: Failed to parse bootstrap AST\n"); return; }
v = JS_RunMachTree(js, ast, hidden_env);
cJSON_Delete(ast);
}
JSValue v = JS_RunMachBin(js, (const uint8_t *)boot_data, boot_size, hidden_env);
free(boot_data);
uncaught_exception(js, v);
crt->state = ACTOR_IDLE;
set_actor_state(crt);
@@ -255,9 +272,15 @@ static void print_usage(const char *prog)
printf("Usage: %s [options] <script> [args...]\n\n", prog);
printf("Run a cell script (.ce actor or .cm module).\n\n");
printf("Options:\n");
printf(" --mcode <script> [args] Run through mcode compilation pipeline\n");
printf(" --core <path> Set core path directly (overrides CELL_CORE)\n");
printf(" --shop <path> Set shop path (overrides CELL_SHOP)\n");
printf(" --emit-qbe Emit QBE IL (for native compilation)\n");
printf(" --dump-mach Dump MACH bytecode disassembly\n");
printf(" --test [heap_size] Run C test suite\n");
printf(" -h, --help Show this help message\n");
printf("\nEnvironment:\n");
printf(" CELL_CORE Core path (default: <shop>/packages/core)\n");
printf(" CELL_SHOP Shop path (default: ~/.cell)\n");
printf("\nRecompile after changes: make\n");
printf("Bootstrap from scratch: make bootstrap\n");
}
@@ -284,46 +307,108 @@ int cell_init(int argc, char **argv)
}
/* Default: run script through bootstrap pipeline */
int use_mcode = 0;
int emit_qbe = 0;
int dump_mach = 0;
int arg_start = 1;
if (argc >= 3 && strcmp(argv[1], "--mcode") == 0) {
use_mcode = 1;
arg_start = 2;
const char *shop_override = NULL;
const char *core_override = NULL;
// Parse flags (order-independent)
while (arg_start < argc && argv[arg_start][0] == '-') {
if (strcmp(argv[arg_start], "--mcode") == 0) {
/* --mcode is now always on; accept and ignore for compat */
arg_start++;
} else if (strcmp(argv[arg_start], "--emit-qbe") == 0) {
emit_qbe = 1;
arg_start++;
} else if (strcmp(argv[arg_start], "--dump-mach") == 0) {
dump_mach = 1;
arg_start++;
} else if (strcmp(argv[arg_start], "--shop") == 0) {
if (arg_start + 1 >= argc) {
printf("ERROR: --shop requires a path argument\n");
return 1;
}
shop_override = argv[arg_start + 1];
arg_start += 2;
} else if (strcmp(argv[arg_start], "--core") == 0) {
if (arg_start + 1 >= argc) {
printf("ERROR: --core requires a path argument\n");
return 1;
}
core_override = argv[arg_start + 1];
arg_start += 2;
} else {
break;
}
}
if (!find_cell_shop()) return 1;
if (!find_cell_shop(shop_override, core_override)) return 1;
actor_initialize();
size_t boot_size;
int boot_is_bin = 1;
char *boot_data = load_core_file(BOOTSTRAP_MACH, &boot_size);
if (!boot_data) {
boot_is_bin = 0;
boot_data = load_core_file(BOOTSTRAP_AST, &boot_size);
}
if (!boot_data) {
printf("ERROR: Could not load bootstrap from %s\n", core_path);
return 1;
}
JSRuntime *rt = JS_NewRuntime();
if (!rt) {
g_runtime = JS_NewRuntime();
if (!g_runtime) {
printf("Failed to create JS runtime\n");
free(boot_data);
return 1;
}
JSContext *ctx = JS_NewContextWithHeapSize(rt, 16 * 1024 * 1024);
JSContext *ctx = JS_NewContextWithHeapSize(g_runtime, 16 * 1024 * 1024);
if (!ctx) {
printf("Failed to create JS context\n");
free(boot_data); JS_FreeRuntime(rt);
free(boot_data); JS_FreeRuntime(g_runtime);
return 1;
}
/* Create a cell_rt for the CLI context so JS-C bridge functions work */
cell_rt *cli_rt = calloc(sizeof(*cli_rt), 1);
cli_rt->mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutexattr_t mattr;
pthread_mutexattr_init(&mattr);
pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(cli_rt->mutex, &mattr);
cli_rt->msg_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(cli_rt->msg_mutex, &mattr);
pthread_mutexattr_destroy(&mattr);
cli_rt->context = ctx;
JS_SetContextOpaque(ctx, cli_rt);
JS_SetInterruptHandler(ctx, (JSInterruptHandler *)actor_interrupt_cb, cli_rt);
JS_AddGCRef(ctx, &cli_rt->idx_buffer_ref);
JS_AddGCRef(ctx, &cli_rt->on_exception_ref);
JS_AddGCRef(ctx, &cli_rt->message_handle_ref);
JS_AddGCRef(ctx, &cli_rt->unneeded_ref);
JS_AddGCRef(ctx, &cli_rt->actor_sym_ref);
cli_rt->idx_buffer_ref.val = JS_NULL;
cli_rt->on_exception_ref.val = JS_NULL;
cli_rt->message_handle_ref.val = JS_NULL;
cli_rt->unneeded_ref.val = JS_NULL;
cli_rt->actor_sym_ref.val = JS_NewObject(ctx);
root_cell = cli_rt;
JS_FreeValue(ctx, js_blob_use(ctx));
JSValue hidden_env = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, hidden_env, "os", js_os_use(ctx));
JS_SetPropertyStr(ctx, hidden_env, "core_path", JS_NewString(ctx, core_path));
JS_SetPropertyStr(ctx, hidden_env, "use_mcode", JS_NewBool(ctx, use_mcode));
JS_SetPropertyStr(ctx, hidden_env, "shop_path",
shop_path ? JS_NewString(ctx, shop_path) : JS_NULL);
JS_SetPropertyStr(ctx, hidden_env, "emit_qbe", JS_NewBool(ctx, emit_qbe));
JS_SetPropertyStr(ctx, hidden_env, "dump_mach", JS_NewBool(ctx, dump_mach));
JS_SetPropertyStr(ctx, hidden_env, "actorsym", JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
JS_SetPropertyStr(ctx, hidden_env, "json", js_json_use(ctx));
JS_SetPropertyStr(ctx, hidden_env, "nota", js_nota_use(ctx));
JS_SetPropertyStr(ctx, hidden_env, "wota", js_wota_use(ctx));
JS_SetPropertyStr(ctx, hidden_env, "init", JS_NULL);
JSValue args_arr = JS_NewArray(ctx);
for (int i = arg_start; i < argc; i++) {
JSValue str = JS_NewString(ctx, argv[i]);
@@ -332,17 +417,8 @@ int cell_init(int argc, char **argv)
JS_SetPropertyStr(ctx, hidden_env, "args", args_arr);
hidden_env = JS_Stone(ctx, hidden_env);
JSValue result;
if (boot_is_bin) {
result = JS_RunMachBin(ctx, (const uint8_t *)boot_data, boot_size, hidden_env);
free(boot_data);
} else {
cJSON *ast = cJSON_Parse(boot_data);
free(boot_data);
if (!ast) { printf("Failed to parse bootstrap AST\n"); JS_FreeContext(ctx); JS_FreeRuntime(rt); return 1; }
result = JS_RunMachTree(ctx, ast, hidden_env);
cJSON_Delete(ast);
}
JSValue result = JS_RunMachBin(ctx, (const uint8_t *)boot_data, boot_size, hidden_env);
free(boot_data);
int exit_code = 0;
if (JS_IsException(result)) {
@@ -356,8 +432,33 @@ int cell_init(int argc, char **argv)
}
}
if (scheduler_actor_count() > 0) {
scheduler_enable_quiescence();
actor_loop();
exit_handler();
exit(0);
}
/* No actors spawned — clean up CLI context */
JS_DeleteGCRef(ctx, &cli_rt->idx_buffer_ref);
JS_DeleteGCRef(ctx, &cli_rt->on_exception_ref);
JS_DeleteGCRef(ctx, &cli_rt->message_handle_ref);
JS_DeleteGCRef(ctx, &cli_rt->unneeded_ref);
JS_DeleteGCRef(ctx, &cli_rt->actor_sym_ref);
JS_SetInterruptHandler(ctx, NULL, NULL);
pthread_mutex_destroy(cli_rt->mutex);
free(cli_rt->mutex);
pthread_mutex_destroy(cli_rt->msg_mutex);
free(cli_rt->msg_mutex);
free(cli_rt);
root_cell = NULL;
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
JS_FreeRuntime(g_runtime);
g_runtime = NULL;
exit_handler();
return exit_code;
}

View File

@@ -54,6 +54,7 @@ typedef struct cell_rt {
double ar_secs; // time for unneeded
int disrupt;
int is_quiescent; // tracked by scheduler for quiescence detection
int main_thread_only;
int affinity;
@@ -81,6 +82,8 @@ int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt);
void actor_loop();
void actor_initialize(void);
void actor_free(cell_rt *actor);
int scheduler_actor_count(void);
void scheduler_enable_quiescence(void);
uint64_t cell_ns();
void cell_sleep(double seconds);

View File

@@ -1327,9 +1327,6 @@ static int re_parse_nested_class(REParseState *s, REStringList *cr, const uint8_
REStringList cr1_s, *cr1 = &cr1_s;
BOOL invert, is_first;
if (lre_check_stack_overflow(s->opaque, 0))
return re_parse_error(s, "stack overflow");
re_string_list_init(s, cr);
p = *pp;
p++; /* skip '[' */
@@ -2356,9 +2353,6 @@ static int re_parse_disjunction(REParseState *s, BOOL is_backward_dir)
{
int start, len, pos;
if (lre_check_stack_overflow(s->opaque, 0))
return re_parse_error(s, "stack overflow");
start = s->byte_code.size;
if (re_parse_alternative(s, is_backward_dir))
return -1;
@@ -3205,11 +3199,6 @@ const char *lre_get_groupnames(const uint8_t *bc_buf)
#ifdef TEST
BOOL lre_check_stack_overflow(void *opaque, size_t alloca_size)
{
return FALSE;
}
void *lre_realloc(void *opaque, void *ptr, size_t size)
{
return realloc(ptr, size);

View File

@@ -52,8 +52,6 @@ int lre_exec(uint8_t **capture,
int lre_parse_escape(const uint8_t **pp, int allow_utf16);
/* must be provided by the user, return non zero if overflow */
int lre_check_stack_overflow(void *opaque, size_t alloca_size);
/* must be provided by the user, return non zero if time out */
int lre_check_timeout(void *opaque);
void *lre_realloc(void *opaque, void *ptr, size_t size);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -192,3 +192,258 @@ JSValue qbe_shift_shr(JSContext *ctx, JSValue a, JSValue b) {
JS_ToInt32(ctx, &ib, b);
return JS_NewInt32(ctx, (uint32_t)ia >> (ib & 31));
}
/* ============================================================
cell_rt_* — Runtime support for QBE-compiled code
============================================================ */
#include <dlfcn.h>
#include <stdio.h>
/* --- Property access --- */
JSValue cell_rt_load_field(JSContext *ctx, JSValue obj, const char *name) {
return JS_GetPropertyStr(ctx, obj, name);
}
void cell_rt_store_field(JSContext *ctx, JSValue val, JSValue obj,
const char *name) {
JS_SetPropertyStr(ctx, obj, name, val);
}
JSValue cell_rt_load_dynamic(JSContext *ctx, JSValue obj, JSValue key) {
if (JS_IsInt(key))
return JS_GetPropertyNumber(ctx, obj, (uint32_t)JS_VALUE_GET_INT(key));
return JS_GetProperty(ctx, obj, key);
}
void cell_rt_store_dynamic(JSContext *ctx, JSValue val, JSValue obj,
JSValue key) {
if (JS_IsInt(key))
JS_SetPropertyNumber(ctx, obj, (uint32_t)JS_VALUE_GET_INT(key), val);
else
JS_SetProperty(ctx, obj, key, val);
}
JSValue cell_rt_load_index(JSContext *ctx, JSValue arr, JSValue idx) {
if (JS_IsInt(idx))
return JS_GetPropertyNumber(ctx, arr, (uint32_t)JS_VALUE_GET_INT(idx));
return JS_GetProperty(ctx, arr, idx);
}
void cell_rt_store_index(JSContext *ctx, JSValue val, JSValue arr,
JSValue idx) {
if (JS_IsInt(idx))
JS_SetPropertyNumber(ctx, arr, (uint32_t)JS_VALUE_GET_INT(idx), val);
else
JS_SetProperty(ctx, arr, idx, val);
}
/* --- Intrinsic/global lookup --- */
JSValue cell_rt_get_intrinsic(JSContext *ctx, const char *name) {
return JS_GetPropertyStr(ctx, ctx->global_obj, name);
}
/* --- Closure access ---
Slot 511 in each frame stores a pointer to the enclosing frame.
Walking depth levels up the chain gives the target frame. */
#define QBE_FRAME_OUTER_SLOT 511
JSValue cell_rt_get_closure(JSContext *ctx, void *fp, int64_t depth,
int64_t slot) {
JSValue *frame = (JSValue *)fp;
for (int64_t d = 0; d < depth; d++) {
void *outer = (void *)(uintptr_t)frame[QBE_FRAME_OUTER_SLOT];
if (!outer) return JS_NULL;
frame = (JSValue *)outer;
}
return frame[slot];
}
void cell_rt_put_closure(JSContext *ctx, void *fp, JSValue val, int64_t depth,
int64_t slot) {
JSValue *frame = (JSValue *)fp;
for (int64_t d = 0; d < depth; d++) {
void *outer = (void *)(uintptr_t)frame[QBE_FRAME_OUTER_SLOT];
if (!outer) return;
frame = (JSValue *)outer;
}
frame[slot] = val;
}
/* --- Function creation and calling --- */
typedef JSValue (*cell_compiled_fn)(JSContext *ctx, void *fp);
/* Table mapping fn_idx → outer_fp at creation time.
Valid for single-threaded, non-recursive closure patterns. */
#define MAX_QBE_FUNCTIONS 256
static void *g_outer_fp[MAX_QBE_FUNCTIONS];
static JSValue cell_fn_trampoline(JSContext *ctx, JSValue this_val,
int argc, JSValue *argv, int magic) {
char name[64];
snprintf(name, sizeof(name), "cell_fn_%d", magic);
cell_compiled_fn fn = (cell_compiled_fn)dlsym(RTLD_DEFAULT, name);
if (!fn)
return JS_ThrowTypeError(ctx, "native function %s not found", name);
/* Allocate frame: slot 0 = this, slots 1..argc = args */
JSValue frame[512];
memset(frame, 0, sizeof(frame));
frame[0] = this_val;
for (int i = 0; i < argc && i < 510; i++)
frame[1 + i] = argv[i];
/* Link to outer frame for closure access */
if (magic >= 0 && magic < MAX_QBE_FUNCTIONS)
frame[QBE_FRAME_OUTER_SLOT] = (JSValue)(uintptr_t)g_outer_fp[magic];
return fn(ctx, frame);
}
JSValue cell_rt_make_function(JSContext *ctx, int64_t fn_idx, void *outer_fp) {
if (fn_idx >= 0 && fn_idx < MAX_QBE_FUNCTIONS)
g_outer_fp[fn_idx] = outer_fp;
return JS_NewCFunction2(ctx, (JSCFunction *)cell_fn_trampoline, "native_fn",
255, JS_CFUNC_generic_magic, (int)fn_idx);
}
/* --- Frame-based function calling --- */
JSValue cell_rt_frame(JSContext *ctx, JSValue fn, int64_t nargs) {
if (!JS_IsFunction(fn)) {
JS_ThrowTypeError(ctx, "not a function");
return JS_EXCEPTION;
}
int nr_slots = (int)nargs + 2;
JSFrameRegister *new_frame = alloc_frame_register(ctx, nr_slots);
if (!new_frame) return JS_EXCEPTION;
new_frame->function = fn;
return JS_MKPTR(new_frame);
}
void cell_rt_setarg(JSValue frame_val, int64_t idx, JSValue val) {
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
fr->slots[idx] = val;
}
JSValue cell_rt_invoke(JSContext *ctx, JSValue frame_val) {
JSFrameRegister *fr = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_val);
int nr_slots = (int)objhdr_cap56(fr->hdr);
int c_argc = (nr_slots >= 2) ? nr_slots - 2 : 0;
/* Copy args to C stack */
JSValue args[c_argc > 0 ? c_argc : 1];
for (int i = 0; i < c_argc; i++)
args[i] = fr->slots[i + 1];
JSValue result = JS_Call(ctx, fr->function, fr->slots[0], c_argc, args);
if (JS_IsException(result))
return JS_EXCEPTION;
return result;
}
JSValue cell_rt_goframe(JSContext *ctx, JSValue fn, int64_t nargs) {
return cell_rt_frame(ctx, fn, nargs);
}
void cell_rt_goinvoke(JSContext *ctx, JSValue frame_val) {
cell_rt_invoke(ctx, frame_val);
}
/* --- Array push/pop --- */
void cell_rt_push(JSContext *ctx, JSValue arr, JSValue val) {
JS_ArrayPush(ctx, &arr, val);
}
JSValue cell_rt_pop(JSContext *ctx, JSValue arr) {
return JS_ArrayPop(ctx, arr);
}
/* --- Delete --- */
JSValue cell_rt_delete(JSContext *ctx, JSValue obj, JSValue key) {
int ret = JS_DeleteProperty(ctx, obj, key);
return JS_NewBool(ctx, ret >= 0);
}
/* --- Typeof --- */
JSValue cell_rt_typeof(JSContext *ctx, JSValue val) {
if (JS_IsNull(val)) return JS_NewString(ctx, "null");
if (JS_IsInt(val) || JS_IsNumber(val)) return JS_NewString(ctx, "number");
if (JS_IsBool(val)) return JS_NewString(ctx, "logical");
if (JS_IsText(val)) return JS_NewString(ctx, "text");
if (JS_IsFunction(val)) return JS_NewString(ctx, "function");
if (JS_IsArray(val)) return JS_NewString(ctx, "array");
if (JS_IsRecord(val)) return JS_NewString(ctx, "object");
return JS_NewString(ctx, "unknown");
}
/* --- Text comparison stubs (called from QBE type-dispatch branches) --- */
JSValue cell_rt_lt_text(JSContext *ctx, JSValue a, JSValue b) {
const char *sa = JS_ToCString(ctx, a);
const char *sb = JS_ToCString(ctx, b);
int r = (sa && sb) ? strcmp(sa, sb) < 0 : 0;
return JS_NewBool(ctx, r);
}
JSValue cell_rt_gt_text(JSContext *ctx, JSValue a, JSValue b) {
const char *sa = JS_ToCString(ctx, a);
const char *sb = JS_ToCString(ctx, b);
int r = (sa && sb) ? strcmp(sa, sb) > 0 : 0;
return JS_NewBool(ctx, r);
}
JSValue cell_rt_le_text(JSContext *ctx, JSValue a, JSValue b) {
const char *sa = JS_ToCString(ctx, a);
const char *sb = JS_ToCString(ctx, b);
int r = (sa && sb) ? strcmp(sa, sb) <= 0 : 0;
return JS_NewBool(ctx, r);
}
JSValue cell_rt_ge_text(JSContext *ctx, JSValue a, JSValue b) {
const char *sa = JS_ToCString(ctx, a);
const char *sb = JS_ToCString(ctx, b);
int r = (sa && sb) ? strcmp(sa, sb) >= 0 : 0;
return JS_NewBool(ctx, r);
}
JSValue cell_rt_eq_tol(JSContext *ctx, JSValue a, JSValue b) {
return JS_NewBool(ctx, a == b);
}
JSValue cell_rt_ne_tol(JSContext *ctx, JSValue a, JSValue b) {
return JS_NewBool(ctx, a != b);
}
/* --- Disruption --- */
void cell_rt_disrupt(JSContext *ctx) {
JS_ThrowTypeError(ctx, "type error in native code");
}
/* --- Module entry point ---
Called as symbol(ctx) by os.dylib_symbol. Looks up cell_main
in the loaded dylib, builds a heap-allocated frame (so closures
can reference it after the module returns), and runs the module body. */
JSValue cell_rt_module_entry(JSContext *ctx) {
cell_compiled_fn fn = (cell_compiled_fn)dlsym(RTLD_DEFAULT, "cell_main");
if (!fn)
return JS_ThrowTypeError(ctx, "cell_main not found in loaded dylib");
/* Heap-allocate so closures created in cell_main can reference
this frame after the module entry returns. */
JSValue *frame = calloc(512, sizeof(JSValue));
if (!frame)
return JS_ThrowTypeError(ctx, "frame allocation failed");
return fn(ctx, frame);
}

View File

@@ -54,14 +54,6 @@
#include "nota.h"
#include "wota.h"
#define OPTIMIZE 1
#define SHORT_OPCODES 1
#if defined(EMSCRIPTEN)
#define DIRECT_DISPATCH 0
#else
#define DIRECT_DISPATCH 1
#endif
#if !defined(_WIN32)
/* define it if printf uses the RNDN rounding mode instead of RNDNA */
#define CONFIG_PRINTF_RNDN
@@ -154,7 +146,6 @@ typedef struct JSCode JSCode;
/* Extract pointer (clear low bits) */
#define JS_VALUE_GET_PTR(v) ((void *)((v) & ~((JSValue)(JSW - 1))))
static inline JS_BOOL JS_VALUE_IS_TEXT (JSValue v) {
int tag = JS_VALUE_GET_TAG (v);
return tag == JS_TAG_STRING_IMM || (JS_IsPtr(v) && objhdr_type(*(objhdr_t *)JS_VALUE_GET_PTR(v)) == OBJ_TEXT);
@@ -361,70 +352,21 @@ typedef struct BuddyAllocator {
/* Forward declarations for buddy allocator functions */
static void buddy_destroy (BuddyAllocator *b);
/* controls a host of contexts, handing out memory and scheduling */
struct JSRuntime {
const char *rt_info;
size_t malloc_limit;
/* Buddy allocator for actor memory blocks */
BuddyAllocator buddy;
/* see JS_SetStripInfo() */
uint8_t strip_flags;
/* User data */
void *user_opaque;
};
struct JSClass {
const char *class_name;
JSClassFinalizer *finalizer;
JSClassGCMark *gc_mark;
uint32_t class_id; /* 0 means free entry */
};
#define JS_MODE_BACKTRACE_BARRIER \
(1 << 3) /* stop backtrace before this frame */
typedef struct JSStackFrame {
struct JSStackFrame *prev_frame; /* NULL if first stack frame */
JSValue cur_func; /* current function, JS_NULL if the frame is detached */
JSValue *arg_buf; /* arguments */
JSValue *var_buf; /* variables */
const uint8_t *cur_pc; /* only used in bytecode functions : PC of the
instruction after the call */
int arg_count;
int js_mode; /* not supported for C functions */
JSValue js_frame; /* GC-managed JSFrame (use JS_VALUE_GET_FRAME to access) */
JSValue *stack_buf; /* operand stack base (for GC scanning) */
JSValue **p_sp; /* pointer to current sp (for GC scanning) */
} JSStackFrame;
/* Heap-allocated VM frame for trampoline execution */
struct VMFrame {
struct JSFunctionBytecode *b; /* current function bytecode */
JSContext *ctx; /* execution context / realm */
const uint8_t *pc; /* program counter */
/* Offset-based storage (safe with realloc) */
int value_stack_base; /* base index into ctx->value_stack */
int sp_offset; /* sp offset from base */
int arg_buf_offset; /* arg buffer offset from base (or -1 if aliased) */
int var_buf_offset; /* var buffer offset from base */
/* Continuation info for return */
const uint8_t *ret_pc; /* where to resume in caller */
int ret_sp_offset; /* caller's sp before call (for cleanup) */
int call_argc; /* number of args to clean up */
int call_has_this; /* whether call had this (method call) */
JSValue cur_func; /* current function object */
JSValue this_obj; /* this binding */
int arg_count;
int js_mode;
int stack_size_allocated; /* total size allocated for this frame */
};
typedef struct JSFrameRegister {
objhdr_t hdr; // capacity in this is the total number of words of the object, including the 4 words of overhead and all slots
JSValue function; // JSFunction, function object being invoked
@@ -464,6 +406,8 @@ typedef struct { uint16_t line; uint16_t col; } MachLineEntry;
#define MACH_GET_sJ(i) ((int32_t)((i) & 0xFFFFFF00) >> 8)
typedef enum MachOpcode {
/* === Legacy opcodes (used by existing .mach files) === */
/* Constants & Loading */
MACH_LOADK, /* R(A) = K(Bx) — load from constant pool (ABx) */
MACH_LOADI, /* R(A) = (int16_t)sBx — load small integer (AsBx) */
@@ -474,7 +418,7 @@ typedef enum MachOpcode {
/* Movement */
MACH_MOVE, /* R(A) = R(B) */
/* Arithmetic (ABC) */
/* Generic arithmetic (ABC) — used by legacy .mach */
MACH_ADD, /* R(A) = R(B) + R(C) */
MACH_SUB, /* R(A) = R(B) - R(C) */
MACH_MUL, /* R(A) = R(B) * R(C) */
@@ -485,7 +429,7 @@ typedef enum MachOpcode {
MACH_INC, /* R(A) = R(B) + 1 */
MACH_DEC, /* R(A) = R(B) - 1 */
/* Comparison (ABC) */
/* Generic comparison (ABC) — used by legacy .mach */
MACH_EQ, /* R(A) = (R(B) == R(C)) */
MACH_NEQ, /* R(A) = (R(B) != R(C)) */
MACH_LT, /* R(A) = (R(B) < R(C)) */
@@ -493,7 +437,7 @@ typedef enum MachOpcode {
MACH_GT, /* R(A) = (R(B) > R(C)) */
MACH_GE, /* R(A) = (R(B) >= R(C)) */
/* Logical/Bitwise */
/* Logical/Bitwise — used by legacy .mach */
MACH_LNOT, /* R(A) = !R(B) */
MACH_BNOT, /* R(A) = ~R(B) */
MACH_BAND, /* R(A) = R(B) & R(C) */
@@ -503,7 +447,7 @@ typedef enum MachOpcode {
MACH_SHR, /* R(A) = R(B) >> R(C) */
MACH_USHR, /* R(A) = R(B) >>> R(C) */
/* Property access */
/* Property access — used by legacy .mach */
MACH_GETFIELD, /* R(A) = R(B)[K(C)] — named property */
MACH_SETFIELD, /* R(A)[K(B)] = R(C) — named property */
MACH_GETINDEX, /* R(A) = R(B)[R(C)] — computed property */
@@ -524,12 +468,12 @@ typedef enum MachOpcode {
MACH_JMPFALSE, /* if !R(A): pc += sBx — (iAsBx format) */
MACH_JMPNULL, /* if R(A)==null: pc += sBx */
/* Function calls — Lua-style consecutive registers */
/* Function calls — Lua-style consecutive registers (legacy .mach) */
MACH_CALL, /* Call R(A) with B args R(A+1)..R(A+B), C=0 discard, C=1 keep result in R(A) */
MACH_RETURN, /* Return R(A) */
MACH_RETNIL, /* Return null */
/* Object/array creation */
/* Object/array creation — legacy .mach */
MACH_NEWOBJECT, /* R(A) = {} */
MACH_NEWARRAY, /* R(A) = new array, B = element count in R(A+1)..R(A+B) */
MACH_CLOSURE, /* R(A) = closure(functions[Bx]) (ABx) */
@@ -551,10 +495,114 @@ typedef enum MachOpcode {
MACH_NOP,
/* === New mcode-derived opcodes (1:1 mapping to mcode IR) === */
/* Typed integer arithmetic (ABC) */
MACH_ADD_INT, /* R(A) = R(B) + R(C) — int, overflow → float */
MACH_SUB_INT, /* R(A) = R(B) - R(C) — int */
MACH_MUL_INT, /* R(A) = R(B) * R(C) — int */
MACH_DIV_INT, /* R(A) = R(B) / R(C) — int */
MACH_MOD_INT, /* R(A) = R(B) % R(C) — int */
MACH_NEG_INT, /* R(A) = -R(B) — int (AB) */
/* Typed float arithmetic (ABC) */
MACH_ADD_FLOAT, /* R(A) = R(B) + R(C) — float */
MACH_SUB_FLOAT, /* R(A) = R(B) - R(C) — float */
MACH_MUL_FLOAT, /* R(A) = R(B) * R(C) — float */
MACH_DIV_FLOAT, /* R(A) = R(B) / R(C) — float */
MACH_MOD_FLOAT, /* R(A) = R(B) % R(C) — float */
MACH_NEG_FLOAT, /* R(A) = -R(B) — float (AB) */
/* Text */
MACH_CONCAT, /* R(A) = R(B) ++ R(C) — string concatenation */
/* Typed integer comparisons (ABC) */
MACH_EQ_INT, /* R(A) = (R(B) == R(C)) — int */
MACH_NE_INT, /* R(A) = (R(B) != R(C)) — int */
MACH_LT_INT, /* R(A) = (R(B) < R(C)) — int */
MACH_LE_INT, /* R(A) = (R(B) <= R(C)) — int */
MACH_GT_INT, /* R(A) = (R(B) > R(C)) — int */
MACH_GE_INT, /* R(A) = (R(B) >= R(C)) — int */
/* Typed float comparisons (ABC) */
MACH_EQ_FLOAT, /* R(A) = (R(B) == R(C)) — float */
MACH_NE_FLOAT, /* R(A) = (R(B) != R(C)) — float */
MACH_LT_FLOAT, /* R(A) = (R(B) < R(C)) — float */
MACH_LE_FLOAT, /* R(A) = (R(B) <= R(C)) — float */
MACH_GT_FLOAT, /* R(A) = (R(B) > R(C)) — float */
MACH_GE_FLOAT, /* R(A) = (R(B) >= R(C)) — float */
/* Typed text comparisons (ABC) */
MACH_EQ_TEXT, /* R(A) = (R(B) == R(C)) — text */
MACH_NE_TEXT, /* R(A) = (R(B) != R(C)) — text */
MACH_LT_TEXT, /* R(A) = (R(B) < R(C)) — text */
MACH_LE_TEXT, /* R(A) = (R(B) <= R(C)) — text */
MACH_GT_TEXT, /* R(A) = (R(B) > R(C)) — text */
MACH_GE_TEXT, /* R(A) = (R(B) >= R(C)) — text */
/* Typed bool comparisons (ABC) */
MACH_EQ_BOOL, /* R(A) = (R(B) == R(C)) — bool */
MACH_NE_BOOL, /* R(A) = (R(B) != R(C)) — bool */
/* Special comparisons */
MACH_IS_IDENTICAL, /* R(A) = (R(B) === R(C)) — identity check (ABC) */
/* Type checks (AB) */
MACH_IS_INT, /* R(A) = is_int(R(B)) */
MACH_IS_NUM, /* R(A) = is_num(R(B)) */
MACH_IS_TEXT, /* R(A) = is_text(R(B)) */
MACH_IS_BOOL, /* R(A) = is_bool(R(B)) */
MACH_IS_NULL, /* R(A) = is_null(R(B)) */
MACH_TYPEOF, /* R(A) = typeof(R(B)) */
/* Logical (mcode-style) */
MACH_NOT, /* R(A) = !R(B) — boolean not (AB) */
MACH_AND, /* R(A) = R(B) && R(C) (ABC) */
MACH_OR, /* R(A) = R(B) || R(C) (ABC) */
/* Bitwise (mcode names) */
MACH_BITNOT, /* R(A) = ~R(B) (AB) */
MACH_BITAND, /* R(A) = R(B) & R(C) (ABC) */
MACH_BITOR, /* R(A) = R(B) | R(C) (ABC) */
MACH_BITXOR, /* R(A) = R(B) ^ R(C) (ABC) */
/* Property access (mcode names) */
MACH_LOAD_FIELD, /* R(A) = R(B).K(C) — named property (ABC) */
MACH_STORE_FIELD, /* R(A).K(B) = R(C) — named property (ABC) */
MACH_LOAD_INDEX, /* R(A) = R(B)[R(C)] — integer index (ABC) */
MACH_STORE_INDEX, /* R(A)[R(B)] = R(C) — integer index (ABC) */
MACH_LOAD_DYNAMIC, /* R(A) = R(B)[R(C)] — dynamic key (ABC) */
MACH_STORE_DYNAMIC, /* R(A)[R(B)] = R(C) — dynamic key (ABC) */
/* Object/Array creation (mcode names) */
MACH_NEWRECORD, /* R(A) = {} — new empty record (A only) */
/* Decomposed function calls (mcode-style) */
MACH_FRAME, /* R(A) = frame(R(B), C) — alloc call frame (ABC) */
MACH_SETARG, /* frame R(A)[B] = R(C) — set arg in frame (ABC) */
MACH_INVOKE, /* R(B) = invoke(R(A)) — call frame, result in R(B) (AB) */
MACH_GOFRAME, /* R(A) = goframe(R(B), C) — async frame (ABC) */
MACH_GOINVOKE, /* goinvoke(R(A)) — async invoke, no result (A only) */
/* Control flow */
MACH_JMPNOTNULL, /* if R(A)!=null: pc += sBx (iAsBx) */
/* Error handling */
MACH_DISRUPT, /* trigger disruption (A only) */
/* Variable storage */
MACH_SET_VAR, /* env/global[K(Bx)] = R(A) — store to var (ABx) */
/* Misc */
MACH_IN, /* R(A) = (R(B) in R(C)) — has property (ABC) */
MACH_IS_FUNC, /* R(A) = is_function(R(B)) */
MACH_IS_PROXY, /* R(A) = is_function(R(B)) && R(B).length == 2 */
MACH_OP_COUNT
} MachOpcode;
static const char *mach_opcode_names[MACH_OP_COUNT] = {
/* Legacy */
[MACH_LOADK] = "loadk",
[MACH_LOADI] = "loadi",
[MACH_LOADNULL] = "loadnull",
@@ -614,6 +662,72 @@ static const char *mach_opcode_names[MACH_OP_COUNT] = {
[MACH_EQ_TOL] = "eq_tol",
[MACH_NEQ_TOL] = "neq_tol",
[MACH_NOP] = "nop",
/* Mcode-derived */
[MACH_ADD_INT] = "add_int",
[MACH_SUB_INT] = "sub_int",
[MACH_MUL_INT] = "mul_int",
[MACH_DIV_INT] = "div_int",
[MACH_MOD_INT] = "mod_int",
[MACH_NEG_INT] = "neg_int",
[MACH_ADD_FLOAT] = "add_float",
[MACH_SUB_FLOAT] = "sub_float",
[MACH_MUL_FLOAT] = "mul_float",
[MACH_DIV_FLOAT] = "div_float",
[MACH_MOD_FLOAT] = "mod_float",
[MACH_NEG_FLOAT] = "neg_float",
[MACH_CONCAT] = "concat",
[MACH_EQ_INT] = "eq_int",
[MACH_NE_INT] = "ne_int",
[MACH_LT_INT] = "lt_int",
[MACH_LE_INT] = "le_int",
[MACH_GT_INT] = "gt_int",
[MACH_GE_INT] = "ge_int",
[MACH_EQ_FLOAT] = "eq_float",
[MACH_NE_FLOAT] = "ne_float",
[MACH_LT_FLOAT] = "lt_float",
[MACH_LE_FLOAT] = "le_float",
[MACH_GT_FLOAT] = "gt_float",
[MACH_GE_FLOAT] = "ge_float",
[MACH_EQ_TEXT] = "eq_text",
[MACH_NE_TEXT] = "ne_text",
[MACH_LT_TEXT] = "lt_text",
[MACH_LE_TEXT] = "le_text",
[MACH_GT_TEXT] = "gt_text",
[MACH_GE_TEXT] = "ge_text",
[MACH_EQ_BOOL] = "eq_bool",
[MACH_NE_BOOL] = "ne_bool",
[MACH_IS_IDENTICAL] = "is_identical",
[MACH_IS_INT] = "is_int",
[MACH_IS_NUM] = "is_num",
[MACH_IS_TEXT] = "is_text",
[MACH_IS_BOOL] = "is_bool",
[MACH_IS_NULL] = "is_null",
[MACH_TYPEOF] = "typeof",
[MACH_NOT] = "not",
[MACH_AND] = "and",
[MACH_OR] = "or",
[MACH_BITNOT] = "bitnot",
[MACH_BITAND] = "bitand",
[MACH_BITOR] = "bitor",
[MACH_BITXOR] = "bitxor",
[MACH_LOAD_FIELD] = "load_field",
[MACH_STORE_FIELD] = "store_field",
[MACH_LOAD_INDEX] = "load_index",
[MACH_STORE_INDEX] = "store_index",
[MACH_LOAD_DYNAMIC] = "load_dynamic",
[MACH_STORE_DYNAMIC] = "store_dynamic",
[MACH_NEWRECORD] = "newrecord",
[MACH_FRAME] = "frame",
[MACH_SETARG] = "setarg",
[MACH_INVOKE] = "invoke",
[MACH_GOFRAME] = "goframe",
[MACH_GOINVOKE] = "goinvoke",
[MACH_JMPNOTNULL] = "jmpnotnull",
[MACH_DISRUPT] = "disrupt",
[MACH_SET_VAR] = "set_var",
[MACH_IN] = "in",
[MACH_IS_FUNC] = "is_func",
[MACH_IS_PROXY] = "is_proxy",
};
/* Compiled register-based code (off-heap, never GC'd).
@@ -644,28 +758,6 @@ typedef struct JSCodeRegister {
uint16_t disruption_pc; /* start of disruption handler (0 = none) */
} JSCodeRegister;
/* Pre-parsed MCODE for a single function (off-heap, never GC'd).
Created by jsmcode_parse from cJSON MCODE output.
Instructions remain as cJSON pointers for string-based dispatch. */
typedef struct JSMCode {
uint16_t nr_args;
uint16_t nr_slots;
/* Pre-flattened instruction array (cJSON array items → C array for O(1) access) */
cJSON **instrs;
uint32_t instr_count;
/* Label map: label string → instruction index */
struct { const char *name; uint32_t index; } *labels;
uint32_t label_count;
/* Nested function definitions (indexes into top-level functions array) */
struct JSMCode **functions;
uint32_t func_count;
/* Keep root cJSON alive (owns all the cJSON nodes instrs[] point into) */
cJSON *json_root;
MachLineEntry *line_table; /* [instr_count], parallel to instrs[] */
const char *name; /* function name (points into cJSON tree) */
const char *filename; /* source filename (points into cJSON tree) */
uint16_t disruption_pc; /* start of disruption handler (0 = none) */
} JSMCode;
/* Frame for closures - used by link-time relocation model where closures
reference outer frames via (depth, slot) addressing.
@@ -675,7 +767,7 @@ typedef struct JSFrame {
JSValue function; /* JSValue for GC safety (use JS_VALUE_GET_FUNCTION) */
JSValue caller; /* JSValue for GC safety (unused currently) */
uint32_t return_pc;
JSValue slots[]; /* args, captured, locals, temps */
JSValue slots[]; /* [this][args][captured][locals][temps] */
} JSFrame;
/* Execution state returned by vm_execute_frame */
@@ -957,31 +1049,19 @@ struct JSContext {
int trace_type;
void *trace_data;
/* Trampoline VM stacks (per actor/context) */
struct VMFrame *frame_stack; /* array of frames */
int frame_stack_top; /* current frame index (-1 = empty) */
int frame_stack_capacity; /* allocated capacity */
JSValue *value_stack; /* array of JSValues for locals/operands */
int value_stack_top; /* current top index */
int value_stack_capacity; /* allocated capacity */
/* Register VM frame root (updated by GC when frame moves) */
JSValue reg_current_frame; /* current JSFrameRegister being executed */
uint32_t current_register_pc; /* PC at exception time */
/* Execution state (moved from JSRuntime — per-actor) */
JSValue current_exception;
struct JSStackFrame *current_stack_frame;
BOOL current_exception_is_uncatchable : 8;
BOOL in_out_of_memory : 8;
JSInterruptHandler *interrupt_handler;
void *interrupt_opaque;
JSValue current_exception;
/* Stack overflow protection */
size_t stack_size;
const uint8_t *stack_top;
const uint8_t *stack_limit;
// todo: want this, but should be a simple increment/decrement counter while frames are pushed
size_t stack_depth;
size_t stack_limit;
/* Parser state (for GC to scan cpool during parsing) */
struct JSFunctionDef *current_parse_fd;
@@ -1138,7 +1218,6 @@ typedef enum {
JS_FUNC_KIND_BYTECODE,
JS_FUNC_KIND_C_DATA,
JS_FUNC_KIND_REGISTER, /* register-based VM function */
JS_FUNC_KIND_MCODE, /* MCODE JSON interpreter */
} JSFunctionKind;
typedef struct JSFunction {
@@ -1162,11 +1241,6 @@ typedef struct JSFunction {
JSValue env_record; /* stone record, module environment */
JSValue outer_frame; /* JSFrame JSValue, for closures */
} reg;
struct {
JSMCode *code; /* pre-parsed MCODE (off-heap) */
JSValue outer_frame; /* lexical parent frame for closures */
JSValue env_record; /* module env or JS_NULL */
} mcode;
} u;
} JSFunction;
@@ -1325,42 +1399,10 @@ typedef struct JSProperty {
#define JS_ARRAY_MAX_CAP ((word_t)((1UL << 24) - 1))
#endif
typedef enum OPCodeFormat {
#define FMT(f) OP_FMT_##f,
#define DEF(id, size, n_pop, n_push, f)
#include "quickjs-opcode.h"
#undef DEF
#undef FMT
} OPCodeFormat;
enum OPCodeEnum {
#define FMT(f)
#define DEF(id, size, n_pop, n_push, f) OP_##id,
#define def(id, size, n_pop, n_push, f)
#include "quickjs-opcode.h"
#undef def
#undef DEF
#undef FMT
OP_COUNT, /* excluding temporary opcodes */
/* temporary opcodes : overlap with the short opcodes */
OP_TEMP_START = OP_nop + 1,
OP___dummy = OP_TEMP_START - 1,
#define FMT(f)
#define DEF(id, size, n_pop, n_push, f)
#define def(id, size, n_pop, n_push, f) OP_##id,
#include "quickjs-opcode.h"
#undef def
#undef DEF
#undef FMT
OP_TEMP_END,
};
JSValue js_call_c_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv);
JSValue js_call_bound_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv);
JSValue JS_CallInternal (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv, int flags);
JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame);
JSValue mcode_exec(JSContext *ctx, JSMCode *code, JSValue this_obj,
int argc, JSValue *argv, JSValue outer_frame);
int JS_DeleteProperty (JSContext *ctx, JSValue obj, JSValue prop);
JSValue __attribute__ ((format (printf, 2, 3)))
JS_ThrowInternalError (JSContext *ctx, const char *fmt, ...);
@@ -1399,8 +1441,6 @@ static JSValue js_cell_text (JSContext *ctx, JSValue this_val, int argc, JSValue
static JSValue js_cell_push (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_pop (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_array_find (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_eval (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_mach_eval (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_stone (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_length (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_reverse (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
@@ -1544,22 +1584,6 @@ static inline void set_value (JSContext *ctx, JSValue *pval, JSValue new_val) {
*pval = new_val;
}
#if !defined(CONFIG_STACK_CHECK)
static inline uintptr_t js_get_stack_pointer (void) { return 0; }
static inline BOOL js_check_stack_overflow (JSContext *ctx, size_t alloca_size) {
return FALSE;
}
#else
static inline uintptr_t js_get_stack_pointer (void) {
return (uintptr_t)__builtin_frame_address (0);
}
static inline BOOL js_check_stack_overflow (JSContext *ctx, size_t alloca_size) {
uintptr_t sp;
sp = js_get_stack_pointer () - alloca_size;
return unlikely (sp < (uintptr_t)ctx->stack_limit);
}
#endif
void JS_ThrowInterrupted (JSContext *ctx);
static no_inline __exception int __js_poll_interrupts (JSContext *ctx) {
@@ -1692,13 +1716,6 @@ typedef struct {
} GetLineColCache;
/* === MachVarInfo (shared by mach.c and mcode.c) === */
typedef struct MachVarInfo {
char *name;
int slot;
int is_const; /* 1 for def, function args; 0 for var */
int is_closure; /* 1 if captured by a nested function */
} MachVarInfo;
/* === PPretext (parser pretext, system-malloc, used by cell_js.c parser) === */
typedef struct PPretext {
@@ -1721,8 +1738,6 @@ int get_line_col_cached (GetLineColCache *s, int *pcol_num, const uint8_t *ptr);
/* runtime.c exports */
JSValue JS_ThrowStackOverflow (JSContext *ctx);
JSValue JS_ThrowSyntaxErrorVarRedeclaration (JSContext *ctx, JSValue prop);
JSValue JS_ThrowReferenceErrorUninitialized (JSContext *ctx, JSValue name);
int JS_DefineObjectName (JSContext *ctx, JSValue obj, JSValue name);
int JS_DefineObjectNameComputed (JSContext *ctx, JSValue obj, JSValue str);
int js_method_set_properties (JSContext *ctx, JSValue func_obj, JSValue name, int flags, JSValue home_obj);
@@ -1736,12 +1751,9 @@ int JS_HasPropertyKey (JSContext *ctx, JSValue obj, JSValue key);
void *js_realloc_rt (void *ptr, size_t size);
char *js_strdup_rt (const char *str);
JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2);
no_inline __exception int js_binary_arith_slow (JSContext *ctx, JSValue *sp, OPCodeEnum op);
no_inline __exception int js_unary_arith_slow (JSContext *ctx, JSValue *sp, OPCodeEnum op);
__exception int js_post_inc_slow (JSContext *ctx, JSValue *sp, OPCodeEnum op);
no_inline int js_not_slow (JSContext *ctx, JSValue *sp);
no_inline int js_relational_slow (JSContext *ctx, JSValue *sp, OPCodeEnum op);
no_inline int js_strict_eq_slow (JSContext *ctx, JSValue *sp, BOOL is_neq);
__exception int js_operator_in (JSContext *ctx, JSValue *sp);
__exception int js_operator_delete (JSContext *ctx, JSValue *sp);
JSText *pretext_init (JSContext *ctx, int capacity);
@@ -1893,8 +1905,5 @@ JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count);
cJSON *mach_find_scope_record(cJSON *scopes, int function_nr);
int reg_vm_check_interrupt(JSContext *ctx);
/* mcode.c exports */
JSValue mcode_exec(JSContext *ctx, JSMCode *code, JSValue this_obj, int argc, JSValue *argv, JSValue outer_frame);
#endif /* QUICKJS_INTERNAL_H */

View File

@@ -1,296 +0,0 @@
/*
* QuickJS opcode definitions
*
* Copyright (c) 2017-2018 Fabrice Bellard
* Copyright (c) 2017-2018 Charlie Gordon
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#ifdef FMT
FMT(none)
FMT(none_int)
FMT(none_loc)
FMT(none_arg)
FMT(u8)
FMT(i8)
FMT(loc8)
FMT(const8)
FMT(label8)
FMT(u16)
FMT(i16)
FMT(label16)
FMT(npop)
FMT(npopx)
FMT(npop_u16)
FMT(loc)
FMT(arg)
FMT(u32)
FMT(i32)
FMT(const)
FMT(label)
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
#endif /* FMT */
#ifdef DEF
#ifndef def
#define def(id, size, n_pop, n_push, f) DEF(id, size, n_pop, n_push, f)
#endif
DEF(invalid, 1, 0, 0, none) /* never emitted */
/* push values */
DEF( push_i32, 5, 0, 1, i32)
DEF( push_const, 5, 0, 1, const)
DEF( fclosure, 5, 0, 1, const) /* must follow push_const */
DEF( null, 1, 0, 1, none)
DEF( push_this, 1, 0, 1, none) /* only used at the start of a function */
DEF( push_false, 1, 0, 1, none)
DEF( push_true, 1, 0, 1, none)
DEF( object, 1, 0, 1, none)
DEF( special_object, 2, 0, 1, u8) /* only used at the start of a function */
DEF( drop, 1, 1, 0, none) /* a -> */
DEF( nip, 1, 2, 1, none) /* a b -> b */
DEF( nip1, 1, 3, 2, none) /* a b c -> b c */
DEF( dup, 1, 1, 2, none) /* a -> a a */
DEF( dup1, 1, 2, 3, none) /* a b -> a a b */
DEF( dup2, 1, 2, 4, none) /* a b -> a b a b */
DEF( dup3, 1, 3, 6, none) /* a b c -> a b c a b c */
DEF( insert2, 1, 2, 3, none) /* obj a -> a obj a (dup_x1) */
DEF( insert3, 1, 3, 4, none) /* obj prop a -> a obj prop a (dup_x2) */
DEF( insert4, 1, 4, 5, none) /* this obj prop a -> a this obj prop a */
DEF( perm3, 1, 3, 3, none) /* obj a b -> a obj b */
DEF( perm4, 1, 4, 4, none) /* obj prop a b -> a obj prop b */
DEF( perm5, 1, 5, 5, none) /* this obj prop a b -> a this obj prop b */
DEF( swap, 1, 2, 2, none) /* a b -> b a */
DEF( swap2, 1, 4, 4, none) /* a b c d -> c d a b */
DEF( rot3l, 1, 3, 3, none) /* x a b -> a b x */
DEF( rot3r, 1, 3, 3, none) /* a b x -> x a b */
DEF( rot4l, 1, 4, 4, none) /* x a b c -> a b c x */
DEF( rot5l, 1, 5, 5, none) /* x a b c d -> a b c d x */
DEF( call, 3, 1, 1, npop) /* arguments are not counted in n_pop */
DEF( tail_call, 3, 1, 0, npop) /* arguments are not counted in n_pop */
DEF( call_method, 3, 2, 1, npop) /* arguments are not counted in n_pop */
DEF(tail_call_method, 3, 2, 0, npop) /* arguments are not counted in n_pop */
DEF( array_from, 3, 0, 1, npop) /* arguments are not counted in n_pop */
DEF( return, 1, 1, 0, none)
DEF( return_undef, 1, 0, 0, none)
DEF( throw, 1, 1, 0, none)
DEF( throw_error, 6, 0, 0, key_u8)
DEF( regexp, 1, 2, 1, none) /* create a RegExp object from the pattern and a
bytecode string */
/* Global variable access - resolved by linker to get/set_global_slot */
DEF( check_var, 5, 0, 1, key) /* check if a variable exists - resolved by linker */
DEF( get_var_undef, 5, 0, 1, key) /* resolved by linker to get_global_slot */
DEF( get_var, 5, 0, 1, key) /* resolved by linker to get_global_slot */
DEF( put_var, 5, 1, 0, key) /* resolved by linker to set_global_slot */
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 */
/* Global variable opcodes - resolved by linker to get/set_global_slot */
DEF( define_var, 6, 0, 0, key_u8)
DEF(check_define_var, 6, 0, 0, key_u8)
DEF( define_func, 6, 1, 0, key_u8)
DEF( get_field, 5, 1, 1, key)
DEF( get_field2, 5, 1, 2, key)
DEF( put_field, 5, 2, 0, key)
DEF( get_array_el, 1, 2, 1, none)
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( put_array_el, 1, 3, 0, none)
DEF( define_field, 5, 2, 1, key)
DEF( set_name, 5, 1, 1, key)
DEF(set_name_computed, 1, 2, 2, none)
DEF(define_array_el, 1, 3, 2, none)
DEF(copy_data_properties, 2, 3, 3, u8)
DEF( define_method, 6, 2, 1, key_u8)
DEF(define_method_computed, 2, 3, 1, u8) /* must come after define_method */
DEF( define_class, 6, 2, 2, key_u8) /* parent ctor -> ctor proto */
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( put_loc, 3, 1, 0, loc) /* must come after get_loc */
DEF( set_loc, 3, 1, 1, loc) /* must come after put_loc */
DEF( get_arg, 3, 0, 1, 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_loc_uninitialized, 3, 0, 0, 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_init, 3, 1, 0, loc)
DEF(get_loc_checkthis, 3, 0, 1, loc)
DEF( if_false, 5, 1, 0, label)
DEF( if_true, 5, 1, 0, label) /* must come after if_false */
DEF( goto, 5, 0, 0, label) /* must come after if_true */
DEF( catch, 5, 0, 1, label)
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( nip_catch, 1, 2, 1, none) /* catch ... a -> a */
DEF( to_propkey, 1, 1, 1, none)
/* arithmetic/logic operations */
DEF( neg, 1, 1, 1, none)
DEF( plus, 1, 1, 1, none)
DEF( dec, 1, 1, 1, none)
DEF( inc, 1, 1, 1, none)
DEF( post_dec, 1, 1, 2, none)
DEF( post_inc, 1, 1, 2, none)
DEF( dec_loc, 2, 0, 0, loc8)
DEF( inc_loc, 2, 0, 0, loc8)
DEF( add_loc, 2, 1, 0, loc8)
DEF( not, 1, 1, 1, none)
DEF( lnot, 1, 1, 1, none)
DEF( delete, 1, 2, 1, none)
DEF( delete_var, 5, 0, 1, key) /* deprecated - global object is immutable */
DEF( mul, 1, 2, 1, none)
DEF( mul_float, 1, 2, 1, none)
DEF( div, 1, 2, 1, none)
DEF( div_float, 1, 2, 1, none)
DEF( mod, 1, 2, 1, none)
DEF( add, 1, 2, 1, none)
DEF( add_float, 1, 2, 1, none)
DEF( sub, 1, 2, 1, none)
DEF( sub_float, 1, 2, 1, none)
DEF( pow, 1, 2, 1, none)
DEF( shl, 1, 2, 1, none)
DEF( sar, 1, 2, 1, none)
DEF( shr, 1, 2, 1, none)
DEF( lt, 1, 2, 1, none)
DEF( lte, 1, 2, 1, none)
DEF( gt, 1, 2, 1, none)
DEF( gte, 1, 2, 1, none)
DEF( in, 1, 2, 1, none)
DEF( strict_eq, 1, 2, 1, none)
DEF( strict_neq, 1, 2, 1, none)
DEF( and, 1, 2, 1, none)
DEF( xor, 1, 2, 1, none)
DEF( or, 1, 2, 1, none)
/* format template - format_string_cpool_idx(u32), expr_count(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( set_env_slot, 3, 1, 0, u16) /* value -> slot (patched from put_var) */
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 */
DEF( nop, 1, 0, 0, none)
/* temporary opcodes: never emitted in the final bytecode */
def( enter_scope, 3, 0, 0, u16) /* emitted in phase 1, removed in phase 2 */
def( leave_scope, 3, 0, 0, u16) /* emitted in phase 1, removed in phase 2 */
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
get_var_undef, get_var and put_var opcodes */
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, key_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, key_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_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(get_field_opt_chain, 5, 1, 1, key) /* 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( line_num, 5, 0, 0, u32) /* emitted in phase 1, removed in phase 3 */
#if SHORT_OPCODES
DEF( push_minus1, 1, 0, 1, none_int)
DEF( push_0, 1, 0, 1, none_int)
DEF( push_1, 1, 0, 1, none_int)
DEF( push_2, 1, 0, 1, none_int)
DEF( push_3, 1, 0, 1, none_int)
DEF( push_4, 1, 0, 1, none_int)
DEF( push_5, 1, 0, 1, none_int)
DEF( push_6, 1, 0, 1, none_int)
DEF( push_7, 1, 0, 1, none_int)
DEF( push_i8, 2, 0, 1, i8)
DEF( push_i16, 3, 0, 1, i16)
DEF( push_const8, 2, 0, 1, const8)
DEF( fclosure8, 2, 0, 1, const8) /* must follow push_const8 */
DEF(push_empty_string, 1, 0, 1, none)
DEF( get_loc8, 2, 0, 1, loc8)
DEF( put_loc8, 2, 1, 0, loc8)
DEF( set_loc8, 2, 1, 1, loc8)
DEF( get_loc0, 1, 0, 1, none_loc)
DEF( get_loc1, 1, 0, 1, none_loc)
DEF( get_loc2, 1, 0, 1, none_loc)
DEF( get_loc3, 1, 0, 1, none_loc)
DEF( put_loc0, 1, 1, 0, none_loc)
DEF( put_loc1, 1, 1, 0, none_loc)
DEF( put_loc2, 1, 1, 0, none_loc)
DEF( put_loc3, 1, 1, 0, none_loc)
DEF( set_loc0, 1, 1, 1, none_loc)
DEF( set_loc1, 1, 1, 1, none_loc)
DEF( set_loc2, 1, 1, 1, none_loc)
DEF( set_loc3, 1, 1, 1, none_loc)
DEF( get_arg0, 1, 0, 1, none_arg)
DEF( get_arg1, 1, 0, 1, none_arg)
DEF( get_arg2, 1, 0, 1, none_arg)
DEF( get_arg3, 1, 0, 1, none_arg)
DEF( put_arg0, 1, 1, 0, none_arg)
DEF( put_arg1, 1, 1, 0, none_arg)
DEF( put_arg2, 1, 1, 0, none_arg)
DEF( put_arg3, 1, 1, 0, none_arg)
DEF( set_arg0, 1, 1, 1, none_arg)
DEF( set_arg1, 1, 1, 1, none_arg)
DEF( set_arg2, 1, 1, 1, none_arg)
DEF( set_arg3, 1, 1, 1, none_arg)
DEF( if_false8, 2, 1, 0, label8)
DEF( if_true8, 2, 1, 0, label8) /* must come after if_false8 */
DEF( goto8, 2, 0, 0, label8) /* must come after if_true8 */
DEF( goto16, 3, 0, 0, label16)
DEF( call0, 1, 1, 1, npopx)
DEF( call1, 1, 1, 1, npopx)
DEF( call2, 1, 1, 1, npopx)
DEF( call3, 1, 1, 1, npopx)
DEF( is_null, 1, 1, 1, none)
#endif
#undef DEF
#undef def
#endif /* DEF */

View File

@@ -55,13 +55,13 @@ enum mist_obj_type {
OBJ_FORWARD = 7
};
typedef uint64_t JSValue;
#define OBJHDR_S_BIT 3u
#define OBJHDR_P_BIT 4u
#define OBJHDR_A_BIT 5u
#define OBJHDR_R_BIT 7u
#define OBJHDR_FLAG(bit) ((objhdr_t)1ull << (bit))
#define OBJHDR_S_MASK OBJHDR_FLAG (OBJHDR_S_BIT)
#define OBJHDR_P_MASK OBJHDR_FLAG (OBJHDR_P_BIT)
@@ -115,39 +115,24 @@ struct JSGCRef;
============================================================ */
#if INTPTR_MAX >= INT64_MAX
#define JS_PTR64
#define JS_PTR64_DEF(a) a
typedef uint64_t JSValue;
#define JSW 8
#else
typedef uint32_t JSValue;
#define JSW 4
#define JS_PTR64_DEF(a)
#endif
#define JSValue JSValue
/* JSValueConst is just JSValue (const is not needed in value semantics) */
typedef JSValue JSValueConst;
#define JSW 8
/* LSB-based tags */
enum {
/* Primary tags (low bits) */
JS_TAG_INT = 0, /* LSB = 0 */
JS_TAG_PTR = 1, /* LSB = 01 */
#ifdef JS_PTR64
JS_TAG_SHORT_FLOAT = 5, /* LSB = 101 */
#endif
JS_TAG_SPECIAL = 3, /* LSB = 11 */
/* Special subtypes (5 bits: xxxx11) */
JS_TAG_BOOL = 0x03, /* 00011 */
JS_TAG_NULL = 0x07, /* 00111 */
JS_TAG_EXCEPTION = 0x0F, /* 01111 */
JS_TAG_UNINITIALIZED = 0x17, /* 10111 */
JS_TAG_STRING_IMM = 0x1B, /* 11011 - immediate ASCII (up to 7 chars) */
JS_TAG_CATCH_OFFSET = 0x1F, /* 11111 */
JS_TAG_STRING_IMM = 0x0B, /* 01011 - immediate ASCII (up to 7 chars) */
};
/* Compatibility tag aliases for external code */
@@ -180,16 +165,10 @@ void JS_DeleteGCRef(JSContext *ctx, JSGCRef *ref);
/* Get primary tag (low 2-3 bits) */
static inline int
JS_VALUE_GET_TAG (JSValue v) {
#ifdef JS_PTR64
if ((v & 1) == 0) return JS_TAG_INT;
if ((v & 7) == JS_TAG_SHORT_FLOAT) return JS_TAG_SHORT_FLOAT;
if ((v & 3) == JS_TAG_PTR) return JS_TAG_PTR;
return (int)(v & 0x1F); /* special tag */
#else
if ((v & 1) == 0) return JS_TAG_INT;
if ((v & 3) == JS_TAG_PTR) return JS_TAG_PTR;
return (int)(v & 0x1F);
#endif
}
#define JS_VALUE_GET_NORM_TAG(v) JS_VALUE_GET_TAG (v)
@@ -220,7 +199,6 @@ static inline JSValue _JS_MkVal (int tag, int32_t val) {
Out of range → JS_NULL
============================================================ */
#ifdef JS_PTR64
static inline JSValue
__JS_NewFloat64 (JSContext *ctx, double d) {
union {
@@ -280,17 +258,6 @@ static inline double JS_VALUE_GET_FLOAT64 (JSValue v) {
#define JS_TAG_IS_FLOAT64(tag) ((tag) == JS_TAG_SHORT_FLOAT)
#define JS_NAN JS_MKVAL (JS_TAG_NULL, 0)
#else /* 32-bit: no short float, use boxed double */
static inline JSValue __JS_NewFloat64 (JSContext *ctx,
double d); /* forward decl */
static inline double JS_VALUE_GET_FLOAT64 (JSValue v);
#define JS_TAG_IS_FLOAT64(tag) (0)
#define JS_NAN JS_MKVAL (JS_TAG_NULL, 0)
#endif /* JS_PTR64 */
/* ============================================================
Type Checks
============================================================ */
@@ -299,14 +266,10 @@ static inline JS_BOOL JS_IsInt (JSValue v) { return (v & 1) == 0; }
static inline JS_BOOL JS_IsPtr (JSValue v) { return (v & 7) == JS_TAG_PTR; }
static inline JS_BOOL JS_IsSpecial (JSValue v) { return (v & 3) == JS_TAG_SPECIAL; }
#ifdef JS_PTR64
static inline JS_BOOL
JS_IsShortFloat (JSValue v) {
return (v & 7) == JS_TAG_SHORT_FLOAT;
}
#endif
#define JS_VALUE_IS_BOTH_INT(v1, v2) (((v1) & 1) == 0 && ((v2) & 1) == 0)
#define JS_VALUE_IS_BOTH_FLOAT(v1, v2) \
@@ -320,7 +283,6 @@ JS_IsShortFloat (JSValue v) {
#define JS_FALSE ((JSValue)JS_TAG_BOOL)
#define JS_TRUE ((JSValue)(JS_TAG_BOOL | (1 << 5)))
#define JS_EXCEPTION ((JSValue)JS_TAG_EXCEPTION)
#define JS_UNINITIALIZED ((JSValue)JS_TAG_UNINITIALIZED)
/* flags for object properties - simplified model:
- No per-property writable/configurable (use stone() for immutability)
@@ -344,27 +306,10 @@ typedef JSValue JSCFunctionData (JSContext *ctx, JSValue this_val,
int argc, JSValue *argv, int magic,
JSValue *data);
typedef struct JSMallocState {
size_t malloc_count;
size_t malloc_size;
size_t malloc_limit;
void *opaque; /* user opaque */
} JSMallocState;
typedef struct JSMallocFunctions {
void *(*js_malloc) (JSMallocState *s, size_t size);
void (*js_free) (JSMallocState *s, void *ptr);
void *(*js_realloc) (JSMallocState *s, void *ptr, size_t size);
size_t (*js_malloc_usable_size) (const void *ptr);
} JSMallocFunctions;
typedef struct JSGCObjectHeader JSGCObjectHeader;
JSValue JS_Stone (JSContext *ctx, JSValue this_val);
JSRuntime *JS_NewRuntime (void);
/* info lifetime must exceed that of rt */
void JS_SetRuntimeInfo (JSRuntime *rt, const char *info);
void JS_SetMemoryLimit (JSRuntime *rt, size_t limit);
/* use 0 to disable maximum stack size check */
void JS_SetMaxStackSize (JSContext *ctx, size_t stack_size);
@@ -372,11 +317,6 @@ void JS_SetMaxStackSize (JSContext *ctx, size_t stack_size);
used to check stack overflow. */
void JS_UpdateStackTop (JSContext *ctx);
void JS_FreeRuntime (JSRuntime *rt);
void *JS_GetRuntimeOpaque (JSRuntime *rt);
void JS_SetRuntimeOpaque (JSRuntime *rt, void *opaque);
typedef void JS_MarkFunc (JSRuntime *rt, JSGCObjectHeader *gp);
/* JS_MarkValue is a no-op with copying GC (values are traced from roots) */
void JS_MarkValue (JSRuntime *rt, JSValue val, JS_MarkFunc *mark_func);
JS_BOOL JS_IsLiveObject (JSRuntime *rt, JSValue obj);
JSContext *JS_NewContext (JSRuntime *rt);
@@ -410,8 +350,6 @@ void JS_ComputeMemoryUsage (JSRuntime *rt, JSMemoryUsage *s);
void JS_DumpMemoryUsage (FILE *fp, const JSMemoryUsage *s, JSRuntime *rt);
typedef void JSClassFinalizer (JSRuntime *rt, JSValue val);
typedef void JSClassGCMark (JSRuntime *rt, JSValue val,
JS_MarkFunc *mark_func);
typedef JSValue JSClassCall (JSContext *ctx, JSValue func_obj,
JSValue this_val, int argc,
JSValue *argv, int flags);
@@ -419,8 +357,6 @@ typedef JSValue JSClassCall (JSContext *ctx, JSValue func_obj,
typedef struct JSClassDef {
const char *class_name;
JSClassFinalizer *finalizer;
JSClassGCMark *gc_mark;
/* if call != NULL, the object is a function */
JSClassCall *call;
} JSClassDef;
@@ -459,11 +395,6 @@ JS_NewInt32 (JSContext *ctx, int32_t val) {
return JS_MKVAL (JS_TAG_INT, val);
}
static inline JSValue
JS_NewCatchOffset (JSContext *ctx, int32_t val) {
return JS_MKVAL (JS_TAG_CATCH_OFFSET, val);
}
static inline JSValue
JS_NewInt64 (JSContext *ctx, int64_t val) {
JSValue v;
@@ -521,10 +452,6 @@ static inline JS_BOOL JS_IsException (JSValue v) {
return (JS_VALUE_GET_TAG (v) == JS_TAG_EXCEPTION);
}
static inline JS_BOOL JS_IsUninitialized (JSValue v) {
return (JS_VALUE_GET_TAG (v) == JS_TAG_UNINITIALIZED);
}
/* Immediate String Helpers */
#define MIST_ASCII_MAX_LEN 7
@@ -538,13 +465,11 @@ MIST_GetImmediateASCIILen (JSValue v) {
return (int)((v >> 5) & 0x7);
}
static inline int
MIST_GetImmediateASCIIChar (JSValue v, int idx) {
static inline int MIST_GetImmediateASCIIChar (JSValue v, int idx) {
return (int)((v >> (8 + idx * 8)) & 0xFF);
}
static inline JSValue
MIST_TryNewImmediateASCII (const char *str, size_t len) {
static inline JSValue MIST_TryNewImmediateASCII (const char *str, size_t len) {
if (len > MIST_ASCII_MAX_LEN) return JS_NULL;
JSValue v = (JSValue)JS_TAG_STRING_IMM | ((JSValue)len << 5);
for (size_t i = 0; i < len; i++) {
@@ -555,20 +480,10 @@ MIST_TryNewImmediateASCII (const char *str, size_t len) {
return v;
}
static inline JS_BOOL JS_IsInteger (JSValue v) {
return JS_VALUE_GET_TAG (v) == JS_TAG_INT;
}
static inline JS_BOOL JS_IsObject (JSValue v) {
return JS_IsPtr (v);
}
JS_BOOL JS_IsArray(JSValue v);
JS_BOOL JS_IsRecord(JSValue v);
#define JS_IsObject JS_IsRecord
JS_BOOL JS_IsFunction(JSValue v);
JS_BOOL JS_IsCode(JSValue v);
JS_BOOL JS_IsForwarded(JSValue v);
JS_BOOL JS_IsFrame(JSValue v);
JS_BOOL JS_IsBlob(JSValue v);
JS_BOOL JS_IsText(JSValue v);
static JS_BOOL JS_IsStone(JSValue v);
@@ -591,6 +506,7 @@ JSValue __js_printf_like (2, 3)
JS_ThrowInternalError (JSContext *ctx, const char *fmt, ...);
JSValue JS_ThrowOutOfMemory (JSContext *ctx);
// TODO: rename this to just "eq"
JS_BOOL JS_StrictEq (JSContext *ctx, JSValue op1, JSValue op2);
int JS_ToBool (JSContext *ctx, JSValue val); /* return -1 for JS_EXCEPTION */
@@ -625,7 +541,6 @@ JSValue JS_NewObject (JSContext *ctx);
JSValue JS_NewArray (JSContext *ctx);
JSValue JS_NewArrayLen (JSContext *ctx, uint32_t len);
/* GC-safe push: takes pointer to array, updates it if array grows */
int JS_ArrayPush (JSContext *ctx, JSValue *arr_ptr, JSValue val);
JSValue JS_ArrayPop (JSContext *ctx, JSValue obj);
@@ -690,24 +605,17 @@ void JS_PrintTextLn (JSContext *ctx, JSValue val);
void JS_PrintFormatted (JSContext *ctx, const char *fmt, int count, JSValue *values);
JSValue JS_GetProperty (JSContext *ctx, JSValue this_obj, JSValue prop);
int JS_SetProperty (JSContext *ctx, JSValue this_obj, JSValue prop, JSValue val);
// For records
JSValue JS_GetPropertyStr (JSContext *ctx, JSValue this_obj, const char *prop);
int JS_SetPropertyStr (JSContext *ctx, JSValue this_obj, const char *prop, JSValue val);
// Set property on the global object
int JS_SetGlobalStr (JSContext *ctx, const char *prop, JSValue val);
int JS_SetProperty (JSContext *ctx, JSValue this_obj, JSValue prop, JSValue val);
JSValue JS_GetPrototype (JSContext *ctx, JSValue val);
// Must be an array
JSValue JS_GetPropertyNumber (JSContext *ctx, JSValue this_obj, int idx);
JSValue JS_SetPropertyNumber (JSContext *ctx, JSValue obj, int idx, JSValue val);
// Indexed property access (works with arrays and objects)
JSValue JS_GetPropertyUint32 (JSContext *ctx, JSValue this_obj, uint32_t idx);
int JS_SetPropertyUint32 (JSContext *ctx, JSValue this_obj, uint32_t idx, JSValue val);
int JS_SetPropertyInt64 (JSContext *ctx, JSValue this_obj, int64_t idx, JSValue val);
JSValue JS_GetPrototype (JSContext *ctx, JSValue val);
/* Get property keys as array of text */
JSValue JS_GetOwnPropertyNames (JSContext *ctx, JSValue obj);
@@ -732,13 +640,6 @@ JSValue JS_JSONStringify (JSContext *ctx, JSValue obj,
typedef int JSInterruptHandler (JSRuntime *rt, void *opaque);
void JS_SetInterruptHandler (JSContext *ctx, JSInterruptHandler *cb,
void *opaque);
/* select which debug info is stripped from the compiled code */
#define JS_STRIP_SOURCE (1 << 0) /* strip source code */
#define JS_STRIP_DEBUG \
(1 << 1) /* strip all debug info including source code */
void JS_SetStripInfo (JSRuntime *rt, int flags);
int JS_GetStripInfo (JSRuntime *rt);
/* C function definition */
typedef enum JSCFunctionEnum {
@@ -1046,12 +947,12 @@ typedef void (*js_hook) (JSContext *, int type, js_debug *dbg, void *user);
void js_debug_sethook (JSContext *ctx, js_hook, int type, void *user);
uint32_t js_debugger_stack_depth (JSContext *ctx);
JSValue js_debugger_backtrace_fns (JSContext *ctx, const uint8_t *cur_pc);
JSValue js_debugger_backtrace_fns (JSContext *ctx);
JSValue js_debugger_closure_variables (JSContext *ctx, JSValue fn);
JSValue js_debugger_local_variables (JSContext *ctx, int stack_index);
void js_debugger_set_closure_variable (JSContext *js, JSValue fn,
JSValue var_name, JSValue val);
JSValue js_debugger_build_backtrace (JSContext *ctx, const uint8_t *cur_pc);
JSValue js_debugger_build_backtrace (JSContext *ctx);
JSValue js_debugger_fn_info (JSContext *ctx, JSValue fn);
JSValue js_debugger_fn_bytecode (JSContext *js, JSValue fn);
void *js_debugger_val_address (JSContext *js, JSValue val);
@@ -1091,30 +992,14 @@ MachCode *JS_DeserializeMachCode(const uint8_t *data, size_t size);
/* Load compiled MachCode into a JSContext, materializing JSValues. */
struct JSCodeRegister *JS_LoadMachCode(JSContext *ctx, MachCode *mc, JSValue env);
/* Dump MACH bytecode to stdout. Takes AST cJSON tree. */
void JS_DumpMachTree (JSContext *ctx, struct cJSON *ast, JSValue env);
/* Dump MACH bytecode to stdout. Takes AST JSON string. */
void JS_DumpMach (JSContext *ctx, const char *ast_json, JSValue env);
/* Compile and execute MACH bytecode from AST cJSON tree. */
JSValue JS_RunMachTree (JSContext *ctx, struct cJSON *ast, JSValue env);
/* Compile and execute MACH bytecode from AST JSON string. */
JSValue JS_RunMach (JSContext *ctx, const char *ast_json, JSValue env);
/* Deserialize and execute pre-compiled MACH binary bytecode. */
JSValue JS_RunMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue env);
/* Execute MCODE from cJSON tree. Takes ownership of root. */
JSValue JS_CallMcodeTree (JSContext *ctx, struct cJSON *root);
/* Dump disassembly of pre-compiled MACH binary bytecode. */
void JS_DumpMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue env);
/* Execute MCODE from cJSON tree with hidden env. Takes ownership of root. */
JSValue JS_CallMcodeTreeEnv (JSContext *ctx, struct cJSON *root, JSValue env);
/* Parse and execute MCODE JSON string.
Returns result of execution, or JS_EXCEPTION on error. */
JSValue JS_CallMcode (JSContext *ctx, const char *mcode_json);
/* Compile mcode JSON IR to MachCode binary. */
MachCode *mach_compile_mcode(struct cJSON *mcode_json);
/* Get stack trace as cJSON array of frame objects.
Returns NULL if no register VM frame is active.

File diff suppressed because it is too large Load Diff

View File

@@ -47,9 +47,11 @@ static struct {
actor_node *main_head; // Main Thread Queue Head
actor_node *main_tail; // Main Thread Queue Tail
int shutting_down;
pthread_t *worker_threads;
int shutting_down;
int quiescence_enabled; // set after bootstrap, before actor_loop
_Atomic int quiescent_count; // actors idle with no messages and no timers
pthread_t *worker_threads;
int num_workers;
pthread_t timer_thread;
} engine;
@@ -258,6 +260,10 @@ void actor_initialize(void) {
void actor_free(cell_rt *actor)
{
if (actor->is_quiescent) {
actor->is_quiescent = 0;
atomic_fetch_sub(&engine.quiescent_count, 1);
}
lockless_shdel(actors, actor->id);
// Do not go forward with actor destruction until the actor is completely free
@@ -303,14 +309,45 @@ void actor_free(cell_rt *actor)
free(actor);
int actor_count = lockless_shlen(actors);
if (actor_count == 0) exit(0);
if (actor_count == 0) {
fprintf(stderr, "all actors are dead\n");
pthread_mutex_lock(&engine.lock);
engine.shutting_down = 1;
pthread_cond_broadcast(&engine.wake_cond);
pthread_cond_broadcast(&engine.timer_cond);
pthread_cond_broadcast(&engine.main_cond);
pthread_mutex_unlock(&engine.lock);
}
}
int scheduler_actor_count(void) {
return (int)lockless_shlen(actors);
}
void scheduler_enable_quiescence(void) {
engine.quiescence_enabled = 1;
// Check if all actors are already quiescent
int qc = atomic_load(&engine.quiescent_count);
int total = (int)lockless_shlen(actors);
if (qc >= total && total > 0) {
pthread_mutex_lock(&engine.lock);
engine.shutting_down = 1;
pthread_cond_broadcast(&engine.wake_cond);
pthread_cond_broadcast(&engine.timer_cond);
pthread_cond_broadcast(&engine.main_cond);
pthread_mutex_unlock(&engine.lock);
}
}
void exit_handler(void) {
static int already_exiting = 0;
if (already_exiting) return;
already_exiting = 1;
pthread_mutex_lock(&engine.lock);
engine.shutting_down = 1;
pthread_cond_broadcast(&engine.wake_cond);
pthread_cond_broadcast(&engine.timer_cond);
pthread_cond_broadcast(&engine.wake_cond);
pthread_cond_broadcast(&engine.timer_cond);
pthread_cond_broadcast(&engine.main_cond);
pthread_mutex_unlock(&engine.lock);
@@ -319,19 +356,17 @@ void exit_handler(void) {
for (int i=0; i < engine.num_workers; i++) {
pthread_join(engine.worker_threads[i], NULL);
}
free(engine.worker_threads);
pthread_mutex_destroy(&engine.lock);
pthread_cond_destroy(&engine.wake_cond);
pthread_cond_destroy(&engine.timer_cond);
pthread_cond_destroy(&engine.main_cond);
pthread_mutex_destroy(actors_mutex);
free(actors_mutex);
arrfree(timer_heap);
exit(0);
}
int actor_exists(const char *id)
@@ -357,9 +392,13 @@ void set_actor_state(cell_rt *actor)
case ACTOR_IDLE:
if (arrlen(actor->letters)) {
if (actor->is_quiescent) {
actor->is_quiescent = 0;
atomic_fetch_sub(&engine.quiescent_count, 1);
}
actor->state = ACTOR_READY;
actor->ar = 0;
actor_node *n = malloc(sizeof(actor_node));
n->actor = actor;
n->next = NULL;
@@ -384,21 +423,46 @@ void set_actor_state(cell_rt *actor)
}
pthread_mutex_unlock(&engine.lock);
} else if (!arrlen(actor->letters) && !hmlen(actor->timers)) {
// Schedule remove timer
static uint32_t global_timer_id = 1;
uint32_t id = global_timer_id++;
actor->ar = id;
uint64_t now = cell_ns();
uint64_t execute_at = now + (uint64_t)(actor->ar_secs * 1e9);
pthread_mutex_lock(&engine.lock);
heap_push(execute_at, actor, id, TIMER_NATIVE_REMOVE);
if (timer_heap[0].timer_id == id) {
pthread_cond_signal(&engine.timer_cond);
} else if (!hmlen(actor->timers)) {
// No messages AND no timers
// Only count as quiescent if no $unneeded callback registered
int has_unneeded = !JS_IsNull(actor->unneeded_ref.val);
if (!actor->is_quiescent && actor->id && !has_unneeded) {
actor->is_quiescent = 1;
int qc = atomic_fetch_add(&engine.quiescent_count, 1) + 1;
int total = (int)lockless_shlen(actors);
if (qc >= total && total > 0 && engine.quiescence_enabled) {
pthread_mutex_lock(&engine.lock);
engine.shutting_down = 1;
pthread_cond_broadcast(&engine.wake_cond);
pthread_cond_broadcast(&engine.timer_cond);
pthread_cond_broadcast(&engine.main_cond);
pthread_mutex_unlock(&engine.lock);
}
}
if (!engine.shutting_down) {
// Schedule remove timer
static uint32_t global_timer_id = 1;
uint32_t id = global_timer_id++;
actor->ar = id;
uint64_t now = cell_ns();
uint64_t execute_at = now + (uint64_t)(actor->ar_secs * 1e9);
pthread_mutex_lock(&engine.lock);
heap_push(execute_at, actor, id, TIMER_NATIVE_REMOVE);
if (timer_heap[0].timer_id == id) {
pthread_cond_signal(&engine.timer_cond);
}
pthread_mutex_unlock(&engine.lock);
}
} else {
// Has timers but no letters — waiting, not quiescent
if (actor->is_quiescent) {
actor->is_quiescent = 0;
atomic_fetch_sub(&engine.quiescent_count, 1);
}
pthread_mutex_unlock(&engine.lock);
}
break;
}
@@ -520,11 +584,17 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
actor->main_thread_only = mainthread;
actor->id = strdup(id);
actor->ar_secs = ar;
int added = lockless_shput_unique(actors, id, actor);
int added = lockless_shput_unique(actors, actor->id, actor);
if (!added) {
free(actor->id);
return "Actor with given ID already exists.";
}
// Now that actor is in the registry, track its quiescent state
if (actor->state == ACTOR_IDLE && !arrlen(actor->letters)
&& !hmlen(actor->timers) && JS_IsNull(actor->unneeded_ref.val)) {
actor->is_quiescent = 1;
atomic_fetch_add(&engine.quiescent_count, 1);
}
return NULL;
}
@@ -578,20 +648,22 @@ void actor_turn(cell_rt *actor)
arrdel(actor->letters, 0); // O(N) but we kept array as requested
pthread_mutex_unlock(actor->msg_mutex);
if (l.type == LETTER_BLOB) {
if (l.type == LETTER_BLOB) {
// Create a JS blob from the C blob
size_t size = blob_length(l.blob_data) / 8; // Convert bits to bytes
JSValue arg = js_new_blob_stoned_copy(actor->context, (void*)blob_data(l.blob_data), size);
blob_destroy(l.blob_data);
result = JS_Call(actor->context, actor->message_handle_ref.val, JS_NULL, 1, &arg);
uncaught_exception(actor->context, result);
if (!uncaught_exception(actor->context, result))
actor->disrupt = 1;
JS_FreeValue(actor->context, arg);
} else if (l.type == LETTER_CALLBACK) {
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
uncaught_exception(actor->context, result);
if (!uncaught_exception(actor->context, result))
actor->disrupt = 1;
JS_FreeValue(actor->context, l.callback);
}
if (actor->disrupt) goto ENDTURN;
ENDTURN:
@@ -599,9 +671,17 @@ void actor_turn(cell_rt *actor)
if (actor->trace_hook)
actor->trace_hook(actor->name, CELL_HOOK_EXIT);
if (actor->disrupt) {
/* Actor must die. Unlock before freeing so actor_free can
lock/unlock/destroy the mutex without use-after-free. */
pthread_mutex_unlock(actor->mutex);
actor_free(actor);
return;
}
set_actor_state(actor);
pthread_mutex_unlock(actor->mutex);
}

View File

@@ -21,7 +21,6 @@ static const char *js_type_name(JSValue v) {
if (JS_IsText(v)) return "string";
if (JS_IsArray(v)) return "array";
if (JS_IsRecord(v)) return "object";
if (JS_IsObject(v)) return "object";
return "unknown";
}
@@ -310,7 +309,6 @@ TEST(string_heap_to_cstring) {
TEST(object_create) {
JSValue obj = JS_NewObject(ctx);
ASSERT(JS_IsObject(obj));
ASSERT(JS_IsRecord(obj));
return 1;
}
@@ -435,7 +433,6 @@ TEST(object_many_properties_resize) {
TEST(array_create) {
JSValue arr = JS_NewArray(ctx);
ASSERT(JS_IsObject(arr));
ASSERT(JS_IsArray(arr));
return 1;
}
@@ -465,9 +462,9 @@ TEST(array_get_by_index) {
JS_ArrayPush(ctx, &arr_ref.val, JS_NewInt32(ctx, 200));
JS_ArrayPush(ctx, &arr_ref.val, JS_NewInt32(ctx, 300));
JSValue v0 = JS_GetPropertyUint32(ctx, arr_ref.val, 0);
JSValue v1 = JS_GetPropertyUint32(ctx, arr_ref.val, 1);
JSValue v2 = JS_GetPropertyUint32(ctx, arr_ref.val, 2);
JSValue v0 = JS_GetPropertyNumber(ctx, arr_ref.val, 0);
JSValue v1 = JS_GetPropertyNumber(ctx, arr_ref.val, 1);
JSValue v2 = JS_GetPropertyNumber(ctx, arr_ref.val, 2);
JS_PopGCRef(ctx, &arr_ref);
ASSERT_INT(v0, 100);
@@ -486,12 +483,12 @@ TEST(array_set_by_index) {
/* Create values first, then read arr_ref.val */
JSValue v55 = JS_NewInt32(ctx, 55);
JS_SetPropertyUint32(ctx, arr_ref.val, 0, v55);
JS_SetPropertyNumber(ctx, arr_ref.val, 0, v55);
JSValue v66 = JS_NewInt32(ctx, 66);
JS_SetPropertyUint32(ctx, arr_ref.val, 1, v66);
JS_SetPropertyNumber(ctx, arr_ref.val, 1, v66);
JSValue v0 = JS_GetPropertyUint32(ctx, arr_ref.val, 0);
JSValue v1 = JS_GetPropertyUint32(ctx, arr_ref.val, 1);
JSValue v0 = JS_GetPropertyNumber(ctx, arr_ref.val, 0);
JSValue v1 = JS_GetPropertyNumber(ctx, arr_ref.val, 1);
JS_PopGCRef(ctx, &arr_ref);
ASSERT_INT(v0, 55);
@@ -525,7 +522,7 @@ TEST(array_out_of_bounds_is_null) {
arr_ref.val = JS_NewArray(ctx);
JS_ArrayPush(ctx, &arr_ref.val, JS_NewInt32(ctx, 1));
JSValue val = JS_GetPropertyUint32(ctx, arr_ref.val, 999);
JSValue val = JS_GetPropertyNumber(ctx, arr_ref.val, 999);
JS_PopGCRef(ctx, &arr_ref);
ASSERT(JS_IsNull(val));
return 1;
@@ -544,10 +541,10 @@ TEST(array_mixed_types) {
JS_ArrayPush(ctx, &arr_ref.val, JS_TRUE);
JS_ArrayPush(ctx, &arr_ref.val, JS_NULL);
JSValue v0 = JS_GetPropertyUint32(ctx, arr_ref.val, 0);
JSValue v1 = JS_GetPropertyUint32(ctx, arr_ref.val, 1);
JSValue v2 = JS_GetPropertyUint32(ctx, arr_ref.val, 2);
JSValue v3 = JS_GetPropertyUint32(ctx, arr_ref.val, 3);
JSValue v0 = JS_GetPropertyNumber(ctx, arr_ref.val, 0);
JSValue v1 = JS_GetPropertyNumber(ctx, arr_ref.val, 1);
JSValue v2 = JS_GetPropertyNumber(ctx, arr_ref.val, 2);
JSValue v3 = JS_GetPropertyNumber(ctx, arr_ref.val, 3);
JS_PopGCRef(ctx, &str_ref);
JS_PopGCRef(ctx, &arr_ref);
@@ -571,9 +568,9 @@ TEST(array_many_elements_resize) {
JS_GetLength(ctx, arr_ref.val, &len);
/* Verify some values */
JSValue v0 = JS_GetPropertyUint32(ctx, arr_ref.val, 0);
JSValue v500 = JS_GetPropertyUint32(ctx, arr_ref.val, 500);
JSValue v999 = JS_GetPropertyUint32(ctx, arr_ref.val, 999);
JSValue v0 = JS_GetPropertyNumber(ctx, arr_ref.val, 0);
JSValue v500 = JS_GetPropertyNumber(ctx, arr_ref.val, 500);
JSValue v999 = JS_GetPropertyNumber(ctx, arr_ref.val, 999);
/* Pop BEFORE assertions */
JS_PopGCRef(ctx, &arr_ref);
@@ -716,9 +713,9 @@ TEST(array_slice_basic) {
JS_GetLength(ctx, sliced, &len);
ASSERT(len == 3);
JSValue v0 = JS_GetPropertyUint32(ctx, sliced, 0);
JSValue v1 = JS_GetPropertyUint32(ctx, sliced, 1);
JSValue v2 = JS_GetPropertyUint32(ctx, sliced, 2);
JSValue v0 = JS_GetPropertyNumber(ctx, sliced, 0);
JSValue v1 = JS_GetPropertyNumber(ctx, sliced, 1);
JSValue v2 = JS_GetPropertyNumber(ctx, sliced, 2);
ASSERT_INT(v0, 10);
ASSERT_INT(v1, 20);
ASSERT_INT(v2, 30);
@@ -747,10 +744,10 @@ TEST(array_concat_basic) {
JS_GetLength(ctx, result, &len);
ASSERT(len == 4);
ASSERT_INT(JS_GetPropertyUint32(ctx, result, 0), 1);
ASSERT_INT(JS_GetPropertyUint32(ctx, result, 1), 2);
ASSERT_INT(JS_GetPropertyUint32(ctx, result, 2), 3);
ASSERT_INT(JS_GetPropertyUint32(ctx, result, 3), 4);
ASSERT_INT(JS_GetPropertyNumber(ctx, result, 0), 1);
ASSERT_INT(JS_GetPropertyNumber(ctx, result, 1), 2);
ASSERT_INT(JS_GetPropertyNumber(ctx, result, 2), 3);
ASSERT_INT(JS_GetPropertyNumber(ctx, result, 3), 4);
return 1;
}
@@ -772,11 +769,11 @@ TEST(array_sort_numbers) {
JS_GetLength(ctx, sorted, &len);
ASSERT(len == 5);
ASSERT_INT(JS_GetPropertyUint32(ctx, sorted, 0), 10);
ASSERT_INT(JS_GetPropertyUint32(ctx, sorted, 1), 20);
ASSERT_INT(JS_GetPropertyUint32(ctx, sorted, 2), 30);
ASSERT_INT(JS_GetPropertyUint32(ctx, sorted, 3), 40);
ASSERT_INT(JS_GetPropertyUint32(ctx, sorted, 4), 50);
ASSERT_INT(JS_GetPropertyNumber(ctx, sorted, 0), 10);
ASSERT_INT(JS_GetPropertyNumber(ctx, sorted, 1), 20);
ASSERT_INT(JS_GetPropertyNumber(ctx, sorted, 2), 30);
ASSERT_INT(JS_GetPropertyNumber(ctx, sorted, 3), 40);
ASSERT_INT(JS_GetPropertyNumber(ctx, sorted, 4), 50);
return 1;
}
@@ -840,8 +837,8 @@ TEST(array_filter_basic) {
JS_GetLength(ctx, filtered, &len);
ASSERT(len == 5); /* 6, 7, 8, 9, 10 */
ASSERT_INT(JS_GetPropertyUint32(ctx, filtered, 0), 6);
ASSERT_INT(JS_GetPropertyUint32(ctx, filtered, 4), 10);
ASSERT_INT(JS_GetPropertyNumber(ctx, filtered, 0), 6);
ASSERT_INT(JS_GetPropertyNumber(ctx, filtered, 4), 10);
return 1;
}
@@ -865,11 +862,11 @@ TEST(array_filter_even) {
JS_GetLength(ctx, filtered, &len);
ASSERT(len == 5); /* 2, 4, 6, 8, 10 */
ASSERT_INT(JS_GetPropertyUint32(ctx, filtered, 0), 2);
ASSERT_INT(JS_GetPropertyUint32(ctx, filtered, 1), 4);
ASSERT_INT(JS_GetPropertyUint32(ctx, filtered, 2), 6);
ASSERT_INT(JS_GetPropertyUint32(ctx, filtered, 3), 8);
ASSERT_INT(JS_GetPropertyUint32(ctx, filtered, 4), 10);
ASSERT_INT(JS_GetPropertyNumber(ctx, filtered, 0), 2);
ASSERT_INT(JS_GetPropertyNumber(ctx, filtered, 1), 4);
ASSERT_INT(JS_GetPropertyNumber(ctx, filtered, 2), 6);
ASSERT_INT(JS_GetPropertyNumber(ctx, filtered, 3), 8);
ASSERT_INT(JS_GetPropertyNumber(ctx, filtered, 4), 10);
return 1;
}
@@ -893,9 +890,9 @@ TEST(array_map_double) {
JS_GetLength(ctx, mapped, &len);
ASSERT(len == 3);
ASSERT_INT(JS_GetPropertyUint32(ctx, mapped, 0), 2);
ASSERT_INT(JS_GetPropertyUint32(ctx, mapped, 1), 4);
ASSERT_INT(JS_GetPropertyUint32(ctx, mapped, 2), 6);
ASSERT_INT(JS_GetPropertyNumber(ctx, mapped, 0), 2);
ASSERT_INT(JS_GetPropertyNumber(ctx, mapped, 1), 4);
ASSERT_INT(JS_GetPropertyNumber(ctx, mapped, 2), 6);
return 1;
}
@@ -1356,9 +1353,9 @@ TEST(cell_reverse_array) {
JSValue reversed = JS_CellReverse(ctx, arr_ref.val);
JS_PopGCRef(ctx, &arr_ref);
ASSERT(JS_IsArray(reversed));
ASSERT_INT(JS_GetPropertyUint32(ctx, reversed, 0), 3);
ASSERT_INT(JS_GetPropertyUint32(ctx, reversed, 1), 2);
ASSERT_INT(JS_GetPropertyUint32(ctx, reversed, 2), 1);
ASSERT_INT(JS_GetPropertyNumber(ctx, reversed, 0), 3);
ASSERT_INT(JS_GetPropertyNumber(ctx, reversed, 1), 2);
ASSERT_INT(JS_GetPropertyNumber(ctx, reversed, 2), 1);
return 1;
}
@@ -1408,9 +1405,9 @@ TEST(parse_json_array) {
int64_t len;
JS_GetLength(ctx, arr, &len);
ASSERT(len == 3);
ASSERT_INT(JS_GetPropertyUint32(ctx, arr, 0), 1);
ASSERT_INT(JS_GetPropertyUint32(ctx, arr, 1), 2);
ASSERT_INT(JS_GetPropertyUint32(ctx, arr, 2), 3);
ASSERT_INT(JS_GetPropertyNumber(ctx, arr, 0), 1);
ASSERT_INT(JS_GetPropertyNumber(ctx, arr, 1), 2);
ASSERT_INT(JS_GetPropertyNumber(ctx, arr, 2), 3);
return 1;
}
@@ -1565,12 +1562,12 @@ TEST(property_type_restrictions) {
/* Setting numeric properties on non-arrays should throw */
JSValue v100 = JS_NewInt32(ctx, 100);
int ret1 = JS_SetPropertyUint32(ctx, obj_ref.val, 0, v100);
int ret1 = JS_IsException(JS_SetPropertyNumber(ctx, obj_ref.val, 0, v100)) ? -1 : 0;
int has_exc1 = JS_HasException(ctx);
JS_GetException(ctx); /* Clear the exception */
/* Getting numeric properties on objects should return null */
JSValue v0 = JS_GetPropertyUint32(ctx, obj_ref.val, 0);
JSValue v0 = JS_GetPropertyNumber(ctx, obj_ref.val, 0);
int v0_is_null = JS_IsNull(v0);
/* Getting text keys from arrays should return null */
@@ -1646,8 +1643,8 @@ TEST(new_array_from) {
int64_t len;
JS_GetLength(ctx, arr, &len);
ASSERT(len == 4);
ASSERT_INT(JS_GetPropertyUint32(ctx, arr, 0), 10);
ASSERT_INT(JS_GetPropertyUint32(ctx, arr, 3), 40);
ASSERT_INT(JS_GetPropertyNumber(ctx, arr, 0), 10);
ASSERT_INT(JS_GetPropertyNumber(ctx, arr, 3), 40);
return 1;
}
@@ -1833,9 +1830,7 @@ TEST(is_function_check) {
TEST(is_integer_vs_number) {
JSValue i = JS_NewInt32(ctx, 42);
JSValue f = JS_NewFloat64(ctx, 3.14);
ASSERT(JS_IsInteger(i));
ASSERT(JS_IsInt(i));
ASSERT(!JS_IsInteger(f));
ASSERT(!JS_IsInt(f));
ASSERT(JS_IsNumber(i));
ASSERT(JS_IsNumber(f));
@@ -1983,9 +1978,9 @@ TEST(wota_encode_nested_array) {
int is_arr = JS_IsArray(decoded);
int64_t len;
JS_GetLength(ctx, decoded, &len);
JSValue v0 = JS_GetPropertyUint32(ctx, decoded, 0);
JSValue v2 = JS_GetPropertyUint32(ctx, decoded, 2);
JSValue inner = JS_GetPropertyUint32(ctx, decoded, 1);
JSValue v0 = JS_GetPropertyNumber(ctx, decoded, 0);
JSValue v2 = JS_GetPropertyNumber(ctx, decoded, 2);
JSValue inner = JS_GetPropertyNumber(ctx, decoded, 1);
int inner_is_arr = JS_IsArray(inner);
int64_t inner_len;
JS_GetLength(ctx, inner, &inner_len);

15
streamline.ce Normal file
View File

@@ -0,0 +1,15 @@
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
var filename = args[0]
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
var optimized = streamline(compiled)
print(json.encode(optimized))

351
streamline.cm Normal file
View File

@@ -0,0 +1,351 @@
// streamline.cm — mcode IR optimizer
// Single forward pass: type inference + strength reduction
var streamline = function(ir) {
// Type constants
var T_UNKNOWN = "unknown"
var T_INT = "int"
var T_FLOAT = "float"
var T_NUM = "num"
var T_TEXT = "text"
var T_BOOL = "bool"
var T_NULL = "null"
// Integer arithmetic ops that produce integer results
var int_result_ops = {
add_int: true, sub_int: true, mul_int: true,
div_int: true, mod_int: true
}
// Float arithmetic ops that produce float results
var float_result_ops = {
add_float: true, sub_float: true, mul_float: true,
div_float: true, mod_float: true
}
// Comparison ops that produce bool results
var bool_result_ops = {
eq_int: true, ne_int: true, lt_int: true, gt_int: true,
le_int: true, ge_int: true,
eq_float: true, ne_float: true, lt_float: true, gt_float: true,
le_float: true, ge_float: true,
eq_text: true, ne_text: true, lt_text: true, gt_text: true,
le_text: true, ge_text: true,
eq_bool: true, ne_bool: true,
eq_tol: true, ne_tol: true,
not: true, and: true, or: true,
is_int: true, is_text: true, is_num: true,
is_bool: true, is_null: true, is_identical: true
}
// Type check opcodes and what type they verify
var type_check_map = {
is_int: T_INT,
is_text: T_TEXT,
is_num: T_NUM,
is_bool: T_BOOL,
is_null: T_NULL
}
// Determine the type of an access literal value
var access_value_type = function(val) {
if (is_number(val)) {
if (is_integer(val)) {
return T_INT
}
return T_FLOAT
}
if (is_text(val)) {
return T_TEXT
}
return T_UNKNOWN
}
// Update slot_types for an instruction (shared tracking logic)
var track_types = function(slot_types, instr) {
var op = instr[0]
var src_type = null
if (op == "access") {
slot_types[text(instr[1])] = access_value_type(instr[2])
} else if (op == "int") {
slot_types[text(instr[1])] = T_INT
} else if (op == "true" || op == "false") {
slot_types[text(instr[1])] = T_BOOL
} else if (op == "null") {
slot_types[text(instr[1])] = T_NULL
} else if (op == "move") {
src_type = slot_types[text(instr[2])]
if (src_type != null) {
slot_types[text(instr[1])] = src_type
} else {
slot_types[text(instr[1])] = T_UNKNOWN
}
} else if (int_result_ops[op] == true) {
slot_types[text(instr[1])] = T_INT
} else if (float_result_ops[op] == true) {
slot_types[text(instr[1])] = T_FLOAT
} else if (op == "concat") {
slot_types[text(instr[1])] = T_TEXT
} else if (bool_result_ops[op] == true) {
slot_types[text(instr[1])] = T_BOOL
} else if (op == "load_field" || op == "load_index" || op == "load_dynamic") {
slot_types[text(instr[1])] = T_UNKNOWN
} else if (op == "invoke") {
slot_types[text(instr[2])] = T_UNKNOWN
} else if (op == "pop" || op == "get" || op == "function") {
slot_types[text(instr[1])] = T_UNKNOWN
} else if (op == "typeof") {
slot_types[text(instr[1])] = T_TEXT
} else if (op == "neg_int") {
slot_types[text(instr[1])] = T_INT
} else if (op == "neg_float") {
slot_types[text(instr[1])] = T_FLOAT
} else if (op == "bitnot" || op == "bitand" || op == "bitor" ||
op == "bitxor" || op == "shl" || op == "shr" || op == "ushr") {
slot_types[text(instr[1])] = T_INT
}
return null
}
// Check if a slot has a known type (with T_NUM subsumption)
var slot_is = function(slot_types, slot, typ) {
var known = slot_types[text(slot)]
if (known == null) {
return false
}
if (known == typ) {
return true
}
if (typ == T_NUM && (known == T_INT || known == T_FLOAT)) {
return true
}
return false
}
// Optimize a single function's instructions
var optimize_function = function(func) {
var instructions = func.instructions
var num_instr = 0
var slot_types = null
var nop_counter = 0
var i = 0
var instr = null
var op = null
var dest = 0
var src = 0
var checked_type = null
var next = null
var next_op = null
var target_label = null
var src_known = null
var jlen = 0
var j = 0
var peek = null
if (instructions == null || length(instructions) == 0) {
return null
}
num_instr = length(instructions)
slot_types = {}
// Peephole optimization pass: type tracking + strength reduction
i = 0
while (i < num_instr) {
instr = instructions[i]
// Labels are join points: clear all type info (conservative)
if (is_text(instr)) {
slot_types = {}
i = i + 1
continue
}
if (!is_array(instr)) {
i = i + 1
continue
}
op = instr[0]
// --- Peephole: type-check + jump where we know the type ---
if (type_check_map[op] != null && i + 1 < num_instr) {
dest = instr[1]
src = instr[2]
checked_type = type_check_map[op]
next = instructions[i + 1]
if (is_array(next)) {
next_op = next[0]
// Pattern: is_<type> t, x -> jump_false t, label
if (next_op == "jump_false" && next[1] == dest) {
target_label = next[2]
if (slot_is(slot_types, src, checked_type)) {
// Known match: check always true, never jumps — eliminate both
nop_counter = nop_counter + 1
instructions[i] = "_nop_" + text(nop_counter)
nop_counter = nop_counter + 1
instructions[i + 1] = "_nop_" + text(nop_counter)
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
}
src_known = slot_types[text(src)]
if (src_known != null && src_known != T_UNKNOWN && src_known != checked_type) {
// Check for T_NUM subsumption: INT and FLOAT match T_NUM
if (checked_type == T_NUM && (src_known == T_INT || src_known == T_FLOAT)) {
// Actually matches — eliminate both
nop_counter = nop_counter + 1
instructions[i] = "_nop_" + text(nop_counter)
nop_counter = nop_counter + 1
instructions[i + 1] = "_nop_" + text(nop_counter)
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
}
// Known mismatch: always jumps — nop the check, rewrite jump
nop_counter = nop_counter + 1
instructions[i] = "_nop_" + text(nop_counter)
jlen = length(next)
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
slot_types[text(dest)] = T_UNKNOWN
i = i + 2
continue
}
// Unknown: can't eliminate, but narrow type on fallthrough
slot_types[text(dest)] = T_BOOL
slot_types[text(src)] = checked_type
i = i + 2
continue
}
// Pattern: is_<type> t, x -> jump_true t, label
if (next_op == "jump_true" && next[1] == dest) {
target_label = next[2]
if (slot_is(slot_types, src, checked_type)) {
// Known match: always true, always jumps — nop check, rewrite to jump
nop_counter = nop_counter + 1
instructions[i] = "_nop_" + text(nop_counter)
jlen = length(next)
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
}
src_known = slot_types[text(src)]
if (src_known != null && src_known != T_UNKNOWN && src_known != checked_type) {
if (checked_type == T_NUM && (src_known == T_INT || src_known == T_FLOAT)) {
// Actually matches T_NUM — always jumps
nop_counter = nop_counter + 1
instructions[i] = "_nop_" + text(nop_counter)
jlen = length(next)
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
}
// Known mismatch: never jumps — eliminate both
nop_counter = nop_counter + 1
instructions[i] = "_nop_" + text(nop_counter)
nop_counter = nop_counter + 1
instructions[i + 1] = "_nop_" + text(nop_counter)
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
}
// Unknown: can't optimize
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
}
}
// Standalone type check (no jump following): just track the result
slot_types[text(dest)] = T_BOOL
i = i + 1
continue
}
// --- Strength reduction: load_dynamic / store_dynamic ---
if (op == "load_dynamic") {
if (slot_is(slot_types, instr[3], T_TEXT)) {
instr[0] = "load_field"
} else if (slot_is(slot_types, instr[3], T_INT)) {
instr[0] = "load_index"
}
slot_types[text(instr[1])] = T_UNKNOWN
i = i + 1
continue
}
if (op == "store_dynamic") {
if (slot_is(slot_types, instr[3], T_TEXT)) {
instr[0] = "store_field"
} else if (slot_is(slot_types, instr[3], T_INT)) {
instr[0] = "store_index"
}
i = i + 1
continue
}
// --- Standard type tracking ---
track_types(slot_types, instr)
i = i + 1
}
// Second pass: remove dead jumps (jump to the immediately next label)
i = 0
while (i < num_instr) {
instr = instructions[i]
if (is_array(instr) && instr[0] == "jump") {
target_label = instr[1]
// Check if the very next non-nop item is that label
j = i + 1
while (j < num_instr) {
peek = instructions[j]
if (is_text(peek)) {
if (peek == target_label) {
nop_counter = nop_counter + 1
instructions[i] = "_nop_" + text(nop_counter)
}
break
}
if (is_array(peek)) {
break
}
j = j + 1
}
}
i = i + 1
}
return null
}
// Process main function
if (ir.main != null) {
optimize_function(ir.main)
}
// Process all sub-functions
var fi = 0
if (ir.functions != null) {
fi = 0
while (fi < length(ir.functions)) {
optimize_function(ir.functions[fi])
fi = fi + 1
}
}
return ir
}
return streamline

BIN
streamline.mach Normal file

Binary file not shown.

382
test.ce
View File

@@ -8,7 +8,7 @@ var dbg = use('js')
// run gc with dbg.gc()
if (!args) args = []
var _args = args == null ? [] : args
var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file
@@ -22,19 +22,20 @@ var actor_test_results = []
// Check if current directory is a valid cell package
function is_valid_package(dir) {
if (!dir) dir = '.'
return fd.is_file(dir + '/cell.toml')
var _dir = dir == null ? '.' : dir
return fd.is_file(_dir + '/cell.toml')
}
// Get current package name from cell.toml or null
function get_current_package_name() {
if (!is_valid_package('.')) return null
try {
var _load = function() {
var config = pkg.load_config(null)
return config.package || 'local'
} catch (e) {
} disruption {
return 'local'
}
return _load()
}
// Parse arguments
@@ -48,16 +49,21 @@ function get_current_package_name() {
function parse_args() {
var cleaned_args = []
for (var i = 0; i < length(args); i++) {
if (args[i] == '-g') {
var i = 0
var name = null
var lock = null
var resolved = null
var test_path = null
for (i = 0; i < length(_args); i++) {
if (_args[i] == '-g') {
gc_after_each_test = true
} else {
push(cleaned_args, args[i])
push(cleaned_args, _args[i])
}
}
args = cleaned_args
_args = cleaned_args
if (length(args) == 0) {
if (length(_args) == 0) {
// cell test - run all tests for current package
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
@@ -67,7 +73,7 @@ function parse_args() {
return true
}
if (args[0] == 'all') {
if (_args[0] == 'all') {
// cell test all - run all tests for current package
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
@@ -77,14 +83,14 @@ function parse_args() {
return true
}
if (args[0] == 'package') {
if (length(args) < 2) {
if (_args[0] == 'package') {
if (length(_args) < 2) {
log.console('Usage: cell test package <name> [test]')
log.console(' cell test package all')
return false
}
if (args[1] == 'all') {
if (_args[1] == 'all') {
// cell test package all - run tests from all packages
all_pkgs = true
log.console('Testing all packages...')
@@ -92,18 +98,19 @@ function parse_args() {
}
// cell test package <name> [test]
var name = args[1]
name = _args[1]
// Check if package exists in lock or is a local path
var lock = shop.load_lock()
lock = shop.load_lock()
if (lock[name]) {
target_pkg = name
} else if (starts_with(name, '/') && is_valid_package(name)) {
target_pkg = name
} else {
// Try to resolve as dependency alias from current package
resolved = null
if (is_valid_package('.')) {
var resolved = pkg.alias_to_package(null, name)
resolved = pkg.alias_to_package(null, name)
if (resolved) {
target_pkg = resolved
} else {
@@ -116,9 +123,9 @@ function parse_args() {
}
}
if (length(args) >= 3) {
if (length(_args) >= 3) {
// cell test package <name> <test>
target_test = args[2]
target_test = _args[2]
}
log.console(`Testing package: ${target_pkg}`)
@@ -126,7 +133,7 @@ function parse_args() {
}
// cell test tests/suite or cell test <path> - specific test file
var test_path = args[0]
test_path = _args[0]
// Normalize path - add tests/ prefix if not present and doesn't start with /
if (!starts_with(test_path, 'tests/') && !starts_with(test_path, '/')) {
@@ -160,9 +167,10 @@ function ensure_dir(path) {
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); i++) {
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current += parts[i] + '/'
current = current + parts[i] + '/'
if (!fd.is_dir(current))
fd.mkdir(current)
}
@@ -189,23 +197,29 @@ function collect_actor_tests(package_name, specific_test) {
var files = pkg.list_files(package_name)
var actor_tests = []
for (var i = 0; i < length(files); i++) {
var f = files[i]
var i = 0
var f = null
var test_name = null
var match_name = null
var test_base = null
var match_base = null
for (i = 0; i < length(files); i++) {
f = files[i]
// Check if file is in tests/ folder and is a .ce actor
if (starts_with(f, "tests/") && ends_with(f, ".ce")) {
// If specific test requested, filter
if (specific_test) {
var test_name = text(f, 0, -3) // remove .ce
var match_name = specific_test
test_name = text(f, 0, -3) // remove .ce
match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
if (!ends_with(match_name, '.ce')) match_name = match_name
// Match without extension
var test_base = test_name
var match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name
test_base = test_name
match_base = ends_with(match_name, '.ce') ? text(match_name, 0, -3) : match_name
if (test_base != match_base) continue
}
push(actor_tests,{
push(actor_tests, {
package: package_name || "local",
file: f,
path: prefix + '/' + f
@@ -229,19 +243,19 @@ function spawn_actor_test(test_info) {
actor: null
}
try {
// Spawn the actor test - it should send back results
var _spawn = function() {
var actor_path = text(test_info.path, 0, -3) // remove .ce
entry.actor = $start(actor_path)
push(pending_actor_tests, entry)
} catch (e) {
} disruption {
entry.status = "failed"
entry.error = { message: `Failed to spawn actor: ${e}` }
entry.error = { message: `Failed to spawn actor` }
entry.duration_ns = 0
push(actor_test_results, entry)
log.console(` FAIL ${test_name}: `)
log.error(e)
log.error()
}
_spawn()
}
function run_tests(package_name, specific_test) {
@@ -260,17 +274,24 @@ function run_tests(package_name, specific_test) {
var files = pkg.list_files(package_name)
var test_files = []
for (var i = 0; i < length(files); i++) {
var f = files[i]
var i = 0
var f = null
var test_name = null
var match_name = null
var match_base = null
var mod_path = null
var file_result = null
for (i = 0; i < length(files); i++) {
f = files[i]
// Check if file is in tests/ folder and is a .cm module (not .ce - those are actor tests)
if (starts_with(f, "tests/") && ends_with(f, ".cm")) {
// If specific test requested, filter
if (specific_test) {
var test_name = text(f, 0, -3) // remove .cm
var match_name = specific_test
test_name = text(f, 0, -3) // remove .cm
match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + match_name
// Match without extension
var match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
match_base = ends_with(match_name, '.cm') ? text(match_name, 0, -3) : match_name
if (test_name != match_base) continue
}
push(test_files, f)
@@ -282,106 +303,138 @@ function run_tests(package_name, specific_test) {
else log.console(`Running tests for local package`)
}
for (var i = 0; i < length(test_files); i++) {
var f = test_files[i]
var mod_path = text(f, 0, -3) // remove .cm
var _load_file = null
var load_error = false
var err_entry = null
for (i = 0; i < length(test_files); i++) {
f = test_files[i]
mod_path = text(f, 0, -3) // remove .cm
load_error = false
var file_result = {
file_result = {
name: f,
tests: [],
passed: 0,
failed: 0
}
try {
var test_mod
// For local packages (null), use the current directory as package context
_load_file = function() {
var test_mod = null
var use_pkg = package_name ? package_name : fd.realpath('.')
test_mod = shop.use(mod_path, use_pkg)
var tests = []
var j = 0
var t = null
var test_entry = null
var start_time = null
var _test_error = null
var end_time = null
var _run_one = null
var all_keys = null
var fn_count = 0
var null_count = 0
var other_count = 0
var first_null_key = null
var first_other_key = null
if (is_function(test_mod)) {
push(tests, {name: 'main', fn: test_mod})
} else if (is_object(test_mod)) {
arrfor(array(test_mod), function(k) {
all_keys = array(test_mod)
log.console(` Found ${length(all_keys)} test entries`)
arrfor(all_keys, function(k) {
if (is_function(test_mod[k])) {
fn_count = fn_count + 1
push(tests, {name: k, fn: test_mod[k]})
} else if (is_null(test_mod[k])) {
null_count = null_count + 1
if (!first_null_key) first_null_key = k
} else {
other_count = other_count + 1
if (!first_other_key) first_other_key = k
}
})
log.console(` functions=${fn_count} nulls=${null_count} other=${other_count}`)
if (first_other_key) {
log.console(` first other key: ${first_other_key}`)
log.console(` is_number=${is_number(test_mod[first_other_key])} is_text=${is_text(test_mod[first_other_key])} is_logical=${is_logical(test_mod[first_other_key])} is_object=${is_object(test_mod[first_other_key])}`)
}
}
if (length(tests) > 0) {
log.console(` ${f}`)
for (var j = 0; j < length(tests); j++) {
var t = tests[j]
var test_entry = {
for (j = 0; j < length(tests); j++) {
t = tests[j]
test_entry = {
package: pkg_result.package,
test: t.name,
status: "pending",
duration_ns: 0
}
var start_time = time.number()
try {
start_time = time.number()
_test_error = null
_run_one = function() {
var ret = t.fn()
if (is_text(ret)) {
throw Error(ret)
} else if (ret && (is_text(ret.message) || is_proto(ret, Error))) {
throw ret
_test_error = ret
disrupt
} else if (ret && is_text(ret.message)) {
_test_error = ret.message
disrupt
}
test_entry.status = "passed"
log.console(` PASS ${t.name}`)
pkg_result.passed++
file_result.passed++
} catch (e) {
} disruption {
var e = _test_error
test_entry.status = "failed"
test_entry.error = {
message: e,
stack: e.stack || ""
stack: (e && e.stack) ? e.stack : ""
}
if (e.name) test_entry.error.name = e.name
if (e && e.name) test_entry.error.name = e.name
if (is_object(e) && e.message) {
test_entry.error.message = e.message
test_entry.error.message = e.message
}
log.console(` FAIL ${t.name} ${test_entry.error.message}`)
if (test_entry.error.stack) {
log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`)
log.console(` ${text(array(test_entry.error.stack, '\n'), '\n ')}`)
}
pkg_result.failed++
file_result.failed++
}
var end_time = time.number()
_run_one()
end_time = time.number()
test_entry.duration_ns = round((end_time - start_time) * 1000000000)
// Update counters at _load_file level (not inside _run_one)
if (test_entry.status == "passed") {
pkg_result.passed = pkg_result.passed + 1
file_result.passed = file_result.passed + 1
} else {
pkg_result.failed = pkg_result.failed + 1
file_result.failed = file_result.failed + 1
}
push(file_result.tests, test_entry)
pkg_result.total++
pkg_result.total = pkg_result.total + 1
if (gc_after_each_test) {
dbg.gc()
}
}
}
} catch (e) {
log.console(` Error loading ${f}: ${e}`)
var test_entry = {
package: pkg_result.package,
test: "load_module",
status: "failed",
duration_ns: 0,
error: { message: `Error loading module: ${e}` }
}
push(file_result.tests, test_entry)
pkg_result.failed++
file_result.failed++
pkg_result.total++
if (gc_after_each_test) {
dbg.gc()
}
} disruption {
load_error = true
}
_load_file()
if (load_error) {
log.console(" Error loading " + f)
pkg_result.failed = pkg_result.failed + 1
file_result.failed = file_result.failed + 1
pkg_result.total = pkg_result.total + 1
}
push(pkg_result.files, file_result)
}
@@ -390,6 +443,8 @@ function run_tests(package_name, specific_test) {
var all_results = []
var all_actor_tests = []
var packages = null
var i = 0
if (all_pkgs) {
// Run local first if we're in a valid package
@@ -399,8 +454,8 @@ if (all_pkgs) {
}
// Then all packages in lock
var packages = shop.list_packages()
for (var i = 0; i < length(packages); i++) {
packages = shop.list_packages()
for (i = 0; i < length(packages); i++) {
push(all_results, run_tests(packages[i], null))
all_actor_tests = array(all_actor_tests, collect_actor_tests(packages[i], null))
}
@@ -412,7 +467,7 @@ if (all_pkgs) {
// Spawn actor tests if any
if (length(all_actor_tests) > 0) {
log.console(`Running ${length(all_actor_tests)} actor test(s)...`)
for (var i = 0; i < length(all_actor_tests); i++) {
for (i = 0; i < length(all_actor_tests); i++) {
spawn_actor_test(all_actor_tests[i])
}
}
@@ -421,7 +476,10 @@ if (length(all_actor_tests) > 0) {
function handle_actor_message(msg) {
var sender = msg.$sender
var found_idx = -1
for (var i = 0; i < length(pending_actor_tests); i++) {
var i = 0
var res = null
var entry = null
for (i = 0; i < length(pending_actor_tests); i++) {
if (pending_actor_tests[i].actor == sender) {
found_idx = i
break
@@ -445,9 +503,9 @@ function handle_actor_message(msg) {
results = [msg]
}
for (var i = 0; i < length(results); i++) {
var res = results[i] || {}
var entry = {
for (i = 0; i < length(results); i++) {
res = results[i] || {}
entry = {
package: base_entry.package,
file: base_entry.file,
test: res.test || base_entry.test + (length(results) > 1 ? `#${i+1}` : ""),
@@ -481,18 +539,22 @@ function handle_actor_message(msg) {
function check_timeouts() {
var now = time.number()
var timed_out = []
var i = 0
var entry = null
var elapsed_ms = null
var idx = null
for (var i = length(pending_actor_tests) - 1; i >= 0; i--) {
var entry = pending_actor_tests[i]
var elapsed_ms = (now - entry.start_time) * 1000
for (i = length(pending_actor_tests) - 1; i >= 0; i--) {
entry = pending_actor_tests[i]
elapsed_ms = (now - entry.start_time) * 1000
if (elapsed_ms > ACTOR_TEST_TIMEOUT) {
push(timed_out, i)
}
}
for (var i = 0; i < length(timed_out); i++) {
var idx = timed_out[i]
var entry = pending_actor_tests[idx]
for (i = 0; i < length(timed_out); i++) {
idx = timed_out[i]
entry = pending_actor_tests[idx]
pending_actor_tests = array(array(pending_actor_tests, 0, idx), array(pending_actor_tests, idx + 1))
entry.status = "failed"
@@ -519,11 +581,17 @@ function check_completion() {
}
function finalize_results() {
var i = 0
var j = 0
var r = null
var pkg_result = null
var file_result = null
// Add actor test results to all_results
for (var i = 0; i < length(actor_test_results); i++) {
var r = actor_test_results[i]
var pkg_result = null
for (var j = 0; j < length(all_results); j++) {
for (i = 0; i < length(actor_test_results); i++) {
r = actor_test_results[i]
pkg_result = null
for (j = 0; j < length(all_results); j++) {
if (all_results[j].package == r.package) {
pkg_result = all_results[j]
break
@@ -534,8 +602,8 @@ function finalize_results() {
push(all_results, pkg_result)
}
var file_result = null
for (var j = 0; j < length(pkg_result.files); j++) {
file_result = null
for (j = 0; j < length(pkg_result.files); j++) {
if (pkg_result.files[j].name == r.file) {
file_result = pkg_result.files[j]
break
@@ -547,22 +615,22 @@ function finalize_results() {
}
push(file_result.tests, r)
pkg_result.total++
pkg_result.total = pkg_result.total + 1
if (r.status == "passed") {
pkg_result.passed++
file_result.passed++
pkg_result.passed = pkg_result.passed + 1
file_result.passed = file_result.passed + 1
} else {
pkg_result.failed++
file_result.failed++
pkg_result.failed = pkg_result.failed + 1
file_result.failed = file_result.failed + 1
}
}
// Calculate totals
var totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < length(all_results); i++) {
totals.total += all_results[i].total
totals.passed += all_results[i].passed
totals.failed += all_results[i].failed
for (i = 0; i < length(all_results); i++) {
totals.total = totals.total + all_results[i].total
totals.passed = totals.passed + all_results[i].passed
totals.failed = totals.failed + all_results[i].failed
}
log.console(`----------------------------------------`)
@@ -573,13 +641,13 @@ function finalize_results() {
}
// If no actor tests, finalize immediately
var totals
var totals = null
if (length(all_actor_tests) == 0) {
totals = { total: 0, passed: 0, failed: 0 }
for (var i = 0; i < length(all_results); i++) {
totals.total += all_results[i].total
totals.passed += all_results[i].passed
totals.failed += all_results[i].failed
for (i = 0; i < length(all_results); i++) {
totals.total = totals.total + all_results[i].total
totals.passed = totals.passed + all_results[i].passed
totals.failed = totals.failed + all_results[i].failed
}
log.console(`----------------------------------------`)
@@ -593,6 +661,16 @@ function generate_reports(totals) {
var timestamp = text(floor(time.number()))
var report_dir = shop.get_reports_dir() + '/test_' + timestamp
ensure_dir(report_dir)
var i = 0
var j = 0
var k = 0
var pkg_res = null
var f = null
var status = null
var t = null
var dur = null
var pkg_tests = null
var json_path = null
var txt_report = `TEST REPORT
Date: ${time.text(time.number())}
@@ -600,53 +678,53 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
=== SUMMARY ===
`
for (var i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i]
for (i = 0; i < length(all_results); i++) {
pkg_res = all_results[i]
if (pkg_res.total == 0) continue
txt_report += `Package: ${pkg_res.package}\n`
for (var j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j]
var status = f.failed == 0 ? "PASS" : "FAIL"
txt_report += ` [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n`
txt_report = txt_report + `Package: ${pkg_res.package}\n`
for (j = 0; j < length(pkg_res.files); j++) {
f = pkg_res.files[j]
status = f.failed == 0 ? "PASS" : "FAIL"
txt_report = txt_report + ` [${status}] ${f.name} (${f.passed}/${length(f.tests)})\n`
}
}
txt_report += `\n=== FAILURES ===\n`
txt_report = txt_report + `\n=== FAILURES ===\n`
var has_failures = false
for (var i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i]
for (var j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
var t = f.tests[k]
for (i = 0; i < length(all_results); i++) {
pkg_res = all_results[i]
for (j = 0; j < length(pkg_res.files); j++) {
f = pkg_res.files[j]
for (k = 0; k < length(f.tests); k++) {
t = f.tests[k]
if (t.status == "failed") {
has_failures = true
txt_report += `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n`
txt_report = txt_report + `FAIL: ${pkg_res.package} :: ${f.name} :: ${t.test}\n`
if (t.error) {
txt_report += ` Message: ${t.error.message}\n`
txt_report = txt_report + ` Message: ${t.error.message}\n`
if (t.error.stack) {
txt_report += ` Stack:\n${text(array(array(t.error.stack, '\n'), l => ` ${l}`), '\n')}\n`
txt_report = txt_report + ` Stack:\n${text(array(array(t.error.stack, '\n'), l => ` ${l}`), '\n')}\n`
}
}
txt_report += `\n`
txt_report = txt_report + `\n`
}
}
}
}
if (!has_failures) txt_report += `None\n`
if (!has_failures) txt_report = txt_report + `None\n`
txt_report += `\n=== DETAILED RESULTS ===\n`
for (var i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i]
txt_report = txt_report + `\n=== DETAILED RESULTS ===\n`
for (i = 0; i < length(all_results); i++) {
pkg_res = all_results[i]
if (pkg_res.total == 0) continue
for (var j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
var t = f.tests[k]
var dur = `${t.duration_ns || 0}ns`
var status = t.status == "passed" ? "PASS" : "FAIL"
txt_report += `[${status}] ${pkg_res.package} ${t.test} (${dur})\n`
for (j = 0; j < length(pkg_res.files); j++) {
f = pkg_res.files[j]
for (k = 0; k < length(f.tests); k++) {
t = f.tests[k]
dur = `${t.duration_ns || 0}ns`
status = t.status == "passed" ? "PASS" : "FAIL"
txt_report = txt_report + `[${status}] ${pkg_res.package} ${t.test} (${dur})\n`
}
}
}
@@ -655,19 +733,19 @@ Total: ${totals.total}, Passed: ${totals.passed}, Failed: ${totals.failed}
log.console(`Report written to ${report_dir}/test.txt`)
// Generate JSON per package
for (var i = 0; i < length(all_results); i++) {
var pkg_res = all_results[i]
for (i = 0; i < length(all_results); i++) {
pkg_res = all_results[i]
if (pkg_res.total == 0) continue
var pkg_tests = []
for (var j = 0; j < length(pkg_res.files); j++) {
var f = pkg_res.files[j]
for (var k = 0; k < length(f.tests); k++) {
pkg_tests = []
for (j = 0; j < length(pkg_res.files); j++) {
f = pkg_res.files[j]
for (k = 0; k < length(f.tests); k++) {
push(pkg_tests, f.tests[k])
}
}
var json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
json_path = `${report_dir}/${replace(pkg_res.package, /\//, '_')}.json`
fd.slurpwrite(json_path, stone(blob(json.encode(pkg_tests))))
}
}

42
test_parse_boot.cm Normal file
View File

@@ -0,0 +1,42 @@
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var src = text(fd.slurp("internal/bootstrap.cm"))
var tok_result = tokenize(src, "bootstrap.cm")
var ast = parse(tok_result.tokens, src, "bootstrap.cm", tokenize)
var i = 0
var folded = null
var ast_json = null
var f = null
var bytecode = null
var has_errors = ast.errors != null && length(ast.errors) > 0
if (has_errors) {
print("PARSE ERRORS:")
while (i < length(ast.errors)) {
print(text(ast.errors[i].line) + ":" + text(ast.errors[i].column) + " " + ast.errors[i].message)
i = i + 1
}
} else {
print("Parse OK")
print(" statements: " + text(length(ast.statements)))
if (ast.functions != null) {
print(" functions: " + text(length(ast.functions)))
} else {
print(" functions: null")
}
folded = fold(ast)
ast_json = json.encode(folded)
f = fd.open("/tmp/bootstrap_ast.json", "w")
fd.write(f, ast_json)
fd.close(f)
print("Wrote AST to /tmp/bootstrap_ast.json")
bytecode = mach_compile_ast("bootstrap", ast_json)
print("Bytecode size: " + text(length(bytecode)))
f = fd.open("/tmp/bootstrap_test.mach", "w")
fd.write(f, bytecode)
fd.close(f)
print("Wrote bytecode to /tmp/bootstrap_test.mach")
}

165
tests/decl_restrictions.ce Normal file
View File

@@ -0,0 +1,165 @@
// Declaration restriction tests
// Run: ./cell tests/decl_restrictions.ce
var tokenize = use("tokenize")
var parse = use("parse")
var passed = 0
var failed = 0
var error_names = []
var error_reasons = []
var fail_msg = ""
var _i = 0
for (_i = 0; _i < 20; _i++) {
error_names[] = null
error_reasons[] = null
}
var fail = function(msg) {
fail_msg = msg
disrupt
}
var run = function(name, fn) {
fail_msg = ""
fn()
passed = passed + 1
} disruption {
error_names[failed] = name
error_reasons[failed] = fail_msg == "" ? "disruption" : fail_msg
failed = failed + 1
}
var parse_snippet = function(src) {
var result = tokenize(src, "<test>")
var ast = parse(result.tokens, src, "<test>", tokenize)
return ast
}
var has_error = function(ast, substring) {
if (ast.errors == null) return false
var i = 0
while (i < length(ast.errors)) {
if (search(ast.errors[i].message, substring) != null) return true
i = i + 1
}
return false
}
var has_no_errors = function(ast) {
return ast.errors == null || length(ast.errors) == 0
}
// === BARE BLOCK ===
run("bare block rejected", function() {
var ast = parse_snippet("{ var x = 1 }")
if (!has_error(ast, "bare block")) fail("expected 'bare block' error, got: " + text(ast.errors))
})
// === VAR IN IF (braces) ===
run("var in if braces", function() {
var ast = parse_snippet("if (true) { var x = 1 }")
if (!has_error(ast, "not inside 'if'")) fail("expected 'not inside if' error, got: " + text(ast.errors))
})
// === VAR IN IF (no braces) ===
run("var in if no braces", function() {
var ast = parse_snippet("if (true) var x = 1")
if (!has_error(ast, "not inside 'if'")) fail("expected 'not inside if' error, got: " + text(ast.errors))
})
// === VAR IN WHILE ===
run("var in while", function() {
var ast = parse_snippet("while (true) { var x = 1; break }")
if (!has_error(ast, "not inside 'while'")) fail("expected 'not inside while' error, got: " + text(ast.errors))
})
// === VAR IN FOR INIT ===
run("var in for init", function() {
var ast = parse_snippet("for (var i = 0; i < 1; i++) {}")
if (!has_error(ast, "for initializer")) fail("expected 'for initializer' error, got: " + text(ast.errors))
})
// === VAR IN FOR BODY ===
run("var in for body", function() {
var ast = parse_snippet("var i = 0; for (i = 0; i < 1; i++) { var x = 1 }")
if (!has_error(ast, "not inside 'for'")) fail("expected 'not inside for' error, got: " + text(ast.errors))
})
// === VAR IN DO ===
run("var in do", function() {
var ast = parse_snippet("do { var x = 1; break } while (true)")
if (!has_error(ast, "not inside 'do'")) fail("expected 'not inside do' error, got: " + text(ast.errors))
})
// === DEF IN IF ===
run("def in if", function() {
var ast = parse_snippet("if (true) { def x = 1 }")
if (!has_error(ast, "not inside 'if'")) fail("expected 'not inside if' error, got: " + text(ast.errors))
})
// === UNINITIALIZED VAR ===
run("uninitialized var", function() {
var ast = parse_snippet("var x")
if (!has_error(ast, "must be initialized")) fail("expected 'must be initialized' error, got: " + text(ast.errors))
})
// === NESTED: VAR IN IF INSIDE WHILE ===
run("nested var in if inside while", function() {
var ast = parse_snippet("while (true) { if (true) { var x = 1 }; break }")
if (!has_error(ast, "not inside 'if'")) fail("expected 'not inside if' error, got: " + text(ast.errors))
})
// === VALID: NESTED FUNCTION ===
run("valid nested fn in control flow", function() {
var ast = parse_snippet("if (true) { var fn = function() { var x = 1; return x } }")
// The var inside the function is fine; only the var fn = ... inside if should error
if (!has_error(ast, "not inside 'if'")) fail("expected error for var fn inside if")
// But there should NOT be an error about var x inside the function body
var i = 0
var bad = false
if (ast.errors != null) {
while (i < length(ast.errors)) {
if (search(ast.errors[i].message, "var x") != null) bad = true
i = i + 1
}
}
if (bad) fail("should not error on var inside nested function")
})
// === VALID: NORMAL VAR ===
run("valid normal var", function() {
var ast = parse_snippet("var x = 1")
if (!has_no_errors(ast)) fail("expected no errors, got: " + text(ast.errors))
})
// === VALID: VAR IN FUNCTION BODY ===
run("valid var in function body", function() {
var ast = parse_snippet("var fn = function() { var x = 1; return x }")
if (!has_no_errors(ast)) fail("expected no errors, got: " + text(ast.errors))
})
// === SUMMARY ===
print(text(passed) + " passed, " + text(failed) + " failed out of " + text(passed + failed))
var _j = 0
if (failed > 0) {
print("")
for (_j = 0; _j < failed; _j++) {
print(" FAIL " + error_names[_j] + ": " + error_reasons[_j])
}
}

File diff suppressed because it is too large Load Diff

344
time.cm
View File

@@ -1,204 +1,212 @@
// epoch = 0000-01-01 00:00:00 +0000
var time = this;
var time = native
/* -------- host helpers -------------------------------------------------- */
var now = time.now; // seconds since Misty epoch (C stub)
var computer_zone = time.computer_zone; // integral hours, no DST
var computer_dst = time.computer_dst; // true ↔ DST in effect
var now = time.now
var computer_zone = time.computer_zone
var computer_dst = time.computer_dst
delete time.now;
delete time.computer_zone;
delete time.computer_dst;
delete time.now
delete time.computer_zone
delete time.computer_dst
/* -------- units & static tables ----------------------------------------- */
time.second = 1;
time.minute = 60;
time.hour = 3_600;
time.day = 86_400;
time.week = 604_800;
time.second = 1
time.minute = 60
time.hour = 3600
time.day = 86400
time.week = 604800
time.weekdays = [
"Sunday", "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday", "Saturday"
];
]
time.monthstr = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
]
time.epoch = 1970;
time.epoch = 1970
/* ratios (kept K&R-style) */
time.hour2minute = function() { return time.hour / time.minute; };
time.day2hour = function() { return time.day / time.hour; };
time.minute2second = function() { return time.minute / time.second; };
time.week2day = function() { return time.week / time.day; };
time.hour2minute = function() { return time.hour / time.minute }
time.day2hour = function() { return time.day / time.hour }
time.minute2second = function() { return time.minute / time.second }
time.week2day = function() { return time.week / time.day }
/* leap-year helpers */
time.yearsize = function yearsize(y)
{
if (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) return 366;
return 365;
};
time.isleap = function(y) { return time.yearsize(y) == 366; };
time.yearsize = function yearsize(y) {
if (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) return 366
return 365
}
time.isleap = function(y) { return time.yearsize(y) == 366 }
/* timecode utility */
time.timecode = function(t, fps = 24)
{
var s = whole(t);
var frac = t - s;
return `${s}:${whole(frac * fps)}`;
};
/* per-month day counts (non-leap) */
time.monthdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// convert seconds-since-epoch → broken-down record
function time_record(num = now(),
zone = computer_zone(),
dst = computer_dst())
{
if (is_object(num)) return num;
var monthdays = array(time.monthdays);
var rec = {
second : 0, minute : 0, hour : 0,
yday : 0, year : 0,
weekday: 0, month : 0, day : 0,
zone : zone, dst : !!dst,
ce : "AD"
};
/* include DST in the effective offset */
var offset = zone + (dst ? 1 : 0);
num += offset * time.hour;
/* split into day + seconds-of-day */
var hms = num % time.day;
var day = floor(num / time.day);
if (hms < 0) { hms += time.day; day--; }
rec.second = hms % time.minute;
var tmp = floor(hms / time.minute);
rec.minute = tmp % time.minute;
rec.hour = floor(tmp / time.minute);
rec.weekday = (day + 4_503_599_627_370_496 + 2) % 7; /* 2 → 1970-01-01 was Thursday */
/* year & day-of-year */
var y = time.epoch;
if (day >= 0) {
for (y = time.epoch; day >= time.yearsize(y); y++)
day -= time.yearsize(y);
} else {
for (y = time.epoch; day < 0; y--)
day += time.yearsize(y - 1);
}
rec.year = y;
rec.ce = (y <= 0) ? "BC" : "AD";
rec.yday = day;
/* month & month-day */
if (time.yearsize(y) == 366) monthdays[1] = 29;
var m = 0;
for (; day >= monthdays[m]; m++) day -= monthdays[m];
rec.month = m;
rec.day = day + 1;
return rec;
time.timecode = function(t) {
var fps = 24
var s = whole(t)
var frac = t - s
return `${s}:${whole(frac * fps)}`
}
function time_number(rec = now())
{
if (is_number(rec)) return rec;
time.monthdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
log.console(rec)
log.console(rec.minute)
// convert seconds-since-epoch -> broken-down record
function time_record(_num, _zone, _dst) {
var n = _num
var z = _zone
var d = _dst
if (n == null) n = now()
if (z == null) z = computer_zone()
if (d == null) d = computer_dst()
var c = 0;
var year = rec.year || 0;
var hour = rec.hour || 0;
var minute = rec.minute || 0;
var second = rec.second || 0;
var zone = rec.zone || 0;
var dst = rec.dst ? 1 : 0;
var yday = rec.yday || 0;
if (is_object(n)) return n
if (year > time.epoch) {
for (var i = time.epoch; i < year; i++)
c += time.day * time.yearsize(i);
} else if (year < time.epoch) {
for (var i = time.epoch - 1; i > year; i--)
c += time.day * time.yearsize(i);
c += (time.yearsize(year) - yday - 1) * time.day;
c += (time.day2hour() - hour - 1) * time.hour;
c += (time.hour2minute() - minute - 1) * time.minute;
c += time.minute2second() - second;
c += (zone + dst) * time.hour;
return -c; /* BCE */
var monthdays = array(time.monthdays)
var rec = {
second: 0, minute: 0, hour: 0,
yday: 0, year: 0,
weekday: 0, month: 0, day: 0,
zone: z, dst: !!d,
ce: "AD"
}
c = second;
c += minute * time.minute;
c += hour * time.hour;
c += yday * time.day;
c -= (zone + dst) * time.hour;
var offset = z + (d ? 1 : 0)
n = n + offset * time.hour
return c;
var hms = n % time.day
var day = floor(n / time.day)
if (hms < 0) { hms = hms + time.day; day = day - 1 }
rec.second = hms % time.minute
var tmp = floor(hms / time.minute)
rec.minute = tmp % time.minute
rec.hour = floor(tmp / time.minute)
rec.weekday = (day + 4503599627370496 + 2) % 7
var y = time.epoch
if (day >= 0) {
y = time.epoch
while (day >= time.yearsize(y)) {
day = day - time.yearsize(y)
y = y + 1
}
} else {
y = time.epoch
while (day < 0) {
y = y - 1
day = day + time.yearsize(y)
}
}
rec.year = y
rec.ce = (y <= 0) ? "BC" : "AD"
rec.yday = day
if (time.yearsize(y) == 366) monthdays[1] = 29
var m = 0
while (day >= monthdays[m]) {
day = day - monthdays[m]
m = m + 1
}
rec.month = m
rec.day = day + 1
return rec
}
function time_number(_rec) {
var r = _rec
if (r == null) r = now()
if (is_number(r)) return r
var c = 0
var year = r.year || 0
var hour = r.hour || 0
var minute = r.minute || 0
var second = r.second || 0
var zone = r.zone || 0
var dst = r.dst ? 1 : 0
var yday = r.yday || 0
var i = 0
if (year > time.epoch) {
i = time.epoch
while (i < year) {
c = c + time.day * time.yearsize(i)
i = i + 1
}
} else if (year < time.epoch) {
i = time.epoch - 1
while (i > year) {
c = c + time.day * time.yearsize(i)
i = i - 1
}
c = c + (time.yearsize(year) - yday - 1) * time.day
c = c + (time.day2hour() - hour - 1) * time.hour
c = c + (time.hour2minute() - minute - 1) * time.minute
c = c + time.minute2second() - second
c = c + (zone + dst) * time.hour
return -c
}
c = second
c = c + minute * time.minute
c = c + hour * time.hour
c = c + yday * time.day
c = c - (zone + dst) * time.hour
return c
}
// text formatting
var default_fmt = "vB mB d hh:nn:ss a z y c"; /* includes new DST token */
var default_fmt = "vB mB d hh:nn:ss a z y c"
function time_text(num = now(),
fmt = default_fmt,
zone = computer_zone(),
dst = computer_dst())
{
var rec = is_number(num) ? time_record(num, zone, dst) : num;
zone = rec.zone;
dst = rec.dst;
function time_text(_num, _fmt, _zone, _dst) {
var n = _num
var f = _fmt
var z = _zone
var d = _dst
if (n == null) n = now()
if (f == null) f = default_fmt
if (z == null) z = computer_zone()
if (d == null) d = computer_dst()
/* am/pm */
if (search(fmt, "a") != null) {
if (rec.hour >= 13) { rec.hour -= 12; fmt = replace(fmt, "a", "PM"); }
else if (rec.hour == 12) { fmt = replace(fmt, "a", "PM"); }
else if (rec.hour == 0) { rec.hour = 12; fmt = replace(fmt, "a", "AM"); }
else fmt = replace(fmt, "a", "AM");
var rec = is_number(n) ? time_record(n, z, d) : n
z = rec.zone
d = rec.dst
if (search(f, "a") != null) {
if (rec.hour >= 13) { rec.hour = rec.hour - 12; f = replace(f, "a", "PM") }
else if (rec.hour == 12) { f = replace(f, "a", "PM") }
else if (rec.hour == 0) { rec.hour = 12; f = replace(f, "a", "AM") }
else f = replace(f, "a", "AM")
}
/* BCE/CE */
var year = rec.year > 0 ? rec.year : rec.year - 1;
if (search(fmt, "c") != null) {
if (year < 0) { year = abs(year); fmt = replace(fmt, "c", "BC"); }
else fmt = replace(fmt, "c", "AD");
var year = rec.year > 0 ? rec.year : rec.year - 1
if (search(f, "c") != null) {
if (year < 0) { year = abs(year); f = replace(f, "c", "BC") }
else f = replace(f, "c", "AD")
}
/* substitutions */
var full_offset = zone + (dst ? 1 : 0);
fmt = replace(fmt, "yyyy", text(year, "i4"))
fmt = replace(fmt, "y", year);
fmt = replace(fmt, "eee", rec.yday + 1);
fmt = replace(fmt, "dd", text(rec.day, "i2"))
fmt = replace(fmt, "d", rec.day);
fmt = replace(fmt, "hh", text(rec.hour, "i2"));
fmt = replace(fmt, "h", rec.hour);
fmt = replace(fmt, "nn", text(rec.minute, "i2"));
fmt = replace(fmt, "n", rec.minute);
fmt = replace(fmt, "ss", text(rec.second, "i2"));
fmt = replace(fmt, "s", rec.second);
fmt = replace(fmt, "x", dst ? "DST" : ""); /* new */
fmt = replace(fmt, "z", (full_offset >= 0 ? "+" : "") + text(full_offset));
fmt = replace(fmt, /mm[^bB]/g, rec.month + 1);
fmt = replace(fmt, /m[^bB]/g, rec.month + 1);
fmt = replace(fmt, /v[^bB]/g, rec.weekday);
fmt = replace(fmt, "mb", text(time.monthstr[rec.month], 0, 3));
fmt = replace(fmt, "mB", time.monthstr[rec.month]);
fmt = replace(fmt, "vB", time.weekdays[rec.weekday]);
fmt = replace(fmt, "vb", text(time.weekdays[rec.weekday], 0, 3));
var full_offset = z + (d ? 1 : 0)
f = replace(f, "yyyy", text(year, "i4"))
f = replace(f, "y", year)
f = replace(f, "eee", rec.yday + 1)
f = replace(f, "dd", text(rec.day, "i2"))
f = replace(f, "d", rec.day)
f = replace(f, "hh", text(rec.hour, "i2"))
f = replace(f, "h", rec.hour)
f = replace(f, "nn", text(rec.minute, "i2"))
f = replace(f, "n", rec.minute)
f = replace(f, "ss", text(rec.second, "i2"))
f = replace(f, "s", rec.second)
f = replace(f, "x", d ? "DST" : "")
f = replace(f, "z", (full_offset >= 0 ? "+" : "") + text(full_offset))
f = replace(f, /mm[^bB]/g, rec.month + 1)
f = replace(f, /m[^bB]/g, rec.month + 1)
f = replace(f, /v[^bB]/g, rec.weekday)
f = replace(f, "mb", text(time.monthstr[rec.month], 0, 3))
f = replace(f, "mB", time.monthstr[rec.month])
f = replace(f, "vB", time.weekdays[rec.weekday])
f = replace(f, "vb", text(time.weekdays[rec.weekday], 0, 3))
return fmt;
return f
}
return { record: time_record, number: time_number, text: time_text };
return { record: time_record, number: time_number, text: time_text }

View File

@@ -1,4 +1,5 @@
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var filename = args[0]
var src = text(fd.slurp(filename))

View File

@@ -1,11 +1,6 @@
var tokenize = function(src, filename) {
var len = length(src)
var cp = []
var _i = 0
while (_i < len) {
push(cp, codepoint(src[_i]))
_i = _i + 1
}
var cp = array(array(src), codepoint)
var pos = 0
var row = 0
@@ -148,46 +143,45 @@ var tokenize = function(src, filename) {
}
var substr = function(start, end) {
var s = ""
var i = start
while (i < end) {
s = s + character(cp[i])
i = i + 1
}
return s
return text(src, start, end)
}
var read_string = function(quote_cp) {
var start = pos
var start_row = row
var start_col = col
var value = ""
var parts = []
var run_start = 0
var esc = 0
adv() // skip opening quote
run_start = pos
while (pos < len && pk() != quote_cp) {
if (pk() == CP_BSLASH) {
if (pos > run_start) push(parts, text(src, run_start, pos))
adv()
esc = adv()
if (esc == CP_n) { value = value + "\n" }
else if (esc == CP_t) { value = value + "\t" }
else if (esc == CP_r) { value = value + "\r" }
else if (esc == CP_BSLASH) { value = value + "\\" }
else if (esc == CP_SQUOTE) { value = value + "'" }
else if (esc == CP_DQUOTE) { value = value + "\"" }
else if (esc == CP_0) { value = value + character(0) }
else if (esc == CP_BACKTICK) { value = value + "`" }
else if (esc == CP_u) { value = value + read_unicode_escape() }
else { value = value + character(esc) }
if (esc == CP_n) { push(parts, "\n") }
else if (esc == CP_t) { push(parts, "\t") }
else if (esc == CP_r) { push(parts, "\r") }
else if (esc == CP_BSLASH) { push(parts, "\\") }
else if (esc == CP_SQUOTE) { push(parts, "'") }
else if (esc == CP_DQUOTE) { push(parts, "\"") }
else if (esc == CP_0) { push(parts, character(0)) }
else if (esc == CP_BACKTICK) { push(parts, "`") }
else if (esc == CP_u) { push(parts, read_unicode_escape()) }
else { push(parts, character(esc)) }
run_start = pos
} else {
value = value + character(adv())
adv()
}
}
if (pos > run_start) push(parts, text(src, run_start, pos))
if (pos < len) adv() // skip closing quote
push(tokens, {
kind: "text", at: start,
from_row: start_row, from_column: start_col,
to_row: row, to_column: col,
value: value
value: text(parts)
})
}
@@ -195,49 +189,54 @@ var tokenize = function(src, filename) {
var start = pos
var start_row = row
var start_col = col
var value = ""
var parts = []
var run_start = 0
var depth = 0
var tc = 0
var q = 0
var interp_start = 0
adv() // skip opening backtick
run_start = pos
while (pos < len && pk() != CP_BACKTICK) {
if (pk() == CP_BSLASH && pos + 1 < len) {
value = value + character(adv())
value = value + character(adv())
if (pos > run_start) push(parts, text(src, run_start, pos))
push(parts, text(src, pos, pos + 2))
adv(); adv()
run_start = pos
} else if (pk() == CP_DOLLAR && pos + 1 < len && pk_at(1) == CP_LBRACE) {
value = value + character(adv()) // $
value = value + character(adv()) // {
if (pos > run_start) push(parts, text(src, run_start, pos))
interp_start = pos
adv(); adv() // $ {
depth = 1
while (pos < len && depth > 0) {
tc = pk()
if (tc == CP_LBRACE) { depth = depth + 1; value = value + character(adv()) }
if (tc == CP_LBRACE) { depth = depth + 1; adv() }
else if (tc == CP_RBRACE) {
depth = depth - 1
if (depth > 0) { value = value + character(adv()) }
else { value = value + character(adv()) }
adv()
}
else if (tc == CP_SQUOTE || tc == CP_DQUOTE || tc == CP_BACKTICK) {
q = adv()
value = value + character(q)
while (pos < len && pk() != q) {
if (pk() == CP_BSLASH && pos + 1 < len) {
value = value + character(adv())
}
value = value + character(adv())
if (pk() == CP_BSLASH && pos + 1 < len) adv()
adv()
}
if (pos < len) { value = value + character(adv()) }
} else { value = value + character(adv()) }
if (pos < len) adv()
} else { adv() }
}
push(parts, text(src, interp_start, pos))
run_start = pos
} else {
value = value + character(adv())
adv()
}
}
if (pos > run_start) push(parts, text(src, run_start, pos))
if (pos < len) adv() // skip closing backtick
push(tokens, {
kind: "text", at: start,
from_row: start_row, from_column: start_col,
to_row: row, to_column: col,
value: value
value: text(parts)
})
}
@@ -344,14 +343,13 @@ var tokenize = function(src, filename) {
var start = pos
var start_row = row
var start_col = col
var val = ""
var i = 0
while (i < count) { val = val + character(adv()); i = i + 1 }
while (i < count) { adv(); i = i + 1 }
push(tokens, {
kind: "name", at: start,
from_row: start_row, from_column: start_col,
to_row: row, to_column: col,
value: val
value: text(src, start, pos)
})
}

Binary file not shown.

127
toml.cm
View File

@@ -3,21 +3,16 @@
function toml_unescape(s) {
if (!is_text(s)) return null
// Order matters:
// "\\\"" (backslash + quote) should become "\"", not just '"'
// So: unescape \" first, then unescape \\.
s = replace(s, '\\"', '"')
s = replace(s, '\\\\', '\\')
return s
var r = replace(s, '\\"', '"')
r = replace(r, '\\\\', '\\')
return r
}
function toml_escape(s) {
if (!is_text(s)) return null
// Order matters:
// escape backslashes first, otherwise escaping quotes introduces new backslashes that would get double-escaped.
s = replace(s, '\\', '\\\\')
s = replace(s, '"', '\\"')
return s
var r = replace(s, '\\', '\\\\')
r = replace(r, '"', '\\"')
return r
}
function parse_toml(toml_text) {
@@ -31,23 +26,33 @@ function parse_toml(toml_text) {
var current_section = result
var current_section_name = ''
for (var i = 0; i < length(lines); i++) {
var line = trim(lines[i])
var i = 0
var line = null
var inner = null
var section_path = null
var j = 0
var key = null
var eq_index = null
var key_part = null
var value = null
var unquoted = null
for (i = 0; i < length(lines); i++) {
line = trim(lines[i])
if (line == null) line = lines[i]
// Skip empty lines and comments
if (!line || starts_with(line, '#')) continue
// Section header
if (starts_with(line, '[') && ends_with(line, ']')) {
var inner = text(line, 1, -1)
var section_path = parse_key_path(inner)
inner = text(line, 1, -1)
section_path = parse_key_path(inner)
if (section_path == null) return null
current_section = result
current_section_name = text(section_path, '.')
for (var j = 0; j < length(section_path); j++) {
var key = section_path[j]
for (j = 0; j < length(section_path); j++) {
key = section_path[j]
// Only treat null as "missing"; do not clobber false/0/""
if (current_section[key] == null) {
@@ -63,18 +68,18 @@ function parse_toml(toml_text) {
}
// Key-value pair
var eq_index = search(line, '=')
eq_index = search(line, '=')
if (eq_index != null && eq_index > 0) {
var key_part = trim(text(line, 0, eq_index))
var value = trim(text(line, eq_index + 1))
key_part = trim(text(line, 0, eq_index))
value = trim(text(line, eq_index + 1))
if (key_part == null) key_part = trim(text(line, 0, eq_index))
if (value == null) value = trim(text(line, eq_index + 1))
var key = parse_key(key_part)
key = parse_key(key_part)
if (key == null) return null
if (starts_with(value, '"') && ends_with(value, '"')) {
var unquoted = text(value, 1, -1)
unquoted = text(value, 1, -1)
current_section[key] = toml_unescape(unquoted)
if (current_section[key] == null) return null
} else if (starts_with(value, '[') && ends_with(value, ']')) {
@@ -96,9 +101,9 @@ function parse_toml(toml_text) {
function parse_key(str) {
if (!is_text(str)) return null
var inner = null
if (starts_with(str, '"') && ends_with(str, '"')) {
var inner = text(str, 1, -1)
inner = text(str, 1, -1)
return toml_unescape(inner)
}
return str
@@ -112,18 +117,21 @@ function parse_key_path(str) {
var current = ''
var in_quote = false
for (var i = 0; i < length(str); i++) {
var c = str[i]
var i = 0
var c = null
var piece = null
for (i = 0; i < length(str); i++) {
c = str[i]
if (c == '"' && (i == 0 || str[i - 1] != '\\')) {
in_quote = !in_quote
} else if (c == '.' && !in_quote) {
var piece = trim(current)
piece = trim(current)
if (piece == null) piece = trim(current)
push(parts, parse_key(piece))
current = ''
continue
}
current += c
current = current + c
}
var tail = trim(current)
@@ -137,27 +145,30 @@ function parse_array(str) {
if (!is_text(str)) return null
// Remove brackets and trim
str = text(str, 1, -1)
str = trim(str)
if (!str) return []
var s = text(str, 1, -1)
s = trim(s)
if (!s) return []
var items = []
var current = ''
var in_quotes = false
for (var i = 0; i < length(str); i++) {
var ch = str[i]
var i = 0
var ch = null
var piece = null
for (i = 0; i < length(s); i++) {
ch = s[i]
if (ch == '"' && (i == 0 || str[i - 1] != '\\')) {
if (ch == '"' && (i == 0 || s[i - 1] != '\\')) {
in_quotes = !in_quotes
current += ch
current = current + ch
} else if (ch == ',' && !in_quotes) {
var piece = trim(current)
piece = trim(current)
if (piece == null) piece = trim(current)
push(items, parse_value(piece))
current = ''
} else {
current += ch
current = current + ch
}
}
@@ -186,12 +197,14 @@ function encode_toml(obj) {
var result = []
function encode_value(value) {
var items = null
var i = 0
if (is_text(value)) return '"' + toml_escape(value) + '"'
if (is_logical(value)) return value ? 'true' : 'false'
if (is_number(value)) return text(value)
if (is_array(value)) {
var items = []
for (var i = 0; i < length(value); i++) push(items, encode_value(value[i]))
items = []
for (i = 0; i < length(value); i++) push(items, encode_value(value[i]))
return '[' + text(items, ', ') + ']'
}
return text(value)
@@ -206,29 +219,41 @@ function encode_toml(obj) {
// First pass: encode top-level simple values
var keys = array(obj)
for (var i = 0; i < length(keys); i++) {
var key = keys[i]
var value = obj[key]
var i = 0
var key = null
var value = null
for (i = 0; i < length(keys); i++) {
key = keys[i]
value = obj[key]
if (!is_object(value)) push(result, quote_key(key) + ' = ' + encode_value(value))
}
// Second pass: encode nested objects
function encode_section(o, path) {
var keys = array(o)
for (var i = 0; i < length(keys); i++) {
var key = keys[i]
var value = o[key]
var i = 0
var key = null
var value = null
var quoted = null
var section_path = null
var section_keys = null
var j = 0
var sk = null
var sv = null
for (i = 0; i < length(keys); i++) {
key = keys[i]
value = o[key]
if (is_object(value)) {
var quoted = quote_key(key)
var section_path = path ? path + '.' + quoted : quoted
quoted = quote_key(key)
section_path = path ? path + '.' + quoted : quoted
push(result, '[' + section_path + ']')
// Direct properties
var section_keys = array(value)
for (var j = 0; j < length(section_keys); j++) {
var sk = section_keys[j]
var sv = value[sk]
section_keys = array(value)
for (j = 0; j < length(section_keys); j++) {
sk = section_keys[j]
sv = value[sk]
if (!is_object(sv)) push(result, quote_key(sk) + ' = ' + encode_value(sv))
}

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
var f = x => { return x }; f(1)

View File

@@ -1 +0,0 @@
var f = (x = 10) => x; f()

View File

@@ -1 +0,0 @@
var f = x => x * 2; f(5)

View File

@@ -1 +0,0 @@
var f = (a, b) => a + b; f(2, 3)

View File

@@ -1 +0,0 @@
var f = () => 42; f()

View File

@@ -1 +0,0 @@
var x = 5; x += 3; x

View File

@@ -1 +0,0 @@
var x = 7; x &= 3; x

View File

@@ -1 +0,0 @@
var x = 6; x /= 2; x

View File

@@ -1 +0,0 @@
var x = 5; x &&= 10; x

View File

@@ -1 +0,0 @@
var x = 0; x ||= 10; x

View File

@@ -1 +0,0 @@
var x = 7; x %= 3; x

View File

@@ -1 +0,0 @@
var x = 5; x *= 3; x

View File

@@ -1 +0,0 @@
var x = null; x ??= 10; x

View File

@@ -1 +0,0 @@
var x = 5; x |= 2; x

View File

@@ -1 +0,0 @@
var x = 2; x **= 3; x

View File

@@ -1 +0,0 @@
var x = 2; x <<= 3; x

View File

@@ -1 +0,0 @@
var x = 8; x >>= 2; x

View File

@@ -1 +0,0 @@
var x = -8; x >>>= 2; x

View File

@@ -1 +0,0 @@
var x = 5; x -= 3; x

View File

@@ -1 +0,0 @@
var x = 5; x ^= 3; x

View File

@@ -1 +0,0 @@
var x, y; x = y = 5; x + y

View File

@@ -1 +0,0 @@
var f = function(x) { return function() { return x } }; f(5)()

View File

@@ -1,11 +0,0 @@
var counter = function() {
var n = 0
return function() {
n = n + 1
return n
}
}
var c = counter()
c()
c()
c()

View File

@@ -1,3 +0,0 @@
// simple test that comments work
var x = 5
// other comment

View File

@@ -1 +0,0 @@
/* comment */ 5

Some files were not shown because too many files have changed in this diff Show More