Merge branch 'pretty_mcode' into mcode_streamline

This commit is contained in:
2026-02-12 18:48:17 -06:00
27 changed files with 26070 additions and 20218 deletions

View File

@@ -1,27 +1,15 @@
#include "cell.h"
// Return the current stack depth.
JSC_CCALL(debug_stack_depth, return number2js(js,js_debugger_stack_depth(js)))
// TODO: Reimplement stack depth for register VM
JSC_CCALL(debug_stack_depth, return number2js(js, 0))
// Return a backtrace of the current call stack.
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]))
JSC_CCALL(debug_set_closure_var,
js_debugger_set_closure_variable(js,argv[0],argv[1],argv[2]);
return JS_NULL;
)
// Return the local variables for a specific stack frame.
JSC_CCALL(debug_local_vars, return js_debugger_local_variables(js, js2number(js,argv[0])))
// Return metadata about a given function.
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))
// TODO: Reimplement debug introspection for register VM
JSC_CCALL(debug_build_backtrace, return JS_NewArray(js))
JSC_CCALL(debug_closure_vars, return JS_NewObject(js))
JSC_CCALL(debug_set_closure_var, return JS_NULL;)
JSC_CCALL(debug_local_vars, return JS_NewObject(js))
JSC_CCALL(debug_fn_info, return JS_NewObject(js))
JSC_CCALL(debug_backtrace_fns, return JS_NewArray(js))
static const JSCFunctionListEntry js_debug_funcs[] = {
MIST_FUNC_DEF(debug, stack_depth, 0),
@@ -37,4 +25,4 @@ JSValue js_debug_use(JSContext *js) {
JSValue mod = JS_NewObject(js);
JS_SetPropertyFunctionList(js,mod,js_debug_funcs,countof(js_debug_funcs));
return mod;
}
}

View File

@@ -1,48 +1,16 @@
#include "cell.h"
JSC_CCALL(os_mem_limit, JS_SetMemoryLimit(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(JS_GetRuntime(js), js2number(js,argv[0])))
JSC_CCALL(os_max_stacksize, JS_SetMaxStackSize(js, js2number(js,argv[0])))
// Compute the approximate size of a single JS value in memory.
// TODO: Reimplement memory usage reporting for new allocator
JSC_CCALL(os_calc_mem,
JSMemoryUsage mu;
JS_ComputeMemoryUsage(JS_GetRuntime(js),&mu);
ret = JS_NewObject(js);
JS_SetPropertyStr(js,ret,"malloc_size",number2js(js,mu.malloc_size));
JS_SetPropertyStr(js,ret,"malloc_limit",number2js(js,mu.malloc_limit));
JS_SetPropertyStr(js,ret,"memory_used_size",number2js(js,mu.memory_used_size));
JS_SetPropertyStr(js,ret,"malloc_count",number2js(js,mu.malloc_count));
JS_SetPropertyStr(js,ret,"memory_used_count",number2js(js,mu.memory_used_count));
JS_SetPropertyStr(js,ret,"str_count",number2js(js,mu.str_count));
JS_SetPropertyStr(js,ret,"str_size",number2js(js,mu.str_size));
JS_SetPropertyStr(js,ret,"obj_count",number2js(js,mu.obj_count));
JS_SetPropertyStr(js,ret,"obj_size",number2js(js,mu.obj_size));
JS_SetPropertyStr(js,ret,"prop_count",number2js(js,mu.prop_count));
JS_SetPropertyStr(js,ret,"prop_size",number2js(js,mu.prop_size));
JS_SetPropertyStr(js,ret,"shape_count",number2js(js,mu.shape_count));
JS_SetPropertyStr(js,ret,"shape_size",number2js(js,mu.shape_size));
JS_SetPropertyStr(js,ret,"js_func_count",number2js(js,mu.js_func_count));
JS_SetPropertyStr(js,ret,"js_func_size",number2js(js,mu.js_func_size));
JS_SetPropertyStr(js,ret,"js_func_code_size",number2js(js,mu.js_func_code_size));
JS_SetPropertyStr(js,ret,"js_func_pc2line_count",number2js(js,mu.js_func_pc2line_count));
JS_SetPropertyStr(js,ret,"js_func_pc2line_size",number2js(js,mu.js_func_pc2line_size));
JS_SetPropertyStr(js,ret,"c_func_count",number2js(js,mu.c_func_count));
JS_SetPropertyStr(js,ret,"array_count",number2js(js,mu.array_count));
JS_SetPropertyStr(js,ret,"fast_array_count",number2js(js,mu.fast_array_count));
JS_SetPropertyStr(js,ret,"fast_array_elements",number2js(js,mu.fast_array_elements));
JS_SetPropertyStr(js,ret,"binary_object_count",number2js(js,mu.binary_object_count));
JS_SetPropertyStr(js,ret,"binary_object_size",number2js(js,mu.binary_object_size));
)
// Disassemble a function object into a string.
JSC_CCALL(js_disassemble,
return js_debugger_fn_bytecode(js, argv[0]);
)
// Return metadata about a given function.
JSC_CCALL(js_fn_info,
return js_debugger_fn_info(js, argv[0]);
)
// TODO: Reimplement for register VM
JSC_CCALL(js_disassemble, return JS_NewArray(js);)
JSC_CCALL(js_fn_info, return JS_NewObject(js);)
static const JSCFunctionListEntry js_js_funcs[] = {
MIST_FUNC_DEF(os, calc_mem, 0),

264
diff.ce Normal file
View File

@@ -0,0 +1,264 @@
// diff.ce — differential testing: run tests optimized vs unoptimized, compare results
//
// Usage:
// cell diff - diff all test files in current package
// cell diff suite - diff a specific test file (tests/suite.cm)
// cell diff tests/foo - diff a specific test file by path
var shop = use('internal/shop')
var pkg = use('package')
var fd = use('fd')
var time = use('time')
var _args = args == null ? [] : args
var analyze = use('os').analyze
var run_ast_fn = use('os').run_ast_fn
var run_ast_noopt_fn = use('os').run_ast_noopt_fn
if (!run_ast_noopt_fn) {
log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)")
$stop()
return
}
// Parse arguments: diff [test_path]
var target_test = null
if (length(_args) > 0) {
target_test = _args[0]
}
function is_valid_package(dir) {
var _dir = dir == null ? '.' : dir
return fd.is_file(_dir + '/cell.toml')
}
if (!is_valid_package('.')) {
log.console('No cell.toml found in current directory')
$stop()
return
}
// Collect test files
function collect_tests(specific_test) {
var files = pkg.list_files(null)
var test_files = []
var i = 0
var f = null
var test_name = null
var match_name = null
var match_base = null
for (i = 0; i < length(files); i++) {
f = files[i]
if (starts_with(f, "tests/") && ends_with(f, ".cm")) {
if (specific_test) {
test_name = text(f, 0, -3)
match_name = specific_test
if (!starts_with(match_name, 'tests/')) match_name = 'tests/' + 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)
}
}
return test_files
}
// Deep comparison of two values
function values_equal(a, b) {
var i = 0
var ka = null
var kb = null
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
i = 0
while (i < length(a)) {
if (!values_equal(a[i], b[i])) return false
i = i + 1
}
return true
}
if (is_object(a) && is_object(b)) {
ka = array(a)
kb = array(b)
if (length(ka) != length(kb)) return false
i = 0
while (i < length(ka)) {
if (!values_equal(a[ka[i]], b[ka[i]])) return false
i = i + 1
}
return true
}
return false
}
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
if (is_array(val)) return `[array length=${text(length(val))}]`
if (is_object(val)) return `{record keys=${text(length(array(val)))}}`
return "<unknown>"
}
// Run a single test file through both paths
function diff_test_file(file_path) {
var mod_path = text(file_path, 0, -3)
var src_path = fd.realpath('.') + '/' + file_path
var src = null
var ast = null
var mod_opt = null
var mod_noopt = null
var results = {file: file_path, tests: [], passed: 0, failed: 0, errors: []}
var use_pkg = fd.realpath('.')
var opt_error = null
var noopt_error = null
var keys = null
var i = 0
var k = null
var opt_result = null
var noopt_result = null
var opt_err = null
var noopt_err = null
var _run_one_opt = null
var _run_one_noopt = null
// Build env for module loading
var make_env = function() {
return {
use: function(path) {
return shop.use(path, use_pkg)
}
}
}
// Read and parse
var _read = function() {
src = text(fd.slurp(src_path))
ast = analyze(src, src_path)
} disruption {
push(results.errors, `failed to parse ${file_path}`)
return results
}
_read()
if (length(results.errors) > 0) return results
// Run optimized
var _run_opt = function() {
mod_opt = run_ast_fn(mod_path, ast, make_env())
} disruption {
opt_error = "disrupted"
}
_run_opt()
// Run unoptimized
var _run_noopt = function() {
mod_noopt = run_ast_noopt_fn(mod_path, ast, make_env())
} disruption {
noopt_error = "disrupted"
}
_run_noopt()
// Compare module-level behavior
if (opt_error != noopt_error) {
push(results.errors, `module load mismatch: opt=${opt_error != null ? opt_error : "ok"} noopt=${noopt_error != null ? noopt_error : "ok"}`)
results.failed = results.failed + 1
return results
}
if (opt_error != null) {
// Both disrupted during load — that's consistent
results.passed = results.passed + 1
push(results.tests, {name: "<module>", status: "passed"})
return results
}
// If module returns a record of functions, test each one
if (is_object(mod_opt) && is_object(mod_noopt)) {
keys = array(mod_opt)
while (i < length(keys)) {
k = keys[i]
if (is_function(mod_opt[k]) && is_function(mod_noopt[k])) {
opt_result = null
noopt_result = null
opt_err = null
noopt_err = null
_run_one_opt = function() {
opt_result = mod_opt[k]()
} disruption {
opt_err = "disrupted"
}
_run_one_opt()
_run_one_noopt = function() {
noopt_result = mod_noopt[k]()
} disruption {
noopt_err = "disrupted"
}
_run_one_noopt()
if (opt_err != noopt_err) {
push(results.tests, {name: k, status: "failed"})
push(results.errors, `${k}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
results.failed = results.failed + 1
} else if (!values_equal(opt_result, noopt_result)) {
push(results.tests, {name: k, status: "failed"})
push(results.errors, `${k}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
results.failed = results.failed + 1
} else {
push(results.tests, {name: k, status: "passed"})
results.passed = results.passed + 1
}
}
i = i + 1
}
} else {
// Compare direct return values
if (!values_equal(mod_opt, mod_noopt)) {
push(results.tests, {name: "<return>", status: "failed"})
push(results.errors, `return value mismatch: opt=${describe(mod_opt)} noopt=${describe(mod_noopt)}`)
results.failed = results.failed + 1
} else {
push(results.tests, {name: "<return>", status: "passed"})
results.passed = results.passed + 1
}
}
return results
}
// Main
var test_files = collect_tests(target_test)
log.console(`Differential testing: ${text(length(test_files))} file(s)`)
var total_passed = 0
var total_failed = 0
var i = 0
var result = null
var j = 0
while (i < length(test_files)) {
result = diff_test_file(test_files[i])
log.console(` ${result.file}: ${text(result.passed)} passed, ${text(result.failed)} failed`)
j = 0
while (j < length(result.errors)) {
log.console(` MISMATCH: ${result.errors[j]}`)
j = j + 1
}
total_passed = total_passed + result.passed
total_failed = total_failed + result.failed
i = i + 1
}
log.console(`----------------------------------------`)
log.console(`Diff: ${text(total_passed)} passed, ${text(total_failed)} failed, ${text(total_passed + total_failed)} total`)
if (total_failed > 0) {
log.console(`DIFFERENTIAL FAILURES DETECTED`)
}
$stop()

View File

@@ -56,6 +56,7 @@ Modules loaded with `use()`:
## Tools
- [**Command Line**](/docs/cli/) — the `pit` tool
- [**Testing**](/docs/testing/) — writing and running tests
- [**Writing C Modules**](/docs/c-modules/) — native extensions
## Architecture

View File

@@ -78,12 +78,13 @@ pit build
### pit test
Run tests.
Run tests. See [Testing](/docs/testing/) for the full guide.
```bash
pit test # run tests in current package
pit test all # run all tests
pit test <package> # run tests in specific package
pit test suite --verify --diff # with IR verification and differential testing
```
### pit link

264
docs/compiler-tools.md Normal file
View File

@@ -0,0 +1,264 @@
---
title: "Compiler Inspection Tools"
description: "Tools for inspecting and debugging the compiler pipeline"
weight: 50
type: "docs"
---
ƿit includes a set of tools for inspecting the compiler pipeline at every stage. These are useful for debugging, testing optimizations, and understanding what the compiler does with your code.
## Pipeline Overview
The compiler runs in stages:
```
source → tokenize → parse → fold → mcode → streamline → output
```
Each stage has a corresponding dump tool that lets you see its output.
| Stage | Tool | What it shows |
|-------------|-------------------|----------------------------------------|
| fold | `dump_ast.cm` | Folded AST as JSON |
| mcode | `dump_mcode.cm` | Raw mcode IR before optimization |
| streamline | `dump_stream.cm` | Before/after instruction counts + IR |
| streamline | `dump_types.cm` | Optimized IR with type annotations |
| streamline | `streamline.ce` | Full optimized IR as JSON |
| all | `ir_report.ce` | Structured optimizer flight recorder |
All tools take a source file as input and run the pipeline up to the relevant stage.
## Quick Start
```bash
# see raw mcode IR
./cell --core . dump_mcode.cm myfile.ce
# see what the optimizer changed
./cell --core . dump_stream.cm myfile.ce
# full optimizer report with events
./cell --core . ir_report.ce --full myfile.ce
```
## dump_ast.cm
Prints the folded AST as JSON. This is the output of the parser and constant folder, before mcode generation.
```bash
./cell --core . dump_ast.cm <file.ce|file.cm>
```
## dump_mcode.cm
Prints the raw mcode IR before any optimization. Shows the instruction array as formatted text with opcode, operands, and program counter.
```bash
./cell --core . dump_mcode.cm <file.ce|file.cm>
```
## dump_stream.cm
Shows a before/after comparison of the optimizer. For each function, prints:
- Instruction count before and after
- Number of eliminated instructions
- The streamlined IR (nops hidden by default)
```bash
./cell --core . dump_stream.cm <file.ce|file.cm>
```
## dump_types.cm
Shows the optimized IR with type annotations. Each instruction is followed by the known types of its slot operands, inferred by walking the instruction stream.
```bash
./cell --core . dump_types.cm <file.ce|file.cm>
```
## streamline.ce
Runs the full pipeline (tokenize, parse, fold, mcode, streamline) and outputs the optimized IR as JSON. Useful for piping to `jq` or saving for comparison.
```bash
./cell --core . streamline.ce <file.ce|file.cm>
```
## ir_report.ce
The optimizer flight recorder. Runs the full pipeline with structured logging and outputs machine-readable, diff-friendly JSON. This is the most detailed tool for understanding what the optimizer did and why.
```bash
./cell --core . ir_report.ce [options] <file.ce|file.cm>
```
### Options
| Flag | Description |
|------|-------------|
| `--summary` | Per-pass JSON summaries with instruction counts and timing (default) |
| `--events` | Include rewrite events showing each optimization applied |
| `--types` | Include type delta records showing inferred slot types |
| `--ir-before=PASS` | Print canonical IR before a specific pass |
| `--ir-after=PASS` | Print canonical IR after a specific pass |
| `--ir-all` | Print canonical IR before and after all passes |
| `--full` | Everything: summary + events + types + ir-all |
With no flags, `--summary` is the default.
### Output Format
Output is line-delimited JSON. Each line is a self-contained JSON object with a `type` field:
**`type: "pass"`** — Per-pass summary with categorized instruction counts before and after:
```json
{
"type": "pass",
"pass": "eliminate_type_checks",
"fn": "fib",
"ms": 0.12,
"changed": true,
"before": {"instr": 77, "nop": 0, "guard": 16, "branch": 28, ...},
"after": {"instr": 77, "nop": 1, "guard": 15, "branch": 28, ...},
"changes": {"guards_removed": 1, "nops_added": 1}
}
```
**`type: "event"`** — Individual rewrite event with before/after instructions and reasoning:
```json
{
"type": "event",
"pass": "eliminate_type_checks",
"rule": "incompatible_type_forces_jump",
"at": 3,
"before": [["is_int", 5, 2, 4, 9], ["jump_false", 5, "rel_ni_2", 4, 9]],
"after": ["_nop_tc_1", ["jump", "rel_ni_2", 4, 9]],
"why": {"slot": 2, "known_type": "float", "checked_type": "int"}
}
```
**`type: "types"`** — Inferred type information for a function:
```json
{
"type": "types",
"fn": "fib",
"param_types": {},
"slot_types": {"25": "null"}
}
```
**`type: "ir"`** — Canonical IR text for a function at a specific point:
```json
{
"type": "ir",
"when": "before",
"pass": "all",
"fn": "fib",
"text": "fn fib (args=1, slots=26)\n @0 access s2, 2\n ..."
}
```
### Rewrite Rules
Each pass records events with named rules:
**eliminate_type_checks:**
- `known_type_eliminates_guard` — type already known, guard removed
- `incompatible_type_forces_jump` — type conflicts, conditional jump becomes unconditional
- `num_subsumes_int_float` — num check satisfied by int or float
- `dynamic_to_field` — load_dynamic/store_dynamic narrowed to field access
- `dynamic_to_index` — load_dynamic/store_dynamic narrowed to index access
**simplify_algebra:**
- `add_zero`, `sub_zero`, `mul_one`, `div_one` — identity operations become moves
- `mul_zero` — multiplication by zero becomes constant
- `self_eq`, `self_ne` — same-slot comparisons become constants
**simplify_booleans:**
- `not_jump_false_fusion` — not + jump_false fused into jump_true
- `not_jump_true_fusion` — not + jump_true fused into jump_false
- `double_not` — not + not collapsed to move
**eliminate_moves:**
- `self_move` — move to same slot becomes nop
**eliminate_dead_jumps:**
- `jump_to_next` — jump to immediately following label becomes nop
### Canonical IR Format
The `--ir-all`, `--ir-before`, and `--ir-after` flags produce a deterministic text representation of the IR:
```
fn fib (args=1, slots=26)
@0 access s2, 2
@1 is_int s4, s1 ; [guard]
@2 jump_false s4, "rel_ni_2" ; [branch]
@3 --- nop (tc) ---
@4 jump "rel_ni_2" ; [branch]
@5 lt_int s3, s1, s2
@6 jump "rel_done_4" ; [branch]
rel_ni_2:
@8 is_num s4, s1 ; [guard]
```
Properties:
- `@N` is the raw array index, stable across passes (passes replace, never insert or delete)
- `sN` prefix distinguishes slot operands from literal values
- String operands are quoted
- Labels appear as indented headers with a colon
- Category tags in brackets: `[guard]`, `[branch]`, `[load]`, `[store]`, `[call]`, `[arith]`, `[move]`, `[const]`
- Nops shown as `--- nop (reason) ---` with reason codes: `tc` (type check), `bl` (boolean), `mv` (move), `dj` (dead jump), `ur` (unreachable)
### Examples
```bash
# what passes changed something?
./cell --core . ir_report.ce --summary myfile.ce | jq 'select(.changed)'
# list all rewrite rules that fired
./cell --core . ir_report.ce --events myfile.ce | jq 'select(.type == "event") | .rule'
# diff IR before and after optimization
./cell --core . ir_report.ce --ir-all myfile.ce | jq -r 'select(.type == "ir") | .text'
# full report for analysis
./cell --core . ir_report.ce --full myfile.ce > report.json
```
## ir_stats.cm
A utility module used by `ir_report.ce` and available for custom tooling. Not a standalone tool.
```javascript
var ir_stats = use("ir_stats")
ir_stats.detailed_stats(func) // categorized instruction counts
ir_stats.ir_fingerprint(func) // djb2 hash of instruction array
ir_stats.canonical_ir(func, name, opts) // deterministic text representation
ir_stats.type_snapshot(slot_types) // frozen copy of type map
ir_stats.type_delta(before_types, after_types) // compute type changes
ir_stats.category_tag(op) // classify an opcode
```
### Instruction Categories
`detailed_stats` classifies each instruction into one of these categories:
| Category | Opcodes |
|----------|---------|
| load | `load_field`, `load_index`, `load_dynamic`, `get`, `access` (non-constant) |
| store | `store_field`, `store_index`, `store_dynamic`, `set_var`, `put`, `push` |
| branch | `jump`, `jump_true`, `jump_false`, `jump_not_null` |
| call | `invoke`, `goinvoke` |
| guard | `is_int`, `is_text`, `is_num`, `is_bool`, `is_null`, `is_array`, `is_func`, `is_record`, `is_stone` |
| arith | `add_int`, `sub_int`, ..., `add_float`, ..., `concat`, `neg_int`, `neg_float`, bitwise ops |
| move | `move` |
| const | `int`, `true`, `false`, `null`, `access` (with constant value) |
| label | string entries that are not nops |
| nop | strings starting with `_nop_` |
| other | everything else (`frame`, `setarg`, `array`, `record`, `function`, `return`, etc.) |

170
docs/testing.md Normal file
View File

@@ -0,0 +1,170 @@
---
title: "Testing"
description: "Writing and running tests in ƿit"
weight: 45
type: "docs"
---
ƿit has built-in support for writing and running tests. Tests live in the `tests/` directory of a package and are `.cm` modules that return a record of test functions.
## Writing Tests
A test file returns a record where each key starting with `test_` is a test function. A test passes if it returns `null` (or nothing). It fails if it returns a text string describing the failure.
```javascript
// tests/math.cm
return {
test_addition: function() {
if (1 + 2 != 3) return "expected 3"
},
test_division: function() {
if (10 / 3 != 3.333333333333333333) return "unexpected result"
}
}
```
Test functions take no arguments. Use early returns with a failure message to report errors:
```javascript
test_array_push: function() {
var a = [1, 2]
a[] = 3
if (length(a) != 3) return "expected length 3, got " + text(length(a))
if (a[2] != 3) return "expected a[2] to be 3"
}
```
## Running Tests
```bash
pit test # run all tests in current package
pit test suite # run a specific test file (tests/suite.cm)
pit test tests/math # same, with explicit path
pit test all # run all tests in current package
pit test package <name> # run all tests in a named package
pit test package <name> <test> # run a specific test in a named package
pit test package all # run tests from all installed packages
```
### Flags
```bash
pit test suite -g # run GC after each test (useful for detecting leaks)
pit test suite --verify # enable IR verification during compilation
pit test suite --diff # run each test optimized and unoptimized, compare results
```
`--verify` and `--diff` can be combined:
```bash
pit test suite --verify --diff
```
## IR Verification
The `--verify` flag enables structural validation of the compiler's intermediate representation after each optimizer pass. This catches bugs like invalid slot references, broken jump targets, and malformed instructions.
When verification fails, errors are printed with the pass name that introduced them:
```
[verify_ir] slot_bounds: slot 12 out of range 0..9 in instruction add_int
[verify_ir] 1 errors after dead_code_elimination
```
IR verification adds overhead and is intended for development, not production use.
## Differential Testing
Differential testing runs each test through two paths — with the optimizer enabled and with it disabled — and compares results. Any mismatch between the two indicates an optimizer bug.
### Inline Mode
The `--diff` flag on `pit test` runs each test module through both paths during a normal test run:
```bash
pit test suite --diff
```
Output includes a mismatch count at the end:
```
Tests: 493 passed, 0 failed, 493 total
Diff mismatches: 0
```
### Standalone Mode
`pit diff` is a dedicated differential testing tool with detailed mismatch reporting:
```bash
pit diff # diff all test files in current package
pit diff suite # diff a specific test file
pit diff tests/math # same, with explicit path
```
For each test function, it reports whether the optimized and unoptimized results match:
```
tests/suite.cm: 493 passed, 0 failed
----------------------------------------
Diff: 493 passed, 0 failed, 493 total
```
When a mismatch is found:
```
tests/suite.cm: 492 passed, 1 failed
MISMATCH: test_foo: result mismatch opt=42 noopt=43
```
## Fuzz Testing
The fuzzer generates random self-checking programs, compiles them, and runs them through both optimized and unoptimized paths. Each generated program contains test functions that validate their own expected results, so failures catch both correctness bugs and optimizer mismatches.
```bash
pit fuzz # 100 iterations, random seed
pit fuzz 500 # 500 iterations, random seed
pit fuzz --seed 42 # 100 iterations, deterministic seed
pit fuzz 1000 --seed 42 # 1000 iterations, deterministic seed
```
The fuzzer generates programs that exercise:
- Integer and float arithmetic with known expected results
- Control flow (if/else, while loops)
- Closures and captured variable mutation
- Records and property access
- Arrays and iteration
- Higher-order functions
- Disruption handling
- Text concatenation
On failure, the generated source is saved to `tests/fuzz_failures/` for reproduction:
```
Fuzzing: 1000 iterations, starting seed=42
FAIL seed=57: diff fuzz_3: opt=10 noopt=11
saved to tests/fuzz_failures/seed_57.cm
----------------------------------------
Fuzz: 999 passed, 1 failed, 1000 total
Failures saved to tests/fuzz_failures/
```
Saved failure files are valid `.cm` modules that can be run directly or added to the test suite.
## Test File Organization
Tests live in the `tests/` directory of a package:
```
mypackage/
├── pit.toml
├── math.cm
└── tests/
├── suite.cm # main test suite
├── math.cm # math-specific tests
└── disrupt.cm # disruption tests
```
All `.cm` files under `tests/` are discovered automatically by `pit test`.

View File

@@ -1,3 +1,7 @@
// dump_ast.cm — pretty-print the folded AST as JSON
//
// Usage: ./cell --core . dump_ast.cm <file.ce|file.cm>
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")

278
fuzz.ce Normal file
View File

@@ -0,0 +1,278 @@
// fuzz.ce — fuzzer driver: generates random programs, runs differential, saves failures
//
// Usage:
// cell fuzz - run 100 iterations with a random seed
// cell fuzz 500 - run 500 iterations with a random seed
// cell fuzz --seed 42 - run 100 iterations starting at seed 42
// cell fuzz 500 --seed 42 - run 500 iterations starting at seed 42
//
// Each iteration generates a random self-checking program, compiles it,
// runs it through both optimized and unoptimized paths, and compares results.
// Failures are saved to tests/fuzz_failures/ for reproduction.
var fd = use('fd')
var time = use('time')
var json = use('json')
var os_ref = use('os')
var analyze = os_ref.analyze
var run_ast_fn = os_ref.run_ast_fn
var run_ast_noopt_fn = os_ref.run_ast_noopt_fn
var fuzzgen = use('fuzzgen')
var _args = args == null ? [] : args
// Parse arguments: fuzz [iterations] [--seed N]
var iterations = 100
var start_seed = null
var i = 0
var n = null
var run_err = null
var _run_one = null
while (i < length(_args)) {
if (_args[i] == '--seed' && i + 1 < length(_args)) {
start_seed = number(_args[i + 1])
i = i + 2
} else {
n = number(_args[i])
if (n != null && n > 0) iterations = n
i = i + 1
}
}
if (start_seed == null) {
start_seed = floor(time.number() * 1000) % 1000000
}
if (!run_ast_noopt_fn) {
log.console("error: run_ast_noopt_fn not available (rebuild bootstrap)")
$stop()
return
}
// Ensure failures directory exists
var failures_dir = "tests/fuzz_failures"
function ensure_dir(path) {
if (fd.is_dir(path)) return
var parts = array(path, '/')
var current = ''
var j = 0
while (j < length(parts)) {
if (parts[j] != '') {
current = current + parts[j] + '/'
if (!fd.is_dir(current)) {
fd.mkdir(current)
}
}
j = j + 1
}
}
// Deep comparison
function values_equal(a, b) {
var j = 0
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
j = 0
while (j < length(a)) {
if (!values_equal(a[j], b[j])) return false
j = j + 1
}
return true
}
return false
}
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
return "<other>"
}
// Run a single fuzz iteration
function run_fuzz(seed_val) {
var src = fuzzgen.generate(seed_val)
var name = "fuzz_" + text(seed_val)
var ast = null
var mod_opt = null
var mod_noopt = null
var opt_err = null
var noopt_err = null
var errors = []
var keys = null
var k = 0
var key = null
var ret = null
var _run = null
var run_err = null
var keys2 = null
var k2 = 0
var key2 = null
var opt_result = null
var noopt_result = null
var opt_fn_err = null
var noopt_fn_err = null
var _run_opt = null
var _run_noopt = null
// Parse
var _parse = function() {
ast = analyze(src, name + ".cm")
} disruption {
push(errors, "parse error")
}
_parse()
if (length(errors) > 0) return {seed: seed_val, errors: errors, src: src}
// Run optimized
var _opt = function() {
mod_opt = run_ast_fn(name, ast, {use: function(p) { return use(p) }})
} disruption {
opt_err = "disrupted"
}
_opt()
// Run unoptimized
var _noopt = function() {
mod_noopt = run_ast_noopt_fn(name + "_noopt", ast, {use: function(p) { return use(p) }})
} disruption {
noopt_err = "disrupted"
}
_noopt()
// Check module-level behavior
if (opt_err != noopt_err) {
push(errors, `module load: opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
return {seed: seed_val, errors: errors, src: src}
}
if (opt_err != null) {
// Both failed to load — consistent
return {seed: seed_val, errors: errors, src: src}
}
// Run self-checks (optimized module)
if (is_object(mod_opt)) {
keys = array(mod_opt)
k = 0
while (k < length(keys)) {
key = keys[k]
if (is_function(mod_opt[key])) {
ret = null
run_err = null
_run = function() {
ret = mod_opt[key]()
} disruption {
run_err = "disrupted"
}
_run()
if (is_text(ret)) {
push(errors, `self-check ${key}: ${ret}`)
}
if (run_err != null) {
push(errors, `self-check ${key}: unexpected disruption`)
}
}
k = k + 1
}
}
// Differential check on each function
if (is_object(mod_opt) && is_object(mod_noopt)) {
keys2 = array(mod_opt)
k2 = 0
while (k2 < length(keys2)) {
key2 = keys2[k2]
if (is_function(mod_opt[key2]) && is_function(mod_noopt[key2])) {
opt_result = null
noopt_result = null
opt_fn_err = null
noopt_fn_err = null
_run_opt = function() {
opt_result = mod_opt[key2]()
} disruption {
opt_fn_err = "disrupted"
}
_run_opt()
_run_noopt = function() {
noopt_result = mod_noopt[key2]()
} disruption {
noopt_fn_err = "disrupted"
}
_run_noopt()
if (opt_fn_err != noopt_fn_err) {
push(errors, `diff ${key2}: opt=${opt_fn_err != null ? opt_fn_err : "ok"} noopt=${noopt_fn_err != null ? noopt_fn_err : "ok"}`)
} else if (!values_equal(opt_result, noopt_result)) {
push(errors, `diff ${key2}: opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
}
}
k2 = k2 + 1
}
}
return {seed: seed_val, errors: errors, src: src}
}
// Main loop
log.console(`Fuzzing: ${text(iterations)} iterations, starting seed=${text(start_seed)}`)
var total_pass = 0
var total_fail = 0
var result = null
var j = 0
var current_seed = 0
var fail_path = null
i = 0
while (i < iterations) {
current_seed = start_seed + i
run_err = null
_run_one = function() {
result = run_fuzz(current_seed)
} disruption {
run_err = "generator crashed"
}
_run_one()
if (run_err != null) {
result = {seed: current_seed, errors: [run_err], src: "// generator crashed"}
}
if (length(result.errors) > 0) {
total_fail = total_fail + 1
log.console(` FAIL seed=${text(current_seed)}: ${result.errors[0]}`)
// Save failure source for reproduction
ensure_dir(failures_dir)
fail_path = failures_dir + "/seed_" + text(current_seed) + ".cm"
fd.slurpwrite(fail_path, stone(blob(result.src)))
log.console(` saved to ${fail_path}`)
} else {
total_pass = total_pass + 1
}
// Progress report every 100 iterations
if ((i + 1) % 100 == 0) {
log.console(` progress: ${text(i + 1)}/${text(iterations)} (${text(total_pass)} passed, ${text(total_fail)} failed)`)
}
i = i + 1
}
log.console(`----------------------------------------`)
log.console(`Fuzz: ${text(total_pass)} passed, ${text(total_fail)} failed, ${text(iterations)} total`)
if (total_fail > 0) {
log.console(`Failures saved to ${failures_dir}/`)
}
$stop()

348
fuzzgen.cm Normal file
View File

@@ -0,0 +1,348 @@
// fuzzgen.cm — generates self-checking .cm programs for fuzz testing
// Each generated program returns a record of test functions that
// validate their own expected results.
// Newline constant — backtick strings don't interpret \n as escape
var NL = "\n"
// Simple seeded PRNG (xorshift32)
var _seed = 1
function seed(s) {
_seed = s != 0 ? s : 1
}
function rand() {
_seed = _seed ^ (_seed << 13)
_seed = _seed ^ (_seed >> 17)
_seed = _seed ^ (_seed << 5)
if (_seed < 0) _seed = -_seed
return _seed
}
function rand_int(lo, hi) {
return lo + (rand() % (hi - lo + 1))
}
function rand_float() {
return rand_int(-10000, 10000) / 100
}
function rand_bool() {
return rand() % 2 == 0
}
function pick(arr) {
return arr[rand() % length(arr)]
}
// Expression generators — each returns {src: "code", val: expected_value}
// depth is decremented to prevent infinite recursion
function gen_int_literal() {
var v = rand_int(-10000, 10000)
return {src: text(v), val: v}
}
function gen_float_literal() {
var v = rand_float()
return {src: text(v), val: v}
}
function gen_bool_literal() {
var v = rand_bool()
var s = "false"
if (v) s = "true"
return {src: s, val: v}
}
function gen_text_literal() {
var words = ["alpha", "beta", "gamma", "delta", "epsilon"]
var w = pick(words)
return {src: `"${w}"`, val: w}
}
function gen_null_literal() {
return {src: "null", val: null}
}
function gen_int_expr(depth) {
var a = null
var b = null
var op = null
var result = null
if (depth <= 0) return gen_int_literal()
a = gen_int_expr(depth - 1)
b = gen_int_expr(depth - 1)
// Avoid division by zero
if (b.val == 0) b = {src: "1", val: 1}
op = pick(["+", "-", "*"])
if (op == "+") {
result = a.val + b.val
} else if (op == "-") {
result = a.val - b.val
} else {
result = a.val * b.val
}
// Guard against overflow beyond safe integer range
if (result > 9007199254740991 || result < -9007199254740991) {
return gen_int_literal()
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
function gen_float_expr(depth) {
var a = null
var b = null
var op = null
var result = null
if (depth <= 0) return gen_float_literal()
a = gen_float_expr(depth - 1)
b = gen_float_expr(depth - 1)
if (b.val == 0) b = {src: "1.0", val: 1.0}
op = pick(["+", "-", "*"])
if (op == "+") {
result = a.val + b.val
} else if (op == "-") {
result = a.val - b.val
} else {
result = a.val * b.val
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
function gen_text_expr(depth) {
var a = null
var b = null
if (depth <= 0) return gen_text_literal()
a = gen_text_literal()
b = gen_text_literal()
return {src: `(${a.src} + ${b.src})`, val: a.val + b.val}
}
function gen_comparison_expr(depth) {
var a = null
var b = null
var op = null
var result = null
a = gen_int_expr(depth > 0 ? depth - 1 : 0)
b = gen_int_expr(depth > 0 ? depth - 1 : 0)
op = pick(["==", "!=", "<", ">", "<=", ">="])
if (op == "==") {
result = a.val == b.val
} else if (op == "!=") {
result = a.val != b.val
} else if (op == "<") {
result = a.val < b.val
} else if (op == ">") {
result = a.val > b.val
} else if (op == "<=") {
result = a.val <= b.val
} else {
result = a.val >= b.val
}
return {src: `(${a.src} ${op} ${b.src})`, val: result}
}
// Generate an if-else expression test
function gen_if_else_test() {
var cond = gen_comparison_expr(1)
var then_val = gen_int_literal()
var else_val = gen_int_literal()
var expected = cond.val ? then_val.val : else_val.val
var body = "var result = null" + NL
body = body + " if (" + cond.src + ") {" + NL
body = body + " result = " + then_val.src + NL
body = body + " } else {" + NL
body = body + " result = " + else_val.src + NL
body = body + " }" + NL
body = body + " if (result != " + text(expected) + ") return \"if_else: expected " + text(expected) + " got \" + text(result)"
return body
}
// Generate a loop accumulator test
function gen_loop_test() {
var count = rand_int(1, 50)
var step = rand_int(1, 10)
var expected = 0
var i = 0
while (i < count) {
expected = expected + step
i = i + 1
}
var body = "var acc = 0" + NL
body = body + " var i = 0" + NL
body = body + " while (i < " + text(count) + ") {" + NL
body = body + " acc = acc + " + text(step) + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (acc != " + text(expected) + ") return \"loop: expected " + text(expected) + " got \" + text(acc)"
return body
}
// Generate a closure test
function gen_closure_test() {
var init_val = rand_int(1, 100)
var inc = rand_int(1, 10)
var calls = rand_int(1, 10)
var expected = init_val + (inc * calls)
var body = "var counter = " + text(init_val) + NL
body = body + " var inc = function() { counter = counter + " + text(inc) + " }" + NL
body = body + " var i = 0" + NL
body = body + " while (i < " + text(calls) + ") {" + NL
body = body + " inc()" + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (counter != " + text(expected) + ") return \"closure: expected " + text(expected) + " got \" + text(counter)"
return body
}
// Generate a record property test
function gen_record_test() {
var a = gen_int_literal()
var b = gen_int_literal()
var sum = a.val + b.val
var body = "var r = {a: " + a.src + ", b: " + b.src + "}" + NL
body = body + " var result = r.a + r.b" + NL
body = body + " if (result != " + text(sum) + ") return \"record: expected " + text(sum) + " got \" + text(result)"
return body
}
// Generate an array test
function gen_array_test() {
var n = rand_int(2, 10)
var vals = []
var i = 0
var sum = 0
var v = 0
while (i < n) {
v = rand_int(-100, 100)
push(vals, v)
sum = sum + v
i = i + 1
}
var val_strs = []
i = 0
while (i < n) {
push(val_strs, text(vals[i]))
i = i + 1
}
var body = "var a = [" + text(val_strs, ", ") + "]" + NL
body = body + " var _sum = 0" + NL
body = body + " var i = 0" + NL
body = body + " while (i < length(a)) {" + NL
body = body + " _sum = _sum + a[i]" + NL
body = body + " i = i + 1" + NL
body = body + " }" + NL
body = body + " if (_sum != " + text(sum) + ") return \"array_sum: expected " + text(sum) + " got \" + text(_sum)"
return body
}
// Generate a nested function / higher-order test
function gen_higher_order_test() {
var mul = rand_int(2, 10)
var input = rand_int(1, 100)
var expected = input * mul
var body = "var make_mul = function(m) {" + NL
body = body + " return function(x) { return x * m }" + NL
body = body + " }" + NL
body = body + " var fn = make_mul(" + text(mul) + ")" + NL
body = body + " var result = fn(" + text(input) + ")" + NL
body = body + " if (result != " + text(expected) + ") return \"higher_order: expected " + text(expected) + " got \" + text(result)"
return body
}
// Generate a disruption handling test
function gen_disrupt_test() {
var body = "var caught = false" + NL
body = body + " var _fn = function() { disrupt } disruption { caught = true }" + NL
body = body + " _fn()" + NL
body = body + " if (!caught) return \"disrupt: expected to catch disruption\""
return body
}
// Generate a text operation test
function gen_text_op_test() {
var words = ["hello", "world", "foo", "bar", "baz"]
var w1 = pick(words)
var w2 = pick(words)
var expected = w1 + w2
var body = "var a = \"" + w1 + "\"" + NL
body = body + " var b = \"" + w2 + "\"" + NL
body = body + " var c = a + b" + NL
body = body + " if (c != \"" + expected + "\") return \"text_op: expected " + expected + " got \" + c"
return body
}
// All generators
var generators = [
gen_if_else_test,
gen_loop_test,
gen_closure_test,
gen_record_test,
gen_array_test,
gen_higher_order_test,
gen_disrupt_test,
gen_text_op_test
]
// Generate a complete self-checking .cm program
function generate(s) {
seed(s)
var num_tests = rand_int(5, 15)
var src = "// Auto-generated fuzz test (seed=" + text(s) + ")\nreturn {\n"
var i = 0
var gen = null
var body = null
while (i < num_tests) {
gen = pick(generators)
body = gen()
if (i > 0) src = src + ",\n"
src = src + " fuzz_" + text(i) + ": function() {\n"
src = src + " " + body + "\n"
src = src + " }"
i = i + 1
}
src = src + "\n}\n"
return src
}
return {
generate: generate,
seed: seed
}

View File

@@ -171,13 +171,34 @@ function load_module(name, env) {
streamline_mod = load_module("streamline", boot_env)
use_cache['streamline'] = streamline_mod
// Lazy-loaded verify_ir module (loaded on first use via use_fn)
var _verify_ir_mod = null
// Run AST through mcode pipeline → register VM
function run_ast(name, ast, env) {
var compiled = mcode_mod(ast)
if (os._verify_ir) {
if (_verify_ir_mod == null) {
_verify_ir_mod = use_fn('verify_ir')
}
compiled._verify = true
compiled._verify_mod = _verify_ir_mod
}
var optimized = streamline_mod(compiled)
// Clean up verify properties before JSON encoding
if (optimized._verify) {
delete optimized._verify
delete optimized._verify_mod
}
return mach_eval_mcode(name, json.encode(optimized), env)
}
// Run AST through mcode pipeline WITHOUT optimization → register VM
function run_ast_noopt(name, ast, env) {
var compiled = mcode_mod(ast)
return mach_eval_mcode(name, json.encode(compiled), env)
}
// use() with ƿit pipeline for .cm modules
function use_fn(path) {
var file_path = null
@@ -277,13 +298,13 @@ if (args != null) {
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
analyze: analyze, run_ast_fn: run_ast, run_ast_noopt_fn: run_ast_noopt
})
} 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
analyze: analyze, run_ast_fn: run_ast, run_ast_noopt_fn: run_ast_noopt
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Hidden vars (os, actorsym, init, core_path, shop_path, analyze, run_ast_fn, json) come from env
// Hidden vars (os, actorsym, init, core_path, shop_path, analyze, run_ast_fn, run_ast_noopt_fn, json) come from env
// In actor spawn mode, also: nota, wota
var ACTORDATA = actorsym
var SYSYM = '__SYSTEM__'
@@ -214,6 +214,7 @@ os.global_shop_path = shop_path
os.$_ = $_
os.analyze = analyze
os.run_ast_fn = run_ast_fn
os.run_ast_noopt_fn = run_ast_noopt_fn
os.json = json
use_cache['core/json'] = json

File diff suppressed because it is too large Load Diff

200
ir_report.ce Normal file
View File

@@ -0,0 +1,200 @@
// ir_report.ce — optimizer flight recorder CLI
//
// Usage: ./cell --core . ir_report.ce [options] <file.cm|file.ce>
//
// Options:
// --summary Per-pass JSON summaries (default)
// --events Include rewrite events
// --types Include type deltas
// --ir-before=PASS Print canonical IR before PASS
// --ir-after=PASS Print canonical IR after PASS
// --ir-all Print canonical IR before/after every pass
// --full Everything (summary + events + types + ir-all)
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 ir_stats = use("ir_stats")
// --- Parse arguments ---
var filename = null
var opt_events = false
var opt_types = false
var opt_ir_before = null
var opt_ir_after = null
var opt_ir_all = false
var i = 0
var arg = null
var p = null
var e = null
var td = null
while (i < length(args)) {
arg = args[i]
if (arg == "--events") {
opt_events = true
} else if (arg == "--types") {
opt_types = true
} else if (arg == "--ir-all") {
opt_ir_all = true
} else if (arg == "--full") {
opt_events = true
opt_types = true
opt_ir_all = true
} else if (arg == "--summary") {
// default, no-op
} else if (starts_with(arg, "--ir-before=")) {
opt_ir_before = text(arg, 12)
} else if (starts_with(arg, "--ir-after=")) {
opt_ir_after = text(arg, 11)
} else if (!starts_with(arg, "--")) {
filename = arg
} else {
print(`unknown option: ${arg}\n`)
print("usage: cell --core . ir_report.ce [options] <file>\n")
$stop()
}
i = i + 1
}
if (filename == null) {
print("usage: cell --core . ir_report.ce [options] <file.cm|file.ce>\n")
print(" --summary per-pass JSON summaries (default)\n")
print(" --events include rewrite events\n")
print(" --types include type deltas\n")
print(" --ir-before=PASS print canonical IR before PASS\n")
print(" --ir-after=PASS print canonical IR after PASS\n")
print(" --ir-all print canonical IR before/after every pass\n")
print(" --full everything\n")
$stop()
}
// --- Compile ---
var src = text(fd.slurp(filename))
var tok = tokenize(src, filename)
var ast = parse(tok.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
// --- Determine which passes need IR snapshots ---
var need_snapshots = opt_ir_all || opt_ir_before != null || opt_ir_after != null
// Deep copy for before snapshot if we need IR printing
var before_ir = null
if (need_snapshots) {
before_ir = json.decode(json.encode(compiled))
}
// --- Set up log ---
var log = {
passes: [],
events: null,
type_deltas: null
}
if (opt_events) {
log.events = []
}
if (opt_types) {
log.type_deltas = []
}
// --- Run optimizer ---
var optimized = streamline(compiled, log)
// --- Output ---
var emit = function(obj) {
print(json.encode(obj))
print("\n")
}
// Pass summaries (always)
i = 0
while (i < length(log.passes)) {
p = log.passes[i]
p.type = "pass"
emit(p)
i = i + 1
}
// Rewrite events
if (opt_events && log.events != null) {
i = 0
while (i < length(log.events)) {
e = log.events[i]
e.type = "event"
emit(e)
i = i + 1
}
}
// Type deltas
if (opt_types && log.type_deltas != null) {
i = 0
while (i < length(log.type_deltas)) {
td = log.type_deltas[i]
td.type = "types"
emit(td)
i = i + 1
}
}
// --- Canonical IR printing ---
var print_ir = function(ir_obj, when_label, pass_name) {
var fname = null
var fi = 0
var func = null
if (ir_obj.main != null) {
fname = ir_obj.name != null ? ir_obj.name : "<main>"
emit({
type: "ir",
when: when_label,
pass: pass_name,
fn: fname,
text: ir_stats.canonical_ir(ir_obj.main, fname, {show_nops: true})
})
}
if (ir_obj.functions != null) {
fi = 0
while (fi < length(ir_obj.functions)) {
func = ir_obj.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
emit({
type: "ir",
when: when_label,
pass: pass_name,
fn: fname,
text: ir_stats.canonical_ir(func, fname, {show_nops: true})
})
fi = fi + 1
}
}
return null
}
if (need_snapshots) {
if (opt_ir_all) {
print_ir(before_ir, "before", "all")
print_ir(optimized, "after", "all")
} else {
if (opt_ir_before != null) {
print_ir(before_ir, "before", opt_ir_before)
}
if (opt_ir_after != null) {
print_ir(optimized, "after", opt_ir_after)
}
}
}
$stop()

357
ir_stats.cm Normal file
View File

@@ -0,0 +1,357 @@
// ir_stats.cm — IR statistics, fingerprinting, and canonical printing
//
// Usage: var ir_stats = use("ir_stats")
// ir_stats.detailed_stats(func)
// ir_stats.ir_fingerprint(func)
// ir_stats.canonical_ir(func, name, opts)
// ir_stats.type_snapshot(slot_types)
// ir_stats.type_delta(before_types, after_types)
var json = use("json")
// --- Category maps ---
var load_ops = {
load_field: true, load_index: true, load_dynamic: true,
get: true
}
var store_ops = {
store_field: true, store_index: true, store_dynamic: true,
set_var: true, put: true, push: true
}
var branch_ops = {
jump: true, jump_true: true, jump_false: true, jump_not_null: true
}
var call_ops = {
invoke: true, goinvoke: true
}
var guard_ops = {
is_int: true, is_text: true, is_num: true, is_bool: true,
is_null: true, is_array: true, is_func: true, is_record: true,
is_stone: true
}
var arith_ops = {
add_int: true, sub_int: true, mul_int: true, div_int: true, mod_int: true,
add_float: true, sub_float: true, mul_float: true, div_float: true, mod_float: true,
concat: true, neg_int: true, neg_float: true,
bitnot: true, bitand: true, bitor: true, bitxor: true,
shl: true, shr: true, ushr: true
}
var move_ops = {
move: true
}
var const_ops = {
int: true, true: true, false: true, null: true
}
var nop_reasons = {
tc: "tc",
bl: "bl",
mv: "mv",
dj: "dj",
ur: "ur"
}
var category_tag = function(op) {
if (guard_ops[op] == true) { return "guard" }
if (branch_ops[op] == true) { return "branch" }
if (load_ops[op] == true) { return "load" }
if (store_ops[op] == true) { return "store" }
if (call_ops[op] == true) { return "call" }
if (arith_ops[op] == true) { return "arith" }
if (move_ops[op] == true) { return "move" }
if (const_ops[op] == true) { return "const" }
return null
}
// --- detailed_stats ---
var detailed_stats = function(func) {
var instructions = func.instructions
var stats = {
instr: 0, nop: 0,
load: 0, store: 0, branch: 0, call: 0,
guard: 0, arith: 0, move: 0, const: 0,
label: 0, other: 0
}
var i = 0
var instr = null
var op = null
var num = 0
if (instructions == null) {
return stats
}
num = length(instructions)
while (i < num) {
instr = instructions[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
stats.nop = stats.nop + 1
stats.instr = stats.instr + 1
} else {
stats.label = stats.label + 1
}
} else if (is_array(instr)) {
stats.instr = stats.instr + 1
op = instr[0]
if (op == "access" && !is_number(instr[2]) && !is_logical(instr[2])) {
stats.load = stats.load + 1
} else if (op == "access") {
stats.const = stats.const + 1
} else if (load_ops[op] == true) {
stats.load = stats.load + 1
} else if (store_ops[op] == true) {
stats.store = stats.store + 1
} else if (branch_ops[op] == true) {
stats.branch = stats.branch + 1
} else if (call_ops[op] == true) {
stats.call = stats.call + 1
} else if (guard_ops[op] == true) {
stats.guard = stats.guard + 1
} else if (arith_ops[op] == true) {
stats.arith = stats.arith + 1
} else if (move_ops[op] == true) {
stats.move = stats.move + 1
} else if (const_ops[op] == true) {
stats.const = stats.const + 1
} else {
stats.other = stats.other + 1
}
}
i = i + 1
}
return stats
}
// --- ir_fingerprint ---
// djb2 hash computed over the JSON-encoded instructions
var djb2 = function(s) {
var chars = array(s)
var hash = 5381
var i = 0
var num = length(chars)
while (i < num) {
hash = ((hash * 33) + number(chars[i])) % 4294967296
i = i + 1
}
return text(hash, 16)
}
var ir_fingerprint = function(func) {
return djb2(json.encode(func.instructions))
}
// --- canonical_ir ---
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var nop_reason = function(s) {
// extract reason from _nop_XX_NNN
var parts = array(s, "_")
// parts: ["", "nop", "XX", "NNN"]
if (length(parts) >= 3) {
return parts[2]
}
return "?"
}
var fmt_operand = function(v) {
if (is_null(v)) {
return "null"
}
if (is_number(v)) {
return text(v)
}
if (is_text(v)) {
return `"${v}"`
}
if (is_logical(v)) {
if (v) { return "true" }
return "false"
}
return text(v)
}
var canonical_ir = function(func, name, opts) {
var instructions = func.instructions
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var show_nops = false
var show_types = false
var slot_types = null
var lines = []
var i = 0
var instr = null
var op = null
var n = 0
var parts = null
var j = 0
var idx_str = null
var op_str = null
var operands = null
var suffix = null
var tag = null
var typ = null
var reason = null
var num = 0
if (opts != null) {
if (opts.show_nops == true) { show_nops = true }
if (opts.show_types == true) { show_types = true }
if (opts.slot_types != null) { slot_types = opts.slot_types }
}
lines[] = `fn ${name != null ? name : "<anon>"} (args=${text(nr_args)}, slots=${text(nr_slots)})`
if (instructions == null) {
return text(lines, "\n")
}
num = length(instructions)
while (i < num) {
instr = instructions[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
if (show_nops) {
reason = nop_reason(instr)
idx_str = pad_right(`@${text(i)}`, 6)
lines[] = ` ${idx_str}--- nop (${reason}) ---`
}
} else {
lines[] = ` ${instr}:`
}
i = i + 1
continue
}
if (!is_array(instr)) {
i = i + 1
continue
}
op = instr[0]
n = length(instr)
parts = []
j = 1
while (j < n - 2) {
if (is_number(instr[j]) && op != "int" && !(op == "access" && j == 2)) {
parts[] = `s${text(instr[j])}`
} else {
parts[] = fmt_operand(instr[j])
}
j = j + 1
}
operands = text(parts, ", ")
idx_str = pad_right(`@${text(i)}`, 6)
op_str = pad_right(op, 16)
suffix = ""
tag = category_tag(op)
if (show_types && slot_types != null) {
// show type for dest slot if known
if (is_number(instr[1])) {
typ = slot_types[text(instr[1])]
if (typ != null) {
suffix = `; -> ${typ}`
}
}
if (tag != null) {
suffix = suffix + ` [${tag}]`
}
} else if (tag != null) {
suffix = suffix + `; [${tag}]`
}
if (length(suffix) > 0) {
lines[] = ` ${idx_str}${op_str}${operands} ${suffix}`
} else {
lines[] = ` ${idx_str}${op_str}${operands}`
}
i = i + 1
}
return text(lines, "\n")
}
// --- type_snapshot ---
var type_snapshot = function(slot_types) {
if (slot_types == null) {
return {}
}
return stone(record(slot_types))
}
// --- type_delta ---
var type_delta = function(before_types, after_types) {
var result = {
added: {},
removed: {},
strengthened: {},
weakened: {}
}
var bt = before_types != null ? before_types : {}
var at = after_types != null ? after_types : {}
var keys = null
var i = 0
var k = null
var bv = null
var av = null
// check after for added/changed
keys = array(at)
i = 0
while (i < length(keys)) {
k = keys[i]
av = at[k]
bv = bt[k]
if (bv == null) {
result.added[k] = av
} else if (bv != av) {
if (bv == "unknown" || (bv == "num" && (av == "int" || av == "float"))) {
result.strengthened[k] = {from: bv, to: av}
} else if (av == "unknown" || (av == "num" && (bv == "int" || bv == "float"))) {
result.weakened[k] = {from: bv, to: av}
} else {
result.strengthened[k] = {from: bv, to: av}
}
}
i = i + 1
}
// check before for removed
keys = array(bt)
i = 0
while (i < length(keys)) {
k = keys[i]
if (at[k] == null) {
result.removed[k] = bt[k]
}
i = i + 1
}
return result
}
return {
detailed_stats: detailed_stats,
ir_fingerprint: ir_fingerprint,
canonical_ir: canonical_ir,
type_snapshot: type_snapshot,
type_delta: type_delta,
category_tag: category_tag
}

View File

@@ -692,10 +692,10 @@ var parse = function(tokens, src, filename, tokenizer) {
if (tok.kind == "?") {
start = tok
advance()
then_expr = parse_expr()
then_expr = parse_assign_expr()
if (tok.kind == ":") advance()
else parse_error(tok, "expected ':' in ternary expression")
else_expr = parse_expr()
else_expr = parse_assign_expr()
node = ast_node("then", start)
node.expression = cond
node.then = then_expr

View File

@@ -8995,7 +8995,7 @@
["null", 26, 694, 7],
["setarg", 25, 0, 26, 694, 7],
["invoke", 25, 23, 694, 7],
["get", 28, 11, 1, 695, 19],
["get", 28, 7, 1, 695, 19],
["frame", 29, 28, 0, 695, 19],
["null", 30, 695, 19],
["setarg", 29, 0, 30, 695, 19],
@@ -9061,7 +9061,7 @@
["setarg", 45, 2, 42, 697, 12],
["invoke", 45, 43, 697, 12],
"if_end_1341",
["get", 48, 11, 1, 698, 19],
["get", 48, 7, 1, 698, 19],
["frame", 49, 48, 0, 698, 19],
["null", 50, 698, 19],
["setarg", 49, 0, 50, 698, 19],

View File

@@ -78,8 +78,7 @@ void cell_trace_sethook(cell_hook);
#define QJSCLASS(TYPE, ...)\
JSClassID js_##TYPE##_id;\
static void js_##TYPE##_finalizer(JSRuntime *rt, JSValue val){\
JSContext *js = JS_GetContext(rt);\
TYPE *n = JS_GetOpaque2(js, val, js_##TYPE##_id); \
TYPE *n = JS_GetOpaque(val, js_##TYPE##_id); \
TYPE##_free(rt,n);}\
static JSClassDef js_##TYPE##_class = {\
.class_name = #TYPE,\

View File

@@ -294,7 +294,6 @@ typedef enum JSErrorEnum {
#define __exception __attribute__ ((warn_unused_result))
/* Forward declaration for bytecode freeing */
struct JSFunctionBytecode;
#define JS_VALUE_GET_ARRAY(v) ((JSArray *)chase (v))
#define JS_VALUE_GET_OBJ(v) ((JSRecord *)chase (v))
@@ -302,15 +301,12 @@ struct JSFunctionBytecode;
#define JS_VALUE_GET_BLOB(v) ((JSBlob *)JS_VALUE_GET_PTR (v))
#define JS_VALUE_GET_FUNCTION(v) ((JSFunction *)chase (v))
#define JS_VALUE_GET_FRAME(v) ((JSFrame *)chase (v))
#define JS_VALUE_GET_CODE(v) ((JSFunctionBytecode *)JS_VALUE_GET_PTR (v))
#define JS_VALUE_GET_CODE(v) (JS_VALUE_GET_PTR (v))
#define JS_VALUE_GET_STRING(v) ((JSText *)chase (v))
/* Compatibility: JS_TAG_STRING is an alias for text type checks */
#define JS_TAG_STRING JS_TAG_STRING_IMM
/* JS_TAG_FUNCTION doesn't exist in new encoding - use JS_IsFunction check instead */
#define JS_TAG_FUNCTION 0xFE /* dummy value, never matches any tag */
/* JS_ThrowMemoryError is an alias for JS_ThrowOutOfMemory */
#define JS_ThrowMemoryError(ctx) JS_ThrowOutOfMemory(ctx)
@@ -319,8 +315,6 @@ static inline objhdr_t objhdr_set_cap56 (objhdr_t h, uint64_t cap) {
return (h & 0xFF) | ((cap & OBJHDR_CAP_MASK) << OBJHDR_CAP_SHIFT);
}
typedef enum OPCodeEnum OPCodeEnum;
/* ============================================================
Buddy Allocator for Actor Memory Blocks
============================================================ */
@@ -776,27 +770,6 @@ typedef struct JSFrame {
JSValue slots[]; /* [this][args][captured][locals][temps] */
} JSFrame;
/* Execution state returned by vm_execute_frame */
typedef enum {
VM_EXEC_NORMAL, /* Continue executing current frame */
VM_EXEC_RETURN, /* Frame returned, pop and resume caller */
VM_EXEC_CALL, /* Need to push new frame for call */
VM_EXEC_EXCEPTION, /* Exception thrown, unwind frames */
} VMExecState;
/* Call info for frame push */
typedef struct {
JSValue func_obj;
JSValue this_obj;
int argc;
JSValue *argv;
const uint8_t *ret_pc;
int ret_sp_offset;
int call_argc;
int call_has_this;
int is_tail_call;
} VMCallInfo;
static inline objhdr_t objhdr_set_s (objhdr_t h, bool s) {
return s ? (h | OBJHDR_S_MASK) : (h & ~OBJHDR_S_MASK);
}
@@ -1068,9 +1041,6 @@ struct JSContext {
// 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;
};
/* ============================================================
@@ -1237,11 +1207,6 @@ typedef struct JSFunction {
uint8_t cproto;
int16_t magic;
} cfunc;
struct {
struct JSFunctionBytecode *function_bytecode;
JSValue outer_frame; /* JSFrame JSValue, lexical parent for closures */
JSValue env_record; /* stone record, module environment */
} func;
struct {
JSCodeRegister *code; /* compiled register code (off-heap) */
JSValue env_record; /* stone record, module environment */
@@ -1250,150 +1215,11 @@ typedef struct JSFunction {
} u;
} JSFunction;
typedef struct JSClosureVar {
uint8_t is_local : 1;
uint8_t is_arg : 1;
uint8_t is_const : 1;
uint8_t is_lexical : 1;
uint8_t var_kind : 4; /* see JSVarKindEnum */
/* 8 bits available */
uint16_t var_idx; /* is_local = TRUE: index to a normal variable of the
parent function. otherwise: index to a closure
variable of the parent function */
JSValue var_name;
} JSClosureVar;
#define ARG_SCOPE_INDEX 1
#define ARG_SCOPE_END (-2)
typedef struct JSVarScope {
int parent; /* index into fd->scopes of the enclosing scope */
int first; /* index into fd->vars of the last variable in this scope */
} JSVarScope;
typedef enum {
/* XXX: add more variable kinds here instead of using bit fields */
JS_VAR_NORMAL,
JS_VAR_FUNCTION_DECL, /* lexical var with function declaration */
JS_VAR_NEW_FUNCTION_DECL, /* lexical var with async/generator
function declaration */
JS_VAR_CATCH,
JS_VAR_FUNCTION_NAME, /* function expression name */
} JSVarKindEnum;
/* XXX: could use a different structure in bytecode functions to save
memory */
typedef struct JSVarDef {
JSValue var_name;
/* index into fd->scopes of this variable lexical scope */
int scope_level;
/* during compilation:
- if scope_level = 0: scope in which the variable is defined
- if scope_level != 0: index into fd->vars of the next
variable in the same or enclosing lexical scope
in a bytecode function:
index into fd->vars of the next
variable in the same or enclosing lexical scope
*/
int scope_next;
uint8_t is_const : 1;
uint8_t is_lexical : 1;
uint8_t is_captured : 1;
uint8_t var_kind : 4; /* see JSVarKindEnum */
/* only used during compilation: function pool index for lexical
variables with var_kind =
JS_VAR_FUNCTION_DECL/JS_VAR_NEW_FUNCTION_DECL or scope level of
the definition of the 'var' variables (they have scope_level =
0) */
int func_pool_idx : 24; /* only used during compilation : index in
the constant pool for hoisted function
definition */
} JSVarDef;
/* for the encoding of the pc2line table */
#define PC2LINE_BASE (-1)
#define PC2LINE_RANGE 5
#define PC2LINE_OP_FIRST 1
#define PC2LINE_DIFF_PC_MAX ((255 - PC2LINE_OP_FIRST) / PC2LINE_RANGE)
typedef struct JSFunctionBytecode {
objhdr_t header; /* must come first */
uint8_t js_mode;
uint8_t has_prototype : 1; /* true if a prototype field is necessary */
uint8_t has_simple_parameter_list : 1;
uint8_t func_kind : 2;
uint8_t has_debug : 1;
uint8_t read_only_bytecode : 1;
uint8_t is_direct_or_indirect_eval
: 1; /* used by JS_GetScriptOrModuleName() */
/* XXX: 10 bits available */
uint8_t *byte_code_buf; /* (self pointer) */
int byte_code_len;
JSValue func_name;
JSVarDef *vardefs; /* arguments + local variables (arg_count + var_count)
(self pointer) */
JSClosureVar
*closure_var; /* list of variables in the closure (self pointer) */
uint16_t arg_count;
uint16_t var_count;
uint16_t defined_arg_count; /* for length function property */
uint16_t stack_size; /* maximum stack size */
JSValue *cpool; /* constant pool (self pointer) */
int cpool_count;
int closure_var_count;
struct {
/* debug info, move to separate structure to save memory? */
JSValue filename;
int source_len;
int pc2line_len;
uint8_t *pc2line_buf;
char *source;
} debug;
} JSFunctionBytecode;
/* New simplified compiled unit structure for Phase 1+ simplification.
Replaces JSFunctionBytecode with a simpler model:
- No closure machinery (uses outer_frame chain at runtime)
- Free variables resolved at link time against env + globals
- Nested functions stored as separate units in cpool */
typedef struct JSCompiledUnit {
objhdr_t header; /* must come first */
/* Bytecode (self pointer) */
uint8_t *byte_code_buf;
int byte_code_len;
/* Constants - strings, numbers, nested unit refs (self pointer) */
JSValue *cpool;
int cpool_count;
/* Stack requirements */
uint16_t local_count; /* total local slots (args + vars) */
uint16_t stack_size; /* operand stack depth */
/* Flags */
uint8_t has_debug : 1;
uint8_t read_only_bytecode : 1;
/* Debug info (optional - only present if has_debug) */
struct {
JSValue filename;
int source_len;
int pc2line_len;
uint8_t *pc2line_buf;
char *source;
} debug;
} JSCompiledUnit;
/* ============================================================
Context-Neutral Module Format (Phase 2+)
Struct definitions are in quickjs.h
============================================================ */
typedef struct JSProperty {
JSValue value;
} JSProperty;
#define JS_PROP_INITIAL_SIZE 2
#define JS_PROP_INITIAL_HASH_SIZE 4 /* must be a power of two */
#define JS_ARRAY_INITIAL_SIZE 2
@@ -1406,7 +1232,6 @@ typedef struct JSProperty {
#endif
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);
int JS_DeleteProperty (JSContext *ctx, JSValue obj, JSValue prop);
@@ -1421,17 +1246,9 @@ void js_dump_value_write (void *opaque, const char *buf, size_t len);
void js_regexp_finalizer (JSRuntime *rt, JSValue val);
JSValue js_new_function (JSContext *ctx, JSFunctionKind kind);
/* Forward declarations for intrinsics (now declared in quickjs.h) */
/* Forward declaration - helper to set cap in objhdr */
static inline objhdr_t objhdr_set_cap56 (objhdr_t h, uint64_t cap);
/* JS_VALUE_GET_STRING is an alias for getting JSText from a string value */
/* Note: Uses chase() for GC safety - already defined at line 293 */
/* JS_ThrowMemoryError is an alias for JS_ThrowOutOfMemory */
#define JS_ThrowMemoryError(ctx) JS_ThrowOutOfMemory(ctx)
/* GC-SAFE: JS_SetPropertyInternal: same as JS_SetProperty but doesn't check stone.
Internal use only. May trigger GC if record needs to resize. */
@@ -1611,118 +1428,6 @@ static inline __exception int js_poll_interrupts (JSContext *ctx) {
}
}
/* === Token enum (shared by parser, tokenizer, AST) === */
enum {
TOK_NUMBER = -128,
TOK_STRING,
TOK_TEMPLATE,
TOK_IDENT,
TOK_REGEXP,
/* warning: order matters (see js_parse_assign_expr) */
TOK_MUL_ASSIGN,
TOK_DIV_ASSIGN,
TOK_MOD_ASSIGN,
TOK_PLUS_ASSIGN,
TOK_MINUS_ASSIGN,
TOK_SHL_ASSIGN,
TOK_SAR_ASSIGN,
TOK_SHR_ASSIGN,
TOK_AND_ASSIGN,
TOK_XOR_ASSIGN,
TOK_OR_ASSIGN,
TOK_POW_ASSIGN,
TOK_LAND_ASSIGN,
TOK_LOR_ASSIGN,
TOK_DEC,
TOK_INC,
TOK_SHL,
TOK_SAR,
TOK_SHR,
TOK_LT,
TOK_LTE,
TOK_GT,
TOK_GTE,
TOK_EQ,
TOK_STRICT_EQ,
TOK_NEQ,
TOK_STRICT_NEQ,
TOK_LAND,
TOK_LOR,
TOK_POW,
TOK_ARROW,
TOK_ERROR,
TOK_PRIVATE_NAME,
TOK_EOF,
/* whitespace/comment tokens for tokenizer */
TOK_COMMENT,
TOK_NEWLINE,
TOK_SPACE,
/* keywords: WARNING: same order as atoms */
TOK_NULL, /* must be first */
TOK_FALSE,
TOK_TRUE,
TOK_IF,
TOK_ELSE,
TOK_RETURN,
TOK_GO,
TOK_VAR,
TOK_DEF,
TOK_THIS,
TOK_DELETE,
TOK_IN,
TOK_DO,
TOK_WHILE,
TOK_FOR,
TOK_BREAK,
TOK_CONTINUE,
TOK_DISRUPT,
TOK_DISRUPTION,
TOK_FUNCTION,
TOK_DEBUGGER,
TOK_WITH,
/* FutureReservedWord */
TOK_CLASS,
TOK_CONST,
TOK_ENUM,
TOK_EXPORT,
TOK_EXTENDS,
TOK_IMPORT,
TOK_SUPER,
/* FutureReservedWords when parsing strict mode code */
TOK_IMPLEMENTS,
TOK_INTERFACE,
TOK_LET,
TOK_PRIVATE,
TOK_PROTECTED,
TOK_PUBLIC,
TOK_STATIC,
TOK_YIELD,
TOK_AWAIT, /* must be last */
TOK_OF, /* only used for js_parse_skip_parens_token() */
};
#define TOK_FIRST_KEYWORD TOK_NULL
#define TOK_LAST_KEYWORD TOK_AWAIT
/* unicode code points */
#define CP_NBSP 0x00a0
#define CP_BOM 0xfeff
#define CP_LS 0x2028
#define CP_PS 0x2029
/* === Line/column cache === */
typedef struct {
/* last source position */
const uint8_t *ptr;
int line_num;
int col_num;
const uint8_t *buf_start;
} GetLineColCache;
/* === PPretext (parser pretext, system-malloc, used by cell_js.c parser) === */
typedef struct PPretext {
uint32_t *data;
@@ -1730,8 +1435,6 @@ typedef struct PPretext {
int cap;
} PPretext;
#define JS_CALL_FLAG_COPY_ARGV (1 << 1)
extern JSClassID js_class_id_alloc;
/* === Forward declarations for functions split across modules === */
@@ -1739,16 +1442,8 @@ extern JSClassID js_class_id_alloc;
/* runtime.c — line/column, GC, and VM dispatch */
int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size);
JSValue JS_CallInternal (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv, int flags);
int get_line_col (int *pcol_num, const uint8_t *buf, size_t len);
int get_line_col_cached (GetLineColCache *s, int *pcol_num, const uint8_t *ptr);
/* runtime.c exports */
JSValue JS_ThrowStackOverflow (JSContext *ctx);
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);
JSValue JS_GetPropertyValue (JSContext *ctx, JSValue this_obj, JSValue prop);
__exception int JS_CopyDataProperties (JSContext *ctx, JSValue target, JSValue source, JSValue excluded, BOOL setprop);
int js_string_compare_value (JSContext *ctx, JSValue op1, JSValue op2, BOOL eq_only);
int js_string_compare_value_nocase (JSContext *ctx, JSValue op1, JSValue op2);
JSValue js_regexp_constructor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
@@ -1757,11 +1452,6 @@ 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);
__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);
__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);
JSText *pretext_putc (JSContext *ctx, JSText *s, uint32_t c);
JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v);
@@ -1787,19 +1477,6 @@ uint64_t get_text_hash (JSText *text);
void pack_utf32_to_words (const uint32_t *utf32, uint32_t len, uint64_t *packed);
int text_equal (JSText *a, const uint64_t *packed_b, uint32_t len_b);
static inline JSValue *get_upvalue_ptr (JSValue frame_val, int depth, int slot) {
if (JS_IsNull(frame_val)) return NULL;
JSFrame *frame = JS_VALUE_GET_FRAME(frame_val);
while (depth > 0) {
JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
frame_val = fn->u.func.outer_frame;
if (JS_IsNull(frame_val)) return NULL;
frame = JS_VALUE_GET_FRAME(frame_val);
depth--;
}
return &frame->slots[slot];
}
void print_backtrace (JSContext *ctx, const char *filename, int line_num, int col_num);
JSValue JS_ThrowError2 (JSContext *ctx, JSErrorEnum error_num, const char *fmt, va_list ap, BOOL add_backtrace);
JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end);
@@ -1811,10 +1488,6 @@ PPretext *ppretext_append_jsvalue (PPretext *p, JSValue str);
PPretext *ppretext_append_int (PPretext *p, int n);
JSValue js_atof (JSContext *ctx, const char *str, const char **pp, int radix, int flags);
/* Defines from runtime section needed by cell_js.c */
#define DEFINE_GLOBAL_LEX_VAR (1 << 7)
#define DEFINE_GLOBAL_FUNC_VAR (1 << 6)
#define ATOD_INT_ONLY (1 << 0)
/* accept Oo and Ob prefixes in addition to 0x prefix if radix = 0 */
#define ATOD_ACCEPT_BIN_OCT (1 << 2)
@@ -1831,14 +1504,6 @@ JSValue js_atof (JSContext *ctx, const char *str, const char **pp, int radix, in
/* accept -0x1 */
#define ATOD_ACCEPT_PREFIX_AFTER_SIGN (1 << 10)
#define GLOBAL_VAR_OFFSET 0x40000000
#define ARGUMENT_VAR_OFFSET 0x20000000
/* Inline functions from runtime section needed by cell_js.c */
static inline void js_dbuf_init (JSContext *ctx, DynBuf *s) {
dbuf_init2 (s, ctx->rt, NULL);
}
static inline int to_digit (int c) {
if (c >= '0' && c <= '9') return c - '0';
else if (c >= 'A' && c <= 'Z') return c - 'A' + 10;
@@ -1846,54 +1511,6 @@ static inline int to_digit (int c) {
else return 36;
}
static inline void dbuf_put_leb128 (DynBuf *s, uint32_t v) {
uint32_t a;
for (;;) {
a = v & 0x7f;
v >>= 7;
if (v != 0) {
dbuf_putc (s, a | 0x80);
} else {
dbuf_putc (s, a);
break;
}
}
}
static inline void dbuf_put_sleb128 (DynBuf *s, int32_t v1) {
uint32_t v = v1;
dbuf_put_leb128 (s, (2 * v) ^ -(v >> 31));
}
static inline int get_leb128 (uint32_t *pval, const uint8_t *buf, const uint8_t *buf_end) {
const uint8_t *ptr = buf;
uint32_t v, a, i;
v = 0;
for (i = 0; i < 5; i++) {
if (unlikely (ptr >= buf_end)) break;
a = *ptr++;
v |= (a & 0x7f) << (i * 7);
if (!(a & 0x80)) {
*pval = v;
return ptr - buf;
}
}
*pval = 0;
return -1;
}
static inline int get_sleb128 (int32_t *pval, const uint8_t *buf, const uint8_t *buf_end) {
int ret;
uint32_t val;
ret = get_leb128 (&val, buf, buf_end);
if (ret < 0) {
*pval = 0;
return -1;
}
*pval = (val >> 1) ^ -(val & 1);
return ret;
}
no_inline int js_realloc_array (JSContext *ctx, void **parray, int elem_size, int *psize, int req_size);
static inline int js_resize_array (JSContext *ctx, void **parray, int elem_size, int *psize, int req_size) {
if (unlikely (req_size > *psize))
@@ -1908,7 +1525,6 @@ JSValue js_key_from_string (JSContext *ctx, JSValue val);
/* mach.c exports */
JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame);
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);

View File

@@ -92,7 +92,6 @@ static inline int objhdr_s (objhdr_t h) { return (h & OBJHDR_S_MASK) != 0; }
typedef struct JSRuntime JSRuntime; // the entire VM
typedef struct JSContext JSContext; // Each actor
typedef struct JSClass JSClass;
typedef struct JSFunctionBytecode JSFunctionBytecode;
typedef uint32_t JSClassID;
/* Forward declaration - JSGCRef moved after JSValue definition */
@@ -284,13 +283,6 @@ JS_IsShortFloat (JSValue v) {
#define JS_TRUE ((JSValue)(JS_TAG_BOOL | (1 << 5)))
#define JS_EXCEPTION ((JSValue)JS_TAG_EXCEPTION)
/* flags for object properties - simplified model:
- No per-property writable/configurable (use stone() for immutability)
- Text keys are enumerable, object keys are not */
#define JS_PROP_TMASK (3 << 4) /* mask for NORMAL, VARREF */
#define JS_PROP_NORMAL (0 << 4)
#define JS_PROP_VARREF (2 << 4) /* used internally for closures */
#ifndef JS_DEFAULT_STACK_SIZE
#define JS_DEFAULT_STACK_SIZE (1024 * 1024)
#endif
@@ -313,16 +305,10 @@ JSRuntime *JS_NewRuntime (void);
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);
/* should be called when changing thread to update the stack top value
used to check stack overflow. */
void JS_UpdateStackTop (JSContext *ctx);
void JS_FreeRuntime (JSRuntime *rt);
JS_BOOL JS_IsLiveObject (JSRuntime *rt, JSValue obj);
JSContext *JS_NewContext (JSRuntime *rt);
void JS_FreeContext (JSContext *s);
JSContext *JS_DupContext (JSContext *ctx);
JSContext *JS_GetContext (JSRuntime *rt);
void *JS_GetContextOpaque (JSContext *ctx);
void JS_SetContextOpaque (JSContext *ctx, void *opaque);
JSRuntime *JS_GetRuntime (JSContext *ctx);
@@ -331,24 +317,6 @@ JSValue JS_GetClassProto (JSContext *ctx, JSClassID class_id);
JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size);
typedef struct JSMemoryUsage {
int64_t malloc_size, malloc_limit, memory_used_size;
int64_t malloc_count;
int64_t memory_used_count;
int64_t str_count, str_size;
int64_t obj_count, obj_size;
int64_t prop_count, prop_size;
int64_t shape_count, shape_size;
int64_t js_func_count, js_func_size, js_func_code_size;
int64_t js_func_pc2line_count, js_func_pc2line_size;
int64_t c_func_count, array_count;
int64_t fast_array_count, fast_array_elements;
int64_t binary_object_count, binary_object_size;
} JSMemoryUsage;
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 JSValue JSClassCall (JSContext *ctx, JSValue func_obj,
JSValue this_val, int argc,
@@ -523,7 +491,6 @@ static inline JSValue JS_NewString (JSContext *ctx, const char *str) {
return JS_NewStringLen (ctx, str, strlen (str));
}
JSValue JS_ToString (JSContext *ctx, JSValue val);
JSValue JS_ToPropertyKey (JSContext *ctx, JSValue val);
const char *JS_ToCStringLen2 (JSContext *ctx, size_t *plen, JSValue val1, JS_BOOL cesu8);
static inline const char * JS_ToCStringLen (JSContext *ctx, size_t *plen, JSValue val1) {
return JS_ToCStringLen2 (ctx, plen, val1, 0);
@@ -946,17 +913,6 @@ typedef void (*js_hook) (JSContext *, int type, js_debug *dbg, void *user);
#define JS_HOOK_GC 8
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);
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);
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);
/* Memory allocation functions (bump allocator) */
void *js_malloc (JSContext *ctx, size_t size);
void *js_mallocz (JSContext *ctx, size_t size);
@@ -974,12 +930,6 @@ struct cJSON;
/* Compiled bytecode (context-free, serializable) */
typedef struct MachCode MachCode;
/* Compile AST cJSON tree to context-free MachCode. */
MachCode *JS_CompileMachTree(struct cJSON *ast);
/* Compile AST JSON string to context-free MachCode. */
MachCode *JS_CompileMach(const char *ast_json);
/* Free a compiled MachCode tree. */
void JS_FreeMachCode(MachCode *mc);

View File

@@ -60,18 +60,6 @@ JS_BOOL JS_IsFunction (JSValue v) {
return JS_IsGCObject(v) && objhdr_type(*chase(v)) == OBJ_FUNCTION;
}
JS_BOOL JS_IsCode (JSValue v) {
return JS_IsGCObject(v) && objhdr_type(*chase(v)) == OBJ_CODE;
}
JS_BOOL JS_IsForwarded (JSValue v) {
return JS_IsGCObject(v) && objhdr_type(*chase(v)) == OBJ_FORWARD;
}
JS_BOOL JS_IsFrame (JSValue v) {
return JS_IsGCObject(v) && objhdr_type(*chase(v)) == OBJ_FRAME;
}
JS_BOOL JS_IsBlob (JSValue v) {
return JS_IsGCObject(v) && objhdr_type(*chase(v)) == OBJ_BLOB;
}
@@ -1058,7 +1046,7 @@ JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *f
continue;
}
if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_BLOB && type != OBJ_CODE && type != OBJ_FRAME) {
if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_BLOB && type != OBJ_FRAME) {
fprintf (stderr, "gc_copy_value: invalid object type %d at %p (hdr=0x%llx)\n", type, ptr, (unsigned long long)hdr);
fprintf (stderr, " This may be an interior pointer or corrupt root\n");
fflush (stderr);
@@ -1150,9 +1138,6 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro
case OBJ_BLOB:
/* No internal references to scan */
break;
case OBJ_CODE:
/* Dead code: old bytecode objects no longer created */
break;
case OBJ_FRAME: {
/* JSFrame - scan function, caller, and slots */
JSFrame *frame = (JSFrame *)ptr;
@@ -1173,14 +1158,6 @@ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *fro
}
}
/* gc_scan_parser_cpool - no-op now that the old compiler is removed.
current_parse_fd is always NULL without cell_js.c. */
void gc_scan_parser_cpool (JSContext *ctx, uint8_t *from_base, uint8_t *from_end,
uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) {
(void)ctx; (void)from_base; (void)from_end;
(void)to_base; (void)to_free; (void)to_end;
}
/* Cheney copying GC - collect garbage and compact live objects
allow_grow: if true, grow heap when recovery is poor
alloc_size: the allocation that triggered GC — used to size the new block */
@@ -1282,11 +1259,6 @@ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) {
ref->val = gc_copy_value (ctx, ref->val, from_base, from_end, to_base, &to_free, to_end);
}
/* Scan parser's cpool (if parsing is in progress) */
if (ctx->current_parse_fd) {
gc_scan_parser_cpool (ctx, from_base, from_end, to_base, &to_free, to_end);
}
/* Cheney scan: scan copied objects to find more references */
uint8_t *scan = to_base;
#ifdef DUMP_GC_DETAIL
@@ -1530,14 +1502,6 @@ void JS_FreeContext (JSContext *ctx) {
JSRuntime *rt = ctx->rt;
int i;
#ifdef DUMP_MEM
{
JSMemoryUsage stats;
JS_ComputeMemoryUsage (rt, &stats);
JS_DumpMemoryUsage (stdout, &stats, rt);
}
#endif
for (i = 0; i < JS_NATIVE_ERROR_COUNT; i++) {
}
for (i = 0; i < ctx->class_count; i++) {
@@ -2295,18 +2259,6 @@ JSValue JS_NewObject (JSContext *ctx) {
}
// TODO: needs reworked
int js_method_set_properties (JSContext *ctx, JSValue func_obj, JSValue name, int flags, JSValue home_obj) {
(void)ctx;
(void)flags;
(void)home_obj;
if (!JS_IsFunction (func_obj)) return -1;
JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj);
/* name is now JSValue text */
if (JS_IsText (name)) { f->name = name; }
return 0;
}
/* Note: at least 'length' arguments will be readable in 'argv' */
static JSValue JS_NewCFunction3 (JSContext *ctx, JSCFunction *func, const char *name, int length, JSCFunctionEnum cproto, int magic) {
JSValue func_obj;
@@ -2454,7 +2406,7 @@ static int js_intrinsic_array_set (JSContext *ctx, JSValue *arr_ptr, word_t idx,
return 0;
}
/* Allocate intrinsic function (JS_TAG_FUNCTION) */
/* Allocate a new function object */
JSValue js_new_function (JSContext *ctx, JSFunctionKind kind) {
JSFunction *func = js_mallocz (ctx, sizeof (JSFunction));
if (!func) return JS_EXCEPTION;
@@ -2462,25 +2414,9 @@ JSValue js_new_function (JSContext *ctx, JSFunctionKind kind) {
func->kind = kind;
func->name = JS_NULL;
func->length = 0;
/* Initialize closure fields for bytecode functions */
if (kind == JS_FUNC_KIND_BYTECODE) {
func->u.func.outer_frame = JS_NULL;
func->u.func.env_record = JS_NULL;
}
return JS_MKPTR (func);
}
/* Get pointer to an upvalue in outer scope frame chain.
depth=0 is current frame, depth=1 is immediate outer, etc.
Returns NULL if depth exceeds the frame chain.
frame_val is a JSValue containing a JSFrame pointer. */
void JS_ComputeMemoryUsage (JSRuntime *rt, JSMemoryUsage *s) {
}
void JS_DumpMemoryUsage (FILE *fp, const JSMemoryUsage *s, JSRuntime *rt) {
}
/* WARNING: obj is freed */
JSValue JS_Throw (JSContext *ctx, JSValue obj) {
ctx->current_exception = obj;
@@ -2494,66 +2430,10 @@ JSValue JS_GetException (JSContext *ctx) {
return val;
}
JS_BOOL
JS_HasException (JSContext *ctx) {
JS_BOOL JS_HasException (JSContext *ctx) {
return !JS_IsNull (ctx->current_exception);
}
/* get_line_col — compute line and column from a byte offset */
int get_line_col (int *pcol_num, const uint8_t *buf, size_t len) {
int line_num, col_num, c;
size_t i;
line_num = 0;
col_num = 0;
for (i = 0; i < len; i++) {
c = buf[i];
if (c == '\n') {
line_num++;
col_num = 0;
} else if (c < 0x80 || c >= 0xc0) {
col_num++;
}
}
*pcol_num = col_num;
return line_num;
}
int get_line_col_cached (GetLineColCache *s, int *pcol_num, const uint8_t *ptr) {
int line_num, col_num;
if (ptr >= s->ptr) {
line_num = get_line_col (&col_num, s->ptr, ptr - s->ptr);
if (line_num == 0) {
s->col_num += col_num;
} else {
s->line_num += line_num;
s->col_num = col_num;
}
} else {
line_num = get_line_col (&col_num, ptr, s->ptr - ptr);
if (line_num == 0) {
s->col_num -= col_num;
} else {
const uint8_t *p;
s->line_num -= line_num;
/* find the absolute column position */
col_num = 0;
for (p = ptr - 1; p >= s->buf_start; p--) {
if (*p == '\n') {
break;
} else if (*p < 0x80 || *p >= 0xc0) {
col_num++;
}
}
s->col_num = col_num;
}
}
s->ptr = ptr;
*pcol_num = s->col_num;
return s->line_num;
}
/* in order to avoid executing arbitrary code during the stack trace
generation, we only look at simple 'name' properties containing a
string. */
@@ -3141,39 +3021,6 @@ int JS_DeletePropertyKey (JSContext *ctx, JSValue obj, JSValue key) {
Note: makes assumption about the bit pattern of the flags
*/
/* return TRUE if 'obj' has a non empty 'name' string */
static BOOL js_object_has_name (JSContext *ctx, JSValue obj) {
if (JS_VALUE_GET_TAG (obj) != JS_TAG_PTR) return FALSE;
JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (obj);
JSValue name_key = MIST_TryNewImmediateASCII ("name", 4);
int slot = rec_find_slot (rec, name_key);
if (slot <= 0) return FALSE;
JSValue val = rec->slots[slot].val;
if (!JS_IsText (val)) return TRUE; /* has name but it's not a string = truthy */
return (js_string_value_len (val) != 0);
}
int JS_DefineObjectName (JSContext *ctx, JSValue obj, JSValue name) {
if (!JS_IsNull (name) && JS_IsGCObject (obj)
&& !js_object_has_name (ctx, obj)) {
JSValue name_key = MIST_TryNewImmediateASCII ("name", 4);
if (JS_SetPropertyInternal (ctx, obj, name_key, name)
< 0)
return -1;
}
return 0;
}
int JS_DefineObjectNameComputed (JSContext *ctx, JSValue obj, JSValue str) {
if (JS_IsGCObject (obj) && !js_object_has_name (ctx, obj)) {
JSValue name_key = MIST_TryNewImmediateASCII ("name", 4);
if (JS_SetPropertyInternal (ctx, obj, name_key, str)
< 0)
return -1;
}
return 0;
}
int JS_DeleteProperty (JSContext *ctx, JSValue obj, JSValue prop) {
JSRecord *rec;
int slot;
@@ -4084,9 +3931,6 @@ __maybe_unused void JS_DumpGCObject (JSRuntime *rt,
JS_DumpObject (rt, (JSRecord *)p);
} else {
switch (objhdr_type (*p)) {
case OBJ_CODE:
printf ("[function bytecode]");
break;
case OBJ_ARRAY:
printf ("[array]");
break;
@@ -4152,73 +3996,10 @@ BOOL JS_StrictEq (JSContext *ctx, JSValue op1, JSValue op2) {
return js_strict_eq (ctx, op1, op2);
}
__exception int js_operator_delete (JSContext *ctx, JSValue *sp) {
JSValue op1, op2;
int ret;
op1 = sp[-2];
op2 = sp[-1];
ret = JS_DeletePropertyKey (ctx, op1, op2);
if (unlikely (ret < 0)) return -1;
sp[-2] = JS_NewBool (ctx, ret);
return 0;
}
/* XXX: not 100% compatible, but mozilla seems to use a similar
implementation to ensure that caller in non strict mode does not
throw (ES5 compatibility) */
static JSValue js_throw_type_error (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {
return JS_ThrowTypeError (ctx, "invalid property access");
}
__exception int JS_CopyDataProperties (JSContext *ctx, JSValue target, JSValue source, JSValue excluded, BOOL setprop) {
JSValue keys, key, val;
uint32_t i, key_count;
int ret;
if (JS_VALUE_GET_TAG (source) != JS_TAG_PTR) return 0;
/* Get all string keys from source */
keys = JS_GetOwnPropertyNames (ctx, source);
if (JS_IsException (keys)) return -1;
if (js_get_length32 (ctx, &key_count, keys)) {
return -1;
}
for (i = 0; i < key_count; i++) {
key = JS_GetPropertyNumber (ctx, keys, i);
if (JS_IsException (key)) goto exception;
/* Check if key is excluded */
if (JS_VALUE_GET_TAG (excluded) == JS_TAG_PTR) {
/* Check if key exists in excluded object */
JSValue test = JS_GetProperty (ctx, excluded, key);
if (!JS_IsNull (test) && !JS_IsException (test)) {
continue;
}
}
/* Get property value from source */
val = JS_GetProperty (ctx, source, key);
if (JS_IsException (val)) {
goto exception;
}
/* Set property on target */
ret = JS_SetProperty (ctx, target, key, val);
if (ret < 0) goto exception;
}
return 0;
exception:
return -1;
}
JSValue js_call_c_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv) {
JSCFunctionType func;
JSFunction *f;
@@ -4361,12 +4142,6 @@ JSValue JS_Call (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, J
/*******************************************************************/
/* runtime functions & objects */
int check_function (JSContext *ctx, JSValue obj) {
if (likely (JS_IsFunction (obj))) return 0;
JS_ThrowTypeError (ctx, "not a function");
return -1;
}
int check_exception_free (JSContext *ctx, JSValue obj) {
return JS_IsException (obj);
}
@@ -4463,7 +4238,7 @@ __exception int js_get_length32 (JSContext *ctx, uint32_t *pres, JSValue obj) {
return 0;
}
if (tag == JS_TAG_FUNCTION) {
if (JS_IsFunction (obj)) {
JSFunction *fn = JS_VALUE_GET_FUNCTION (obj);
*pres = fn->length;
return 0;
@@ -6562,22 +6337,13 @@ static JSValue js_cell_text (JSContext *ctx, JSValue this_val, int argc, JSValue
/* Handle function - return source or native stub */
if (JS_IsFunction (arg)) {
JSFunction *fn = JS_VALUE_GET_FUNCTION (arg);
if (fn->kind == JS_FUNC_KIND_BYTECODE) {
JSFunctionBytecode *b = fn->u.func.function_bytecode;
if (b->has_debug && b->debug.source)
return JS_NewStringLen (ctx, b->debug.source, b->debug.source_len);
}
const char *pref = "function ";
const char *suff = "() {\n [native code]\n}";
const char *name = "";
const char *name_cstr = NULL;
if (fn->kind == JS_FUNC_KIND_BYTECODE) {
JSFunctionBytecode *fb = fn->u.func.function_bytecode;
name_cstr = JS_ToCString (ctx, fb->func_name);
if (name_cstr) name = name_cstr;
} else if (!JS_IsNull (fn->name)) {
if (!JS_IsNull (fn->name)) {
name_cstr = JS_ToCString (ctx, fn->name);
if (name_cstr) name = name_cstr;
}
@@ -7627,259 +7393,6 @@ static JSValue js_stacktrace (JSContext *ctx, JSValue this_val, int argc, JSValu
return JS_NULL;
}
/* dump_bytecode_opcodes and JS_DumpFunctionBytecode removed — old stack VM deleted */
#if 0 /* dead code — old bytecode dump */
static void dump_bytecode_opcodes_DEAD (JSContext *ctx, JSFunctionBytecode *b) {
const uint8_t *tab = b->byte_code_buf;
int len = b->byte_code_len;
const JSValue *cpool = b->cpool;
uint32_t cpool_count = b->cpool_count;
const JSVarDef *vars = b->vardefs ? b->vardefs + b->arg_count : NULL;
int var_count = b->var_count;
int pos = 0;
while (pos < len) {
int op = tab[pos];
if (op >= OP_COUNT) {
printf (" %5d: <invalid opcode 0x%02x>\n", pos, op);
pos++;
continue;
}
const JSOpCode *oi = &short_opcode_info (op);
int size = oi->size;
if (pos + size > len) {
printf (" %5d: <truncated opcode 0x%02x>\n", pos, op);
break;
}
printf (" %5d: %s", pos, dump_opcode_names[op]);
pos++;
switch (oi->fmt) {
case OP_FMT_none_int:
printf (" %d", op - OP_push_0);
break;
case OP_FMT_npopx:
printf (" %d", op - OP_call0);
break;
case OP_FMT_u8:
printf (" %u", get_u8 (tab + pos));
break;
case OP_FMT_i8:
printf (" %d", get_i8 (tab + pos));
break;
case OP_FMT_u16:
case OP_FMT_npop:
printf (" %u", get_u16 (tab + pos));
break;
case OP_FMT_npop_u16:
printf (" %u,%u", get_u16 (tab + pos), get_u16 (tab + pos + 2));
break;
case OP_FMT_i16:
printf (" %d", get_i16 (tab + pos));
break;
case OP_FMT_i32:
printf (" %d", get_i32 (tab + pos));
break;
case OP_FMT_u32:
printf (" %u", get_u32 (tab + pos));
break;
case OP_FMT_label8:
printf (" ->%d", pos + get_i8 (tab + pos));
break;
case OP_FMT_label16:
printf (" ->%d", pos + get_i16 (tab + pos));
break;
case OP_FMT_label:
printf (" ->%u", pos + get_u32 (tab + pos));
break;
case OP_FMT_label_u16:
printf (" ->%u,%u", pos + get_u32 (tab + pos), get_u16 (tab + pos + 4));
break;
case OP_FMT_const8: {
uint32_t idx = get_u8 (tab + pos);
printf (" [%u]", idx);
if (idx < cpool_count) {
printf (": ");
JS_PrintValue (ctx, js_dump_value_write, stdout, cpool[idx], NULL);
}
break;
}
case OP_FMT_const: {
uint32_t idx = get_u32 (tab + pos);
printf (" [%u]", idx);
if (idx < cpool_count) {
printf (": ");
JS_PrintValue (ctx, js_dump_value_write, stdout, cpool[idx], NULL);
}
break;
}
case OP_FMT_key: {
uint32_t idx = get_u32 (tab + pos);
printf (" [%u]", idx);
if (idx < cpool_count) {
printf (": ");
JS_PrintValue (ctx, js_dump_value_write, stdout, cpool[idx], NULL);
}
break;
}
case OP_FMT_key_u8: {
uint32_t idx = get_u32 (tab + pos);
printf (" [%u],%d", idx, get_u8 (tab + pos + 4));
if (idx < cpool_count) {
printf (": ");
JS_PrintValue (ctx, js_dump_value_write, stdout, cpool[idx], NULL);
}
break;
}
case OP_FMT_key_u16: {
uint32_t idx = get_u32 (tab + pos);
printf (" [%u],%d", idx, get_u16 (tab + pos + 4));
if (idx < cpool_count) {
printf (": ");
JS_PrintValue (ctx, js_dump_value_write, stdout, cpool[idx], NULL);
}
break;
}
case OP_FMT_none_loc:
printf (" loc%d", (op - OP_get_loc0) % 4);
break;
case OP_FMT_loc8: {
int idx = get_u8 (tab + pos);
printf (" loc%d", idx);
if (vars && idx < var_count) {
char buf[KEY_GET_STR_BUF_SIZE];
printf (": %s", JS_KeyGetStr (ctx, buf, sizeof(buf), vars[idx].var_name));
}
break;
}
case OP_FMT_loc: {
int idx = get_u16 (tab + pos);
printf (" loc%d", idx);
if (vars && idx < var_count) {
char buf[KEY_GET_STR_BUF_SIZE];
printf (": %s", JS_KeyGetStr (ctx, buf, sizeof(buf), vars[idx].var_name));
}
break;
}
case OP_FMT_none_arg:
printf (" arg%d", (op - OP_get_arg0) % 4);
break;
case OP_FMT_arg:
printf (" arg%d", get_u16 (tab + pos));
break;
default:
break;
}
printf ("\n");
pos += size - 1; /* -1 because we already incremented pos after reading opcode */
}
}
void JS_DumpFunctionBytecode (JSContext *ctx, JSValue func_val) {
JSFunctionBytecode *b = NULL;
if (!JS_IsPtr (func_val)) {
printf ("JS_DumpFunctionBytecode: not a pointer value\n");
return;
}
/* Get the object header to check type */
void *ptr = JS_VALUE_GET_PTR (func_val);
objhdr_t hdr = *(objhdr_t *)ptr;
uint8_t type = objhdr_type (hdr);
if (type == OBJ_FUNCTION) {
/* It's a JSFunction - extract bytecode */
JSFunction *fn = (JSFunction *)ptr;
if (fn->kind != JS_FUNC_KIND_BYTECODE) {
printf ("JS_DumpFunctionBytecode: not a bytecode function (kind=%d)\n", fn->kind);
return;
}
b = fn->u.func.function_bytecode;
} else if (type == OBJ_CODE) {
/* It's raw bytecode from js_create_function */
b = (JSFunctionBytecode *)ptr;
} else {
printf ("JS_DumpFunctionBytecode: not a function or bytecode (type=%d)\n", type);
return;
}
if (!b) {
printf ("JS_DumpFunctionBytecode: no bytecode\n");
return;
}
char buf[KEY_GET_STR_BUF_SIZE];
printf ("=== Bytecode Dump ===\n");
/* Function name */
const char *fname = JS_KeyGetStr (ctx, buf, sizeof(buf), b->func_name);
printf ("Function: %s\n", fname ? fname : "<anonymous>");
/* Debug info */
if (b->has_debug && !JS_IsNull (b->debug.filename)) {
printf ("File: %s\n", JS_KeyGetStr (ctx, buf, sizeof(buf), b->debug.filename));
}
/* Basic stats */
printf ("Args: %d, Vars: %d, Stack: %d\n", b->arg_count, b->var_count, b->stack_size);
printf ("Bytecode length: %d bytes\n", b->byte_code_len);
/* Arguments */
if (b->arg_count > 0 && b->vardefs) {
printf ("\nArguments:\n");
for (int i = 0; i < b->arg_count; i++) {
printf (" %d: %s\n", i, JS_KeyGetStr (ctx, buf, sizeof(buf), b->vardefs[i].var_name));
}
}
/* Local variables */
if (b->var_count > 0 && b->vardefs) {
printf ("\nLocal variables:\n");
for (int i = 0; i < b->var_count; i++) {
JSVarDef *vd = &b->vardefs[b->arg_count + i];
const char *kind = vd->is_const ? "const" : vd->is_lexical ? "let" : "var";
printf (" %d: %s %s", i, kind, JS_KeyGetStr (ctx, buf, sizeof(buf), vd->var_name));
if (vd->scope_level) printf (" [scope:%d]", vd->scope_level);
printf ("\n");
}
}
/* Closure variables */
if (b->closure_var_count > 0) {
printf ("\nClosure variables:\n");
for (int i = 0; i < b->closure_var_count; i++) {
JSClosureVar *cv = &b->closure_var[i];
printf (" %d: %s (%s:%s%d)\n", i,
JS_KeyGetStr (ctx, buf, sizeof(buf), cv->var_name),
cv->is_local ? "local" : "parent",
cv->is_arg ? "arg" : "loc",
cv->var_idx);
}
}
/* Constant pool */
if (b->cpool_count > 0) {
printf ("\nConstant pool (%d entries):\n", b->cpool_count);
for (uint32_t i = 0; i < b->cpool_count; i++) {
printf (" [%u]: ", i);
JS_PrintValue (ctx, js_dump_value_write, stdout, b->cpool[i], NULL);
printf ("\n");
}
}
/* Bytecode instructions */
printf ("\nBytecode:\n");
dump_bytecode_opcodes (ctx, b);
printf ("=== End Bytecode Dump ===\n");
}
#endif /* dead code */
/* ----------------------------------------------------------------------------
* array function and sub-functions
* ----------------------------------------------------------------------------
@@ -11108,78 +10621,6 @@ void js_debug_sethook (JSContext *ctx, js_hook hook, int type, void *user) {
ctx->trace_data = user;
}
uint32_t js_debugger_stack_depth (JSContext *ctx) {
return ctx->stack_depth;
}
/* return an array of the actual fn objects as a backtrace */
JSValue js_debugger_backtrace_fns (JSContext *ctx) {
/* JSValue ret = JS_NewArray (ctx);
uint32_t stack_index = 0;
for (sf = ctx->current_stack_frame; sf != NULL; sf = sf->prev_frame) {
uint32_t id = stack_index++;
JS_SetPropertyNumber (ctx, ret, id, sf->cur_func);
}
return ret;
*/
}
/* build a backtrace of info for printing */
JSValue js_debugger_build_backtrace (JSContext *ctx) {
/*
JSStackFrame *sf;
JSValue ret = JS_NewArray (ctx);
uint32_t stack_index = 0;
for (sf = ctx->current_stack_frame; sf != NULL; sf = sf->prev_frame) {
JSValue current_frame = JS_NewObject (ctx);
uint32_t id = stack_index++;
JS_SetPropertyStr (ctx, current_frame, "id", JS_NewUint32 (ctx, id));
const char *func_name_str = get_func_name (ctx, sf->cur_func);
if (!func_name_str || func_name_str[0] == '\0')
JS_SetPropertyStr (ctx, current_frame, "name", JS_NewString (ctx, "<anonymous>"));
else
JS_SetPropertyStr (ctx, current_frame, "name", JS_NewString (ctx, func_name_str));
JS_FreeCString (ctx, func_name_str);
JS_SetPropertyNumber (ctx, ret, id, current_frame);
}
return ret;
*/
}
JSValue js_debugger_fn_info (JSContext *ctx, JSValue fn) {
(void)fn;
return JS_NewObject (ctx);
}
JSValue js_debugger_fn_bytecode (JSContext *ctx, JSValue fn) {
(void)fn;
return JS_NewArray (ctx);
}
JSValue js_debugger_local_variables (JSContext *ctx, int stack_index) {
(void)stack_index;
return JS_NewObject (ctx);
}
void js_debugger_set_closure_variable (JSContext *ctx, JSValue fn, JSValue var_name, JSValue val) {
/* TODO: Reimplement using outer_frame mechanism if debugging is needed */
(void)ctx; (void)fn; (void)var_name; (void)val;
}
JSValue js_debugger_closure_variables (JSContext *ctx, JSValue fn) {
/* TODO: Reimplement using outer_frame mechanism if debugging is needed */
(void)fn;
return JS_NewObject (ctx);
}
void *js_debugger_val_address (JSContext *ctx, JSValue val) {
return JS_VALUE_GET_PTR (val);
}
/* ============================================================================
* Cell Script Module: json
* Provides json.encode() and json.decode() using pure C implementation

View File

@@ -1,3 +1,7 @@
// streamline.ce — run the full compile + optimize pipeline, output JSON
//
// Usage: ./cell --core . streamline.ce <file.ce|file.cm>
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")

View File

@@ -1,7 +1,27 @@
// streamline.cm — mcode IR optimizer
// Composed of independent passes, each a separate function.
// Optional `log` parameter enables structured observability.
var streamline = function(ir, log) {
// IR verification support — verifier module passed via ir._verify_mod
// (streamline's use() is use_basic from bootstrap, which can't load source)
var verify_fn = null
var verifier = null
if (ir._verify && ir._verify_mod) {
verifier = ir._verify_mod
verify_fn = function(func, pass_name) {
var errs = verifier.verify_all(func, pass_name)
var i = 0
while (i < length(errs)) {
print(`[verify_ir] ${errs[i]}\n`)
i = i + 1
}
if (length(errs) > 0) {
print(`[verify_ir] ${text(length(errs))} errors after ${pass_name}\n`)
}
}
}
var streamline = function(ir) {
// Type constants
var T_UNKNOWN = "unknown"
var T_INT = "int"
@@ -44,6 +64,50 @@ var streamline = function(ir) {
is_record: T_RECORD
}
// --- Logging support ---
var ir_stats = null
var time_mod = null
if (log != null) {
ir_stats = use("ir_stats")
time_mod = use("time")
}
var run_pass = function(func, pass_name, pass_fn) {
var before = null
var after = null
var t0 = null
var t1 = null
var ms = null
var changed = false
var result = null
if (log == null) {
return pass_fn()
}
before = ir_stats.detailed_stats(func)
t0 = time_mod.number()
result = pass_fn()
t1 = time_mod.number()
after = ir_stats.detailed_stats(func)
ms = (t1 - t0) * 1000
changed = before.instr != after.instr ||
before.nop != after.nop ||
before.guard != after.guard
log.passes[] = {
pass: pass_name,
fn: func.name,
ms: ms,
before: before,
after: after,
changed: changed,
changes: {
nops_added: after.nop - before.nop,
guards_removed: before.guard - after.guard
}
}
return result
}
// --- Shared helpers ---
var access_value_type = function(val) {
@@ -244,7 +308,7 @@ var streamline = function(ir) {
// Eliminates is_<type>/jump pairs when type is known.
// Reduces load_dynamic/store_dynamic to field/index forms.
// =========================================================
var eliminate_type_checks = function(func, param_types) {
var eliminate_type_checks = function(func, param_types, log) {
var instructions = func.instructions
var nr_args = func.nr_args != null ? func.nr_args : 0
var has_params = false
@@ -263,9 +327,15 @@ var streamline = function(ir) {
var target_label = null
var src_known = null
var jlen = 0
var events = null
var old_op = null
if (instructions == null || length(instructions) == 0) {
return null
return {}
}
if (log != null && log.events != null) {
events = log.events
}
num_instr = length(instructions)
@@ -319,6 +389,17 @@ var streamline = function(ir) {
instructions[i] = "_nop_tc_" + text(nc)
nc = nc + 1
instructions[i + 1] = "_nop_tc_" + text(nc)
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "known_type_eliminates_guard",
at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]],
why: {slot: src, known_type: slot_types[text(src)], checked_type: checked_type}
}
}
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
@@ -330,6 +411,17 @@ var streamline = function(ir) {
instructions[i] = "_nop_tc_" + text(nc)
nc = nc + 1
instructions[i + 1] = "_nop_tc_" + text(nc)
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "num_subsumes_int_float",
at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]],
why: {slot: src, known_type: src_known, checked_type: checked_type}
}
}
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
@@ -338,6 +430,17 @@ var streamline = function(ir) {
instructions[i] = "_nop_tc_" + text(nc)
jlen = length(next)
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "incompatible_type_forces_jump",
at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]],
why: {slot: src, known_type: src_known, checked_type: checked_type}
}
}
slot_types[text(dest)] = T_UNKNOWN
i = i + 2
continue
@@ -355,6 +458,17 @@ var streamline = function(ir) {
instructions[i] = "_nop_tc_" + text(nc)
jlen = length(next)
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "known_type_eliminates_guard",
at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]],
why: {slot: src, known_type: slot_types[text(src)], checked_type: checked_type}
}
}
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
@@ -366,6 +480,17 @@ var streamline = function(ir) {
instructions[i] = "_nop_tc_" + text(nc)
jlen = length(next)
instructions[i + 1] = ["jump", target_label, next[jlen - 2], next[jlen - 1]]
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "num_subsumes_int_float",
at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]],
why: {slot: src, known_type: src_known, checked_type: checked_type}
}
}
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
@@ -374,6 +499,17 @@ var streamline = function(ir) {
instructions[i] = "_nop_tc_" + text(nc)
nc = nc + 1
instructions[i + 1] = "_nop_tc_" + text(nc)
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "incompatible_type_forces_jump",
at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]],
why: {slot: src, known_type: src_known, checked_type: checked_type}
}
}
slot_types[text(dest)] = T_BOOL
i = i + 2
continue
@@ -391,20 +527,58 @@ var streamline = function(ir) {
// Dynamic access reduction
if (op == "load_dynamic") {
old_op = op
if (slot_is(slot_types, instr[3], T_TEXT)) {
instr[0] = "load_field"
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "dynamic_to_field",
at: i, before: old_op, after: instr[0],
why: {slot: instr[3], known_type: slot_types[text(instr[3])]}
}
}
} else if (slot_is(slot_types, instr[3], T_INT)) {
instr[0] = "load_index"
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "dynamic_to_index",
at: i, before: old_op, after: instr[0],
why: {slot: instr[3], known_type: slot_types[text(instr[3])]}
}
}
}
slot_types[text(instr[1])] = T_UNKNOWN
i = i + 1
continue
}
if (op == "store_dynamic") {
old_op = op
if (slot_is(slot_types, instr[3], T_TEXT)) {
instr[0] = "store_field"
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "dynamic_to_field",
at: i, before: old_op, after: instr[0],
why: {slot: instr[3], known_type: slot_types[text(instr[3])]}
}
}
} else if (slot_is(slot_types, instr[3], T_INT)) {
instr[0] = "store_index"
if (events != null) {
events[] = {
event: "rewrite",
pass: "eliminate_type_checks",
rule: "dynamic_to_index",
at: i, before: old_op, after: instr[0],
why: {slot: instr[3], known_type: slot_types[text(instr[3])]}
}
}
}
i = i + 1
continue
@@ -414,7 +588,7 @@ var streamline = function(ir) {
i = i + 1
}
return null
return slot_types
}
// =========================================================
@@ -422,7 +596,7 @@ var streamline = function(ir) {
// Tracks known constant values. Rewrites identity ops to
// moves or constants. Folds same-slot comparisons.
// =========================================================
var simplify_algebra = function(func) {
var simplify_algebra = function(func, log) {
var instructions = func.instructions
var num_instr = 0
var slot_values = null
@@ -434,11 +608,17 @@ var streamline = function(ir) {
var v2 = null
var v3 = null
var sv = null
var events = null
var rule = null
if (instructions == null || length(instructions) == 0) {
return null
}
if (log != null && log.events != null) {
events = log.events
}
num_instr = length(instructions)
slot_values = {}
@@ -481,7 +661,16 @@ var streamline = function(ir) {
if (op == "add_int" || op == "sub_int") {
v3 = slot_values[text(instr[3])]
if (v3 == 0) {
rule = op == "add_int" ? "add_zero" : "sub_zero"
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: rule, at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 0}
}
}
i = i + 1
continue
}
@@ -489,6 +678,14 @@ var streamline = function(ir) {
v2 = slot_values[text(instr[2])]
if (v2 == 0) {
instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "add_zero", at: i,
before: instr, after: instructions[i],
why: {slot: instr[2], value: 0}
}
}
i = i + 1
continue
}
@@ -498,16 +695,40 @@ var streamline = function(ir) {
v2 = slot_values[text(instr[2])]
if (v3 == 1) {
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 1}
}
}
i = i + 1
continue
}
if (v2 == 1) {
instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[2], value: 1}
}
}
i = i + 1
continue
}
if (v3 == 0 || v2 == 0) {
instructions[i] = ["int", instr[1], 0, instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_zero", at: i,
before: instr, after: instructions[i],
why: {value: 0}
}
}
slot_values[text(instr[1])] = 0
i = i + 1
continue
@@ -516,6 +737,14 @@ var streamline = function(ir) {
v3 = slot_values[text(instr[3])]
if (v3 == 1) {
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "div_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 1}
}
}
i = i + 1
continue
}
@@ -526,7 +755,16 @@ var streamline = function(ir) {
if (op == "add_float" || op == "sub_float") {
v3 = slot_values[text(instr[3])]
if (v3 == 0) {
rule = op == "add_float" ? "add_zero" : "sub_zero"
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: rule, at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 0}
}
}
i = i + 1
continue
}
@@ -534,6 +772,14 @@ var streamline = function(ir) {
v2 = slot_values[text(instr[2])]
if (v2 == 0) {
instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "add_zero", at: i,
before: instr, after: instructions[i],
why: {slot: instr[2], value: 0}
}
}
i = i + 1
continue
}
@@ -543,11 +789,27 @@ var streamline = function(ir) {
v2 = slot_values[text(instr[2])]
if (v3 == 1) {
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 1}
}
}
i = i + 1
continue
}
if (v2 == 1) {
instructions[i] = ["move", instr[1], instr[3], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "mul_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[2], value: 1}
}
}
i = i + 1
continue
}
@@ -555,6 +817,14 @@ var streamline = function(ir) {
v3 = slot_values[text(instr[3])]
if (v3 == 1) {
instructions[i] = ["move", instr[1], instr[2], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "div_one", at: i,
before: instr, after: instructions[i],
why: {slot: instr[3], value: 1}
}
}
i = i + 1
continue
}
@@ -567,6 +837,14 @@ var streamline = function(ir) {
op == "le_int" || op == "le_float" || op == "le_text" ||
op == "ge_int" || op == "ge_float" || op == "ge_text") {
instructions[i] = ["true", instr[1], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "self_eq", at: i,
before: instr, after: instructions[i],
why: {op: op, slot: instr[2]}
}
}
slot_values[text(instr[1])] = true
i = i + 1
continue
@@ -576,6 +854,14 @@ var streamline = function(ir) {
op == "lt_int" || op == "lt_float" || op == "lt_text" ||
op == "gt_int" || op == "gt_float" || op == "gt_text") {
instructions[i] = ["false", instr[1], instr[ilen - 2], instr[ilen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_algebra",
rule: "self_ne", at: i,
before: instr, after: instructions[i],
why: {op: op, slot: instr[2]}
}
}
slot_values[text(instr[1])] = false
i = i + 1
continue
@@ -605,7 +891,7 @@ var streamline = function(ir) {
// =========================================================
// Pass: simplify_booleans — not+jump fusion, double-not
// =========================================================
var simplify_booleans = function(func) {
var simplify_booleans = function(func, log) {
var instructions = func.instructions
var num_instr = 0
var nc = 0
@@ -614,11 +900,16 @@ var streamline = function(ir) {
var next = null
var next_op = null
var nlen = 0
var events = null
if (instructions == null || length(instructions) == 0) {
return null
}
if (log != null && log.events != null) {
events = log.events
}
num_instr = length(instructions)
i = 0
while (i < num_instr) {
@@ -642,6 +933,14 @@ var streamline = function(ir) {
nc = nc + 1
instructions[i] = "_nop_bl_" + text(nc)
instructions[i + 1] = ["jump_true", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_booleans",
rule: "not_jump_false_fusion", at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]]
}
}
i = i + 2
continue
}
@@ -651,6 +950,14 @@ var streamline = function(ir) {
nc = nc + 1
instructions[i] = "_nop_bl_" + text(nc)
instructions[i + 1] = ["jump_false", instr[2], next[2], next[nlen - 2], next[nlen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_booleans",
rule: "not_jump_true_fusion", at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]]
}
}
i = i + 2
continue
}
@@ -660,6 +967,14 @@ var streamline = function(ir) {
nc = nc + 1
instructions[i] = "_nop_bl_" + text(nc)
instructions[i + 1] = ["move", next[1], instr[2], next[nlen - 2], next[nlen - 1]]
if (events != null) {
events[] = {
event: "rewrite", pass: "simplify_booleans",
rule: "double_not", at: i,
before: [instr, next],
after: [instructions[i], instructions[i + 1]]
}
}
i = i + 2
continue
}
@@ -673,17 +988,22 @@ var streamline = function(ir) {
// =========================================================
// Pass: eliminate_moves — move a, a → nop
// =========================================================
var eliminate_moves = function(func) {
var eliminate_moves = function(func, log) {
var instructions = func.instructions
var num_instr = 0
var nc = 0
var i = 0
var instr = null
var events = null
if (instructions == null || length(instructions) == 0) {
return null
}
if (log != null && log.events != null) {
events = log.events
}
num_instr = length(instructions)
i = 0
while (i < num_instr) {
@@ -691,6 +1011,13 @@ var streamline = function(ir) {
if (is_array(instr) && instr[0] == "move" && instr[1] == instr[2]) {
nc = nc + 1
instructions[i] = "_nop_mv_" + text(nc)
if (events != null) {
events[] = {
event: "rewrite", pass: "eliminate_moves",
rule: "self_move", at: i,
before: instr, after: instructions[i]
}
}
}
i = i + 1
}
@@ -738,7 +1065,7 @@ var streamline = function(ir) {
// =========================================================
// Pass: eliminate_dead_jumps — jump to next label → nop
// =========================================================
var eliminate_dead_jumps = function(func) {
var eliminate_dead_jumps = function(func, log) {
var instructions = func.instructions
var num_instr = 0
var nc = 0
@@ -747,11 +1074,16 @@ var streamline = function(ir) {
var instr = null
var target_label = null
var peek = null
var events = null
if (instructions == null || length(instructions) == 0) {
return null
}
if (log != null && log.events != null) {
events = log.events
}
num_instr = length(instructions)
i = 0
while (i < num_instr) {
@@ -765,6 +1097,14 @@ var streamline = function(ir) {
if (peek == target_label) {
nc = nc + 1
instructions[i] = "_nop_dj_" + text(nc)
if (events != null) {
events[] = {
event: "rewrite", pass: "eliminate_dead_jumps",
rule: "jump_to_next", at: i,
before: instr, after: instructions[i],
why: {label: target_label}
}
}
}
break
}
@@ -783,27 +1123,55 @@ var streamline = function(ir) {
// =========================================================
// Compose all passes
// =========================================================
var optimize_function = function(func) {
var optimize_function = function(func, log) {
var param_types = null
var slot_types = null
if (func.instructions == null || length(func.instructions) == 0) {
return null
}
param_types = infer_param_types(func)
eliminate_type_checks(func, param_types)
simplify_algebra(func)
simplify_booleans(func)
eliminate_moves(func)
run_pass(func, "infer_param_types", function() {
param_types = infer_param_types(func)
return param_types
})
if (verify_fn) verify_fn(func, "after infer_param_types")
run_pass(func, "eliminate_type_checks", function() {
slot_types = eliminate_type_checks(func, param_types, log)
return slot_types
})
if (verify_fn) verify_fn(func, "after eliminate_type_checks")
if (log != null && log.type_deltas != null && slot_types != null) {
log.type_deltas[] = {
fn: func.name,
param_types: param_types,
slot_types: slot_types
}
}
run_pass(func, "simplify_algebra", function() {
return simplify_algebra(func, log)
})
if (verify_fn) verify_fn(func, "after simplify_algebra")
run_pass(func, "simplify_booleans", function() {
return simplify_booleans(func, log)
})
if (verify_fn) verify_fn(func, "after simplify_booleans")
run_pass(func, "eliminate_moves", function() {
return eliminate_moves(func, log)
})
if (verify_fn) verify_fn(func, "after eliminate_moves")
// NOTE: eliminate_unreachable is disabled because disruption handler
// code is placed after return/disrupt without label boundaries.
// Re-enable once mcode.cm emits labels for handler entry points.
//eliminate_unreachable(func)
eliminate_dead_jumps(func)
run_pass(func, "eliminate_dead_jumps", function() {
return eliminate_dead_jumps(func, log)
})
if (verify_fn) verify_fn(func, "after eliminate_dead_jumps")
return null
}
// Process main function
if (ir.main != null) {
optimize_function(ir.main)
optimize_function(ir.main, log)
}
// Process all sub-functions
@@ -811,7 +1179,7 @@ var streamline = function(ir) {
if (ir.functions != null) {
fi = 0
while (fi < length(ir.functions)) {
optimize_function(ir.functions[fi])
optimize_function(ir.functions[fi], log)
fi = fi + 1
}
}

File diff suppressed because it is too large Load Diff

116
test.ce
View File

@@ -14,6 +14,13 @@ var target_pkg = null // null = current package
var target_test = null // null = all tests, otherwise specific test file
var all_pkgs = false
var gc_after_each_test = false
var verify_ir = false
var diff_mode = false
var os_ref = use('os')
var analyze = os_ref.analyze
var run_ast_fn = os_ref.run_ast_fn
var run_ast_noopt_fn = os_ref.run_ast_noopt_fn
// Actor test support
def ACTOR_TEST_TIMEOUT = 30000 // 30 seconds timeout for actor tests
@@ -46,6 +53,11 @@ function get_current_package_name() {
// cell test package <name> - run all tests for named package
// cell test package <name> <test> - run specific test in named package
// cell test package all - run all tests from all packages
//
// Flags:
// -g - run GC after each test
// --verify - enable IR verification (validates mcode IR after each optimizer pass)
// --diff - enable differential testing (run each test optimized and unoptimized, compare results)
function parse_args() {
var cleaned_args = []
@@ -57,6 +69,10 @@ function parse_args() {
for (i = 0; i < length(_args); i++) {
if (_args[i] == '-g') {
gc_after_each_test = true
} else if (_args[i] == '--verify') {
verify_ir = true
} else if (_args[i] == '--diff') {
diff_mode = true
} else {
push(cleaned_args, _args[i])
}
@@ -162,6 +178,77 @@ if (!parse_args()) {
return
}
// Enable IR verification if requested
if (verify_ir) {
os_ref._verify_ir = true
log.console('IR verification enabled')
}
if (diff_mode && !run_ast_noopt_fn) {
log.console('error: --diff requires run_ast_noopt_fn (rebuild bootstrap)')
$stop()
return
}
// Diff mode: deep comparison helper
function values_equal(a, b) {
var i = 0
if (a == b) return true
if (is_null(a) && is_null(b)) return true
if (is_null(a) || is_null(b)) return false
if (is_array(a) && is_array(b)) {
if (length(a) != length(b)) return false
i = 0
while (i < length(a)) {
if (!values_equal(a[i], b[i])) return false
i = i + 1
}
return true
}
return false
}
function describe(val) {
if (is_null(val)) return "null"
if (is_text(val)) return `"${val}"`
if (is_number(val)) return text(val)
if (is_logical(val)) return text(val)
if (is_function(val)) return "<function>"
return "<other>"
}
// Diff mode: run a test function through noopt and compare
var diff_mismatches = 0
function diff_check(test_name, file_path, opt_fn, noopt_fn) {
if (!diff_mode) return
var opt_result = null
var noopt_result = null
var opt_err = null
var noopt_err = null
var _opt = function() {
opt_result = opt_fn()
} disruption {
opt_err = "disrupted"
}
_opt()
var _noopt = function() {
noopt_result = noopt_fn()
} disruption {
noopt_err = "disrupted"
}
_noopt()
if (opt_err != noopt_err) {
log.console(` DIFF ${test_name}: disruption mismatch opt=${opt_err != null ? opt_err : "ok"} noopt=${noopt_err != null ? noopt_err : "ok"}`)
diff_mismatches = diff_mismatches + 1
} else if (!values_equal(opt_result, noopt_result)) {
log.console(` DIFF ${test_name}: result mismatch opt=${describe(opt_result)} noopt=${describe(noopt_result)}`)
diff_mismatches = diff_mismatches + 1
}
}
function ensure_dir(path) {
if (fd.is_dir(path)) return true
@@ -320,9 +407,26 @@ function run_tests(package_name, specific_test) {
_load_file = function() {
var test_mod = null
var test_mod_noopt = null
var use_pkg = package_name ? package_name : fd.realpath('.')
var _load_noopt = null
test_mod = shop.use(mod_path, use_pkg)
// Load noopt version for diff mode
if (diff_mode) {
_load_noopt = function() {
var src_path = prefix + '/' + f
var src = text(fd.slurp(src_path))
var ast = analyze(src, src_path)
test_mod_noopt = run_ast_noopt_fn(mod_path + '_noopt', ast, {
use: function(path) { return shop.use(path, use_pkg) }
})
} disruption {
log.console(` DIFF: failed to load noopt module for ${f}`)
}
_load_noopt()
}
var tests = []
var j = 0
var t = null
@@ -406,6 +510,12 @@ function run_tests(package_name, specific_test) {
}
}
_run_one()
// Differential check: compare opt vs noopt
if (diff_mode && test_mod_noopt && is_object(test_mod_noopt) && is_function(test_mod_noopt[t.name])) {
diff_check(t.name, f, t.fn, test_mod_noopt[t.name])
}
end_time = time.number()
test_entry.duration_ns = round((end_time - start_time) * 1000000000)
@@ -635,6 +745,9 @@ function finalize_results() {
log.console(`----------------------------------------`)
log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`)
if (diff_mode) {
log.console(`Diff mismatches: ${text(diff_mismatches)}`)
}
generate_reports(totals)
$stop()
@@ -652,6 +765,9 @@ if (length(all_actor_tests) == 0) {
log.console(`----------------------------------------`)
log.console(`Tests: ${totals.passed} passed, ${totals.failed} failed, ${totals.total} total`)
if (diff_mode) {
log.console(`Diff mismatches: ${text(diff_mismatches)}`)
}
} else {
$delay(check_timeouts, 1000)
}

467
verify_ir.cm Normal file
View File

@@ -0,0 +1,467 @@
// verify_ir.cm — validates mcode IR structure after optimizer passes
// Used to catch structural bugs introduced by optimization.
// Operand positions that are slots for each opcode.
// Positions are 0-indexed from the first operand (after the opcode),
// excluding the trailing line/col pair.
var slot_positions = {
// Constant loaders — only dest
access: [0],
int: [0],
true: [0],
false: [0],
null: [0],
function: [0],
array: [0],
record: [0],
// Unary — dest, src
move: [0, 1],
not: [0, 1],
neg_int: [0, 1],
neg_float: [0, 1],
bitnot: [0, 1],
length: [0, 1],
typeof: [0, 1],
is_int: [0, 1],
is_text: [0, 1],
is_num: [0, 1],
is_bool: [0, 1],
is_null: [0, 1],
is_array: [0, 1],
is_func: [0, 1],
is_record: [0, 1],
is_stone: [0, 1],
is_identical: [0, 1, 2],
// Binary arithmetic/comparison — dest, src1, src2
add: [0, 1, 2],
subtract: [0, 1, 2],
multiply: [0, 1, 2],
divide: [0, 1, 2],
modulo: [0, 1, 2],
pow: [0, 1, 2],
add_int: [0, 1, 2],
sub_int: [0, 1, 2],
mul_int: [0, 1, 2],
div_int: [0, 1, 2],
mod_int: [0, 1, 2],
add_float: [0, 1, 2],
sub_float: [0, 1, 2],
mul_float: [0, 1, 2],
div_float: [0, 1, 2],
mod_float: [0, 1, 2],
eq: [0, 1, 2],
ne: [0, 1, 2],
lt: [0, 1, 2],
le: [0, 1, 2],
gt: [0, 1, 2],
ge: [0, 1, 2],
eq_int: [0, 1, 2],
ne_int: [0, 1, 2],
lt_int: [0, 1, 2],
gt_int: [0, 1, 2],
le_int: [0, 1, 2],
ge_int: [0, 1, 2],
eq_float: [0, 1, 2],
ne_float: [0, 1, 2],
lt_float: [0, 1, 2],
gt_float: [0, 1, 2],
le_float: [0, 1, 2],
ge_float: [0, 1, 2],
eq_text: [0, 1, 2],
ne_text: [0, 1, 2],
lt_text: [0, 1, 2],
gt_text: [0, 1, 2],
le_text: [0, 1, 2],
ge_text: [0, 1, 2],
eq_bool: [0, 1, 2],
ne_bool: [0, 1, 2],
eq_tol: [0, 1, 2],
ne_tol: [0, 1, 2],
concat: [0, 1, 2],
and: [0, 1, 2],
or: [0, 1, 2],
bitand: [0, 1, 2],
bitor: [0, 1, 2],
bitxor: [0, 1, 2],
shl: [0, 1, 2],
shr: [0, 1, 2],
ushr: [0, 1, 2],
in: [0, 1, 2],
// Element access — all operands are slots
load_index: [0, 1, 2],
load_dynamic: [0, 1, 2],
load_field: [0, 1],
store_index: [0, 1, 2],
store_dynamic: [0, 1, 2],
store_field: [0, 1],
// Push/pop
push: [0, 1],
pop: [0, 1],
get: [0, 1],
// Control flow — slot positions only
return: [0],
jump: [],
jump_true: [0],
jump_false: [0],
jump_not_null: [0],
disrupt: [],
// Invoke
invoke: [0, 1],
goinvoke: [0],
frame: [0, 1],
setarg: [0, 2]
}
// Opcodes that write to their first operand (position 0)
var writes_dest = {
access: true, int: true, true: true, false: true, null: true,
function: true, array: true, record: true,
move: true, not: true, neg_int: true, neg_float: true, bitnot: true,
length: true, typeof: true,
is_int: true, is_text: true, is_num: true,
is_bool: true, is_null: true, is_array: true,
is_func: true, is_record: true, is_stone: true, is_identical: true,
add: true, subtract: true, multiply: true, divide: true,
modulo: true, pow: true,
add_int: true, sub_int: true, mul_int: true, div_int: true, mod_int: true,
add_float: true, sub_float: true, mul_float: true, div_float: true, mod_float: true,
eq: true, ne: true, lt: true, le: true, gt: true, ge: true,
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,
concat: true, and: true, or: true,
bitand: true, bitor: true, bitxor: true, shl: true, shr: true, ushr: true,
in: true,
load_index: true, load_dynamic: true, load_field: true,
pop: true, get: true,
invoke: true
}
// Opcodes where invoke writes to position 1 (result slot), not position 0
var invoke_result_pos = 1
// Jump opcodes and the position of their label operand (0-indexed from first operand)
var jump_label_pos = {
jump: 0,
jump_true: 1,
jump_false: 1,
jump_not_null: 1
}
// --- Check: slot_bounds ---
// Verifies every slot operand is in 0..nr_slots-1.
var check_slot_bounds = function(func) {
var instructions = func.instructions
var nr_slots = func.nr_slots
var errors = []
var i = 0
var instr = null
var op = null
var positions = null
var j = 0
var pos = null
var val = null
if (instructions == null) return errors
while (i < length(instructions)) {
instr = instructions[i]
if (is_array(instr)) {
op = instr[0]
positions = slot_positions[op]
if (positions != null) {
j = 0
while (j < length(positions)) {
pos = positions[j] + 1
if (pos < length(instr) - 2) {
val = instr[pos]
if (is_number(val) && (val < 0 || val >= nr_slots)) {
push(errors, `slot_bounds: instr ${text(i)} op=${op} slot[${text(positions[j])}]=${text(val)} out of range 0..${text(nr_slots - 1)}`)
}
}
j = j + 1
}
}
}
i = i + 1
}
return errors
}
// --- Check: jump_targets ---
// Verifies every jump target label exists in the instruction stream.
var check_jump_targets = function(func) {
var instructions = func.instructions
var errors = []
var labels = {}
var i = 0
var instr = null
var op = null
var label_pos = null
var target = null
if (instructions == null) return errors
// Collect all labels (non-nop strings)
while (i < length(instructions)) {
instr = instructions[i]
if (is_text(instr) && !starts_with(instr, "_nop_")) {
labels[instr] = true
}
i = i + 1
}
// Check jump targets
i = 0
while (i < length(instructions)) {
instr = instructions[i]
if (is_array(instr)) {
op = instr[0]
label_pos = jump_label_pos[op]
if (label_pos != null) {
target = instr[label_pos + 1]
if (is_text(target) && labels[target] != true) {
push(errors, `jump_targets: instr ${text(i)} op=${op} target label "${target}" not found`)
}
}
}
i = i + 1
}
return errors
}
// --- Check: type_consistency ---
// Verifies typed operators receive compatible known types.
var check_type_consistency = function(func) {
var instructions = func.instructions
var errors = []
var slot_types = {}
var i = 0
var instr = null
var op = null
var s2 = null
var s3 = null
var t2 = null
var t3 = null
if (instructions == null) return errors
// Type constants
var T_INT = "int"
var T_FLOAT = "float"
var T_TEXT = "text"
var T_BOOL = "bool"
var int_ops = {
add_int: true, sub_int: true, mul_int: true, div_int: true, mod_int: true,
eq_int: true, ne_int: true, lt_int: true, gt_int: true, le_int: true, ge_int: true,
neg_int: true
}
var float_ops = {
add_float: true, sub_float: true, mul_float: true, div_float: true, mod_float: true,
eq_float: true, ne_float: true, lt_float: true, gt_float: true, le_float: true, ge_float: true,
neg_float: true
}
var text_ops = {
eq_text: true, ne_text: true, lt_text: true, gt_text: true, le_text: true, ge_text: true,
concat: true
}
var bool_ops = {
eq_bool: true, ne_bool: true, not: true, and: true, or: true
}
while (i < length(instructions)) {
instr = instructions[i]
// Reset type info at labels (basic block boundaries)
if (is_text(instr) && !starts_with(instr, "_nop_")) {
slot_types = {}
i = i + 1
continue
}
if (!is_array(instr)) {
i = i + 1
continue
}
op = instr[0]
// Track known types from constant-producing ops
if (op == "int") {
slot_types[text(instr[1])] = T_INT
} else if (op == "access") {
if (is_number(instr[2])) {
if (is_integer(instr[2])) {
slot_types[text(instr[1])] = T_INT
} else {
slot_types[text(instr[1])] = T_FLOAT
}
} else if (is_text(instr[2])) {
slot_types[text(instr[1])] = T_TEXT
}
} else if (op == "true" || op == "false") {
slot_types[text(instr[1])] = T_BOOL
}
// Check typed binary ops
if (int_ops[op] == true && length(instr) >= 5) {
s2 = text(instr[2])
t2 = slot_types[s2]
if (t2 != null && t2 != T_INT && t2 != "unknown") {
push(errors, `type_consistency: instr ${text(i)} op=${op} src1 slot ${s2} has type ${t2}, expected int`)
}
if (length(instr) >= 6) {
s3 = text(instr[3])
t3 = slot_types[s3]
if (t3 != null && t3 != T_INT && t3 != "unknown") {
push(errors, `type_consistency: instr ${text(i)} op=${op} src2 slot ${s3} has type ${t3}, expected int`)
}
}
} else if (float_ops[op] == true && length(instr) >= 5) {
s2 = text(instr[2])
t2 = slot_types[s2]
if (t2 != null && t2 != T_FLOAT && t2 != "unknown") {
push(errors, `type_consistency: instr ${text(i)} op=${op} src1 slot ${s2} has type ${t2}, expected float`)
}
if (length(instr) >= 6) {
s3 = text(instr[3])
t3 = slot_types[s3]
if (t3 != null && t3 != T_FLOAT && t3 != "unknown") {
push(errors, `type_consistency: instr ${text(i)} op=${op} src2 slot ${s3} has type ${t3}, expected float`)
}
}
} else if (text_ops[op] == true && length(instr) >= 5) {
s2 = text(instr[2])
t2 = slot_types[s2]
if (t2 != null && t2 != T_TEXT && t2 != "unknown") {
push(errors, `type_consistency: instr ${text(i)} op=${op} src1 slot ${s2} has type ${t2}, expected text`)
}
if (length(instr) >= 6) {
s3 = text(instr[3])
t3 = slot_types[s3]
if (t3 != null && t3 != T_TEXT && t3 != "unknown") {
push(errors, `type_consistency: instr ${text(i)} op=${op} src2 slot ${s3} has type ${t3}, expected text`)
}
}
} else if (bool_ops[op] == true && length(instr) >= 5) {
s2 = text(instr[2])
t2 = slot_types[s2]
if (t2 != null && t2 != T_BOOL && t2 != "unknown") {
push(errors, `type_consistency: instr ${text(i)} op=${op} src1 slot ${s2} has type ${t2}, expected bool`)
}
}
// Clear type info for dest-producing ops
if (writes_dest[op] == true) {
slot_types[text(instr[1])] = null
// Restore type for known-result ops
if (op == "int" || (op == "access" && is_number(instr[2]))) {
// already set above
}
}
if (op == "invoke") {
slot_types[text(instr[2])] = null
}
i = i + 1
}
return errors
}
// --- Check: nop_consistency ---
// Verifies nop markers are not referenced by jumps.
var check_nop_consistency = function(func) {
var instructions = func.instructions
var errors = []
var nops = {}
var i = 0
var instr = null
var op = null
var label_pos = null
var target = null
if (instructions == null) return errors
// Collect all nop markers
while (i < length(instructions)) {
instr = instructions[i]
if (is_text(instr) && starts_with(instr, "_nop_")) {
nops[instr] = true
}
i = i + 1
}
// Check that no jump targets a nop
i = 0
while (i < length(instructions)) {
instr = instructions[i]
if (is_array(instr)) {
op = instr[0]
label_pos = jump_label_pos[op]
if (label_pos != null) {
target = instr[label_pos + 1]
if (is_text(target) && nops[target] == true) {
push(errors, `nop_consistency: instr ${text(i)} op=${op} jumps to nop marker "${target}"`)
}
}
}
i = i + 1
}
return errors
}
// --- verify_all ---
// Runs all checks on a function. Returns array of error strings (empty = pass).
var verify_all = function(func, pass_name) {
var all_errors = []
var check_errors = null
var i = 0
var prefix = pass_name != null ? pass_name + ": " : ""
var fn_name = func.name != null ? func.name : "<unknown>"
check_errors = check_slot_bounds(func)
i = 0
while (i < length(check_errors)) {
push(all_errors, `${prefix}${fn_name}: ${check_errors[i]}`)
i = i + 1
}
check_errors = check_jump_targets(func)
i = 0
while (i < length(check_errors)) {
push(all_errors, `${prefix}${fn_name}: ${check_errors[i]}`)
i = i + 1
}
check_errors = check_type_consistency(func)
i = 0
while (i < length(check_errors)) {
push(all_errors, `${prefix}${fn_name}: ${check_errors[i]}`)
i = i + 1
}
check_errors = check_nop_consistency(func)
i = 0
while (i < length(check_errors)) {
push(all_errors, `${prefix}${fn_name}: ${check_errors[i]}`)
i = i + 1
}
return all_errors
}
return {
verify_all: verify_all,
check_slot_bounds: check_slot_bounds,
check_jump_targets: check_jump_targets,
check_type_consistency: check_type_consistency,
check_nop_consistency: check_nop_consistency
}