Merge branch 'master' into fix_aot

This commit is contained in:
2026-02-18 14:16:42 -06:00
63 changed files with 127439 additions and 123140 deletions

View File

@@ -179,6 +179,26 @@ When running locally with `./cell --dev`, these commands manage packages:
Local paths are symlinked into `.cell/packages/`. The build step compiles C files to `.cell/lib/<pkg>/<stem>.dylib`. C files in `src/` are support files linked into module dylibs, not standalone modules. Local paths are symlinked into `.cell/packages/`. The build step compiles C files to `.cell/lib/<pkg>/<stem>.dylib`. C files in `src/` are support files linked into module dylibs, not standalone modules.
## Debugging Compiler Issues
When investigating bugs in compiled output (wrong values, missing operations, incorrect comparisons), **start from the optimizer down, not the VM up**. The compiler inspection tools will usually identify the problem faster than adding C-level tracing:
```
./cell --dev streamline --types <file> # show inferred slot types — look for wrong types
./cell --dev ir_report --events <file> # show every optimization applied and why
./cell --dev ir_report --types <file> # show type inference results per function
./cell --dev mcode --pretty <file> # show raw IR before optimization
./cell --dev streamline --ir <file> # show human-readable optimized IR
```
**Triage order:**
1. `streamline --types` — are slot types correct? Wrong type inference causes wrong optimizations.
2. `ir_report --events` — are type checks being incorrectly eliminated? Look for `known_type_eliminates_guard` on slots that shouldn't have known types.
3. `mcode --pretty` — is the raw IR correct before optimization? If so, the bug is in streamline.
4. Only dig into `source/mach.c` if the IR looks correct at all levels.
See `docs/compiler-tools.md` for the full tool reference and `docs/spec/streamline.md` for pass details.
## Testing ## Testing
After any C runtime changes, run all three test suites before considering the work done: After any C runtime changes, run all three test suites before considering the work done:

View File

@@ -1,3 +1,5 @@
// cell bench - Run benchmarks with statistical analysis
var shop = use('internal/shop') var shop = use('internal/shop')
var pkg = use('package') var pkg = use('package')
var fd = use('fd') var fd = use('fd')

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -573,23 +573,11 @@ Build.compile_native = function(src_path, target, buildtype, pkg) {
var dylib_ext = tc.system == 'windows' ? '.dll' : (tc.system == 'darwin' ? '.dylib' : '.so') var dylib_ext = tc.system == 'windows' ? '.dll' : (tc.system == 'darwin' ? '.dylib' : '.so')
var cc = tc.c var cc = tc.c
// Step 1: Read source and compile through pipeline // Step 1: Compile through pipeline
var content = fd.slurp(src_path) var optimized = shop.compile_file(src_path)
var src = text(content)
var tokenize = use('tokenize')
var parse = use('parse')
var fold = use('fold')
var mcode_mod = use('mcode')
var streamline_mod = use('streamline')
var qbe_macros = use('qbe') var qbe_macros = use('qbe')
var qbe_emit = use('qbe_emit') var qbe_emit = use('qbe_emit')
var tok_result = tokenize(src, src_path)
var ast = parse(tok_result.tokens, src, src_path, tokenize)
var folded = fold(ast)
var compiled = mcode_mod(folded)
var optimized = streamline_mod(compiled)
// Step 2: Generate QBE IL // Step 2: Generate QBE IL
var sym_name = null var sym_name = null
if (pkg) { if (pkg) {
@@ -598,7 +586,7 @@ Build.compile_native = function(src_path, target, buildtype, pkg) {
var il_parts = qbe_emit(optimized, qbe_macros, sym_name) var il_parts = qbe_emit(optimized, qbe_macros, sym_name)
// Content hash for cache key // Content hash for cache key
var hash = content_hash(src + '\n' + _target + '\nnative') var hash = content_hash(text(fd.slurp(src_path)) + '\n' + _target + '\nnative')
var build_dir = get_build_dir() var build_dir = get_build_dir()
ensure_dir(build_dir) ensure_dir(build_dir)
@@ -747,19 +735,8 @@ Build.compile_cm_to_mach = function(src_path) {
if (!fd.is_file(src_path)) { if (!fd.is_file(src_path)) {
print('Source file not found: ' + src_path); disrupt print('Source file not found: ' + src_path); disrupt
} }
var src = text(fd.slurp(src_path))
var tokenize = use('tokenize')
var parse = use('parse')
var fold = use('fold')
var mcode_mod = use('mcode')
var streamline_mod = use('streamline')
var json = use('json') var json = use('json')
var optimized = shop.compile_file(src_path)
var tok_result = tokenize(src, src_path)
var ast = parse(tok_result.tokens, src, src_path, tokenize)
var folded = fold(ast)
var compiled = mcode_mod(folded)
var optimized = streamline_mod(compiled)
return mach_compile_mcode_bin(src_path, json.encode(optimized)) return mach_compile_mcode_bin(src_path, json.encode(optimized))
} }

View File

@@ -33,7 +33,7 @@ if (!fd_mod.is_file(file)) {
var abs = fd_mod.realpath(file) var abs = fd_mod.realpath(file)
// Shared compilation front-end // Shared compilation front-end — uses raw modules for per-stage timing
var tokenize = use('tokenize') var tokenize = use('tokenize')
var parse_mod = use('parse') var parse_mod = use('parse')
var fold = use('fold') var fold = use('fold')

View File

@@ -111,6 +111,18 @@ $stop() // stop self
$stop(child) // stop a child actor $stop(child) // stop a child actor
``` ```
**Important:** `$stop()` does not halt execution immediately. Code after the call continues running in the current turn — it only prevents the actor from receiving future messages. Structure your code so that nothing runs after `$stop()`, or use `return` to exit the current function first.
```javascript
// Wrong — code after $stop() still runs
if (done) $stop()
do_more_work() // this still executes!
// Right — return after $stop()
if (done) { $stop(); return }
do_more_work()
```
### $start(callback, program) ### $start(callback, program)
Start a new child actor from a script. The callback receives lifecycle events: Start a new child actor from a script. The callback receives lifecycle events:
@@ -272,7 +284,9 @@ if (is_actor(some_value)) {
### log ### log
Logging functions: `log.console(msg)`, `log.error(msg)`, `log.system(msg)`. Channel-based logging. Any `log.X(value)` writes to channel `"X"`. Three channels are conventional: `log.console(msg)`, `log.error(msg)`, `log.system(msg)` — but any name works.
Channels are routed to configurable **sinks** (console or file) defined in `.cell/log.toml`. See [Logging](/docs/logging/) for the full guide.
### use(path) ### use(path)

View File

@@ -286,6 +286,84 @@ Clean build artifacts.
pit clean pit clean
``` ```
## Logging
### pit log
Manage log sinks and read log files. See [Logging](/docs/logging/) for the full guide.
### pit log list
List configured sinks.
```bash
pit log list
```
### pit log add
Add a log sink.
```bash
pit log add <name> console [options] # add a console sink
pit log add <name> file <path> [options] # add a file sink
```
Options:
- `--format=pretty|bare|json` — output format (default: `pretty` for console, `json` for file)
- `--channels=ch1,ch2` — channels to subscribe (default: `console,error,system`). Use `'*'` for all channels (quote to prevent shell glob expansion).
- `--exclude=ch1,ch2` — channels to exclude (useful with `'*'`)
```bash
pit log add terminal console --format=bare --channels=console
pit log add errors file .cell/logs/errors.jsonl --channels=error
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
```
### pit log remove
Remove a sink.
```bash
pit log remove <name>
```
### pit log read
Read entries from a file sink.
```bash
pit log read <sink> [options]
```
Options:
- `--lines=N` — show last N entries
- `--channel=X` — filter by channel
- `--since=timestamp` — only show entries after timestamp (seconds since epoch)
```bash
pit log read errors --lines=50
pit log read dump --channel=debug --lines=10
pit log read errors --since=1702656000
```
### pit log tail
Follow a file sink in real time.
```bash
pit log tail <sink> [--lines=N]
```
`--lines=N` controls how many existing entries to show on start (default: 10).
```bash
pit log tail dump
pit log tail errors --lines=20
```
## Developer Commands ## Developer Commands
Compiler pipeline tools, analysis, and testing. These are primarily useful for developing the ƿit compiler and runtime. Compiler pipeline tools, analysis, and testing. These are primarily useful for developing the ƿit compiler and runtime.

View File

@@ -15,65 +15,51 @@ The compiler runs in stages:
source → tokenize → parse → fold → mcode → streamline → output source → tokenize → parse → fold → mcode → streamline → output
``` ```
Each stage has a corresponding dump tool that lets you see its output. Each stage has a corresponding CLI tool that lets you see its output.
| Stage | Tool | What it shows | | Stage | Tool | What it shows |
|-------------|-------------------|----------------------------------------| |-------------|---------------------------|----------------------------------------|
| fold | `dump_ast.cm` | Folded AST as JSON | | tokenize | `tokenize.ce` | Token stream as JSON |
| mcode | `dump_mcode.cm` | Raw mcode IR before optimization | | parse | `parse.ce` | Unfolded AST as JSON |
| streamline | `dump_stream.cm` | Before/after instruction counts + IR | | fold | `fold.ce` | Folded AST as JSON |
| streamline | `dump_types.cm` | Optimized IR with type annotations | | mcode | `mcode.ce` | Raw mcode IR as JSON |
| streamline | `streamline.ce` | Full optimized IR as JSON | | mcode | `mcode.ce --pretty` | Human-readable mcode IR |
| all | `ir_report.ce` | Structured optimizer flight recorder | | streamline | `streamline.ce` | Full optimized IR as JSON |
| streamline | `streamline.ce --types` | Optimized IR with type annotations |
| streamline | `streamline.ce --stats` | Per-function summary stats |
| streamline | `streamline.ce --ir` | Human-readable canonical IR |
| 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. All tools take a source file as input and run the pipeline up to the relevant stage.
## Quick Start ## Quick Start
```bash ```bash
# see raw mcode IR # see raw mcode IR (pretty-printed)
./cell --core . dump_mcode.cm myfile.ce cell mcode --pretty myfile.ce
# see what the optimizer changed # see optimized IR with type annotations
./cell --core . dump_stream.cm myfile.ce cell streamline --types myfile.ce
# full optimizer report with events # full optimizer report with events
./cell --core . ir_report.ce --full myfile.ce cell ir_report --full myfile.ce
``` ```
## dump_ast.cm ## fold.ce
Prints the folded AST as JSON. This is the output of the parser and constant folder, before mcode generation. Prints the folded AST as JSON. This is the output of the parser and constant folder, before mcode generation.
```bash ```bash
./cell --core . dump_ast.cm <file.ce|file.cm> cell fold <file.ce|file.cm>
``` ```
## dump_mcode.cm ## mcode.ce
Prints the raw mcode IR before any optimization. Shows the instruction array as formatted text with opcode, operands, and program counter. Prints mcode IR. Default output is JSON; use `--pretty` for human-readable format with opcodes, operands, and program counter.
```bash ```bash
./cell --core . dump_mcode.cm <file.ce|file.cm> cell mcode <file.ce|file.cm> # JSON (default)
``` cell mcode --pretty <file.ce|file.cm> # human-readable IR
## 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 ## streamline.ce
@@ -81,10 +67,11 @@ Shows the optimized IR with type annotations. Each instruction is followed by th
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. 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 ```bash
./cell --core . streamline.ce <file.ce|file.cm> # full JSON (default) cell streamline <file.ce|file.cm> # full JSON (default)
./cell --core . streamline.ce --stats <file.ce|file.cm> # summary stats per function cell streamline --stats <file.ce|file.cm> # summary stats per function
./cell --core . streamline.ce --ir <file.ce|file.cm> # human-readable IR cell streamline --ir <file.ce|file.cm> # human-readable IR
./cell --core . streamline.ce --check <file.ce|file.cm> # warnings only cell streamline --check <file.ce|file.cm> # warnings only
cell streamline --types <file.ce|file.cm> # IR with type annotations
``` ```
| Flag | Description | | Flag | Description |
@@ -93,6 +80,7 @@ Runs the full pipeline (tokenize, parse, fold, mcode, streamline) and outputs th
| `--stats` | Per-function summary: args, slots, instruction counts by category, nops eliminated | | `--stats` | Per-function summary: args, slots, instruction counts by category, nops eliminated |
| `--ir` | Human-readable canonical IR (same format as `ir_report.ce`) | | `--ir` | Human-readable canonical IR (same format as `ir_report.ce`) |
| `--check` | Warnings only (e.g. `nr_slots > 200` approaching 255 limit) | | `--check` | Warnings only (e.g. `nr_slots > 200` approaching 255 limit) |
| `--types` | Optimized IR with inferred type annotations per slot |
Flags can be combined. Flags can be combined.
@@ -101,8 +89,8 @@ Flags can be combined.
Regenerates the boot seed files in `boot/`. These are pre-compiled mcode IR (JSON) files that bootstrap the compilation pipeline on cold start. Regenerates the boot seed files in `boot/`. These are pre-compiled mcode IR (JSON) files that bootstrap the compilation pipeline on cold start.
```bash ```bash
./cell --core . seed.ce # regenerate all boot seeds cell seed # regenerate all boot seeds
./cell --core . seed.ce --clean # also clear the build cache after cell seed --clean # also clear the build cache after
``` ```
The script compiles each pipeline module (tokenize, parse, fold, mcode, streamline) and `internal/bootstrap.cm` through the current pipeline, encodes the output as JSON, and writes it to `boot/<name>.cm.mcode`. The script compiles each pipeline module (tokenize, parse, fold, mcode, streamline) and `internal/bootstrap.cm` through the current pipeline, encodes the output as JSON, and writes it to `boot/<name>.cm.mcode`.
@@ -117,7 +105,7 @@ The script compiles each pipeline module (tokenize, parse, fold, mcode, streamli
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. 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 ```bash
./cell --core . ir_report.ce [options] <file.ce|file.cm> cell ir_report [options] <file.ce|file.cm>
``` ```
### Options ### Options
@@ -246,16 +234,16 @@ Properties:
```bash ```bash
# what passes changed something? # what passes changed something?
./cell --core . ir_report.ce --summary myfile.ce | jq 'select(.changed)' cell ir_report --summary myfile.ce | jq 'select(.changed)'
# list all rewrite rules that fired # list all rewrite rules that fired
./cell --core . ir_report.ce --events myfile.ce | jq 'select(.type == "event") | .rule' cell ir_report --events myfile.ce | jq 'select(.type == "event") | .rule'
# diff IR before and after optimization # diff IR before and after optimization
./cell --core . ir_report.ce --ir-all myfile.ce | jq -r 'select(.type == "ir") | .text' cell ir_report --ir-all myfile.ce | jq -r 'select(.type == "ir") | .text'
# full report for analysis # full report for analysis
./cell --core . ir_report.ce --full myfile.ce > report.json cell ir_report --full myfile.ce > report.json
``` ```
## ir_stats.cm ## ir_stats.cm

View File

@@ -15,10 +15,14 @@ var json = use('json')
### json.encode(value, space, replacer, whitelist) ### json.encode(value, space, replacer, whitelist)
Convert a value to JSON text. Convert a value to JSON text. With no `space` argument, output is pretty-printed with 1-space indent. Pass `false` or `0` for compact single-line output.
```javascript ```javascript
json.encode({a: 1, b: 2}) json.encode({a: 1, b: 2})
// '{ "a": 1, "b": 2 }'
// Compact (no whitespace)
json.encode({a: 1, b: 2}, false)
// '{"a":1,"b":2}' // '{"a":1,"b":2}'
// Pretty print with 2-space indent // Pretty print with 2-space indent
@@ -32,7 +36,7 @@ json.encode({a: 1, b: 2}, 2)
**Parameters:** **Parameters:**
- **value** — the value to encode - **value** — the value to encode
- **space** — indentation (number of spaces or string) - **space** — indentation: number of spaces, string, or `false`/`0` for compact output. Default is pretty-printed.
- **replacer** — function to transform values - **replacer** — function to transform values
- **whitelist** — array of keys to include - **whitelist** — array of keys to include

233
docs/logging.md Normal file
View File

@@ -0,0 +1,233 @@
---
title: "Logging"
description: "Configurable channel-based logging with sinks"
weight: 25
type: "docs"
---
Logging in ƿit is channel-based. Any `log.X(value)` call writes to channel `"X"`. Channels are routed to **sinks** — named destinations that format and deliver log output to the console or to files.
## Channels
Three channels are conventional:
| Channel | Usage |
|---------|-------|
| `log.console(msg)` | General output |
| `log.error(msg)` | Errors and warnings |
| `log.system(msg)` | Internal system messages |
Any name works. `log.debug(msg)` creates channel `"debug"`, `log.perf(msg)` creates `"perf"`, and so on.
```javascript
log.console("server started on port 8080")
log.error("connection refused")
log.debug({query: "SELECT *", rows: 42})
```
Non-text values are JSON-encoded automatically.
## Default Behavior
With no configuration, a default sink routes `console`, `error`, and `system` to the terminal in pretty format:
```
[a3f12] [console] main.ce:5 server started on port 8080
[a3f12] [error] main.ce:12 connection refused
```
The format is `[actor_id] [channel] file:line message`.
## Configuration
Logging is configured in `.cell/log.toml`. Each `[sink.NAME]` section defines a sink.
```toml
[sink.terminal]
type = "console"
format = "bare"
channels = ["console"]
[sink.errors]
type = "file"
path = ".cell/logs/errors.jsonl"
channels = ["error"]
[sink.everything]
type = "file"
path = ".cell/logs/all.jsonl"
channels = ["*"]
exclude = ["console"]
```
### Sink fields
| Field | Values | Description |
|-------|--------|-------------|
| `type` | `"console"`, `"file"` | Where output goes |
| `format` | `"pretty"`, `"bare"`, `"json"` | How output is formatted |
| `channels` | array of names, or `["*"]` | Which channels this sink receives. Quote `'*'` on the CLI to prevent shell glob expansion. |
| `exclude` | array of names | Channels to skip (useful with `"*"`) |
| `stack` | array of channel names | Channels that capture a stack trace |
| `path` | file path | Output file (file sinks only) |
### Formats
**pretty** — human-readable, one line per message. Includes actor ID, channel, source location, and message.
```
[a3f12] [console] main.ce:5 server started
```
**bare** — minimal. Actor ID and message only.
```
[a3f12] server started
```
**json** — structured JSONL (one JSON object per line). Used for file sinks and machine consumption.
```json
{"actor_id":"a3f12...","timestamp":1702656000.5,"channel":"console","event":"server started","source":{"file":"main.ce","line":5,"col":3,"fn":"init"}}
```
## Log Records
Every log call produces a record:
```javascript
{
actor_id: "a3f12...", // full actor GUID
timestamp: 1702656000.5, // seconds since epoch
channel: "console", // channel name
event: "the message", // value passed to log
source: {
file: "main.ce",
line: 5,
col: 3,
fn: "init"
}
}
```
File sinks write one JSON-encoded record per line. Console sinks format the record according to their format setting.
## Stack Traces
Add a `stack` field to a sink to capture a full call stack for specific channels. The value is an array of channel names.
```toml
[sink.terminal]
type = "console"
format = "bare"
channels = ["console", "error"]
stack = ["error"]
```
Only channels listed in `stack` get stack traces. Other channels on the same sink print without one:
```
[a3f12] server started
[a3f12] connection failed
at handle_request (server.ce:42:3)
at process (router.ce:18:5)
at main (main.ce:5:1)
```
With JSON format, a `stack` array is added to the record for channels that have stack capture enabled:
```json
{"actor_id":"a3f12...","channel":"error","event":"connection failed","source":{"file":"server.ce","line":42,"col":3,"fn":"handle_request"},"stack":[{"fn":"handle_request","file":"server.ce","line":42,"col":3},{"fn":"process","file":"router.ce","line":18,"col":5},{"fn":"main","file":"main.ce","line":5,"col":1}]}
```
Channels without `stack` configuration produce no stack field. Capturing stacks adds overhead — enable it for debugging, not production.
## CLI
The `pit log` command manages sinks and reads log files. See [CLI — pit log](/docs/cli/#pit-log) for the full reference.
```bash
pit log list # show sinks
pit log add terminal console --format=bare --channels=console
pit log add dump file .cell/logs/dump.jsonl '--channels=*' --exclude=console
pit log remove terminal
pit log read dump --lines=20 --channel=error
pit log tail dump
```
## Examples
### Development setup
Route console output to the terminal with minimal formatting. Send everything else to a structured log file for debugging.
```toml
[sink.terminal]
type = "console"
format = "bare"
channels = ["console"]
[sink.debug]
type = "file"
path = ".cell/logs/debug.jsonl"
channels = ["*"]
exclude = ["console"]
```
```javascript
log.console("listening on :8080") // -> terminal: [a3f12] listening on :8080
log.error("bad request") // -> debug.jsonl only
log.debug({latency: 0.042}) // -> debug.jsonl only
```
### Separate error log
Keep a dedicated error log alongside a full dump.
```toml
[sink.terminal]
type = "console"
format = "pretty"
channels = ["console", "error", "system"]
[sink.errors]
type = "file"
path = ".cell/logs/errors.jsonl"
channels = ["error"]
[sink.all]
type = "file"
path = ".cell/logs/all.jsonl"
channels = ["*"]
```
### JSON console
Output structured JSON to the console for piping into other tools.
```toml
[sink.json_out]
type = "console"
format = "json"
channels = ["console", "error"]
```
```bash
pit run myapp.ce | jq '.event'
```
### Reading logs
```bash
# Last 50 error entries
pit log read errors --lines=50
# Errors since a timestamp
pit log read errors --since=1702656000
# Filter a wildcard sink to one channel
pit log read all --channel=debug --lines=10
# Follow a log file in real time
pit log tail all
```

View File

@@ -217,21 +217,21 @@ This tells you: `connect` is a function taking `(from, to, label)`, declared on
The index and explain modules can be used directly from ƿit scripts: The index and explain modules can be used directly from ƿit scripts:
### index.cm ### Via shop (recommended)
```javascript ```javascript
var tokenize_mod = use('tokenize') var shop = use('internal/shop')
var parse_mod = use('parse') var idx = shop.index_file(path)
var fold_mod = use('fold')
var index_mod = use('index')
var pipeline = {tokenize: tokenize_mod, parse: parse_mod, fold: fold_mod}
var idx = index_mod.index_file(src, filename, pipeline)
``` ```
`index_file` runs the full pipeline (tokenize, parse, fold) and returns the index. If you already have a parsed AST and tokens, use `index_ast` instead: `shop.index_file` runs the full pipeline (tokenize, parse, index, resolve imports) and caches the result.
### index.cm (direct)
If you already have a parsed AST and tokens, use `index_ast` directly:
```javascript ```javascript
var index_mod = use('index')
var idx = index_mod.index_ast(ast, tokens, filename) var idx = index_mod.index_ast(ast, tokens, filename)
``` ```

View File

@@ -62,12 +62,13 @@ See [Mcode IR](mcode.md) for the instruction format and complete instruction ref
Optimizes the Mcode IR through a series of independent passes. Operates per-function: Optimizes the Mcode IR through a series of independent passes. Operates per-function:
1. **Backward type inference**: Infers parameter types from how they are used in typed operators (`add_int`, `store_index`, `load_field`, `push`, `pop`, etc.). Immutable `def` parameters keep their inferred type across label join points. 1. **Backward type inference**: Infers parameter types from how they are used in typed operators (`add_int`, `store_index`, `load_field`, `push`, `pop`, etc.). Immutable `def` parameters keep their inferred type across label join points.
2. **Type-check elimination**: When a slot's type is known, eliminates `is_<type>` + conditional jump pairs. Narrows `load_dynamic`/`store_dynamic` to typed variants. 2. **Write-type invariance**: Determines which local slots have a consistent write type across all instructions. Slots written by child closures (via `put`) are excluded (forced to unknown).
3. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons. 3. **Type-check elimination**: When a slot's type is known, eliminates `is_<type>` + conditional jump pairs. Narrows `load_dynamic`/`store_dynamic` to typed variants.
4. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition. 4. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons.
5. **Move elimination**: Removes self-moves (`move a, a`). 5. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition.
6. **Unreachable elimination**: Nops dead code after `return` until the next label. 6. **Move elimination**: Removes self-moves (`move a, a`).
7. **Dead jump elimination**: Removes jumps to the immediately following label. 7. **Unreachable elimination**: Nops dead code after `return` until the next label.
8. **Dead jump elimination**: Removes jumps to the immediately following label.
See [Streamline Optimizer](streamline.md) for detailed pass descriptions. See [Streamline Optimizer](streamline.md) for detailed pass descriptions.
@@ -130,9 +131,9 @@ Seeds are used during cold start (empty cache) to compile the pipeline modules f
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `dump_mcode.cm` | Print raw Mcode IR before streamlining | | `mcode.ce --pretty` | Print raw Mcode IR before streamlining |
| `dump_stream.cm` | Print IR after streamlining with before/after stats | | `streamline.ce --types` | Print streamlined IR with type annotations |
| `dump_types.cm` | Print streamlined IR with type annotations | | `streamline.ce --stats` | Print IR after streamlining with before/after stats |
## Test Files ## Test Files

View File

@@ -95,7 +95,9 @@ Write type mapping:
| `move`, `load_field`, `load_index`, `load_dynamic`, `pop`, `get` | T_UNKNOWN | | `move`, `load_field`, `load_index`, `load_dynamic`, `pop`, `get` | T_UNKNOWN |
| `invoke`, `tail_invoke` | T_UNKNOWN | | `invoke`, `tail_invoke` | T_UNKNOWN |
The result is a map of slot→type for slots where all writes agree on a single known type. Parameter slots (1..nr_args) and slot 0 are excluded. Before filtering, a pre-pass (`mark_closure_writes`) scans all inner functions for `put` instructions (closure variable writes). For each `put`, the pass traverses the parent chain to find the target ancestor function and marks the written slot as `closure_written`. Slots marked as closure-written are forced to T_UNKNOWN regardless of what the local write analysis infers, because the actual runtime write happens in a child function and can produce any type.
The result is a map of slot→type for slots where all writes agree on a single known type. Parameter slots (1..nr_args) and slot 0 are excluded. Closure-written slots are excluded (forced to unknown).
Common patterns this enables: Common patterns this enables:
@@ -257,17 +259,17 @@ The `+` operator is excluded from target slot propagation when it would use the
## Debugging Tools ## Debugging Tools
Three dump tools inspect the IR at different stages: CLI tools inspect the IR at different stages:
- **`dump_mcode.cm`** — prints the raw Mcode IR after `mcode.cm`, before streamlining - **`cell mcode --pretty`** — prints the raw Mcode IR after `mcode.cm`, before streamlining
- **`dump_stream.cm`** — prints the IR after streamlining, with before/after instruction counts - **`cell streamline --stats`** — prints the IR after streamlining, with before/after instruction counts
- **`dump_types.cm`** — prints the streamlined IR with type annotations on each instruction - **`cell streamline --types`** — prints the streamlined IR with type annotations on each instruction
Usage: Usage:
``` ```
./cell --core . dump_mcode.cm <file.ce|file.cm> cell mcode --pretty <file.ce|file.cm>
./cell --core . dump_stream.cm <file.ce|file.cm> cell streamline --stats <file.ce|file.cm>
./cell --core . dump_types.cm <file.ce|file.cm> cell streamline --types <file.ce|file.cm>
``` ```
## Tail Call Marking ## Tail Call Marking

View File

@@ -1,16 +0,0 @@
// 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")
var parse = use("parse")
var fold = use("fold")
var filename = args[0]
var src = text(fd.slurp(filename))
var tok = tokenize(src, filename)
var ast = parse(tok.tokens, src, filename, tokenize)
var folded = fold(ast)
print(json.encode(folded))

View File

@@ -1,18 +1,9 @@
var tokenize = use('tokenize') // cell dump_ir - Dump intermediate representation for a source file
var parse_mod = use('parse')
var fold = use('fold')
var mcode_mod = use('mcode')
var streamline_mod = use('streamline')
var json = use('json')
var fd = use('fd')
var file = args[0] var shop = use('internal/shop')
var src = text(fd.slurp(file)) var json = use('json')
var tok = tokenize(src, file)
var ast = parse_mod(tok.tokens, src, file, tokenize) var optimized = shop.compile_file(args[0])
var folded = fold(ast)
var compiled = mcode_mod(folded)
var optimized = streamline_mod(compiled)
var instrs = optimized.main.instructions var instrs = optimized.main.instructions
var i = 0 var i = 0

View File

@@ -1,117 +0,0 @@
// dump_mcode.cm — pretty-print mcode IR (before streamlining)
//
// Usage: ./cell --core . dump_mcode.cm <file.ce|file.cm>
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
if (length(args) < 1) {
print("usage: cell --core . dump_mcode.cm <file>")
return
}
var filename = args[0]
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)
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var fmt_val = function(v) {
if (is_null(v)) {
return "null"
}
if (is_number(v)) {
return text(v)
}
if (is_text(v)) {
return `"${v}"`
}
if (is_object(v)) {
return json.encode(v)
}
if (is_logical(v)) {
return v ? "true" : "false"
}
return text(v)
}
var dump_function = function(func, name) {
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var nr_close = func.nr_close_slots != null ? func.nr_close_slots : 0
var instrs = func.instructions
var i = 0
var pc = 0
var instr = null
var op = null
var n = 0
var parts = null
var j = 0
var operands = null
var pc_str = null
var op_str = null
print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}, closures=${text(nr_close)}) ===`)
if (instrs == null || length(instrs) == 0) {
print(" (empty)")
return null
}
while (i < length(instrs)) {
instr = instrs[i]
if (is_text(instr)) {
if (!starts_with(instr, "_nop_")) {
print(`${instr}:`)
}
} else if (is_array(instr)) {
op = instr[0]
n = length(instr)
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
j = j + 1
}
operands = text(parts, ", ")
pc_str = pad_right(text(pc), 5)
op_str = pad_right(op, 14)
print(` ${pc_str} ${op_str} ${operands}`)
pc = pc + 1
}
i = i + 1
}
return null
}
var main_name = null
var fi = 0
var func = null
var fname = null
// Dump main
if (compiled.main != null) {
main_name = compiled.name != null ? compiled.name : "<main>"
dump_function(compiled.main, main_name)
}
// Dump sub-functions
if (compiled.functions != null) {
fi = 0
while (fi < length(compiled.functions)) {
func = compiled.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
dump_function(func, `[${text(fi)}] ${fname}`)
fi = fi + 1
}
}

View File

@@ -1,237 +0,0 @@
// dump_types.cm — show streamlined IR with type annotations
//
// Usage: ./cell --core . dump_types.cm <file.ce|file.cm>
var fd = use("fd")
var json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
if (length(args) < 1) {
print("usage: cell --core . dump_types.cm <file>")
return
}
var filename = args[0]
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)
var optimized = streamline(compiled)
// Type constants
def T_UNKNOWN = "unknown"
def T_INT = "int"
def T_FLOAT = "float"
def T_NUM = "num"
def T_TEXT = "text"
def T_BOOL = "bool"
def T_NULL = "null"
def T_ARRAY = "array"
def T_RECORD = "record"
def T_FUNCTION = "function"
def int_result_ops = {
bitnot: true, bitand: true, bitor: true,
bitxor: true, shl: true, shr: true, ushr: true
}
def bool_result_ops = {
eq_int: true, ne_int: true, lt_int: true, gt_int: true,
le_int: true, ge_int: true,
eq_float: true, ne_float: true, lt_float: true, gt_float: true,
le_float: true, ge_float: true,
eq_text: true, ne_text: true, lt_text: true, gt_text: true,
le_text: true, ge_text: true,
eq_bool: true, ne_bool: true,
not: true, and: true, or: true,
is_int: true, is_text: true, is_num: true,
is_bool: true, is_null: true, is_identical: true,
is_array: true, is_func: true, is_record: true, is_stone: true
}
var access_value_type = function(val) {
if (is_number(val)) {
return is_integer(val) ? T_INT : T_FLOAT
}
if (is_text(val)) {
return T_TEXT
}
return T_UNKNOWN
}
var track_types = function(slot_types, instr) {
var op = instr[0]
var src_type = null
if (op == "access") {
slot_types[text(instr[1])] = access_value_type(instr[2])
} else if (op == "int") {
slot_types[text(instr[1])] = T_INT
} else if (op == "true" || op == "false") {
slot_types[text(instr[1])] = T_BOOL
} else if (op == "null") {
slot_types[text(instr[1])] = T_NULL
} else if (op == "move") {
src_type = slot_types[text(instr[2])]
slot_types[text(instr[1])] = src_type != null ? src_type : T_UNKNOWN
} else if (int_result_ops[op] == true) {
slot_types[text(instr[1])] = T_INT
} else if (op == "concat") {
slot_types[text(instr[1])] = T_TEXT
} else if (bool_result_ops[op] == true) {
slot_types[text(instr[1])] = T_BOOL
} else if (op == "typeof") {
slot_types[text(instr[1])] = T_TEXT
} else if (op == "array") {
slot_types[text(instr[1])] = T_ARRAY
} else if (op == "record") {
slot_types[text(instr[1])] = T_RECORD
} else if (op == "function") {
slot_types[text(instr[1])] = T_FUNCTION
} else if (op == "invoke" || op == "tail_invoke") {
slot_types[text(instr[2])] = T_UNKNOWN
} else if (op == "load_field" || op == "load_index" || op == "load_dynamic") {
slot_types[text(instr[1])] = T_UNKNOWN
} else if (op == "pop" || op == "get") {
slot_types[text(instr[1])] = T_UNKNOWN
} else if (op == "length") {
slot_types[text(instr[1])] = T_INT
} else if (op == "add" || op == "subtract" || op == "multiply" ||
op == "divide" || op == "modulo" || op == "pow" || op == "negate") {
slot_types[text(instr[1])] = T_UNKNOWN
}
return null
}
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var fmt_val = function(v) {
if (is_null(v)) {
return "null"
}
if (is_number(v)) {
return text(v)
}
if (is_text(v)) {
return `"${v}"`
}
if (is_object(v)) {
return json.encode(v)
}
if (is_logical(v)) {
return v ? "true" : "false"
}
return text(v)
}
// Build type annotation string for an instruction
var type_annotation = function(slot_types, instr) {
var n = length(instr)
var parts = []
var j = 1
var v = null
var t = null
while (j < n - 2) {
v = instr[j]
if (is_number(v)) {
t = slot_types[text(v)]
if (t != null && t != T_UNKNOWN) {
push(parts, `s${text(v)}:${t}`)
}
}
j = j + 1
}
if (length(parts) == 0) {
return ""
}
return text(parts, " ")
}
var dump_function_typed = function(func, name) {
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var instrs = func.instructions
var slot_types = {}
var i = 0
var pc = 0
var instr = null
var op = null
var n = 0
var annotation = null
var operand_parts = null
var j = 0
var operands = null
var pc_str = null
var op_str = null
var line = null
print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`)
if (instrs == null || length(instrs) == 0) {
print(" (empty)")
return null
}
while (i < length(instrs)) {
instr = instrs[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
i = i + 1
continue
}
slot_types = {}
print(`${instr}:`)
} else if (is_array(instr)) {
op = instr[0]
n = length(instr)
annotation = type_annotation(slot_types, instr)
operand_parts = []
j = 1
while (j < n - 2) {
push(operand_parts, fmt_val(instr[j]))
j = j + 1
}
operands = text(operand_parts, ", ")
pc_str = pad_right(text(pc), 5)
op_str = pad_right(op, 14)
line = pad_right(` ${pc_str} ${op_str} ${operands}`, 50)
if (length(annotation) > 0) {
print(`${line} ; ${annotation}`)
} else {
print(line)
}
track_types(slot_types, instr)
pc = pc + 1
}
i = i + 1
}
return null
}
var main_name = null
var fi = 0
var func = null
var fname = null
// Dump main
if (optimized.main != null) {
main_name = optimized.name != null ? optimized.name : "<main>"
dump_function_typed(optimized.main, main_name)
}
// Dump sub-functions
if (optimized.functions != null) {
fi = 0
while (fi < length(optimized.functions)) {
func = optimized.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
dump_function_typed(func, `[${text(fi)}] ${fname}`)
fi = fi + 1
}
}

View File

@@ -8,36 +8,9 @@
var fd = use('fd') var fd = use('fd')
var json = use('json') var json = use('json')
var tokenize_mod = use('tokenize')
var parse_mod = use('parse')
var fold_mod = use('fold')
var index_mod = use('index')
var explain_mod = use('explain') var explain_mod = use('explain')
var shop = use('internal/shop') var shop = use('internal/shop')
// Resolve import paths on an index in-place.
var resolve_imports = function(idx_obj, fname) {
var fi = shop.file_info(fd.realpath(fname))
var ctx = fi.package
var ri = 0
var rp = null
var lp = null
while (ri < length(idx_obj.imports)) {
rp = shop.resolve_use_path(idx_obj.imports[ri].module_path, ctx)
// Fallback: check sibling files in the same directory.
if (rp == null) {
lp = fd.dirname(fd.realpath(fname)) + '/' + idx_obj.imports[ri].module_path + '.cm'
if (fd.is_file(lp)) {
rp = lp
}
}
if (rp != null) {
idx_obj.imports[ri].resolved_path = rp
}
ri = ri + 1
}
}
var mode = null var mode = null
var span_arg = null var span_arg = null
var symbol_name = null var symbol_name = null
@@ -47,12 +20,10 @@ var parts = null
var filename = null var filename = null
var line = null var line = null
var col = null var col = null
var src = null
var idx = null var idx = null
var indexes = [] var indexes = []
var explain = null var explain = null
var result = null var result = null
var pipeline = {tokenize: tokenize_mod, parse: parse_mod, fold: fold_mod}
for (i = 0; i < length(args); i++) { for (i = 0; i < length(args); i++) {
if (args[i] == '--span') { if (args[i] == '--span') {
@@ -108,9 +79,7 @@ if (mode == "span") {
$stop() $stop()
} }
src = text(fd.slurp(filename)) idx = shop.index_file(filename)
idx = index_mod.index_file(src, filename, pipeline)
resolve_imports(idx, filename)
explain = explain_mod.make(idx) explain = explain_mod.make(idx)
result = explain.at_span(line, col) result = explain.at_span(line, col)
@@ -139,11 +108,8 @@ if (mode == "symbol") {
} }
if (length(files) == 1) { if (length(files) == 1) {
// Single file: use by_symbol for a focused result.
filename = files[0] filename = files[0]
src = text(fd.slurp(filename)) idx = shop.index_file(filename)
idx = index_mod.index_file(src, filename, pipeline)
resolve_imports(idx, filename)
explain = explain_mod.make(idx) explain = explain_mod.make(idx)
result = explain.by_symbol(symbol_name) result = explain.by_symbol(symbol_name)
@@ -154,13 +120,10 @@ if (mode == "symbol") {
print("\n") print("\n")
} }
} else if (length(files) > 1) { } else if (length(files) > 1) {
// Multiple files: index each and search across all.
indexes = [] indexes = []
i = 0 i = 0
while (i < length(files)) { while (i < length(files)) {
src = text(fd.slurp(files[i])) idx = shop.index_file(files[i])
idx = index_mod.index_file(src, files[i], pipeline)
resolve_imports(idx, files[i])
indexes[] = idx indexes[] = idx
i = i + 1 i = i + 1
} }

14
fold.ce
View File

@@ -1,13 +1,7 @@
var fd = use("fd") // cell fold - Run constant folding on a source file
var json = use("json") var json = use("json")
var shop = use("internal/shop")
var filename = args[0] var filename = args[0]
var src = text(fd.slurp(filename)) var folded = shop.analyze_file(filename)
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var tok_result = tokenize(src, filename)
var ast = parse(tok_result.tokens, src, filename, tokenize)
var folded = fold(ast)
print(json.encode(folded)) print(json.encode(folded))

150
help.ce
View File

@@ -1,11 +1,28 @@
// cell help - Display help information for cell commands // cell help - Display help information for cell commands
var fd = use('fd') var fd = use('fd')
var package = use('package')
var shop = use('internal/shop')
var command = length(args) > 0 ? args[0] : null var command = length(args) > 0 ? args[0] : null
var core_dir = shop.get_package_dir('core')
var man_file = null var man_file = null
var stat = null var stat = null
var content = null var content = null
var lines = null
var line = null
var block = null
var ce_path = null
var i = 0
var programs = null
var top_level = []
var descriptions = []
var max_len = 0
var first_line = null
var desc = null
var sep = null
var padding = null
var j = 0
// Display specific command help // Display specific command help
if (command) { if (command) {
@@ -15,56 +32,95 @@ if (command) {
content = text(fd.slurp(man_file)) content = text(fd.slurp(man_file))
log.console(content) log.console(content)
} else { } else {
log.error("No help available for command: " + command) ce_path = core_dir + '/' + command + '.ce'
log.console("Run 'cell help' to see available commands.") stat = fd.stat(ce_path)
if (stat && stat.isFile) {
content = text(fd.slurp(ce_path))
lines = array(content, '\n')
block = ""
for (i = 0; i < length(lines); i++) {
line = lines[i]
if (starts_with(line, '// ')) {
block = block + text(line, 3, length(line)) + '\n'
} else if (line == '//') {
block = block + '\n'
} else if (starts_with(line, '//')) {
block = block + text(line, 2, length(line)) + '\n'
} else {
i = length(lines)
}
}
if (length(block) > 0) {
log.console(trim(block))
} else {
log.error("No help available for command: " + command)
log.console("Run 'cell help' to see available commands.")
}
} else {
log.error("No help available for command: " + command)
log.console("Run 'cell help' to see available commands.")
}
}
} else {
// Display general help — dynamic listing
programs = sort(package.list_programs('core'))
for (i = 0; i < length(programs); i++) {
if (search(programs[i], '/') == null) {
top_level[] = programs[i]
} }
$stop()
} }
// Display general help for (i = 0; i < length(top_level); i++) {
man_file = 'scripts/man/cell.man' ce_path = core_dir + '/' + top_level[i] + '.ce'
stat = fd.stat(man_file) desc = ""
if (stat && stat.isFile) { stat = fd.stat(ce_path)
content = text(fd.slurp(man_file)) if (stat && stat.isFile) {
log.console(content) content = text(fd.slurp(ce_path))
} else { lines = array(content, '\n')
// Fallback if man file doesn't exist first_line = length(lines) > 0 ? lines[0] : ""
log.console("cell - The Cell package manager") if (starts_with(first_line, '//')) {
log.console("") if (starts_with(first_line, '// ')) {
log.console("Usage: cell <command> [arguments]") first_line = text(first_line, 3, length(first_line))
log.console("") } else {
log.console("Package Management:") first_line = text(first_line, 2, length(first_line))
log.console(" install <locator> Install a package and its dependencies") }
log.console(" update [locator] Update packages from remote sources") sep = search(first_line, ' - ')
log.console(" remove <locator> Remove a package from the shop") if (sep != null) {
log.console(" add <locator> Add a dependency to current package") desc = text(first_line, sep + 3, length(first_line))
log.console("") } else {
log.console("Building:") sep = search(first_line, ' — ')
log.console(" build [locator] Build dynamic libraries for packages") if (sep != null) {
log.console(" clean [scope] Remove build artifacts") desc = text(first_line, sep + 3, length(first_line))
log.console("") } else {
log.console("Linking (Local Development):") desc = first_line
log.console(" link <origin> <target> Link a package to a local path") }
log.console(" unlink <origin> Remove a package link") }
log.console(" clone <origin> <path> Clone and link a package locally") }
log.console("") }
log.console("Information:") descriptions[] = desc
log.console(" list [scope] List packages and dependencies") if (length(top_level[i]) > max_len) {
log.console(" ls [locator] List modules and actors in a package") max_len = length(top_level[i])
log.console(" why <locator> Show reverse dependencies") }
log.console(" search <query> Search for packages, modules, or actors") }
log.console("")
log.console("Diagnostics:") log.console("cell - the cell package manager")
log.console(" resolve [locator] Print fully resolved dependency closure") log.console("")
log.console(" graph [locator] Emit dependency graph (tree, dot, json)") log.console("usage: cell <command> [arguments]")
log.console(" verify [scope] Verify integrity and consistency") log.console("")
log.console(" audit [locator] Test-compile all scripts in package(s)") log.console("available commands:")
log.console("") for (i = 0; i < length(top_level); i++) {
log.console("Other:") padding = ""
log.console(" help [command] Show help for a command") for (j = 0; j < max_len - length(top_level[i]); j++) {
log.console(" version Show cell version") padding = padding + " "
log.console("") }
log.console("Run 'cell <command> --help' for more information on a command.") if (length(descriptions[i]) > 0) {
log.console(" " + top_level[i] + padding + " " + descriptions[i])
} else {
log.console(" " + top_level[i])
}
}
log.console("")
log.console("Run 'cell help <command>' for more information.")
} }
$stop() $stop()

View File

@@ -7,19 +7,11 @@
var fd = use('fd') var fd = use('fd')
var json = use('json') var json = use('json')
var tokenize_mod = use('tokenize')
var parse_mod = use('parse')
var fold_mod = use('fold')
var index_mod = use('index')
var shop = use('internal/shop') var shop = use('internal/shop')
var filename = null var filename = null
var output_path = null var output_path = null
var i = 0 var i = 0
var file_info = null
var pkg_ctx = null
var resolved = null
var local_path = null
for (i = 0; i < length(args); i++) { for (i = 0; i < length(args); i++) {
if (args[i] == '-o' || args[i] == '--output') { if (args[i] == '-o' || args[i] == '--output') {
@@ -53,29 +45,7 @@ if (!fd.is_file(filename)) {
$stop() $stop()
} }
var src = text(fd.slurp(filename)) var idx = shop.index_file(filename)
var pipeline = {tokenize: tokenize_mod, parse: parse_mod, fold: fold_mod}
var idx = index_mod.index_file(src, filename, pipeline)
// Resolve import paths to filesystem locations.
file_info = shop.file_info(fd.realpath(filename))
pkg_ctx = file_info.package
i = 0
while (i < length(idx.imports)) {
resolved = shop.resolve_use_path(idx.imports[i].module_path, pkg_ctx)
// Fallback: check sibling files in the same directory.
if (resolved == null) {
local_path = fd.dirname(fd.realpath(filename)) + '/' + idx.imports[i].module_path + '.cm'
if (fd.is_file(local_path)) {
resolved = local_path
}
}
if (resolved != null) {
idx.imports[i].resolved_path = resolved
}
i = i + 1
}
var out = json.encode(idx, true) var out = json.encode(idx, true)
if (output_path != null) { if (output_path != null) {

View File

@@ -1,8 +1,7 @@
// index.cm — Core semantic indexing module. // index.cm — Core semantic indexing module.
// Walks AST output from parse (+ optional fold) to build a semantic index. // Walks AST output from parse (+ optional fold) to build a semantic index.
// //
// Two entry points: // Entry point:
// index_file(src, filename, tokenize_mod, parse_mod, fold_mod) — full pipeline
// index_ast(ast, tokens, filename) — index a pre-parsed AST // index_ast(ast, tokens, filename) — index a pre-parsed AST
var make_span = function(node) { var make_span = function(node) {
@@ -22,6 +21,7 @@ var index_ast = function(ast, tokens, filename) {
var references = [] var references = []
var call_sites = [] var call_sites = []
var exports_list = [] var exports_list = []
var intrinsic_refs = []
var node_counter = 0 var node_counter = 0
var fn_map = {} var fn_map = {}
var _i = 0 var _i = 0
@@ -147,6 +147,29 @@ var index_ast = function(ast, tokens, filename) {
nid = next_id() nid = next_id()
// this keyword
if (node.kind == "this") {
references[] = {
node_id: nid,
name: "this",
symbol_id: null,
span: make_span(node),
enclosing: enclosing,
ref_kind: "read"
}
return
}
// Capture intrinsic refs with positions (intrinsics lack function_nr).
if (node.kind == "name" && node.name != null && node.intrinsic == true) {
intrinsic_refs[] = {
node_id: nid,
name: node.name,
span: make_span(node),
enclosing: enclosing
}
}
// Name reference — has function_nr when it's a true variable reference. // Name reference — has function_nr when it's a true variable reference.
if (node.kind == "name" && node.name != null && node.function_nr != null) { if (node.kind == "name" && node.name != null && node.function_nr != null) {
if (node.intrinsic != true) { if (node.intrinsic != true) {
@@ -208,6 +231,17 @@ var index_ast = function(ast, tokens, filename) {
} }
} }
// Capture intrinsic callee refs (e.g., print, length).
if (node.expression != null && node.expression.kind == "name" &&
node.expression.intrinsic == true && node.expression.name != null) {
intrinsic_refs[] = {
node_id: nid,
name: node.expression.name,
span: make_span(node.expression),
enclosing: enclosing
}
}
// Walk callee expression (skip name — already recorded above). // Walk callee expression (skip name — already recorded above).
if (node.expression != null && node.expression.kind != "name") { if (node.expression != null && node.expression.kind != "name") {
walk_expr(node.expression, enclosing, false) walk_expr(node.expression, enclosing, false)
@@ -227,8 +261,12 @@ var index_ast = function(ast, tokens, filename) {
// Function / arrow function expression — walk body. // Function / arrow function expression — walk body.
if (node.kind == "function" || node.kind == "arrow function") { if (node.kind == "function" || node.kind == "arrow function") {
enc = enclosing enc = enclosing
if (node.name != null && node.function_nr != null) { if (node.function_nr != null) {
enc = filename + ":" + node.name + ":fn" if (node.name != null) {
enc = filename + ":" + node.name + ":fn"
} else {
enc = filename + ":anon_" + text(node.function_nr) + ":fn"
}
} }
// Record params as symbols. // Record params as symbols.
if (node.list != null) { if (node.list != null) {
@@ -596,24 +634,13 @@ var index_ast = function(ast, tokens, filename) {
imports: imports, imports: imports,
symbols: symbols, symbols: symbols,
references: references, references: references,
intrinsic_refs: intrinsic_refs,
call_sites: call_sites, call_sites: call_sites,
exports: exports_list, exports: exports_list,
reverse_refs: reverse reverse_refs: reverse
} }
} }
// Run the full pipeline (tokenize -> parse -> fold) and index.
// pipeline is {tokenize, parse, fold} — pass fold as null to skip folding.
var index_file = function(src, filename, pipeline) {
var tok_result = pipeline.tokenize(src, filename)
var ast = pipeline.parse(tok_result.tokens, src, filename, pipeline.tokenize)
if (pipeline.fold != null) {
ast = pipeline.fold(ast)
}
return index_ast(ast, tok_result.tokens, filename)
}
return { return {
index_file: index_file,
index_ast: index_ast index_ast: index_ast
} }

View File

@@ -78,6 +78,7 @@ function load_pipeline_module(name, env) {
var boot_par = null var boot_par = null
var boot_fld = null var boot_fld = null
var boot_mc = null var boot_mc = null
var boot_sl = null
var tok_result = null var tok_result = null
var ast = null var ast = null
var compiled = null var compiled = null
@@ -105,6 +106,8 @@ function load_pipeline_module(name, env) {
} }
ast = boot_fld(ast) ast = boot_fld(ast)
compiled = boot_mc(ast) compiled = boot_mc(ast)
boot_sl = boot_load("streamline")
compiled = boot_sl(compiled)
mcode_json = json.encode(compiled) mcode_json = json.encode(compiled)
mach_blob = mach_compile_mcode_bin(name, mcode_json) mach_blob = mach_compile_mcode_bin(name, mcode_json)
if (cached) { if (cached) {
@@ -312,38 +315,24 @@ var actor_mod = use_core('actor')
var wota = use_core('wota') var wota = use_core('wota')
var nota = use_core('nota') var nota = use_core('nota')
function is_actor(value) {
return is_object(value) && value[ACTORDATA]
}
var ENETSERVICE = 0.1 var ENETSERVICE = 0.1
var REPLYTIMEOUT = 60 // seconds before replies are ignored var REPLYTIMEOUT = 60 // seconds before replies are ignored
function caller_data(depth) // --- Logging system (bootstrap phase) ---
{ // Early log: prints to console before toml/time/json are loaded.
return {file: "nofile", line: 0} // Upgraded to full sink-based system after config loads (see load_log_config below).
}
function console_rec(line, file, msg) { var log_config = null
return `[${text(_cell.id, 0, 5)}] [${file}:${line}]: ${msg}\n` var channel_sinks = {}
// time: [${time.text("mb d yyyy h:nn:ss")}] var wildcard_sinks = []
} var warned_channels = {}
var stack_channels = {}
function log(name, args) { function log(name, args) {
var caller = caller_data(1)
var msg = args[0] var msg = args[0]
if (msg == null) msg = ""
if (name == 'console') { os.print(`[${text(_cell.id, 0, 5)}] [${name}]: ${msg}\n`)
os.print(console_rec(caller.line, caller.file, msg))
} else if (name == 'error') {
if (msg == null) msg = "error"
os.print(console_rec(caller.line, caller.file, msg))
} else if (name == 'system') {
msg = "[SYSTEM] " + msg
os.print(console_rec(caller.line, caller.file, msg))
} else {
log.console(`unknown log type: ${name}`)
}
} }
function actor_die(err) function actor_die(err)
@@ -386,7 +375,7 @@ function actor_die(err)
//actor_mod.on_exception(actor_die) actor_mod.on_exception(actor_die)
_cell.args = _init != null ? _init : {} _cell.args = _init != null ? _init : {}
@@ -431,6 +420,166 @@ core_extras.native_mode = native_mode
var shop = use_core('internal/shop') var shop = use_core('internal/shop')
if (native_mode) use_core('build') if (native_mode) use_core('build')
var time = use_core('time') var time = use_core('time')
var toml = use_core('toml')
// --- Logging system (full version) ---
// Now that toml, time, fd, and json are available, upgrade the log function
// from the bootstrap version to a configurable sink-based system.
function ensure_log_dir(path) {
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
var i = 0
// ensure parent dir (skip last element which is the filename)
for (i = 0; i < length(parts) - 1; i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.is_dir(current)) fd.mkdir(current)
}
}
function build_sink_routing() {
channel_sinks = {}
wildcard_sinks = []
stack_channels = {}
var names = array(log_config.sink)
arrfor(names, function(name) {
var sink = log_config.sink[name]
sink._name = name
if (!is_array(sink.channels)) sink.channels = []
if (is_text(sink.exclude)) sink.exclude = [sink.exclude]
if (!is_array(sink.exclude)) sink.exclude = []
if (is_text(sink.stack)) sink.stack = [sink.stack]
if (!is_array(sink.stack)) sink.stack = []
if (sink.type == "file" && sink.path) ensure_log_dir(sink.path)
arrfor(sink.stack, function(ch) {
stack_channels[ch] = true
})
arrfor(sink.channels, function(ch) {
if (ch == "*") {
wildcard_sinks[] = sink
return
}
if (!channel_sinks[ch]) channel_sinks[ch] = []
channel_sinks[ch][] = sink
})
})
}
function load_log_config() {
var log_path = null
if (shop_path) {
log_path = shop_path + '/log.toml'
if (fd.is_file(log_path)) {
log_config = toml.decode(text(fd.slurp(log_path)))
}
}
if (!log_config || !log_config.sink) {
log_config = {
sink: {
terminal: {
type: "console",
format: "pretty",
channels: ["console", "error", "system"],
stack: ["error"]
}
}
}
}
build_sink_routing()
}
function pretty_format(rec) {
var aid = text(rec.actor_id, 0, 5)
var src = ""
var ev = null
var out = null
var i = 0
var fr = null
if (rec.source && rec.source.file)
src = rec.source.file + ":" + text(rec.source.line)
ev = is_text(rec.event) ? rec.event : json.encode(rec.event, false)
out = `[${aid}] [${rec.channel}] ${src} ${ev}\n`
if (rec.stack && length(rec.stack) > 0) {
for (i = 0; i < length(rec.stack); i = i + 1) {
fr = rec.stack[i]
out = out + ` at ${fr.fn} (${fr.file}:${text(fr.line)}:${text(fr.col)})\n`
}
}
return out
}
function bare_format(rec) {
var aid = text(rec.actor_id, 0, 5)
var ev = is_text(rec.event) ? rec.event : json.encode(rec.event, false)
var out = `[${aid}] ${ev}\n`
var i = 0
var fr = null
if (rec.stack && length(rec.stack) > 0) {
for (i = 0; i < length(rec.stack); i = i + 1) {
fr = rec.stack[i]
out = out + ` at ${fr.fn} (${fr.file}:${text(fr.line)}:${text(fr.col)})\n`
}
}
return out
}
function sink_excluded(sink, channel) {
var excluded = false
if (!sink.exclude || length(sink.exclude) == 0) return false
arrfor(sink.exclude, function(ex) {
if (ex == channel) excluded = true
})
return excluded
}
function dispatch_to_sink(sink, rec) {
var line = null
if (sink_excluded(sink, rec.channel)) return
if (sink.type == "console") {
if (sink.format == "json")
os.print(json.encode(rec, false) + "\n")
else if (sink.format == "bare")
os.print(bare_format(rec))
else
os.print(pretty_format(rec))
} else if (sink.type == "file") {
line = json.encode(rec, false) + "\n"
fd.slurpappend(sink.path, stone(blob(line)))
}
}
load_log_config()
log = function(name, args) {
var sinks = channel_sinks[name]
var event = args[0]
var caller = null
var stack = null
var rec = null
if (!sinks && length(wildcard_sinks) == 0) {
if (!warned_channels[name]) {
warned_channels[name] = true
os.print(`[warn] log channel '${name}' has no sinks configured\n`)
}
return
}
caller = caller_info(2)
if (stack_channels[name]) stack = os.stack(1)
rec = {
actor_id: _cell.id,
timestamp: time.number(),
channel: name,
event: event,
source: caller
}
if (stack) rec.stack = stack
if (sinks) arrfor(sinks, function(sink) { dispatch_to_sink(sink, rec) })
arrfor(wildcard_sinks, function(sink) { dispatch_to_sink(sink, rec) })
}
var pronto = use_core('pronto') var pronto = use_core('pronto')
var fallback = pronto.fallback var fallback = pronto.fallback
@@ -439,9 +588,9 @@ var race = pronto.race
var sequence = pronto.sequence var sequence = pronto.sequence
runtime_env.actor = actor runtime_env.actor = actor
runtime_env.is_actor = is_actor
runtime_env.log = log runtime_env.log = log
runtime_env.send = send runtime_env.send = send
runtime_env.shop_path = shop_path
runtime_env.fallback = fallback runtime_env.fallback = fallback
runtime_env.parallel = parallel runtime_env.parallel = parallel
runtime_env.race = race runtime_env.race = race
@@ -1052,7 +1201,7 @@ var prog_path = prog + ".ce"
var pkg_dir = null var pkg_dir = null
var core_dir = null var core_dir = null
if (!fd.is_file(prog_path)) { if (!fd.is_file(prog_path)) {
pkg_dir = package.find_package_dir(prog_path) pkg_dir = package.find_package_dir(".")
if (pkg_dir) if (pkg_dir)
prog_path = pkg_dir + '/' + prog + '.ce' prog_path = pkg_dir + '/' + prog + '.ce'
} }

View File

@@ -584,6 +584,37 @@ JSC_CCALL(fd_slurpwrite,
return JS_NULL; return JS_NULL;
) )
JSC_CCALL(fd_slurpappend,
size_t len;
const char *data = js_get_blob_data(js, &len, argv[1]);
if (!data && len > 0)
return JS_EXCEPTION;
const char *str = JS_ToCString(js, argv[0]);
if (!str) return JS_EXCEPTION;
int fd = open(str, O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd < 0) {
ret = JS_ThrowInternalError(js, "open failed for %s: %s", str, strerror(errno));
JS_FreeCString(js, str);
return ret;
}
ssize_t written = write(fd, data, len);
close(fd);
if (written != (ssize_t)len) {
ret = JS_ThrowInternalError(js, "write failed for %s: %s", str, strerror(errno));
JS_FreeCString(js, str);
return ret;
}
JS_FreeCString(js, str);
return JS_NULL;
)
// Helper function for recursive enumeration // Helper function for recursive enumeration
static void visit_directory(JSContext *js, JSValue *results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) { static void visit_directory(JSContext *js, JSValue *results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) {
if (!curr_path) return; if (!curr_path) return;
@@ -733,6 +764,7 @@ static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, read, 2), MIST_FUNC_DEF(fd, read, 2),
MIST_FUNC_DEF(fd, slurp, 1), MIST_FUNC_DEF(fd, slurp, 1),
MIST_FUNC_DEF(fd, slurpwrite, 2), MIST_FUNC_DEF(fd, slurpwrite, 2),
MIST_FUNC_DEF(fd, slurpappend, 2),
MIST_FUNC_DEF(fd, lseek, 3), MIST_FUNC_DEF(fd, lseek, 3),
MIST_FUNC_DEF(fd, getcwd, 0), MIST_FUNC_DEF(fd, getcwd, 0),
MIST_FUNC_DEF(fd, rmdir, 2), MIST_FUNC_DEF(fd, rmdir, 2),

View File

@@ -358,6 +358,41 @@ static JSValue js_os_dylib_open(JSContext *js, JSValue self, int argc, JSValue *
return dylib_obj; return dylib_obj;
} }
static JSValue js_os_dylib_preload(JSContext *js, JSValue self, int argc, JSValue *argv)
{
if (argc < 1)
return JS_NULL;
const char *path = JS_ToCString(js, argv[0]);
if (!path)
return JS_NULL;
void *handle;
#ifdef _WIN32
handle = LoadLibraryA(path);
#else
handle = dlopen(path, RTLD_LAZY | RTLD_GLOBAL);
#endif
JS_FreeCString(js, path);
if (!handle)
return JS_NULL;
JSValue dylib_obj = JS_NewObjectClass(js, js_dylib_class_id);
if (JS_IsException(dylib_obj)) {
#ifdef _WIN32
FreeLibrary((HMODULE)handle);
#else
dlclose(handle);
#endif
return JS_NULL;
}
JS_SetOpaque(dylib_obj, handle);
return dylib_obj;
}
static JSValue js_os_dylib_symbol(JSContext *js, JSValue self, int argc, JSValue *argv) static JSValue js_os_dylib_symbol(JSContext *js, JSValue self, int argc, JSValue *argv)
{ {
if (argc < 2) { if (argc < 2) {
@@ -639,6 +674,27 @@ static JSValue js_os_embedded_module(JSContext *js, JSValue self, int argc, JSVa
return js_new_blob_stoned_copy(js, (void *)entry->data, entry->size); return js_new_blob_stoned_copy(js, (void *)entry->data, entry->size);
} }
static JSValue js_os_stack(JSContext *js, JSValue self, int argc, JSValue *argv) {
(void)self;
int skip = 0;
if (argc > 0) JS_ToInt32(js, &skip, argv[0]);
JSValue full = JS_GetStack(js);
if (skip <= 0) return full;
int64_t n = 0;
JS_GetLength(js, full, &n);
if (skip >= (int)n) return JS_NewArray(js);
JS_FRAME(js);
JS_ROOT(arr, JS_NewArray(js));
for (int i = skip; i < (int)n; i++) {
JSValue item = JS_GetPropertyNumber(js, full, i);
JS_SetPropertyNumber(js, arr.val, i - skip, item);
}
JS_RETURN(arr.val);
}
static const JSCFunctionListEntry js_os_funcs[] = { static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, platform, 0), MIST_FUNC_DEF(os, platform, 0),
MIST_FUNC_DEF(os, arch, 0), MIST_FUNC_DEF(os, arch, 0),
@@ -653,6 +709,7 @@ static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, exit, 0), MIST_FUNC_DEF(os, exit, 0),
MIST_FUNC_DEF(os, sleep, 1), MIST_FUNC_DEF(os, sleep, 1),
MIST_FUNC_DEF(os, dylib_open, 1), MIST_FUNC_DEF(os, dylib_open, 1),
MIST_FUNC_DEF(os, dylib_preload, 1),
MIST_FUNC_DEF(os, dylib_close, 1), MIST_FUNC_DEF(os, dylib_close, 1),
MIST_FUNC_DEF(os, dylib_symbol, 2), MIST_FUNC_DEF(os, dylib_symbol, 2),
MIST_FUNC_DEF(os, dylib_has_symbol, 2), MIST_FUNC_DEF(os, dylib_has_symbol, 2),
@@ -665,6 +722,7 @@ static const JSCFunctionListEntry js_os_funcs[] = {
MIST_FUNC_DEF(os, random, 0), MIST_FUNC_DEF(os, random, 0),
MIST_FUNC_DEF(os, getenv, 1), MIST_FUNC_DEF(os, getenv, 1),
MIST_FUNC_DEF(os, qbe, 1), MIST_FUNC_DEF(os, qbe, 1),
MIST_FUNC_DEF(os, stack, 1),
}; };
JSValue js_core_os_use(JSContext *js) { JSValue js_core_os_use(JSContext *js) {

View File

@@ -510,6 +510,155 @@ function inject_env(inject) {
return env return env
} }
// --- Pipeline API ---
// Lazy-loaded pipeline modules from use_cache (no re-entrancy risk).
var _tokenize_mod = null
var _parse_mod = null
var _fold_mod = null
var _mcode_mod = null
var _streamline_mod = null
var _index_mod = null
var _token_cache = {}
var _ast_cache = {}
var _analyze_cache = {}
var _compile_cache = {}
var _index_cache = {}
var get_tokenize = function() {
if (!_tokenize_mod) _tokenize_mod = use_cache['core/tokenize'] || use_cache['tokenize']
return _tokenize_mod
}
var get_parse = function() {
if (!_parse_mod) _parse_mod = use_cache['core/parse'] || use_cache['parse']
return _parse_mod
}
var get_fold = function() {
if (!_fold_mod) _fold_mod = use_cache['core/fold'] || use_cache['fold']
return _fold_mod
}
var get_mcode = function() {
if (!_mcode_mod) _mcode_mod = use_cache['core/mcode'] || use_cache['mcode']
return _mcode_mod
}
var get_streamline = function() {
if (!_streamline_mod) _streamline_mod = use_cache['core/streamline'] || use_cache['streamline']
return _streamline_mod
}
var get_index = function() {
if (!_index_mod) {
_index_mod = use_cache['core/index'] || use_cache['index']
if (!_index_mod) _index_mod = Shop.use('index', 'core')
}
return _index_mod
}
Shop.tokenize_file = function(path) {
var src = text(fd.slurp(path))
var key = content_hash(stone(blob(src)))
if (_token_cache[key]) return _token_cache[key]
var result = get_tokenize()(src, path)
_token_cache[key] = result
return result
}
Shop.parse_file = function(path) {
var src = text(fd.slurp(path))
var key = content_hash(stone(blob(src)))
if (_ast_cache[key]) return _ast_cache[key]
var tok = Shop.tokenize_file(path)
var ast = get_parse()(tok.tokens, src, path, get_tokenize())
_ast_cache[key] = ast
return ast
}
Shop.analyze_file = function(path) {
var src = text(fd.slurp(path))
var key = content_hash(stone(blob(src)))
if (_analyze_cache[key]) return _analyze_cache[key]
var ast = Shop.parse_file(path)
var folded = get_fold()(ast)
_analyze_cache[key] = folded
return folded
}
// Resolve import paths on an index in-place.
Shop.resolve_imports = function(idx_obj, fname) {
var fi = Shop.file_info(fd.realpath(fname))
var ctx = fi.package
var ri = 0
var rp = null
var lp = null
while (ri < length(idx_obj.imports)) {
rp = Shop.resolve_use_path(idx_obj.imports[ri].module_path, ctx)
if (rp == null) {
lp = fd.dirname(fd.realpath(fname)) + '/' + idx_obj.imports[ri].module_path + '.cm'
if (fd.is_file(lp)) {
rp = lp
}
}
if (rp != null) {
idx_obj.imports[ri].resolved_path = rp
}
ri = ri + 1
}
}
Shop.index_file = function(path) {
var src = text(fd.slurp(path))
var key = content_hash(stone(blob(src)))
if (_index_cache[key]) return _index_cache[key]
var tok = Shop.tokenize_file(path)
var ast = get_parse()(tok.tokens, src, path, get_tokenize())
var idx = get_index().index_ast(ast, tok.tokens, path)
Shop.resolve_imports(idx, path)
_index_cache[key] = idx
return idx
}
Shop.mcode_file = function(path) {
var folded = Shop.analyze_file(path)
return get_mcode()(folded)
}
Shop.compile_file = function(path) {
var src = text(fd.slurp(path))
var key = content_hash(stone(blob(src)))
if (_compile_cache[key]) return _compile_cache[key]
var compiled = Shop.mcode_file(path)
var optimized = get_streamline()(compiled)
_compile_cache[key] = optimized
return optimized
}
Shop.all_script_paths = function() {
var packages = Shop.list_packages()
var result = []
var i = 0
var j = 0
var scripts = null
var pkg_dir = null
var has_core = false
for (i = 0; i < length(packages); i++) {
if (packages[i] == 'core') has_core = true
}
if (!has_core) {
packages = array(packages, ['core'])
}
for (i = 0; i < length(packages); i++) {
pkg_dir = starts_with(packages[i], '/') ? packages[i] : get_packages_dir() + '/' + safe_package_path(packages[i])
scripts = get_package_scripts(packages[i])
for (j = 0; j < length(scripts); j++) {
result[] = {
package: packages[i],
rel_path: scripts[j],
full_path: pkg_dir + '/' + scripts[j]
}
}
}
return result
}
// Lazy-loaded compiler modules for on-the-fly compilation // Lazy-loaded compiler modules for on-the-fly compilation
var _mcode_mod = null var _mcode_mod = null
var _streamline_mod = null var _streamline_mod = null
@@ -717,9 +866,25 @@ function get_mach_path(pkg, stem) {
} }
// Open a per-module dylib and return the dlopen handle // Open a per-module dylib and return the dlopen handle
// Pre-loads sibling dylibs with RTLD_LAZY|RTLD_GLOBAL so cross-dylib symbols resolve
function open_module_dylib(dylib_path) { function open_module_dylib(dylib_path) {
if (open_dls[dylib_path]) return open_dls[dylib_path] if (open_dls[dylib_path]) return open_dls[dylib_path]
if (!fd.is_file(dylib_path)) return null if (!fd.is_file(dylib_path)) return null
var dir = fd.dirname(dylib_path)
var entries = fd.readdir(dir)
var i = 0
var sibling = null
var handle = null
while (i < length(entries)) {
if (ends_with(entries[i], dylib_ext) && entries[i] != fd.basename(dylib_path)) {
sibling = dir + '/' + entries[i]
if (!open_dls[sibling]) {
handle = os.dylib_preload(sibling)
if (handle) open_dls[sibling] = handle
}
}
i = i + 1
}
open_dls[dylib_path] = os.dylib_open(dylib_path) open_dls[dylib_path] = os.dylib_open(dylib_path)
return open_dls[dylib_path] return open_dls[dylib_path]
} }

343
log.ce Normal file
View File

@@ -0,0 +1,343 @@
// cell log - Manage and read log sinks
//
// Usage:
// cell log list List configured sinks
// cell log add <name> console [opts] Add a console sink
// cell log add <name> file <path> [opts] Add a file sink
// cell log remove <name> Remove a sink
// cell log read <sink> [opts] Read from a file sink
// cell log tail <sink> [--lines=N] Follow a file sink
var toml = use('toml')
var fd = use('fd')
var json = use('json')
var log_path = shop_path + '/log.toml'
function load_config() {
if (fd.is_file(log_path)) {
return toml.decode(text(fd.slurp(log_path)))
}
return null
}
function ensure_dir(path) {
if (fd.is_dir(path)) return
var parts = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
var i = 0
for (i = 0; i < length(parts); i++) {
if (parts[i] == '') continue
current = current + parts[i] + '/'
if (!fd.is_dir(current)) fd.mkdir(current)
}
}
function save_config(config) {
ensure_dir(shop_path)
fd.slurpwrite(log_path, stone(blob(toml.encode(config))))
}
function print_help() {
log.console("Usage: cell log <command> [options]")
log.console("")
log.console("Commands:")
log.console(" list List configured sinks")
log.console(" add <name> console [opts] Add a console sink")
log.console(" add <name> file <path> [opts] Add a file sink")
log.console(" remove <name> Remove a sink")
log.console(" read <sink> [opts] Read from a file sink")
log.console(" tail <sink> [--lines=N] Follow a file sink")
log.console("")
log.console("Options for add:")
log.console(" --format=pretty|bare|json Output format (default: pretty for console, json for file)")
log.console(" --channels=ch1,ch2 Channels to subscribe (default: console,error,system)")
log.console(" --exclude=ch1,ch2 Channels to exclude (for wildcard sinks)")
log.console("")
log.console("Options for read:")
log.console(" --lines=N Show last N lines (default: all)")
log.console(" --channel=X Filter by channel")
log.console(" --since=timestamp Only show entries after timestamp")
}
function parse_opt(arg, prefix) {
var full = '--' + prefix + '='
if (starts_with(arg, full))
return text(arg, length(full), length(arg))
return null
}
function format_entry(entry) {
var aid = text(entry.actor_id, 0, 5)
var src = ""
var ev = null
if (entry.source && entry.source.file)
src = entry.source.file + ":" + text(entry.source.line)
ev = is_text(entry.event) ? entry.event : json.encode(entry.event)
return "[" + aid + "] [" + entry.channel + "] " + src + " " + ev
}
function do_list() {
var config = load_config()
var names = null
if (!config || !config.sink) {
log.console("No log sinks configured.")
log.console("Default: console pretty for console/error/system")
return
}
names = array(config.sink)
arrfor(names, function(n) {
var s = config.sink[n]
var ch = is_array(s.channels) ? text(s.channels, ', ') : '(none)'
var ex = is_array(s.exclude) ? " exclude=" + text(s.exclude, ',') : ""
var fmt = s.format || (s.type == 'file' ? 'json' : 'pretty')
if (s.type == 'file')
log.console(" " + n + ": " + s.type + " -> " + s.path + " [" + ch + "] format=" + fmt + ex)
else
log.console(" " + n + ": " + s.type + " [" + ch + "] format=" + fmt + ex)
})
}
function do_add() {
var name = null
var sink_type = null
var path = null
var format = null
var channels = ["console", "error", "system"]
var exclude = null
var config = null
var val = null
var i = 0
if (length(args) < 3) {
log.error("Usage: cell log add <name> console|file [path] [options]")
return
}
name = args[1]
sink_type = args[2]
if (sink_type == 'file') {
if (length(args) < 4) {
log.error("Usage: cell log add <name> file <path> [options]")
return
}
path = args[3]
format = "json"
i = 4
} else if (sink_type == 'console') {
format = "pretty"
i = 3
} else {
log.error("Unknown sink type: " + sink_type + " (use 'console' or 'file')")
return
}
for (i = i; i < length(args); i++) {
val = parse_opt(args[i], 'format')
if (val) { format = val; continue }
val = parse_opt(args[i], 'channels')
if (val) { channels = array(val, ','); continue }
val = parse_opt(args[i], 'exclude')
if (val) { exclude = array(val, ','); continue }
}
config = load_config()
if (!config) config = {}
if (!config.sink) config.sink = {}
config.sink[name] = {type: sink_type, format: format, channels: channels}
if (path) config.sink[name].path = path
if (exclude) config.sink[name].exclude = exclude
save_config(config)
log.console("Added sink: " + name)
}
function do_remove() {
var name = null
var config = null
if (length(args) < 2) {
log.error("Usage: cell log remove <name>")
return
}
name = args[1]
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
return
}
config.sink[name] = null
save_config(config)
log.console("Removed sink: " + name)
}
function do_read() {
var name = null
var max_lines = 0
var filter_channel = null
var since = 0
var config = null
var sink = null
var content = null
var lines = null
var entries = []
var entry = null
var val = null
var i = 0
if (length(args) < 2) {
log.error("Usage: cell log read <sink_name> [options]")
return
}
name = args[1]
for (i = 2; i < length(args); i++) {
val = parse_opt(args[i], 'lines')
if (val) { max_lines = number(val); continue }
val = parse_opt(args[i], 'channel')
if (val) { filter_channel = val; continue }
val = parse_opt(args[i], 'since')
if (val) { since = number(val); continue }
}
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
return
}
sink = config.sink[name]
if (sink.type != 'file') {
log.error("Can only read from file sinks")
return
}
if (!fd.is_file(sink.path)) {
log.console("Log file does not exist yet: " + sink.path)
return
}
content = text(fd.slurp(sink.path))
lines = array(content, '\n')
arrfor(lines, function(line) {
var parse_fn = null
if (length(line) == 0) return
parse_fn = function() {
entry = json.decode(line)
} disruption {
entry = null
}
parse_fn()
if (!entry) return
if (filter_channel && entry.channel != filter_channel) return
if (since > 0 && entry.timestamp < since) return
entries[] = entry
})
if (max_lines > 0 && length(entries) > max_lines)
entries = array(entries, length(entries) - max_lines, length(entries))
arrfor(entries, function(e) {
log.console(format_entry(e))
})
}
function do_tail() {
var name = null
var tail_lines = 10
var config = null
var sink = null
var last_size = 0
var val = null
var i = 0
if (length(args) < 2) {
log.error("Usage: cell log tail <sink_name> [--lines=N]")
return
}
name = args[1]
for (i = 2; i < length(args); i++) {
val = parse_opt(args[i], 'lines')
if (val) { tail_lines = number(val); continue }
}
config = load_config()
if (!config || !config.sink || !config.sink[name]) {
log.error("Sink not found: " + name)
return
}
sink = config.sink[name]
if (sink.type != 'file') {
log.error("Can only tail file sinks")
return
}
if (!fd.is_file(sink.path))
log.console("Waiting for log file: " + sink.path)
function poll() {
var st = null
var poll_content = null
var poll_lines = null
var start = 0
var poll_entry = null
var old_line_count = 0
var idx = 0
var parse_fn = null
if (!fd.is_file(sink.path)) {
$delay(poll, 1)
return
}
st = fd.stat(sink.path)
if (st.size == last_size) {
$delay(poll, 1)
return
}
poll_content = text(fd.slurp(sink.path))
poll_lines = array(poll_content, '\n')
if (last_size == 0 && length(poll_lines) > tail_lines) {
start = length(poll_lines) - tail_lines
} else if (last_size > 0) {
old_line_count = length(array(text(poll_content, 0, last_size), '\n'))
start = old_line_count
}
last_size = st.size
for (idx = start; idx < length(poll_lines); idx++) {
if (length(poll_lines[idx]) == 0) continue
parse_fn = function() {
poll_entry = json.decode(poll_lines[idx])
} disruption {
poll_entry = null
}
parse_fn()
if (!poll_entry) continue
os.print(format_entry(poll_entry) + "\n")
}
$delay(poll, 1)
}
poll()
}
// Main dispatch
if (length(args) == 0) {
print_help()
} else if (args[0] == 'help' || args[0] == '-h' || args[0] == '--help') {
print_help()
} else if (args[0] == 'list') {
do_list()
} else if (args[0] == 'add') {
do_add()
} else if (args[0] == 'remove') {
do_remove()
} else if (args[0] == 'read') {
do_read()
} else if (args[0] == 'tail') {
do_tail()
} else {
log.error("Unknown command: " + args[0])
print_help()
}
$stop()

140
ls.ce
View File

@@ -1,35 +1,131 @@
// list modules and actors in a package // list modules and actors in packages
// if args[0] is a package alias, list that one //
// otherwise, list the local one // Usage:
// cell ls [<package>] List modules and programs
// cell ls --all List across all packages
// cell ls --modules|-m [<package>] Modules only
// cell ls --programs|-p [<package>] Programs only
// cell ls --paths [<package>] One absolute path per line
var shop = use('internal/shop') var shop = use('internal/shop')
var package = use('package') var package = use('package')
var ctx = null var show_all = false
var pkg = args[0] || package.find_package_dir('.') var show_modules = true
var modules = package.list_modules(pkg) var show_programs = true
var programs = package.list_programs(pkg) var show_paths = false
var filter_modules = false
var filter_programs = false
var pkg_arg = null
var show_help = false
var i = 0 var i = 0
log.console("Modules in " + pkg + ":") for (i = 0; i < length(args); i++) {
modules = sort(modules) if (args[i] == '--all' || args[i] == '-a') {
if (length(modules) == 0) { show_all = true
log.console(" (none)") } else if (args[i] == '--modules' || args[i] == '-m') {
} else { filter_modules = true
for (i = 0; i < length(modules); i++) { } else if (args[i] == '--programs' || args[i] == '-p') {
log.console(" " + modules[i]) filter_programs = true
} else if (args[i] == '--paths') {
show_paths = true
} else if (args[i] == '--help' || args[i] == '-h') {
show_help = true
} else if (!starts_with(args[i], '-')) {
pkg_arg = args[i]
} }
} }
log.console("") if (filter_modules || filter_programs) {
log.console("Programs in " + pkg + ":") show_modules = filter_modules
programs = sort(programs) show_programs = filter_programs
if (length(programs) == 0) { }
log.console(" (none)")
} else { var list_one_package = function(pkg) {
for (i = 0; i < length(programs); i++) { var pkg_dir = null
log.console(" " + programs[i]) var modules = null
var programs = null
var j = 0
if (starts_with(pkg, '/')) {
pkg_dir = pkg
} else {
pkg_dir = shop.get_package_dir(pkg)
} }
if (show_modules) {
modules = sort(package.list_modules(pkg))
if (show_paths) {
for (j = 0; j < length(modules); j++) {
log.console(pkg_dir + '/' + modules[j] + '.cm')
}
} else {
if (!filter_modules || show_all) {
log.console("Modules in " + pkg + ":")
}
if (length(modules) == 0) {
log.console(" (none)")
} else {
for (j = 0; j < length(modules); j++) {
log.console(" " + modules[j])
}
}
}
}
if (show_programs) {
programs = sort(package.list_programs(pkg))
if (show_paths) {
for (j = 0; j < length(programs); j++) {
log.console(pkg_dir + '/' + programs[j] + '.ce')
}
} else {
if (!show_paths && show_modules && !filter_programs) {
log.console("")
}
if (!filter_programs || show_all) {
log.console("Programs in " + pkg + ":")
}
if (length(programs) == 0) {
log.console(" (none)")
} else {
for (j = 0; j < length(programs); j++) {
log.console(" " + programs[j])
}
}
}
}
}
var packages = null
var pkg = null
if (show_help) {
log.console("Usage: cell ls [options] [<package>]")
log.console("")
log.console("Options:")
log.console(" --all, -a List across all installed packages")
log.console(" --modules, -m Show modules only")
log.console(" --programs, -p Show programs only")
log.console(" --paths Output one absolute path per line")
} else if (show_all) {
packages = shop.list_packages()
if (find(packages, function(p) { return p == 'core' }) == null) {
packages[] = 'core'
}
packages = sort(packages)
for (i = 0; i < length(packages); i++) {
if (!show_paths && i > 0) {
log.console("")
}
if (!show_paths) {
log.console("--- " + packages[i] + " ---")
}
list_one_package(packages[i])
}
} else {
pkg = pkg_arg || package.find_package_dir('.')
list_one_package(pkg)
} }
$stop() $stop()

131
mcode.ce
View File

@@ -1,13 +1,122 @@
// mcode.ce — compile to mcode IR
//
// Usage:
// cell mcode <file> Full mcode IR as JSON (default)
// cell mcode --pretty <file> Pretty-printed human-readable IR
var fd = use("fd") var fd = use("fd")
var json = use("json") var json = use("json")
var tokenize = use("tokenize") var shop = use("internal/shop")
var parse = use("parse")
var fold = use("fold") var show_pretty = false
var mcode = use("mcode") var filename = null
var filename = args[0] var i = 0
var src = text(fd.slurp(filename))
var result = tokenize(src, filename) for (i = 0; i < length(args); i++) {
var ast = parse(result.tokens, src, filename, tokenize) if (args[i] == '--pretty') {
var folded = fold(ast) show_pretty = true
var compiled = mcode(folded) } else if (args[i] == '--help' || args[i] == '-h') {
print(json.encode(compiled)) log.console("Usage: cell mcode [--pretty] <file>")
$stop()
} else if (!starts_with(args[i], '-')) {
filename = args[i]
}
}
if (!filename) {
log.console("usage: cell mcode [--pretty] <file>")
$stop()
}
var compiled = shop.mcode_file(filename)
if (!show_pretty) {
print(json.encode(compiled))
$stop()
}
// Pretty-print helpers (from dump_mcode.cm)
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
r = r + " "
}
return r
}
var fmt_val = function(v) {
if (is_null(v)) return "null"
if (is_number(v)) return text(v)
if (is_text(v)) return `"${v}"`
if (is_object(v)) return json.encode(v)
if (is_logical(v)) return v ? "true" : "false"
return text(v)
}
var dump_function = function(func, name) {
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var nr_close = func.nr_close_slots != null ? func.nr_close_slots : 0
var instrs = func.instructions
var i = 0
var pc = 0
var instr = null
var op = null
var n = 0
var parts = null
var j = 0
var operands = null
var pc_str = null
var op_str = null
print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}, closures=${text(nr_close)}) ===`)
if (instrs == null || length(instrs) == 0) {
print(" (empty)")
return null
}
while (i < length(instrs)) {
instr = instrs[i]
if (is_text(instr)) {
if (!starts_with(instr, "_nop_")) {
print(`${instr}:`)
}
} else if (is_array(instr)) {
op = instr[0]
n = length(instr)
parts = []
j = 1
while (j < n - 2) {
push(parts, fmt_val(instr[j]))
j = j + 1
}
operands = text(parts, ", ")
pc_str = pad_right(text(pc), 5)
op_str = pad_right(op, 14)
print(` ${pc_str} ${op_str} ${operands}`)
pc = pc + 1
}
i = i + 1
}
return null
}
var main_name = null
var fi = 0
var func = null
var fname = null
if (compiled.main != null) {
main_name = compiled.name != null ? compiled.name : "<main>"
dump_function(compiled.main, main_name)
}
if (compiled.functions != null) {
fi = 0
while (fi < length(compiled.functions)) {
func = compiled.functions[fi]
fname = func.name != null ? func.name : `<func_${text(fi)}>`
dump_function(func, `[${text(fi)}] ${fname}`)
fi = fi + 1
}
}
$stop()

View File

@@ -245,11 +245,18 @@ package.list_modules = function(name) {
var files = package.list_files(name) var files = package.list_files(name)
var modules = [] var modules = []
var i = 0 var i = 0
var stem = null
for (i = 0; i < length(files); i++) { for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.cm')) { if (ends_with(files[i], '.cm')) {
push(modules, text(files[i], 0, -3)) push(modules, text(files[i], 0, -3))
} }
} }
var c_files = package.get_c_files(name, null, true)
for (i = 0; i < length(c_files); i++) {
stem = ends_with(c_files[i], '.cpp') ? text(c_files[i], 0, -4) : text(c_files[i], 0, -2)
if (find(modules, function(m) { return m == stem }) == null)
push(modules, stem)
}
return modules return modules
} }

View File

@@ -1,9 +1,7 @@
var fd = use("fd") // cell parse - Parse a source file and output AST as JSON
var json = use("json") var json = use("json")
var tokenize = use("tokenize") var shop = use("internal/shop")
var parse = use("parse")
var filename = args[0] var filename = args[0]
var src = text(fd.slurp(filename)) var ast = shop.parse_file(filename)
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
print(json.encode(ast, true)) print(json.encode(ast, true))

2
qbe.ce
View File

@@ -1,3 +1,5 @@
// cell qbe - Compile a source file to QBE intermediate language
var fd = use("fd") var fd = use("fd")
var json = use("json") var json = use("json")
var tokenize = use("tokenize") var tokenize = use("tokenize")

View File

@@ -1,3 +1,5 @@
// cell qopconv - Convert QOP archive formats
var fd = use('fd') var fd = use('fd')
var qop = use('qop') var qop = use('qop')

123
query.ce Normal file
View File

@@ -0,0 +1,123 @@
// cell query — Semantic queries across packages.
//
// Usage:
// cell query --this [--top|--fn] [<package>] this references
// cell query --intrinsic <name> [<package>] Find built-in intrinsic usage
// cell query --decl <name> [<package>] Variable declarations by name
// cell query --help Show usage
var shop = use('internal/shop')
var query_mod = use('query')
var fd = use('fd')
var mode = null
var name = null
var this_scope = null
var pkg_filter = null
var show_help = false
var i = 0
for (i = 0; i < length(args); i++) {
if (args[i] == '--this') {
mode = "this"
} else if (args[i] == '--top') {
this_scope = "top"
} else if (args[i] == '--fn') {
this_scope = "fn"
} else if (args[i] == '--intrinsic') {
mode = "intrinsic"
if (i + 1 < length(args) && !starts_with(args[i + 1], '-')) {
name = args[i + 1]
i = i + 1
} else {
log.error('--intrinsic requires a name')
mode = "error"
}
} else if (args[i] == '--decl') {
mode = "decl"
if (i + 1 < length(args) && !starts_with(args[i + 1], '-')) {
name = args[i + 1]
i = i + 1
} else {
log.error('--decl requires a name')
mode = "error"
}
} else if (args[i] == '--help' || args[i] == '-h') {
show_help = true
} else if (!starts_with(args[i], '-')) {
pkg_filter = args[i]
}
}
var all_files = null
var files = []
var j = 0
var idx = null
var hits = null
var hit = null
var k = 0
// Use return pattern to avoid closure-over-object issue with disruption.
var safe_index = function(path) {
return shop.index_file(path)
} disruption {
return null
}
if (show_help) {
log.console("Usage: cell query [options] [<package>]")
log.console("")
log.console("Semantic queries across packages.")
log.console("")
log.console("Options:")
log.console(" --this All this references")
log.console(" --this --top Top-level this only (not inside functions)")
log.console(" --this --fn this inside functions only")
log.console(" --intrinsic <name> Find built-in intrinsic usage (e.g., print)")
log.console(" --decl <name> Variable declarations by name")
log.console("")
log.console("Without a package argument, searches all installed packages.")
} else if (mode == null || mode == "error") {
if (mode != "error") {
log.error('Specify --this, --intrinsic, or --decl. Use --help for usage.')
}
} else {
all_files = shop.all_script_paths()
if (pkg_filter != null) {
for (j = 0; j < length(all_files); j++) {
if (all_files[j].package == pkg_filter) {
files[] = all_files[j]
}
}
} else {
files = all_files
}
for (j = 0; j < length(files); j++) {
idx = safe_index(files[j].full_path)
if (idx == null) continue
hits = null
if (mode == "this") {
hits = query_mod.find_this(idx, this_scope)
} else if (mode == "intrinsic") {
hits = query_mod.intrinsic(idx, name)
} else if (mode == "decl") {
hits = query_mod.find_decl(idx, name, null)
}
if (hits != null && length(hits) > 0) {
for (k = 0; k < length(hits); k++) {
hit = hits[k]
if (hit.span != null) {
log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.span.from_row)}:${text(hit.span.from_col)}: ${hit.name}`)
} else if (hit.decl_span != null) {
log.console(`${files[j].package}:${files[j].rel_path}:${text(hit.decl_span.from_row)}:${text(hit.decl_span.from_col)}: ${hit.kind} ${hit.name}`)
}
}
}
}
}
$stop()

62
query.cm Normal file
View File

@@ -0,0 +1,62 @@
// query.cm — Semantic queries over index data.
//
// All functions take an index object (from index.cm) and return arrays of hits.
var query = {}
// Find this references. scope: "top" (top-level only), "fn" (in functions), null (all).
query.find_this = function(idx, scope) {
var hits = []
var i = 0
var ref = null
while (i < length(idx.references)) {
ref = idx.references[i]
if (ref.name == "this") {
if (scope == null) {
hits[] = ref
} else if (scope == "top" && ref.enclosing == null) {
hits[] = ref
} else if (scope == "fn" && ref.enclosing != null) {
hits[] = ref
}
}
i = i + 1
}
return hits
}
// Intrinsic usage: find refs to a built-in name (e.g., print).
query.intrinsic = function(idx, name) {
var hits = []
var i = 0
var ref = null
if (idx.intrinsic_refs == null) return hits
while (i < length(idx.intrinsic_refs)) {
ref = idx.intrinsic_refs[i]
if (ref.name == name) {
hits[] = ref
}
i = i + 1
}
return hits
}
// Variable declarations matching a name and optional kind filter.
// kind is one of "var", "def", "fn", "param", or null (any).
query.find_decl = function(idx, name, kind) {
var hits = []
var i = 0
var sym = null
while (i < length(idx.symbols)) {
sym = idx.symbols[i]
if (sym.name == name) {
if (kind == null || sym.kind == kind) {
hits[] = sym
}
}
i = i + 1
}
return hits
}
return query

View File

@@ -54,6 +54,7 @@ var compiled = null
var optimized = null var optimized = null
var mcode_json = null var mcode_json = null
var out_path = null var out_path = null
var build_dir = null
// Regenerate pipeline module seeds // Regenerate pipeline module seeds
for (i = 0; i < length(pipeline_modules); i++) { for (i = 0; i < length(pipeline_modules); i++) {
@@ -103,7 +104,7 @@ if (fd.is_file(bootstrap_path)) {
print('\nRegenerated ' + text(generated) + ' seed(s)') print('\nRegenerated ' + text(generated) + ' seed(s)')
if (clean) { if (clean) {
var build_dir = shop.get_build_dir() build_dir = shop.get_build_dir()
if (fd.is_dir(build_dir)) { if (fd.is_dir(build_dir)) {
print('Clearing build cache: ' + build_dir) print('Clearing build cache: ' + build_dir)
os.system('rm -rf "' + build_dir + '"') os.system('rm -rf "' + build_dir + '"')

View File

@@ -252,6 +252,7 @@ const char* cell_get_core_path(void) {
void actor_disrupt(cell_rt *crt) void actor_disrupt(cell_rt *crt)
{ {
crt->disrupt = 1; crt->disrupt = 1;
JS_SetPauseFlag(crt->context, 2);
if (crt->state != ACTOR_RUNNING) if (crt->state != ACTOR_RUNNING)
actor_free(crt); actor_free(crt);
} }
@@ -265,11 +266,13 @@ void script_startup(cell_rt *prt)
g_runtime = JS_NewRuntime(); g_runtime = JS_NewRuntime();
} }
JSContext *js = JS_NewContext(g_runtime); JSContext *js = JS_NewContext(g_runtime);
JS_SetInterruptHandler(js, (JSInterruptHandler *)actor_interrupt_cb, prt);
JS_SetContextOpaque(js, prt); JS_SetContextOpaque(js, prt);
prt->context = js; prt->context = js;
/* Set per-actor heap memory limit */
JS_SetHeapMemoryLimit(js, ACTOR_MEMORY_LIMIT);
/* Register all GCRef fields so the Cheney GC can relocate them. */ /* Register all GCRef fields so the Cheney GC can relocate them. */
JS_AddGCRef(js, &prt->idx_buffer_ref); JS_AddGCRef(js, &prt->idx_buffer_ref);
JS_AddGCRef(js, &prt->on_exception_ref); JS_AddGCRef(js, &prt->on_exception_ref);
@@ -345,7 +348,8 @@ void script_startup(cell_rt *prt)
tmp = js_core_json_use(js); tmp = js_core_json_use(js);
JS_SetPropertyStr(js, env_ref.val, "json", tmp); JS_SetPropertyStr(js, env_ref.val, "json", tmp);
crt->actor_sym_ref.val = JS_NewString(js, "__ACTOR__"); crt->actor_sym_ref.val = JS_NewObject(js);
JS_CellStone(js, crt->actor_sym_ref.val);
JS_SetActorSym(js, JS_DupValue(js, crt->actor_sym_ref.val)); JS_SetActorSym(js, JS_DupValue(js, crt->actor_sym_ref.val));
JS_SetPropertyStr(js, env_ref.val, "actorsym", JS_DupValue(js, crt->actor_sym_ref.val)); JS_SetPropertyStr(js, env_ref.val, "actorsym", JS_DupValue(js, crt->actor_sym_ref.val));
@@ -444,6 +448,7 @@ static void print_usage(const char *prog)
printf(" CELL_SHOP Shop path (default: ~/.cell)\n"); printf(" CELL_SHOP Shop path (default: ~/.cell)\n");
printf("\nRecompile after changes: make\n"); printf("\nRecompile after changes: make\n");
printf("Bootstrap from scratch: make bootstrap\n"); printf("Bootstrap from scratch: make bootstrap\n");
printf("Run the 'help' script like 'cell help' to see available scripts\n");
} }
int cell_init(int argc, char **argv) int cell_init(int argc, char **argv)
@@ -554,7 +559,6 @@ int cell_init(int argc, char **argv)
cli_rt->context = ctx; cli_rt->context = ctx;
JS_SetContextOpaque(ctx, cli_rt); JS_SetContextOpaque(ctx, cli_rt);
JS_SetInterruptHandler(ctx, (JSInterruptHandler *)actor_interrupt_cb, cli_rt);
JS_AddGCRef(ctx, &cli_rt->idx_buffer_ref); JS_AddGCRef(ctx, &cli_rt->idx_buffer_ref);
JS_AddGCRef(ctx, &cli_rt->on_exception_ref); JS_AddGCRef(ctx, &cli_rt->on_exception_ref);
@@ -565,7 +569,8 @@ int cell_init(int argc, char **argv)
cli_rt->on_exception_ref.val = JS_NULL; cli_rt->on_exception_ref.val = JS_NULL;
cli_rt->message_handle_ref.val = JS_NULL; cli_rt->message_handle_ref.val = JS_NULL;
cli_rt->unneeded_ref.val = JS_NULL; cli_rt->unneeded_ref.val = JS_NULL;
cli_rt->actor_sym_ref.val = JS_NewString(ctx, "__ACTOR__"); cli_rt->actor_sym_ref.val = JS_NewObject(ctx);
JS_CellStone(ctx, cli_rt->actor_sym_ref.val);
JS_SetActorSym(ctx, JS_DupValue(ctx, cli_rt->actor_sym_ref.val)); JS_SetActorSym(ctx, JS_DupValue(ctx, cli_rt->actor_sym_ref.val));
root_cell = cli_rt; root_cell = cli_rt;
@@ -704,7 +709,6 @@ check_actors:
JS_DeleteGCRef(ctx, &cli_rt->message_handle_ref); JS_DeleteGCRef(ctx, &cli_rt->message_handle_ref);
JS_DeleteGCRef(ctx, &cli_rt->unneeded_ref); JS_DeleteGCRef(ctx, &cli_rt->unneeded_ref);
JS_DeleteGCRef(ctx, &cli_rt->actor_sym_ref); JS_DeleteGCRef(ctx, &cli_rt->actor_sym_ref);
JS_SetInterruptHandler(ctx, NULL, NULL);
pthread_mutex_destroy(cli_rt->mutex); pthread_mutex_destroy(cli_rt->mutex);
free(cli_rt->mutex); free(cli_rt->mutex);
@@ -757,11 +761,24 @@ void cell_trace_sethook(cell_hook)
int uncaught_exception(JSContext *js, JSValue v) int uncaught_exception(JSContext *js, JSValue v)
{ {
(void)v; int has_exc = JS_HasException(js);
if (!JS_HasException(js)) int is_exc = JS_IsException(v);
if (!has_exc && !is_exc)
return 1; return 1;
/* Error message and backtrace were already printed to stderr /* Error message and backtrace were already printed to stderr
by JS_ThrowError2 / print_backtrace. Just clear the flag. */ by JS_ThrowError2 / print_backtrace. Just clear the flag. */
JS_GetException(js); if (has_exc)
JS_GetException(js);
cell_rt *crt = JS_GetContextOpaque(js);
if (crt && !JS_IsNull(crt->on_exception_ref.val)) {
/* Disable interruption so actor_die can send messages
without being re-interrupted. */
JS_SetPauseFlag(js, 0);
JSValue err = JS_NewString(js, "interrupted");
JS_Call(js, crt->on_exception_ref.val, JS_NULL, 1, &err);
/* Clear any secondary exception from the callback. */
if (JS_HasException(js))
JS_GetException(js);
}
return 0; return 0;
} }

View File

@@ -1,5 +1,6 @@
#include <pthread.h> #include <pthread.h>
#include <stdatomic.h>
/* Letter type for unified message queue */ /* Letter type for unified message queue */
typedef enum { typedef enum {
@@ -21,6 +22,14 @@ typedef struct letter {
#define ACTOR_EXHAUSTED 3 // Actor waiting for GC #define ACTOR_EXHAUSTED 3 // Actor waiting for GC
#define ACTOR_RECLAIMING 4 // Actor running GC #define ACTOR_RECLAIMING 4 // Actor running GC
#define ACTOR_SLOW 5 // Actor going slowly; deprioritize #define ACTOR_SLOW 5 // Actor going slowly; deprioritize
#define ACTOR_REFRESHED 6 // GC finished, ready to resume
// #define ACTOR_TRACE
#define ACTOR_FAST_TIMER_NS (10ULL * 1000000) // 10ms per turn
#define ACTOR_SLOW_TIMER_NS (5000ULL * 1000000) // 5s for slow actors
#define ACTOR_SLOW_STRIKES_MAX 3 // consecutive slow turns -> kill
#define ACTOR_MEMORY_LIMIT (16ULL * 1024 * 1024) // 16MB heap cap
typedef struct cell_rt { typedef struct cell_rt {
JSContext *context; JSContext *context;
@@ -58,6 +67,11 @@ typedef struct cell_rt {
int main_thread_only; int main_thread_only;
int affinity; int affinity;
uint64_t turn_start_ns; // cell_ns() when turn began
_Atomic uint32_t turn_gen; // incremented each turn start
int slow_strikes; // consecutive slow-completed turns
int vm_suspended; // 1 if VM is paused mid-turn
const char *name; // human friendly name const char *name; // human friendly name
cell_hook trace_hook; cell_hook trace_hook;
} cell_rt; } cell_rt;
@@ -74,17 +88,19 @@ int uncaught_exception(JSContext *js, JSValue v);
int actor_exists(const char *id); int actor_exists(const char *id);
void set_actor_state(cell_rt *actor); void set_actor_state(cell_rt *actor);
void enqueue_actor_priority(cell_rt *actor);
void actor_clock(cell_rt *actor, JSValue fn); void actor_clock(cell_rt *actor, JSValue fn);
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds); uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds);
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id); JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id);
void exit_handler(void); void exit_handler(void);
int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt);
void actor_loop(); void actor_loop();
void actor_initialize(void); void actor_initialize(void);
void actor_free(cell_rt *actor); void actor_free(cell_rt *actor);
int scheduler_actor_count(void); int scheduler_actor_count(void);
void scheduler_enable_quiescence(void); void scheduler_enable_quiescence(void);
JSValue JS_ResumeRegisterVM(JSContext *ctx);
uint64_t cell_ns(); uint64_t cell_ns();
void cell_sleep(double seconds); void cell_sleep(double seconds);
int randombytes(void *buf, size_t n); int randombytes(void *buf, size_t n);

View File

@@ -704,18 +704,6 @@ static JSValue reg_vm_binop(JSContext *ctx, int op, JSValue a, JSValue b) {
return JS_ThrowTypeError(ctx, "type mismatch in binary operation"); return JS_ThrowTypeError(ctx, "type mismatch in binary operation");
} }
/* Check for interrupt */
int reg_vm_check_interrupt(JSContext *ctx) {
if (--ctx->interrupt_counter <= 0) {
ctx->interrupt_counter = JS_INTERRUPT_COUNTER_INIT;
if (ctx->interrupt_handler) {
if (ctx->interrupt_handler(ctx->rt, ctx->interrupt_opaque)) {
return -1;
}
}
}
return 0;
}
#ifdef HAVE_ASAN #ifdef HAVE_ASAN
void __asan_on_error(void) { void __asan_on_error(void) {
@@ -757,6 +745,31 @@ void __asan_on_error(void) {
JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
JSValue this_obj, int argc, JSValue *argv, JSValue this_obj, int argc, JSValue *argv,
JSValue env, JSValue outer_frame) { JSValue env, JSValue outer_frame) {
JSGCRef frame_ref;
JSFrameRegister *frame;
uint32_t pc;
JSValue result;
/* Resume path: if VM was suspended, restore state and jump into dispatch */
if (ctx->suspended) {
ctx->suspended = 0;
JS_AddGCRef(ctx, &frame_ref);
frame_ref.val = ctx->suspended_frame_ref.val;
ctx->suspended_frame_ref.val = JS_NULL;
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function);
code = fn->u.reg.code;
env = fn->u.reg.env_record;
pc = ctx->suspended_pc;
result = JS_NULL;
#ifdef HAVE_ASAN
__asan_js_ctx = ctx;
#endif
goto vm_dispatch;
}
{
/* Normal path: set up a new call */
/* Protect env and outer_frame from GC — alloc_frame_register can trigger /* Protect env and outer_frame from GC — alloc_frame_register can trigger
collection which moves heap objects, invalidating stack-local copies */ collection which moves heap objects, invalidating stack-local copies */
JSGCRef env_gc, of_gc; JSGCRef env_gc, of_gc;
@@ -778,7 +791,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
} }
/* Allocate initial frame */ /* Allocate initial frame */
JSFrameRegister *frame = alloc_frame_register(ctx, code->nr_slots); frame = alloc_frame_register(ctx, code->nr_slots);
if (!frame) { if (!frame) {
for (int i = nargs_copy - 1; i >= 0; i--) JS_PopGCRef(ctx, &arg_gcs[i]); for (int i = nargs_copy - 1; i >= 0; i--) JS_PopGCRef(ctx, &arg_gcs[i]);
JS_PopGCRef(ctx, &this_gc); JS_PopGCRef(ctx, &this_gc);
@@ -788,7 +801,6 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
} }
/* Protect frame from GC */ /* Protect frame from GC */
JSGCRef frame_ref;
JS_AddGCRef(ctx, &frame_ref); JS_AddGCRef(ctx, &frame_ref);
frame_ref.val = JS_MKPTR(frame); frame_ref.val = JS_MKPTR(frame);
#ifdef HAVE_ASAN #ifdef HAVE_ASAN
@@ -812,9 +824,11 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
JS_PopGCRef(ctx, &of_gc); JS_PopGCRef(ctx, &of_gc);
JS_PopGCRef(ctx, &env_gc); JS_PopGCRef(ctx, &env_gc);
uint32_t pc = code->entry_point; pc = code->entry_point;
JSValue result = JS_NULL; result = JS_NULL;
} /* end normal path block */
vm_dispatch:
/* Execution loop — 32-bit instruction dispatch */ /* Execution loop — 32-bit instruction dispatch */
for (;;) { for (;;) {
#ifndef NDEBUG #ifndef NDEBUG
@@ -1405,9 +1419,18 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
VM_CASE(MACH_JMP): { VM_CASE(MACH_JMP): {
int offset = MACH_GET_sJ(instr); int offset = MACH_GET_sJ(instr);
pc = (uint32_t)((int32_t)pc + offset); pc = (uint32_t)((int32_t)pc + offset);
if (offset < 0 && reg_vm_check_interrupt(ctx)) { if (offset < 0) {
result = JS_ThrowInternalError(ctx, "interrupted"); int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
goto done; if (pf == 2) {
result = JS_ThrowInternalError(ctx, "interrupted");
goto done;
}
if (pf == 1) {
if (ctx->vm_call_depth > 0)
atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
else
goto suspend;
}
} }
VM_BREAK(); VM_BREAK();
} }
@@ -1421,9 +1444,18 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
if (cond) { if (cond) {
int offset = MACH_GET_sBx(instr); int offset = MACH_GET_sBx(instr);
pc = (uint32_t)((int32_t)pc + offset); pc = (uint32_t)((int32_t)pc + offset);
if (offset < 0 && reg_vm_check_interrupt(ctx)) { if (offset < 0) {
result = JS_ThrowInternalError(ctx, "interrupted"); int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
goto done; if (pf == 2) {
result = JS_ThrowInternalError(ctx, "interrupted");
goto done;
}
if (pf == 1) {
if (ctx->vm_call_depth > 0)
atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
else
goto suspend;
}
} }
} }
VM_BREAK(); VM_BREAK();
@@ -1438,9 +1470,18 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
if (!cond) { if (!cond) {
int offset = MACH_GET_sBx(instr); int offset = MACH_GET_sBx(instr);
pc = (uint32_t)((int32_t)pc + offset); pc = (uint32_t)((int32_t)pc + offset);
if (offset < 0 && reg_vm_check_interrupt(ctx)) { if (offset < 0) {
result = JS_ThrowInternalError(ctx, "interrupted"); int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
goto done; if (pf == 2) {
result = JS_ThrowInternalError(ctx, "interrupted");
goto done;
}
if (pf == 1) {
if (ctx->vm_call_depth > 0)
atomic_store_explicit(&ctx->pause_flag, 0, memory_order_relaxed);
else
goto suspend;
}
} }
} }
VM_BREAK(); VM_BREAK();
@@ -1953,6 +1994,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
/* C, native, or bytecode function */ /* C, native, or bytecode function */
ctx->reg_current_frame = frame_ref.val; ctx->reg_current_frame = frame_ref.val;
ctx->current_register_pc = pc > 0 ? pc - 1 : 0; ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
ctx->vm_call_depth++;
JSValue ret; JSValue ret;
if (fn->kind == JS_FUNC_KIND_C) if (fn->kind == JS_FUNC_KIND_C)
ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
@@ -1960,6 +2002,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
ret = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); ret = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
else else
ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0); ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0);
ctx->vm_call_depth--;
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
ctx->reg_current_frame = JS_NULL; ctx->reg_current_frame = JS_NULL;
if (JS_IsException(ret)) goto disrupt; if (JS_IsException(ret)) goto disrupt;
@@ -2038,6 +2081,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
/* C, native, or bytecode function: call it, then return result to our caller */ /* C, native, or bytecode function: call it, then return result to our caller */
ctx->reg_current_frame = frame_ref.val; ctx->reg_current_frame = frame_ref.val;
ctx->current_register_pc = pc > 0 ? pc - 1 : 0; ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
ctx->vm_call_depth++;
JSValue ret; JSValue ret;
if (fn->kind == JS_FUNC_KIND_C) if (fn->kind == JS_FUNC_KIND_C)
ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); ret = js_call_c_function(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
@@ -2045,6 +2089,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
ret = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]); ret = cell_native_dispatch(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1]);
else else
ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0); ret = JS_CallInternal(ctx, fn_val, fr->slots[0], c_argc, &fr->slots[1], 0);
ctx->vm_call_depth--;
frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val);
ctx->reg_current_frame = JS_NULL; ctx->reg_current_frame = JS_NULL;
if (JS_IsException(ret)) goto disrupt; if (JS_IsException(ret)) goto disrupt;
@@ -2174,6 +2219,17 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
} }
} }
suspend:
ctx->suspended = 1;
ctx->suspended_pc = pc;
ctx->suspended_frame_ref.val = frame_ref.val;
result = JS_SUSPENDED;
JS_DeleteGCRef(ctx, &frame_ref);
#ifdef HAVE_ASAN
__asan_js_ctx = NULL;
#endif
return result;
done: done:
#ifdef HAVE_ASAN #ifdef HAVE_ASAN
__asan_js_ctx = NULL; __asan_js_ctx = NULL;
@@ -2187,6 +2243,13 @@ done:
return result; return result;
} }
JSValue JS_ResumeRegisterVM(JSContext *ctx) {
if (!ctx->suspended)
return JS_ThrowInternalError(ctx, "no suspended VM to resume");
/* ctx->suspended is set; JS_CallRegisterVM will take the resume path */
return JS_CallRegisterVM(ctx, NULL, JS_NULL, 0, NULL, JS_NULL, JS_NULL);
}
/* ============================================================ /* ============================================================
MCODE Lowering — mcode JSON IR → MachInstr32 MCODE Lowering — mcode JSON IR → MachInstr32
============================================================ */ ============================================================ */

View File

@@ -33,6 +33,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdatomic.h>
#include <sys/time.h> #include <sys/time.h>
#include <time.h> #include <time.h>
#if defined(__APPLE__) #if defined(__APPLE__)
@@ -898,12 +899,12 @@ typedef struct JSArray {
JSValue values[]; /* inline flexible array member */ JSValue values[]; /* inline flexible array member */
} JSArray; } JSArray;
/* JSBlob — not allocated on GC heap (blobs use JSRecord + opaque). /* JSBlob — inline bit data on the GC heap.
Struct kept for reference; gc_object_size/gc_scan_object do not handle OBJ_BLOB. */ cap56 = capacity in bits, S bit = stone (immutable). */
typedef struct JSBlob { typedef struct JSBlob {
objhdr_t mist_hdr; objhdr_t mist_hdr; /* type=OBJ_BLOB, cap56=capacity_bits, S=stone */
word_t length; word_t length; /* used bits */
uint8_t bits[]; word_t bits[]; /* inline bit data, ceil(cap56/64) words */
} JSBlob; } JSBlob;
typedef struct JSText { typedef struct JSText {
@@ -1063,10 +1064,6 @@ static JS_BOOL JSText_equal_ascii (const JSText *text, JSValue imm) {
/* Forward declarations for stone arena functions (defined after JSContext) */ /* Forward declarations for stone arena functions (defined after JSContext) */
/* must be large enough to have a negligible runtime cost and small
enough to call the interrupt callback often. */
#define JS_INTERRUPT_COUNTER_INIT 10000
/* Auto-rooted C call argv — GC updates values in-place */ /* Auto-rooted C call argv — GC updates values in-place */
typedef struct CCallRoot { typedef struct CCallRoot {
JSValue *argv; /* points to C-stack-local array */ JSValue *argv; /* points to C-stack-local array */
@@ -1123,8 +1120,8 @@ struct JSContext {
uint64_t random_state; uint64_t random_state;
/* when the counter reaches zero, JSRutime.interrupt_handler is called */ /* 0 = normal, 1 = suspend (fast timer), 2 = kill (slow timer) */
int interrupt_counter; _Atomic int pause_flag;
/* if NULL, RegExp compilation is not supported */ /* if NULL, RegExp compilation is not supported */
JSValue (*compile_regexp) (JSContext *ctx, JSValue pattern, JSValue flags); JSValue (*compile_regexp) (JSContext *ctx, JSValue pattern, JSValue flags);
@@ -1138,8 +1135,12 @@ struct JSContext {
JSValue reg_current_frame; /* current JSFrameRegister being executed */ JSValue reg_current_frame; /* current JSFrameRegister being executed */
uint32_t current_register_pc; /* PC at exception time */ uint32_t current_register_pc; /* PC at exception time */
JSInterruptHandler *interrupt_handler; /* VM suspend/resume state */
void *interrupt_opaque; int suspended; /* 1 = VM was suspended (not exception) */
JSGCRef suspended_frame_ref; /* GC-rooted saved frame for resume */
uint32_t suspended_pc; /* saved PC for resume */
int vm_call_depth; /* 0 = pure bytecode, >0 = C frames on stack */
size_t heap_memory_limit; /* 0 = no limit, else max heap bytes */
JSValue current_exception; JSValue current_exception;
@@ -1434,7 +1435,7 @@ static JSValue js_cell_number_remainder (JSContext *ctx, JSValue this_val, int a
static JSValue js_cell_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_cell_text_format (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_cell_text_format (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
static JSValue js_print (JSContext *ctx, JSValue this_val, int argc, JSValue *argv); static JSValue js_print (JSContext *ctx, JSValue this_val, int argc, JSValue *argv);
cJSON *JS_GetStack(JSContext *ctx); JSValue JS_GetStack(JSContext *ctx);
JSValue JS_ThrowOutOfMemory (JSContext *ctx); JSValue JS_ThrowOutOfMemory (JSContext *ctx);
@@ -1545,25 +1546,14 @@ static inline void set_value (JSContext *ctx, JSValue *pval, JSValue new_val) {
void JS_ThrowInterrupted (JSContext *ctx); void JS_ThrowInterrupted (JSContext *ctx);
static no_inline __exception int __js_poll_interrupts (JSContext *ctx) { static inline __exception int js_poll_interrupts (JSContext *ctx) {
ctx->interrupt_counter = JS_INTERRUPT_COUNTER_INIT; if (unlikely (atomic_load_explicit (&ctx->pause_flag, memory_order_relaxed) >= 2)) {
if (ctx->interrupt_handler) { JS_ThrowInterrupted (ctx);
if (ctx->interrupt_handler (ctx->rt, ctx->interrupt_opaque)) { return -1;
JS_ThrowInterrupted (ctx);
return -1;
}
} }
return 0; return 0;
} }
static inline __exception int js_poll_interrupts (JSContext *ctx) {
if (unlikely (--ctx->interrupt_counter <= 0)) {
return __js_poll_interrupts (ctx);
} else {
return 0;
}
}
/* === PPretext (parser pretext, system-malloc, used by cell_js.c parser) === */ /* === PPretext (parser pretext, system-malloc, used by cell_js.c parser) === */
typedef struct PPretext { typedef struct PPretext {
uint32_t *data; uint32_t *data;
@@ -1613,7 +1603,7 @@ uint64_t get_text_hash (JSText *text);
void pack_utf32_to_words (const uint32_t *utf32, uint32_t len, uint64_t *packed); 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); int text_equal (JSText *a, const uint64_t *packed_b, uint32_t len_b);
void print_backtrace (JSContext *ctx, const char *filename, int line_num, int col_num); void print_backtrace (JSContext *ctx, JSValue stack, 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 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); 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);
PPretext *ppretext_init (int capacity); PPretext *ppretext_init (int capacity);
@@ -1662,7 +1652,6 @@ JSValue js_key_from_string (JSContext *ctx, JSValue val);
JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame); JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, JSValue this_obj, int argc, JSValue *argv, JSValue env, JSValue outer_frame);
JSValue js_new_native_function(JSContext *ctx, void *fn_ptr, void *dl_handle, uint16_t nr_slots, int arity, JSValue outer_frame); JSValue js_new_native_function(JSContext *ctx, void *fn_ptr, void *dl_handle, uint16_t nr_slots, int arity, JSValue outer_frame);
JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count); JSFrameRegister *alloc_frame_register(JSContext *ctx, int slot_count);
int reg_vm_check_interrupt(JSContext *ctx);
#endif /* QUICKJS_INTERNAL_H */ #endif /* QUICKJS_INTERNAL_H */

View File

@@ -294,6 +294,12 @@ JS_IsShortFloat (JSValue v) {
#define JS_FALSE ((JSValue)JS_TAG_BOOL) #define JS_FALSE ((JSValue)JS_TAG_BOOL)
#define JS_TRUE ((JSValue)(JS_TAG_BOOL | (1 << 5))) #define JS_TRUE ((JSValue)(JS_TAG_BOOL | (1 << 5)))
#define JS_EXCEPTION ((JSValue)JS_TAG_EXCEPTION) #define JS_EXCEPTION ((JSValue)JS_TAG_EXCEPTION)
#define JS_TAG_SUSPENDED 0x13 /* 10011 - distinct special tag */
#define JS_SUSPENDED ((JSValue)JS_TAG_SUSPENDED)
static inline JS_BOOL JS_IsSuspended(JSValue v) {
return JS_VALUE_GET_TAG(v) == JS_TAG_SUSPENDED;
}
#ifndef JS_DEFAULT_STACK_SIZE #ifndef JS_DEFAULT_STACK_SIZE
#define JS_DEFAULT_STACK_SIZE (1024 * 1024) #define JS_DEFAULT_STACK_SIZE (1024 * 1024)
@@ -333,10 +339,14 @@ void JS_SetMaxStackSize (JSContext *ctx, size_t stack_size);
used to check stack overflow. */ used to check stack overflow. */
void JS_UpdateStackTop (JSContext *ctx); void JS_UpdateStackTop (JSContext *ctx);
/* return != 0 if the JS code needs to be interrupted */ /* Returns the current VM call depth (0 = pure bytecode, >0 = C frames) */
typedef int JSInterruptHandler (JSRuntime *rt, void *opaque); int JS_GetVMCallDepth(JSContext *ctx);
void JS_SetInterruptHandler (JSContext *ctx, JSInterruptHandler *cb,
void *opaque); /* Set per-context heap memory limit (0 = no limit) */
void JS_SetHeapMemoryLimit(JSContext *ctx, size_t limit);
/* Set the pause flag on a context (0=normal, 1=suspend, 2=kill) */
void JS_SetPauseFlag(JSContext *ctx, int value);
JS_BOOL JS_IsLiveObject (JSRuntime *rt, JSValue obj); JS_BOOL JS_IsLiveObject (JSRuntime *rt, JSValue obj);
@@ -1037,10 +1047,10 @@ void JS_DumpMachBin(JSContext *ctx, const uint8_t *data, size_t size, JSValue en
/* Compile mcode JSON IR to MachCode binary. */ /* Compile mcode JSON IR to MachCode binary. */
MachCode *mach_compile_mcode(struct cJSON *mcode_json); MachCode *mach_compile_mcode(struct cJSON *mcode_json);
/* Get stack trace as cJSON array of frame objects. /* Get stack trace as JS array of {fn, file, line, col} objects.
Returns NULL if no register VM frame is active. Returns an empty array if no register VM frame is active.
Caller must call cJSON_Delete() on the result. */ Does NOT clear reg_current_frame — caller is responsible if needed. */
struct cJSON *JS_GetStack (JSContext *ctx); JSValue JS_GetStack (JSContext *ctx);
#undef js_unlikely #undef js_unlikely
#undef inline #undef inline

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,9 @@ typedef struct actor_node {
typedef enum { typedef enum {
TIMER_JS, TIMER_JS,
TIMER_NATIVE_REMOVE TIMER_NATIVE_REMOVE,
TIMER_PAUSE,
TIMER_KILL
} timer_type; } timer_type;
typedef struct { typedef struct {
@@ -30,23 +32,28 @@ typedef struct {
cell_rt *actor; cell_rt *actor;
uint32_t timer_id; uint32_t timer_id;
timer_type type; timer_type type;
uint32_t turn_gen; /* generation at registration time */
} timer_node; } timer_node;
static timer_node *timer_heap = NULL; static timer_node *timer_heap = NULL;
// Priority queue indices
#define PQ_READY 0
#define PQ_REFRESHED 1
#define PQ_EXHAUSTED 2
#define PQ_SLOW 3
#define PQ_COUNT 4
// --- 3. The Global Engine State --- // --- 3. The Global Engine State ---
static struct { static struct {
pthread_mutex_t lock; // Protects queue, shutdown flag, and timers pthread_mutex_t lock; // Protects queue, shutdown flag, and timers
pthread_cond_t wake_cond; // Wakes up workers pthread_cond_t wake_cond; // Wakes up workers
pthread_cond_t timer_cond; // Wakes up the timer thread pthread_cond_t timer_cond; // Wakes up the timer thread
pthread_cond_t main_cond; // Wakes up the main thread pthread_cond_t main_cond; // Wakes up the main thread
actor_node *head; // Ready Queue Head actor_node *q_head[PQ_COUNT], *q_tail[PQ_COUNT]; // worker queues
actor_node *tail; // Ready Queue Tail actor_node *mq_head[PQ_COUNT], *mq_tail[PQ_COUNT]; // main-thread queues
actor_node *main_head; // Main Thread Queue Head
actor_node *main_tail; // Main Thread Queue Tail
int shutting_down; int shutting_down;
int quiescence_enabled; // set after bootstrap, before actor_loop int quiescence_enabled; // set after bootstrap, before actor_loop
_Atomic int quiescent_count; // actors idle with no messages and no timers _Atomic int quiescent_count; // actors idle with no messages and no timers
@@ -56,6 +63,33 @@ static struct {
pthread_t timer_thread; pthread_t timer_thread;
} engine; } engine;
static int state_to_pq(int state) {
switch (state) {
case ACTOR_REFRESHED: return PQ_REFRESHED;
case ACTOR_EXHAUSTED: return PQ_EXHAUSTED;
case ACTOR_SLOW: return PQ_SLOW;
default: return PQ_READY;
}
}
static actor_node *dequeue_priority(actor_node *heads[], actor_node *tails[]) {
for (int i = 0; i < PQ_COUNT; i++) {
if (heads[i]) {
actor_node *n = heads[i];
heads[i] = n->next;
if (!heads[i]) tails[i] = NULL;
return n;
}
}
return NULL;
}
static int has_any_work(actor_node *heads[]) {
for (int i = 0; i < PQ_COUNT; i++)
if (heads[i]) return 1;
return 0;
}
static pthread_mutex_t *actors_mutex; static pthread_mutex_t *actors_mutex;
static struct { char *key; cell_rt *value; } *actors = NULL; static struct { char *key; cell_rt *value; } *actors = NULL;
@@ -91,7 +125,9 @@ uint32_t actor_remove_cb(cell_rt *actor, uint32_t id, uint32_t interval);
void actor_turn(cell_rt *actor); void actor_turn(cell_rt *actor);
void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, timer_type type) { void heap_push(uint64_t when, cell_rt *actor, uint32_t timer_id, timer_type type) {
timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .type = type }; timer_node node = { .execute_at_ns = when, .actor = actor, .timer_id = timer_id, .type = type, .turn_gen = 0 };
if (type == TIMER_PAUSE || type == TIMER_KILL)
node.turn_gen = atomic_load_explicit(&actor->turn_gen, memory_order_relaxed);
arrput(timer_heap, node); arrput(timer_heap, node);
// Bubble up // Bubble up
@@ -161,8 +197,19 @@ void *timer_thread_func(void *arg) {
if (t.type == TIMER_NATIVE_REMOVE) { if (t.type == TIMER_NATIVE_REMOVE) {
actor_remove_cb(t.actor, t.timer_id, 0); actor_remove_cb(t.actor, t.timer_id, 0);
} else if (t.type == TIMER_PAUSE || t.type == TIMER_KILL) {
/* Only fire if turn_gen still matches (stale timers are ignored) */
uint32_t cur = atomic_load_explicit(&t.actor->turn_gen, memory_order_relaxed);
if (cur == t.turn_gen) {
if (t.type == TIMER_PAUSE) {
JS_SetPauseFlag(t.actor->context, 1);
} else {
t.actor->disrupt = 1;
JS_SetPauseFlag(t.actor->context, 2);
}
}
} else { } else {
pthread_mutex_lock(t.actor->msg_mutex); pthread_mutex_lock(t.actor->msg_mutex);
int idx = hmgeti(t.actor->timers, t.timer_id); int idx = hmgeti(t.actor->timers, t.timer_id);
if (idx != -1) { if (idx != -1) {
JSValue cb = t.actor->timers[idx].value; JSValue cb = t.actor->timers[idx].value;
@@ -196,33 +243,48 @@ void *timer_thread_func(void *arg) {
return NULL; return NULL;
} }
void enqueue_actor_priority(cell_rt *actor) {
actor_node *n = malloc(sizeof(actor_node));
n->actor = actor;
n->next = NULL;
int pq = state_to_pq(actor->state);
pthread_mutex_lock(&engine.lock);
if (actor->main_thread_only) {
if (engine.mq_tail[pq]) engine.mq_tail[pq]->next = n;
else engine.mq_head[pq] = n;
engine.mq_tail[pq] = n;
pthread_cond_signal(&engine.main_cond);
} else {
if (engine.q_tail[pq]) engine.q_tail[pq]->next = n;
else engine.q_head[pq] = n;
engine.q_tail[pq] = n;
pthread_cond_signal(&engine.wake_cond);
}
pthread_mutex_unlock(&engine.lock);
}
void *actor_runner(void *arg) { void *actor_runner(void *arg) {
while (1) { while (1) {
pthread_mutex_lock(&engine.lock); pthread_mutex_lock(&engine.lock);
// Wait while queue is empty AND not shutting down
while (engine.head == NULL && !engine.shutting_down) {
pthread_cond_wait(&engine.wake_cond, &engine.lock);
}
if (engine.shutting_down && engine.head == NULL) { while (!has_any_work(engine.q_head) && !engine.shutting_down) {
pthread_mutex_unlock(&engine.lock); pthread_cond_wait(&engine.wake_cond, &engine.lock);
break; // Exit thread
}
// Pop from Linked List
actor_node *node = engine.head;
engine.head = node->next;
if (engine.head == NULL) engine.tail = NULL;
pthread_mutex_unlock(&engine.lock);
if (node) {
actor_turn(node->actor);
free(node);
}
} }
return NULL;
if (engine.shutting_down && !has_any_work(engine.q_head)) {
pthread_mutex_unlock(&engine.lock);
break;
}
actor_node *node = dequeue_priority(engine.q_head, engine.q_tail);
pthread_mutex_unlock(&engine.lock);
if (node) {
actor_turn(node->actor);
free(node);
}
}
return NULL;
} }
void actor_initialize(void) { void actor_initialize(void) {
@@ -230,12 +292,14 @@ void actor_initialize(void) {
pthread_cond_init(&engine.wake_cond, NULL); pthread_cond_init(&engine.wake_cond, NULL);
pthread_cond_init(&engine.timer_cond, NULL); pthread_cond_init(&engine.timer_cond, NULL);
pthread_cond_init(&engine.main_cond, NULL); pthread_cond_init(&engine.main_cond, NULL);
engine.shutting_down = 0; engine.shutting_down = 0;
engine.head = NULL; for (int i = 0; i < PQ_COUNT; i++) {
engine.tail = NULL; engine.q_head[i] = NULL;
engine.main_head = NULL; engine.q_tail[i] = NULL;
engine.main_tail = NULL; engine.mq_head[i] = NULL;
engine.mq_tail[i] = NULL;
}
actors_mutex = malloc(sizeof(pthread_mutex_t)); actors_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(actors_mutex, NULL); pthread_mutex_init(actors_mutex, NULL);
@@ -295,7 +359,6 @@ void actor_free(cell_rt *actor)
arrfree(actor->letters); arrfree(actor->letters);
JS_SetInterruptHandler(js, NULL, NULL);
JS_FreeContext(js); JS_FreeContext(js);
free(actor->id); free(actor->id);
@@ -406,30 +469,7 @@ void set_actor_state(cell_rt *actor)
#endif #endif
actor->state = ACTOR_READY; actor->state = ACTOR_READY;
actor->ar = 0; actor->ar = 0;
enqueue_actor_priority(actor);
actor_node *n = malloc(sizeof(actor_node));
n->actor = actor;
n->next = NULL;
pthread_mutex_lock(&engine.lock);
if (actor->main_thread_only) {
if (engine.main_tail) {
engine.main_tail->next = n;
} else {
engine.main_head = n;
}
engine.main_tail = n;
pthread_cond_signal(&engine.main_cond);
} else {
if (engine.tail) {
engine.tail->next = n;
} else {
engine.head = n;
}
engine.tail = n;
pthread_cond_signal(&engine.wake_cond);
}
pthread_mutex_unlock(&engine.lock);
} else if (!hmlen(actor->timers)) { } else if (!hmlen(actor->timers)) {
// No messages AND no timers // No messages AND no timers
@@ -531,26 +571,23 @@ cell_rt *get_actor(char *id)
void actor_loop() void actor_loop()
{ {
while (!engine.shutting_down) { // Direct read safe enough here or use lock while (!engine.shutting_down) {
pthread_mutex_lock(&engine.lock); pthread_mutex_lock(&engine.lock);
while (engine.main_head == NULL && !engine.shutting_down) { while (!has_any_work(engine.mq_head) && !engine.shutting_down) {
pthread_cond_wait(&engine.main_cond, &engine.lock); pthread_cond_wait(&engine.main_cond, &engine.lock);
} }
if (engine.shutting_down && engine.main_head == NULL) { if (engine.shutting_down && !has_any_work(engine.mq_head)) {
pthread_mutex_unlock(&engine.lock); pthread_mutex_unlock(&engine.lock);
break; break;
} }
actor_node *node = engine.main_head; actor_node *node = dequeue_priority(engine.mq_head, engine.mq_tail);
engine.main_head = node->next;
if (engine.main_head == NULL) engine.main_tail = NULL;
pthread_mutex_unlock(&engine.lock); pthread_mutex_unlock(&engine.lock);
if (node) { if (node) {
actor_turn(node->actor); actor_turn(node->actor);
free(node); free(node);
} }
} }
} }
@@ -606,10 +643,6 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
return NULL; return NULL;
} }
int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt)
{
return engine.shutting_down || crt->disrupt;
}
const char *send_message(const char *id, void *msg) const char *send_message(const char *id, void *msg)
{ {
@@ -638,58 +671,131 @@ const char *send_message(const char *id, void *msg)
void actor_turn(cell_rt *actor) void actor_turn(cell_rt *actor)
{ {
pthread_mutex_lock(actor->mutex); pthread_mutex_lock(actor->mutex);
int prev_state = actor->state;
actor->state = ACTOR_RUNNING; actor->state = ACTOR_RUNNING;
#ifdef ACTOR_TRACE
fprintf(stderr, "[ACTOR_TRACE] %s: %d -> RUNNING\n",
actor->name ? actor->name : actor->id, prev_state);
#endif
if (actor->trace_hook) if (actor->trace_hook)
actor->trace_hook(actor->name, CELL_HOOK_ENTER); actor->trace_hook(actor->name, CELL_HOOK_ENTER);
pthread_mutex_lock(actor->msg_mutex);
JSValue result; JSValue result;
if (actor->vm_suspended) {
/* RESUME path: continue suspended turn under kill timer only */
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
JS_SetPauseFlag(actor->context, 0);
actor->turn_start_ns = cell_ns();
/* Register kill timer only for resume */
pthread_mutex_lock(&engine.lock);
heap_push(actor->turn_start_ns + ACTOR_SLOW_TIMER_NS,
actor, 0, TIMER_KILL);
pthread_cond_signal(&engine.timer_cond);
pthread_mutex_unlock(&engine.lock);
result = JS_ResumeRegisterVM(actor->context);
actor->vm_suspended = 0;
if (JS_IsSuspended(result)) {
/* Still suspended after kill timer — shouldn't happen, kill it */
actor->disrupt = 1;
goto ENDTURN;
}
if (JS_IsException(result)) {
if (!uncaught_exception(actor->context, result))
actor->disrupt = 1;
}
actor->slow_strikes++;
#ifdef ACTOR_TRACE
fprintf(stderr, "[ACTOR_TRACE] %s: slow strike %d/%d\n",
actor->name ? actor->name : actor->id,
actor->slow_strikes, ACTOR_SLOW_STRIKES_MAX);
#endif
if (actor->slow_strikes >= ACTOR_SLOW_STRIKES_MAX) {
#ifdef ACTOR_TRACE
fprintf(stderr, "[ACTOR_TRACE] %s: %d slow strikes, killing\n",
actor->name ? actor->name : actor->id, actor->slow_strikes);
#endif
actor->disrupt = 1;
}
goto ENDTURN;
}
/* NORMAL path: pop a message and execute */
pthread_mutex_lock(actor->msg_mutex);
int pending = arrlen(actor->letters); int pending = arrlen(actor->letters);
if (!pending) { if (!pending) {
pthread_mutex_unlock(actor->msg_mutex); pthread_mutex_unlock(actor->msg_mutex);
goto ENDTURN; goto ENDTURN;
} }
#ifdef SCHEDULER_DEBUG #ifdef SCHEDULER_DEBUG
fprintf(stderr, "actor_turn: %s has %d letters, type=%d\n", actor->name ? actor->name : actor->id, pending, actor->letters[0].type); fprintf(stderr, "actor_turn: %s has %d letters, type=%d\n",
actor->name ? actor->name : actor->id, pending, actor->letters[0].type);
#endif #endif
letter l = actor->letters[0]; letter l = actor->letters[0];
arrdel(actor->letters, 0); arrdel(actor->letters, 0);
pthread_mutex_unlock(actor->msg_mutex); pthread_mutex_unlock(actor->msg_mutex);
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
JS_SetPauseFlag(actor->context, 0);
actor->turn_start_ns = cell_ns();
/* Register both pause and kill timers */
pthread_mutex_lock(&engine.lock);
heap_push(actor->turn_start_ns + ACTOR_FAST_TIMER_NS,
actor, 0, TIMER_PAUSE);
heap_push(actor->turn_start_ns + ACTOR_SLOW_TIMER_NS + ACTOR_FAST_TIMER_NS,
actor, 0, TIMER_KILL);
pthread_cond_signal(&engine.timer_cond);
pthread_mutex_unlock(&engine.lock);
if (l.type == LETTER_BLOB) { if (l.type == LETTER_BLOB) {
// Create a JS blob from the C blob size_t size = blob_length(l.blob_data) / 8;
size_t size = blob_length(l.blob_data) / 8; // Convert bits to bytes JSValue arg = js_new_blob_stoned_copy(actor->context,
#ifdef SCHEDULER_DEBUG (void *)blob_data(l.blob_data), size);
fprintf(stderr, "actor_turn BLOB: %s size=%zu, calling message_handle\n", actor->name ? actor->name : actor->id, size);
#endif
JSValue arg = js_new_blob_stoned_copy(actor->context, (void*)blob_data(l.blob_data), size);
blob_destroy(l.blob_data); blob_destroy(l.blob_data);
result = JS_Call(actor->context, actor->message_handle_ref.val, JS_NULL, 1, &arg); result = JS_Call(actor->context, actor->message_handle_ref.val,
JS_NULL, 1, &arg);
if (JS_IsSuspended(result)) {
actor->vm_suspended = 1;
actor->state = ACTOR_SLOW;
JS_FreeValue(actor->context, arg);
goto ENDTURN_SLOW;
}
if (!uncaught_exception(actor->context, result)) if (!uncaught_exception(actor->context, result))
actor->disrupt = 1; actor->disrupt = 1;
JS_FreeValue(actor->context, arg); JS_FreeValue(actor->context, arg);
} else if (l.type == LETTER_CALLBACK) { } else if (l.type == LETTER_CALLBACK) {
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL); result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
if (JS_IsSuspended(result)) {
actor->vm_suspended = 1;
actor->state = ACTOR_SLOW;
JS_FreeValue(actor->context, l.callback);
goto ENDTURN_SLOW;
}
if (!uncaught_exception(actor->context, result)) if (!uncaught_exception(actor->context, result))
actor->disrupt = 1; actor->disrupt = 1;
JS_FreeValue(actor->context, l.callback); JS_FreeValue(actor->context, l.callback);
} }
if (actor->disrupt) goto ENDTURN; if (actor->disrupt) goto ENDTURN;
actor->slow_strikes = 0; /* completed within fast timer */
ENDTURN: ENDTURN:
/* Invalidate any outstanding pause/kill timers for this turn */
atomic_fetch_add_explicit(&actor->turn_gen, 1, memory_order_relaxed);
actor->state = ACTOR_IDLE; actor->state = ACTOR_IDLE;
if (actor->trace_hook) if (actor->trace_hook)
actor->trace_hook(actor->name, CELL_HOOK_EXIT); actor->trace_hook(actor->name, CELL_HOOK_EXIT);
if (actor->disrupt) { if (actor->disrupt) {
/* Actor must die. Unlock before freeing so actor_free can
lock/unlock/destroy the mutex without use-after-free. */
#ifdef SCHEDULER_DEBUG #ifdef SCHEDULER_DEBUG
fprintf(stderr, "actor_turn ENDTURN: %s disrupted, freeing\n", actor->name ? actor->name : actor->id); fprintf(stderr, "actor_turn ENDTURN: %s disrupted, freeing\n",
actor->name ? actor->name : actor->id);
#endif #endif
pthread_mutex_unlock(actor->mutex); pthread_mutex_unlock(actor->mutex);
actor_free(actor); actor_free(actor);
@@ -697,10 +803,21 @@ void actor_turn(cell_rt *actor)
} }
#ifdef SCHEDULER_DEBUG #ifdef SCHEDULER_DEBUG
fprintf(stderr, "actor_turn ENDTURN: %s has %ld letters, calling set_actor_state\n", actor->name ? actor->name : actor->id, (long)arrlen(actor->letters)); fprintf(stderr, "actor_turn ENDTURN: %s has %ld letters, calling set_actor_state\n",
actor->name ? actor->name : actor->id, (long)arrlen(actor->letters));
#endif #endif
set_actor_state(actor); set_actor_state(actor);
pthread_mutex_unlock(actor->mutex);
return;
ENDTURN_SLOW:
#ifdef ACTOR_TRACE
fprintf(stderr, "[ACTOR_TRACE] %s: suspended mid-turn -> SLOW\n",
actor->name ? actor->name : actor->id);
#endif
if (actor->trace_hook)
actor->trace_hook(actor->name, CELL_HOOK_EXIT);
enqueue_actor_priority(actor);
pthread_mutex_unlock(actor->mutex); pthread_mutex_unlock(actor->mutex);
} }

View File

@@ -142,7 +142,6 @@ void actor_free(cell_rt *actor)
arrfree(actor->letters); arrfree(actor->letters);
JS_SetInterruptHandler(js, NULL, NULL);
JS_FreeContext(js); JS_FreeContext(js);
free(actor->id); free(actor->id);
@@ -353,10 +352,6 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
return NULL; return NULL;
} }
int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt)
{
return shutting_down || crt->disrupt;
}
const char *send_message(const char *id, void *msg) const char *send_message(const char *id, void *msg)
{ {

View File

@@ -1,22 +1,20 @@
// streamline.ce — run the full compile + optimize pipeline // streamline.ce — run the full compile + optimize pipeline
// //
// Usage: // Usage:
// pit streamline <file> Full optimized IR as JSON (default) // cell streamline <file> Full optimized IR as JSON (default)
// pit streamline --stats <file> Summary stats per function // cell streamline --stats <file> Summary stats per function
// pit streamline --ir <file> Human-readable IR // cell streamline --ir <file> Human-readable IR
// pit streamline --check <file> Warnings only (e.g. high slot count) // cell streamline --check <file> Warnings only (e.g. high slot count)
// cell streamline --types <file> Optimized IR with type annotations
var fd = use("fd") var fd = use("fd")
var json = use("json") var json = use("json")
var tokenize = use("tokenize") var shop = use("internal/shop")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
var show_stats = false var show_stats = false
var show_ir = false var show_ir = false
var show_check = false var show_check = false
var show_types = false
var filename = null var filename = null
var i = 0 var i = 0
@@ -27,40 +25,37 @@ for (i = 0; i < length(args); i++) {
show_ir = true show_ir = true
} else if (args[i] == '--check') { } else if (args[i] == '--check') {
show_check = true show_check = true
} else if (args[i] == '--types') {
show_types = true
} else if (args[i] == '--help' || args[i] == '-h') {
log.console("Usage: cell streamline [--stats] [--ir] [--check] [--types] <file>")
$stop()
} else if (!starts_with(args[i], '-')) { } else if (!starts_with(args[i], '-')) {
filename = args[i] filename = args[i]
} }
} }
if (!filename) { if (!filename) {
print("usage: pit streamline [--stats] [--ir] [--check] <file>") print("usage: cell streamline [--stats] [--ir] [--check] [--types] <file>")
$stop() $stop()
} }
var src = text(fd.slurp(filename)) // Deep copy mcode for before snapshot (needed by --stats, streamline mutates)
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
// Deep copy for before snapshot (needed by --stats)
var before = null var before = null
if (show_stats) { if (show_stats) {
before = json.decode(json.encode(compiled)) before = json.decode(json.encode(shop.mcode_file(filename)))
} }
var optimized = streamline(compiled) var optimized = shop.compile_file(filename)
// If no flags, default to full JSON output // If no flags, default to full JSON output
if (!show_stats && !show_ir && !show_check) { if (!show_stats && !show_ir && !show_check && !show_types) {
print(json.encode(optimized, true)) print(json.encode(optimized, true))
$stop() $stop()
} }
// --- Helpers --- // --- Helpers ---
var ir_stats = use("ir_stats")
var pad_right = function(s, w) { var pad_right = function(s, w) {
var r = s var r = s
while (length(r) < w) { while (length(r) < w) {
@@ -69,6 +64,15 @@ var pad_right = function(s, w) {
return r return r
} }
var fmt_val = function(v) {
if (is_null(v)) return "null"
if (is_number(v)) return text(v)
if (is_text(v)) return `"${v}"`
if (is_object(v)) return json.encode(v)
if (is_logical(v)) return v ? "true" : "false"
return text(v)
}
var count_nops = function(func) { var count_nops = function(func) {
var instrs = func.instructions var instrs = func.instructions
var nops = 0 var nops = 0
@@ -83,6 +87,13 @@ var count_nops = function(func) {
return nops return nops
} }
// --- Stats mode ---
var ir_stats = null
if (show_stats || show_ir) {
ir_stats = use("ir_stats")
}
var print_func_stats = function(func, before_func, name) { var print_func_stats = function(func, before_func, name) {
var nr_args = func.nr_args != null ? func.nr_args : 0 var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0 var nr_slots = func.nr_slots != null ? func.nr_slots : 0
@@ -118,6 +129,164 @@ var check_func = function(func, name) {
} }
} }
// --- Types mode (from dump_types.cm) ---
def T_UNKNOWN = "unknown"
def T_INT = "int"
def T_FLOAT = "float"
def T_NUM = "num"
def T_TEXT = "text"
def T_BOOL = "bool"
def T_NULL = "null"
def T_ARRAY = "array"
def T_RECORD = "record"
def T_FUNCTION = "function"
def int_result_ops = {
bitnot: true, bitand: true, bitor: true,
bitxor: true, shl: true, shr: true, ushr: true
}
def bool_result_ops = {
eq_int: true, ne_int: true, lt_int: true, gt_int: true,
le_int: true, ge_int: true,
eq_float: true, ne_float: true, lt_float: true, gt_float: true,
le_float: true, ge_float: true,
eq_text: true, ne_text: true, lt_text: true, gt_text: true,
le_text: true, ge_text: true,
eq_bool: true, ne_bool: true,
not: true, and: true, or: true,
is_int: true, is_text: true, is_num: true,
is_bool: true, is_null: true, is_identical: true,
is_array: true, is_func: true, is_record: true, is_stone: true
}
var access_value_type = function(val) {
if (is_number(val)) return is_integer(val) ? T_INT : T_FLOAT
if (is_text(val)) return T_TEXT
return T_UNKNOWN
}
var track_types = function(slot_types, instr) {
var op = instr[0]
var src_type = null
if (op == "access") {
slot_types[text(instr[1])] = access_value_type(instr[2])
} else if (op == "int") {
slot_types[text(instr[1])] = T_INT
} else if (op == "true" || op == "false") {
slot_types[text(instr[1])] = T_BOOL
} else if (op == "null") {
slot_types[text(instr[1])] = T_NULL
} else if (op == "move") {
src_type = slot_types[text(instr[2])]
slot_types[text(instr[1])] = src_type != null ? src_type : T_UNKNOWN
} else if (int_result_ops[op] == true) {
slot_types[text(instr[1])] = T_INT
} else if (op == "concat") {
slot_types[text(instr[1])] = T_TEXT
} else if (bool_result_ops[op] == true) {
slot_types[text(instr[1])] = T_BOOL
} else if (op == "typeof") {
slot_types[text(instr[1])] = T_TEXT
} else if (op == "array") {
slot_types[text(instr[1])] = T_ARRAY
} else if (op == "record") {
slot_types[text(instr[1])] = T_RECORD
} else if (op == "function") {
slot_types[text(instr[1])] = T_FUNCTION
} else if (op == "invoke" || op == "tail_invoke") {
slot_types[text(instr[2])] = T_UNKNOWN
} else if (op == "load_field" || op == "load_index" || op == "load_dynamic") {
slot_types[text(instr[1])] = T_UNKNOWN
} else if (op == "pop" || op == "get") {
slot_types[text(instr[1])] = T_UNKNOWN
} else if (op == "length") {
slot_types[text(instr[1])] = T_INT
} else if (op == "add" || op == "subtract" || op == "multiply" ||
op == "divide" || op == "modulo" || op == "pow" || op == "negate") {
slot_types[text(instr[1])] = T_UNKNOWN
}
return null
}
var type_annotation = function(slot_types, instr) {
var n = length(instr)
var parts = []
var j = 1
var v = null
var t = null
while (j < n - 2) {
v = instr[j]
if (is_number(v)) {
t = slot_types[text(v)]
if (t != null && t != T_UNKNOWN) {
push(parts, `s${text(v)}:${t}`)
}
}
j = j + 1
}
if (length(parts) == 0) return ""
return text(parts, " ")
}
var dump_function_typed = function(func, name) {
var nr_args = func.nr_args != null ? func.nr_args : 0
var nr_slots = func.nr_slots != null ? func.nr_slots : 0
var instrs = func.instructions
var slot_types = {}
var i = 0
var pc = 0
var instr = null
var op = null
var n = 0
var annotation = null
var operand_parts = null
var j = 0
var operands = null
var pc_str = null
var op_str = null
var line = null
print(`\n=== ${name} (args=${text(nr_args)}, slots=${text(nr_slots)}) ===`)
if (instrs == null || length(instrs) == 0) {
print(" (empty)")
return null
}
while (i < length(instrs)) {
instr = instrs[i]
if (is_text(instr)) {
if (starts_with(instr, "_nop_")) {
i = i + 1
continue
}
slot_types = {}
print(`${instr}:`)
} else if (is_array(instr)) {
op = instr[0]
n = length(instr)
annotation = type_annotation(slot_types, instr)
operand_parts = []
j = 1
while (j < n - 2) {
push(operand_parts, fmt_val(instr[j]))
j = j + 1
}
operands = text(operand_parts, ", ")
pc_str = pad_right(text(pc), 5)
op_str = pad_right(op, 14)
line = pad_right(` ${pc_str} ${op_str} ${operands}`, 50)
if (length(annotation) > 0) {
print(`${line} ; ${annotation}`)
} else {
print(line)
}
track_types(slot_types, instr)
pc = pc + 1
}
i = i + 1
}
return null
}
// --- Process functions --- // --- Process functions ---
var main_name = optimized.name != null ? optimized.name : "<main>" var main_name = optimized.name != null ? optimized.name : "<main>"
@@ -141,6 +310,9 @@ if (optimized.main != null) {
if (show_check) { if (show_check) {
check_func(optimized.main, main_name) check_func(optimized.main, main_name)
} }
if (show_types) {
dump_function_typed(optimized.main, main_name)
}
} }
// Sub-functions // Sub-functions
@@ -160,6 +332,9 @@ if (optimized.functions != null) {
if (show_check) { if (show_check) {
check_func(func, fname) check_func(func, fname)
} }
if (show_types) {
dump_function_typed(func, fname)
}
fi = fi + 1 fi = fi + 1
} }
} }

View File

@@ -336,6 +336,7 @@ var streamline = function(ir, log) {
var slot = 0 var slot = 0
var typ = null var typ = null
var rule = null var rule = null
var cw_keys = null
if (instructions == null) { if (instructions == null) {
return array(func.nr_slots) return array(func.nr_slots)
@@ -362,6 +363,19 @@ var streamline = function(ir, log) {
i = i + 1 i = i + 1
} }
// Closure-written slots can have any type at runtime — mark unknown
if (func.closure_written != null) {
cw_keys = array(func.closure_written)
k = 0
while (k < length(cw_keys)) {
slot = number(cw_keys[k])
if (slot >= 0 && slot < length(write_types)) {
write_types[slot] = T_UNKNOWN
}
k = k + 1
}
}
// Filter to only slots with known (non-unknown) types // Filter to only slots with known (non-unknown) types
k = 0 k = 0
while (k < length(write_types)) { while (k < length(write_types)) {
@@ -1475,6 +1489,100 @@ var streamline = function(ir, log) {
return null return null
} }
// =========================================================
// Pre-pass: mark closure-written slots
// Scans child functions for 'put' instructions and annotates
// the target ancestor func with closure_written[slot] = true.
// =========================================================
var mark_closure_writes = function(ir) {
var functions = ir.functions != null ? ir.functions : []
var fc = length(functions)
var parent_of = array(fc, -1)
var instrs = null
var instr = null
var fi = 0
var i = 0
var j = 0
var level = 0
var anc = 0
var slot = 0
var target = null
if (fc == 0) {
return null
}
// Build parent_of map
if (ir.main != null && ir.main.instructions != null) {
instrs = ir.main.instructions
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && instr[0] == "function") {
if (instr[2] >= 0 && instr[2] < fc) {
parent_of[instr[2]] = fc
}
}
i = i + 1
}
}
fi = 0
while (fi < fc) {
instrs = functions[fi].instructions
if (instrs != null) {
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && instr[0] == "function") {
if (instr[2] >= 0 && instr[2] < fc) {
parent_of[instr[2]] = fi
}
}
i = i + 1
}
}
fi = fi + 1
}
// Scan for 'put' instructions and mark ancestor slots
fi = 0
while (fi < fc) {
instrs = functions[fi].instructions
if (instrs != null) {
i = 0
while (i < length(instrs)) {
instr = instrs[i]
if (is_array(instr) && instr[0] == "put") {
slot = instr[2]
level = instr[3]
anc = fi
j = 0
while (j < level && anc >= 0) {
anc = parent_of[anc]
j = j + 1
}
if (anc >= 0) {
if (anc == fc) {
target = ir.main
} else {
target = functions[anc]
}
if (target != null) {
if (target.closure_written == null) {
target.closure_written = {}
}
target.closure_written[text(slot)] = true
}
}
}
i = i + 1
}
}
fi = fi + 1
}
return null
}
// ========================================================= // =========================================================
// Compose all passes // Compose all passes
// ========================================================= // =========================================================
@@ -1530,6 +1638,12 @@ var streamline = function(ir, log) {
return null return null
} }
// Pre-pass: mark slots written by child closures via 'put' instructions.
// Without this, infer_slot_write_types would assume those slots keep their
// initial type (e.g. T_NULL from 'var x = null'), causing the type-check
// eliminator to mis-optimize comparisons on closure-written variables.
mark_closure_writes(ir)
// Process main function // Process main function
if (ir.main != null) { if (ir.main != null) {
optimize_function(ir.main, log) optimize_function(ir.main, log)

View File

@@ -1,3 +1,5 @@
// cell test - Run tests for packages
var shop = use('internal/shop') var shop = use('internal/shop')
var pkg = use('package') var pkg = use('package')
var fd = use('fd') var fd = use('fd')

View File

@@ -0,0 +1,12 @@
// Test: actor that allocates too much memory is killed
// Parent starts a child that allocates arrays in a loop.
// The child should be killed when it exceeds the heap memory limit.
$start(function(event) {
if (event.type == 'greet') {
// child started, wait for it to die from memory abuse
}
if (event.type == 'disrupt') {
print("PASS: memory abusing actor killed")
$stop()
}
}, 'tests/hang_actor_memory')

View File

@@ -0,0 +1,13 @@
// Test: actor with infinite loop is killed by slow timer
// Parent starts a child that hangs forever. The child should be
// killed by the slow timer, triggering a 'disrupt' event.
$start(function(event) {
if (event.type == 'greet') {
// child started, just wait for it to die
}
if (event.type == 'disrupt') {
// child was killed by the timer — success
print("PASS: slow actor killed by timer")
$stop()
}
}, 'tests/hang_actor')

View File

@@ -0,0 +1,20 @@
// Test: actor is suspended by fast timer and resumed under slow timer,
// completing the computation correctly.
$receiver(function(msg) {
if (msg.sum == 12499997500000) {
print("PASS: actor suspended and resumed correctly")
} else {
print(`FAIL: expected 12499997500000, got ${msg.sum}`)
}
$stop()
})
$start(function(event) {
if (event.type == 'greet') {
send(event.actor, {count: 5000000, reply: $self})
}
if (event.type == 'disrupt') {
print("FAIL: actor was killed instead of completing")
$stop()
}
}, 'tests/slow_compute_actor')

View File

@@ -0,0 +1,5 @@
// Actor that allocates memory until killed by memory limit
var big = []
while (1) {
big[] = array(10000, 0)
}

View File

@@ -0,0 +1,11 @@
// Child actor that does a slow computation and replies with the result
$receiver(function(msg) {
var n = msg.count
var sum = 0
var i = 0
while (i < n) {
sum = sum + i
i = i + 1
}
send(msg.reply, {sum: sum})
})

View File

@@ -1,7 +1,7 @@
var fd = use("fd") // cell tokenize - Tokenize a source file and output token stream
var json = use("json") var json = use("json")
var tokenize = use("tokenize") var shop = use("internal/shop")
var filename = args[0] var filename = args[0]
var src = text(fd.slurp(filename)) var result = shop.tokenize_file(filename)
var result = tokenize(src, filename)
print(json.encode({filename: result.filename, tokens: result.tokens})) print(json.encode({filename: result.filename, tokens: result.tokens}))

View File

@@ -1,3 +1,5 @@
// cell upgrade - Upgrade the cell installation
var shop = use('internal/shop') var shop = use('internal/shop')
var fd = use('fd') var fd = use('fd')

View File

@@ -1,2 +1,4 @@
// cell version - Show cell version
log.console("0.1.0") log.console("0.1.0")
$stop() $stop()

View File

@@ -4305,6 +4305,32 @@ run("closure set and get", function() {
assert_eq(o.get(), 42, "overwrite") assert_eq(o.get(), 42, "overwrite")
}) })
run("closure write heap values visible to outer scope", function() {
var a = null
var b = null
var c = null
var d = null
var e = null
var f1 = function() { a = 42 }
var f2 = function() { b = true }
var f3 = function() { c = "hello" }
var f4 = function() { d = {x: 1} }
var f5 = function() { e = [1, 2] }
f1()
f2()
f3()
f4()
f5()
assert_eq(a, 42, "closure write number")
assert_eq(b, true, "closure write boolean")
assert_eq(c != null, true, "closure write text not null")
assert_eq(c, "hello", "closure write text value")
assert_eq(d != null, true, "closure write object not null")
assert_eq(d.x, 1, "closure write object property")
assert_eq(e != null, true, "closure write array not null")
assert_eq(e[0], 1, "closure write array element")
})
// ============================================================================ // ============================================================================
// STRING COMPARISON OPERATORS // STRING COMPARISON OPERATORS
// ============================================================================ // ============================================================================
@@ -5121,6 +5147,380 @@ run("json roundtrip preserves types", function() {
assert_eq(decoded.sub.a, 1, "sub-object preserved") assert_eq(decoded.sub.a, 1, "sub-object preserved")
}) })
// ============================================================================
// BLOB - GC HEAP INTEGRATION
// ============================================================================
run("blob basic create and stone", function() {
var b = blob()
if (!is_blob(b)) fail("empty blob is not a blob")
b.write_bit(true)
b.write_bit(false)
b.write_bit(true)
stone(b)
if (length(b) != 3) fail("blob length should be 3, got " + text(length(b)))
if (!b.read_logical(0)) fail("bit 0 should be true")
if (b.read_logical(1)) fail("bit 1 should be false")
if (!b.read_logical(2)) fail("bit 2 should be true")
})
run("blob write_number read_number", function() {
var b = blob()
b.write_number(3.14)
b.write_number(2.718)
stone(b)
if (length(b) != 128) fail("expected 128 bits")
var pi = b.read_number(0)
var e = b.read_number(64)
if (pi != 3.14) fail("pi read back wrong: " + text(pi))
if (e != 2.718) fail("e read back wrong: " + text(e))
})
run("blob write_fit read_fit", function() {
var b = blob()
b.write_fit(42, 8)
b.write_fit(-7, 8)
stone(b)
if (b.read_fit(0, 8) != 42) fail("fit 42 failed")
if (b.read_fit(8, 8) != -7) fail("fit -7 failed")
})
run("blob write_text read_text", function() {
var b = blob()
b.write_text("hello world")
stone(b)
var t = b.read_text(0)
if (t != "hello world") fail("text roundtrip failed: " + t)
})
run("blob from text", function() {
var b = blob("abc")
stone(b)
if (length(b) != 24) fail("blob from text length wrong")
var t = text(b)
if (t != "abc") fail("blob to text failed: " + t)
})
run("blob copy slice", function() {
var b = blob()
b.write_fit(100, 16)
b.write_fit(200, 16)
b.write_fit(300, 16)
stone(b)
var slice = blob(b, 16, 32)
stone(slice)
if (slice.read_fit(0, 16) != 200) fail("blob slice failed")
})
run("blob write_blob", function() {
var a = blob()
a.write_fit(1, 8)
a.write_fit(2, 8)
var b = blob()
b.write_fit(3, 8)
b.write_blob(a)
stone(b)
if (b.read_fit(0, 8) != 3) fail("first byte wrong")
if (b.read_fit(8, 8) != 1) fail("second byte wrong")
if (b.read_fit(16, 8) != 2) fail("third byte wrong")
})
run("blob write_pad pad?", function() {
var b = blob()
b.write_fit(7, 4)
b.write_pad(8)
stone(b)
if (length(b) != 8) fail("pad didn't align to 8, got " + text(length(b)))
if (!b["pad?"](4, 8)) fail("pad? should be true")
})
run("blob w16 w32 wf", function() {
var b = blob()
b.w16(1000)
b.w32(100000)
b.wf(1.5)
stone(b)
if (length(b) != 80) fail("expected 80 bits, got " + text(length(b)))
})
run("blob is_data false for blob", function() {
var b = blob()
if (is_data(b)) fail("blob should not be is_data")
})
run("blob text hex format", function() {
var b = blob("AB")
stone(b)
var hex = text(b, "h")
if (hex != "4142") fail("hex encoding wrong: " + hex)
})
run("blob text binary format", function() {
var b = blob()
b.write_bit(true)
b.write_bit(false)
b.write_bit(true)
b.write_bit(true)
stone(b)
var bits = text(b, "b")
if (bits != "1011") fail("binary encoding wrong: " + bits)
})
run("blob(capacity) preallocates", function() {
var b = blob(1024)
if (!is_blob(b)) fail("capacity blob not a blob")
if (length(b) != 0) fail("capacity blob should start empty")
var i = 0
for (i = 0; i < 128; i = i + 1) {
b.write_fit(i, 8)
}
if (length(b) != 1024) fail("after fill length wrong")
})
run("blob(length, bool) fill", function() {
var b = blob(16, true)
stone(b)
if (length(b) != 16) fail("filled blob length wrong")
var i = 0
for (i = 0; i < 16; i = i + 1) {
if (!b.read_logical(i)) fail("bit " + text(i) + " should be true")
}
var z = blob(8, false)
stone(z)
for (i = 0; i < 8; i = i + 1) {
if (z.read_logical(i)) fail("zero bit " + text(i) + " should be false")
}
})
// --- GC stress tests: verify blobs survive collection and don't corrupt ---
run("gc blob survives collection", function() {
var b = blob()
var garbage = null
var i = 0
var v1 = null
var t = null
b.write_number(123.456)
b.write_text("test data")
// Trigger GC pressure by allocating many objects
for (i = 0; i < 500; i = i + 1) {
garbage = {a: i, b: text(i), c: [i, i+1, i+2]}
}
// blob should still be valid after GC
b.write_number(789.012)
stone(b)
v1 = b.read_number(0)
if (v1 != 123.456) fail("blob data corrupted after gc: " + text(v1))
t = b.read_text(64)
if (t != "test data") fail("blob text corrupted after gc: " + t)
})
run("gc blob growth across collections", function() {
var b = blob()
var i = 0
var junk = null
var v = null
for (i = 0; i < 200; i = i + 1) {
b.write_fit(i, 16)
junk = [text(i), {v: i}, text(i) + "_end"]
}
stone(b)
for (i = 0; i < 200; i = i + 1) {
v = b.read_fit(i * 16, 16)
if (v != i) fail("blob growth gc: slot " + text(i) + " = " + text(v))
}
})
run("gc many blobs alive simultaneously", function() {
var blobs = []
var i = 0
var b = null
var trash = null
var v1 = null
var v2 = null
for (i = 0; i < 100; i = i + 1) {
b = blob()
b.write_fit(i * 7, 16)
b.write_fit(i * 13, 16)
stone(b)
blobs[i] = b
}
for (i = 0; i < 200; i = i + 1) {
trash = {x: text(i), y: [i]}
}
for (i = 0; i < 100; i = i + 1) {
v1 = blobs[i].read_fit(0, 16)
v2 = blobs[i].read_fit(16, 16)
if (v1 != i * 7) fail("multi blob " + text(i) + " v1 = " + text(v1))
if (v2 != i * 13) fail("multi blob " + text(i) + " v2 = " + text(v2))
}
})
run("gc blob not polluting other objects", function() {
var results = []
var i = 0
var b = null
var obj = null
var tmp = null
var entry = null
var bv = null
var bt = null
for (i = 0; i < 50; i = i + 1) {
b = blob()
b.write_fit(i, 16)
b.write_text("item" + text(i))
stone(b)
obj = {index: i, name: "obj" + text(i)}
results[i] = {blob_val: b, obj_val: obj}
}
for (i = 0; i < 300; i = i + 1) {
tmp = {a: text(i), b: [i, i]}
}
for (i = 0; i < 50; i = i + 1) {
entry = results[i]
bv = entry.blob_val.read_fit(0, 16)
if (bv != i) fail("pollute test blob " + text(i) + " = " + text(bv))
bt = entry.blob_val.read_text(16)
if (bt != "item" + text(i)) fail("pollute test text " + text(i))
if (entry.obj_val.index != i) fail("pollute test obj index " + text(i))
if (entry.obj_val.name != "obj" + text(i)) fail("pollute test obj name " + text(i))
}
})
run("gc dead blobs are collected", function() {
// Verify that dead blobs don't cause leaks by checking heap stays bounded.
// We do two phases: allocate a batch, check heap, allocate another, check again.
// If blobs leaked, the second batch would cause unbounded growth.
var i = 0
var b = null
var junk = null
var stats1 = null
var stats2 = null
gc_stats()
for (i = 0; i < 200; i = i + 1) {
b = blob(1024)
b.write_fit(i, 16)
junk = {a: text(i), b: [i]}
}
stats1 = gc_stats()
// Second identical batch — should reuse collected space
for (i = 0; i < 200; i = i + 1) {
b = blob(1024)
b.write_fit(i, 16)
junk = {a: text(i), b: [i]}
}
stats2 = gc_stats()
// Heap should not have grown more than 4x between phases
// (some growth is normal from doubling, but not unbounded)
if (stats2.heap_size > stats1.heap_size * 4) {
fail("heap grew too much: " + text(stats1.heap_size) + " -> " + text(stats2.heap_size))
}
})
run("gc blob write_blob both survive", function() {
var src = blob()
var i = 0
var v = null
var dst = null
for (i = 0; i < 100; i = i + 1) {
src.write_fit(i, 16)
}
dst = blob()
dst.write_fit(99, 16)
dst.write_blob(src)
stone(dst)
if (dst.read_fit(0, 16) != 99) fail("dst first word wrong")
for (i = 0; i < 100; i = i + 1) {
v = dst.read_fit(16 + i * 16, 16)
if (v != i) fail("write_blob gc: word " + text(i) + " = " + text(v))
}
})
run("gc blob read_blob across allocation", function() {
var big = blob()
var i = 0
var slices = []
var s = null
var v = null
for (i = 0; i < 200; i = i + 1) {
big.write_fit(i, 16)
}
stone(big)
for (i = 0; i < 50; i = i + 1) {
s = big.read_blob(i * 16, (i + 1) * 16)
stone(s)
slices[i] = s
}
for (i = 0; i < 50; i = i + 1) {
v = slices[i].read_fit(0, 16)
if (v != i) fail("read_blob gc slice " + text(i) + " = " + text(v))
}
})
run("gc blob with random fill", function() {
var b = blob(256, function() { return 42 })
stone(b)
if (length(b) != 256) fail("random fill length wrong")
})
run("gc blob in record values", function() {
var rec = {}
var i = 0
var b = null
var junk = null
var v = null
for (i = 0; i < 50; i = i + 1) {
b = blob()
b.write_fit(i * 3, 16)
stone(b)
rec["k" + text(i)] = b
junk = {x: text(i), y: [i, i+1]}
}
for (i = 0; i < 50; i = i + 1) {
v = rec["k" + text(i)].read_fit(0, 16)
if (v != i * 3) fail("blob in record " + text(i) + " = " + text(v))
}
})
run("gc blob in array elements", function() {
var arr = []
var i = 0
var b = null
var garbage = null
var v = null
for (i = 0; i < 100; i = i + 1) {
b = blob()
b.write_number(i * 1.5)
stone(b)
arr[i] = b
}
for (i = 0; i < 500; i = i + 1) {
garbage = text(i) + text(i)
}
for (i = 0; i < 100; i = i + 1) {
v = arr[i].read_number(0)
if (v != i * 1.5) fail("blob in array " + text(i) + " = " + text(v))
}
})
run("gc blob forward pointer chase", function() {
var b = blob()
var i = 0
var junk = null
var v = null
for (i = 0; i < 500; i = i + 1) {
b.write_fit(i % 128, 8)
}
for (i = 0; i < 300; i = i + 1) {
junk = {a: [i, text(i)], b: text(i) + "!"}
}
stone(b)
for (i = 0; i < 500; i = i + 1) {
v = b.read_fit(i * 8, 8)
if (v != i % 128) fail("fwd chase " + text(i) + " = " + text(v))
}
})
// ============================================================================ // ============================================================================
// SUMMARY // SUMMARY
// ============================================================================ // ============================================================================

View File

@@ -7,6 +7,8 @@ sections:
url: "/docs/actors/" url: "/docs/actors/"
- title: "Requestors" - title: "Requestors"
url: "/docs/requestors/" url: "/docs/requestors/"
- title: "Logging"
url: "/docs/logging/"
- title: "Reference" - title: "Reference"
pages: pages:
- title: "Built-in Functions" - title: "Built-in Functions"

2
why.ce
View File

@@ -1,3 +1,5 @@
// cell why - Show reverse dependencies for a package
var shop = use('internal/shop') var shop = use('internal/shop')
var pkg = use('package') var pkg = use('package')