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.
## 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
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 pkg = use('package')
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 cc = tc.c
// Step 1: Read source and compile through pipeline
var content = fd.slurp(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')
// Step 1: Compile through pipeline
var optimized = shop.compile_file(src_path)
var qbe_macros = use('qbe')
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
var sym_name = null
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)
// 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()
ensure_dir(build_dir)
@@ -747,19 +735,8 @@ Build.compile_cm_to_mach = function(src_path) {
if (!fd.is_file(src_path)) {
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 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)
var optimized = shop.compile_file(src_path)
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)
// Shared compilation front-end
// Shared compilation front-end — uses raw modules for per-stage timing
var tokenize = use('tokenize')
var parse_mod = use('parse')
var fold = use('fold')

View File

@@ -111,6 +111,18 @@ $stop() // stop self
$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 a new child actor from a script. The callback receives lifecycle events:
@@ -272,7 +284,9 @@ if (is_actor(some_value)) {
### 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)

View File

@@ -286,6 +286,84 @@ Clean build artifacts.
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
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
```
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 |
|-------------|-------------------|----------------------------------------|
| fold | `dump_ast.cm` | Folded AST as JSON |
| mcode | `dump_mcode.cm` | Raw mcode IR before optimization |
| streamline | `dump_stream.cm` | Before/after instruction counts + IR |
| streamline | `dump_types.cm` | Optimized IR with type annotations |
| streamline | `streamline.ce` | Full optimized IR as JSON |
| all | `ir_report.ce` | Structured optimizer flight recorder |
| Stage | Tool | What it shows |
|-------------|---------------------------|----------------------------------------|
| tokenize | `tokenize.ce` | Token stream as JSON |
| parse | `parse.ce` | Unfolded AST as JSON |
| fold | `fold.ce` | Folded AST as JSON |
| mcode | `mcode.ce` | Raw mcode IR as JSON |
| mcode | `mcode.ce --pretty` | Human-readable mcode IR |
| 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.
## Quick Start
```bash
# see raw mcode IR
./cell --core . dump_mcode.cm myfile.ce
# see raw mcode IR (pretty-printed)
cell mcode --pretty myfile.ce
# see what the optimizer changed
./cell --core . dump_stream.cm myfile.ce
# see optimized IR with type annotations
cell streamline --types myfile.ce
# 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.
```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
./cell --core . dump_mcode.cm <file.ce|file.cm>
```
## dump_stream.cm
Shows a before/after comparison of the optimizer. For each function, prints:
- Instruction count before and after
- Number of eliminated instructions
- The streamlined IR (nops hidden by default)
```bash
./cell --core . dump_stream.cm <file.ce|file.cm>
```
## dump_types.cm
Shows the optimized IR with type annotations. Each instruction is followed by the known types of its slot operands, inferred by walking the instruction stream.
```bash
./cell --core . dump_types.cm <file.ce|file.cm>
cell mcode <file.ce|file.cm> # JSON (default)
cell mcode --pretty <file.ce|file.cm> # human-readable IR
```
## 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.
```bash
./cell --core . streamline.ce <file.ce|file.cm> # full JSON (default)
./cell --core . streamline.ce --stats <file.ce|file.cm> # summary stats per function
./cell --core . streamline.ce --ir <file.ce|file.cm> # human-readable IR
./cell --core . streamline.ce --check <file.ce|file.cm> # warnings only
cell streamline <file.ce|file.cm> # full JSON (default)
cell streamline --stats <file.ce|file.cm> # summary stats per function
cell streamline --ir <file.ce|file.cm> # human-readable IR
cell streamline --check <file.ce|file.cm> # warnings only
cell streamline --types <file.ce|file.cm> # IR with type annotations
```
| 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 |
| `--ir` | Human-readable canonical IR (same format as `ir_report.ce`) |
| `--check` | Warnings only (e.g. `nr_slots > 200` approaching 255 limit) |
| `--types` | Optimized IR with inferred type annotations per slot |
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.
```bash
./cell --core . seed.ce # regenerate all boot seeds
./cell --core . seed.ce --clean # also clear the build cache after
cell seed # regenerate all boot seeds
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`.
@@ -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.
```bash
./cell --core . ir_report.ce [options] <file.ce|file.cm>
cell ir_report [options] <file.ce|file.cm>
```
### Options
@@ -246,16 +234,16 @@ Properties:
```bash
# 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
./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
./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
./cell --core . ir_report.ce --full myfile.ce > report.json
cell ir_report --full myfile.ce > report.json
```
## ir_stats.cm

View File

@@ -15,10 +15,14 @@ var json = use('json')
### 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
json.encode({a: 1, b: 2})
// '{ "a": 1, "b": 2 }'
// Compact (no whitespace)
json.encode({a: 1, b: 2}, false)
// '{"a":1,"b":2}'
// Pretty print with 2-space indent
@@ -32,7 +36,7 @@ json.encode({a: 1, b: 2}, 2)
**Parameters:**
- **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
- **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:
### index.cm
### Via shop (recommended)
```javascript
var tokenize_mod = use('tokenize')
var parse_mod = use('parse')
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)
var shop = use('internal/shop')
var idx = shop.index_file(path)
```
`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
var index_mod = use('index')
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:
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.
3. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons.
4. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition.
5. **Move elimination**: Removes self-moves (`move a, a`).
6. **Unreachable elimination**: Nops dead code after `return` until the next label.
7. **Dead jump elimination**: Removes jumps to the immediately following label.
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. **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. **Algebraic simplification**: Rewrites identity operations (add 0, multiply 1, divide 1) and folds same-slot comparisons.
5. **Boolean simplification**: Fuses `not` + conditional jump into a single jump with inverted condition.
6. **Move elimination**: Removes self-moves (`move a, a`).
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.
@@ -130,9 +131,9 @@ Seeds are used during cold start (empty cache) to compile the pipeline modules f
| File | Purpose |
|------|---------|
| `dump_mcode.cm` | Print raw Mcode IR before streamlining |
| `dump_stream.cm` | Print IR after streamlining with before/after stats |
| `dump_types.cm` | Print streamlined IR with type annotations |
| `mcode.ce --pretty` | Print raw Mcode IR before streamlining |
| `streamline.ce --types` | Print streamlined IR with type annotations |
| `streamline.ce --stats` | Print IR after streamlining with before/after stats |
## Test Files

View File

@@ -95,7 +95,9 @@ Write type mapping:
| `move`, `load_field`, `load_index`, `load_dynamic`, `pop`, `get` | 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:
@@ -257,17 +259,17 @@ The `+` operator is excluded from target slot propagation when it would use the
## 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
- **`dump_stream.cm`** — prints the IR after streamlining, with before/after instruction counts
- **`dump_types.cm`** — prints the streamlined IR with type annotations on each instruction
- **`cell mcode --pretty`** — prints the raw Mcode IR after `mcode.cm`, before streamlining
- **`cell streamline --stats`** — prints the IR after streamlining, with before/after instruction counts
- **`cell streamline --types`** — prints the streamlined IR with type annotations on each instruction
Usage:
```
./cell --core . dump_mcode.cm <file.ce|file.cm>
./cell --core . dump_stream.cm <file.ce|file.cm>
./cell --core . dump_types.cm <file.ce|file.cm>
cell mcode --pretty <file.ce|file.cm>
cell streamline --stats <file.ce|file.cm>
cell streamline --types <file.ce|file.cm>
```
## 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')
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')
// cell dump_ir - Dump intermediate representation for a source file
var file = args[0]
var src = text(fd.slurp(file))
var tok = tokenize(src, file)
var ast = parse_mod(tok.tokens, src, file, tokenize)
var folded = fold(ast)
var compiled = mcode_mod(folded)
var optimized = streamline_mod(compiled)
var shop = use('internal/shop')
var json = use('json')
var optimized = shop.compile_file(args[0])
var instrs = optimized.main.instructions
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 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 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 span_arg = null
var symbol_name = null
@@ -47,12 +20,10 @@ var parts = null
var filename = null
var line = null
var col = null
var src = null
var idx = null
var indexes = []
var explain = null
var result = null
var pipeline = {tokenize: tokenize_mod, parse: parse_mod, fold: fold_mod}
for (i = 0; i < length(args); i++) {
if (args[i] == '--span') {
@@ -108,9 +79,7 @@ if (mode == "span") {
$stop()
}
src = text(fd.slurp(filename))
idx = index_mod.index_file(src, filename, pipeline)
resolve_imports(idx, filename)
idx = shop.index_file(filename)
explain = explain_mod.make(idx)
result = explain.at_span(line, col)
@@ -139,11 +108,8 @@ if (mode == "symbol") {
}
if (length(files) == 1) {
// Single file: use by_symbol for a focused result.
filename = files[0]
src = text(fd.slurp(filename))
idx = index_mod.index_file(src, filename, pipeline)
resolve_imports(idx, filename)
idx = shop.index_file(filename)
explain = explain_mod.make(idx)
result = explain.by_symbol(symbol_name)
@@ -154,13 +120,10 @@ if (mode == "symbol") {
print("\n")
}
} else if (length(files) > 1) {
// Multiple files: index each and search across all.
indexes = []
i = 0
while (i < length(files)) {
src = text(fd.slurp(files[i]))
idx = index_mod.index_file(src, files[i], pipeline)
resolve_imports(idx, files[i])
idx = shop.index_file(files[i])
indexes[] = idx
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 shop = use("internal/shop")
var filename = args[0]
var src = text(fd.slurp(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)
var folded = shop.analyze_file(filename)
print(json.encode(folded))

150
help.ce
View File

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

View File

@@ -7,19 +7,11 @@
var fd = use('fd')
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 filename = null
var output_path = null
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++) {
if (args[i] == '-o' || args[i] == '--output') {
@@ -53,29 +45,7 @@ if (!fd.is_file(filename)) {
$stop()
}
var src = text(fd.slurp(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 idx = shop.index_file(filename)
var out = json.encode(idx, true)
if (output_path != null) {

View File

@@ -1,8 +1,7 @@
// index.cm — Core semantic indexing module.
// Walks AST output from parse (+ optional fold) to build a semantic index.
//
// Two entry points:
// index_file(src, filename, tokenize_mod, parse_mod, fold_mod) — full pipeline
// Entry point:
// index_ast(ast, tokens, filename) — index a pre-parsed AST
var make_span = function(node) {
@@ -22,6 +21,7 @@ var index_ast = function(ast, tokens, filename) {
var references = []
var call_sites = []
var exports_list = []
var intrinsic_refs = []
var node_counter = 0
var fn_map = {}
var _i = 0
@@ -147,6 +147,29 @@ var index_ast = function(ast, tokens, filename) {
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.
if (node.kind == "name" && node.name != null && node.function_nr != null) {
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).
if (node.expression != null && node.expression.kind != "name") {
walk_expr(node.expression, enclosing, false)
@@ -227,8 +261,12 @@ var index_ast = function(ast, tokens, filename) {
// Function / arrow function expression — walk body.
if (node.kind == "function" || node.kind == "arrow function") {
enc = enclosing
if (node.name != null && node.function_nr != null) {
enc = filename + ":" + node.name + ":fn"
if (node.function_nr != null) {
if (node.name != null) {
enc = filename + ":" + node.name + ":fn"
} else {
enc = filename + ":anon_" + text(node.function_nr) + ":fn"
}
}
// Record params as symbols.
if (node.list != null) {
@@ -596,24 +634,13 @@ var index_ast = function(ast, tokens, filename) {
imports: imports,
symbols: symbols,
references: references,
intrinsic_refs: intrinsic_refs,
call_sites: call_sites,
exports: exports_list,
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 {
index_file: index_file,
index_ast: index_ast
}

View File

@@ -78,6 +78,7 @@ function load_pipeline_module(name, env) {
var boot_par = null
var boot_fld = null
var boot_mc = null
var boot_sl = null
var tok_result = null
var ast = null
var compiled = null
@@ -105,6 +106,8 @@ function load_pipeline_module(name, env) {
}
ast = boot_fld(ast)
compiled = boot_mc(ast)
boot_sl = boot_load("streamline")
compiled = boot_sl(compiled)
mcode_json = json.encode(compiled)
mach_blob = mach_compile_mcode_bin(name, mcode_json)
if (cached) {
@@ -312,38 +315,24 @@ var actor_mod = use_core('actor')
var wota = use_core('wota')
var nota = use_core('nota')
function is_actor(value) {
return is_object(value) && value[ACTORDATA]
}
var ENETSERVICE = 0.1
var REPLYTIMEOUT = 60 // seconds before replies are ignored
function caller_data(depth)
{
return {file: "nofile", line: 0}
}
// --- Logging system (bootstrap phase) ---
// Early log: prints to console before toml/time/json are loaded.
// Upgraded to full sink-based system after config loads (see load_log_config below).
function console_rec(line, file, msg) {
return `[${text(_cell.id, 0, 5)}] [${file}:${line}]: ${msg}\n`
// time: [${time.text("mb d yyyy h:nn:ss")}]
}
var log_config = null
var channel_sinks = {}
var wildcard_sinks = []
var warned_channels = {}
var stack_channels = {}
function log(name, args) {
var caller = caller_data(1)
var msg = args[0]
if (name == 'console') {
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}`)
}
if (msg == null) msg = ""
os.print(`[${text(_cell.id, 0, 5)}] [${name}]: ${msg}\n`)
}
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 : {}
@@ -431,6 +420,166 @@ core_extras.native_mode = native_mode
var shop = use_core('internal/shop')
if (native_mode) use_core('build')
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 fallback = pronto.fallback
@@ -439,9 +588,9 @@ var race = pronto.race
var sequence = pronto.sequence
runtime_env.actor = actor
runtime_env.is_actor = is_actor
runtime_env.log = log
runtime_env.send = send
runtime_env.shop_path = shop_path
runtime_env.fallback = fallback
runtime_env.parallel = parallel
runtime_env.race = race
@@ -1052,7 +1201,7 @@ var prog_path = prog + ".ce"
var pkg_dir = null
var core_dir = null
if (!fd.is_file(prog_path)) {
pkg_dir = package.find_package_dir(prog_path)
pkg_dir = package.find_package_dir(".")
if (pkg_dir)
prog_path = pkg_dir + '/' + prog + '.ce'
}

View File

@@ -584,6 +584,37 @@ JSC_CCALL(fd_slurpwrite,
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
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;
@@ -733,6 +764,7 @@ static const JSCFunctionListEntry js_fd_funcs[] = {
MIST_FUNC_DEF(fd, read, 2),
MIST_FUNC_DEF(fd, slurp, 1),
MIST_FUNC_DEF(fd, slurpwrite, 2),
MIST_FUNC_DEF(fd, slurpappend, 2),
MIST_FUNC_DEF(fd, lseek, 3),
MIST_FUNC_DEF(fd, getcwd, 0),
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;
}
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)
{
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);
}
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[] = {
MIST_FUNC_DEF(os, platform, 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, sleep, 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_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, getenv, 1),
MIST_FUNC_DEF(os, qbe, 1),
MIST_FUNC_DEF(os, stack, 1),
};
JSValue js_core_os_use(JSContext *js) {

View File

@@ -510,6 +510,155 @@ function inject_env(inject) {
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
var _mcode_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
// Pre-loads sibling dylibs with RTLD_LAZY|RTLD_GLOBAL so cross-dylib symbols resolve
function open_module_dylib(dylib_path) {
if (open_dls[dylib_path]) return open_dls[dylib_path]
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)
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
// if args[0] is a package alias, list that one
// otherwise, list the local one
// list modules and actors in packages
//
// 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 package = use('package')
var ctx = null
var pkg = args[0] || package.find_package_dir('.')
var modules = package.list_modules(pkg)
var programs = package.list_programs(pkg)
var show_all = false
var show_modules = true
var show_programs = true
var show_paths = false
var filter_modules = false
var filter_programs = false
var pkg_arg = null
var show_help = false
var i = 0
log.console("Modules in " + pkg + ":")
modules = sort(modules)
if (length(modules) == 0) {
log.console(" (none)")
} else {
for (i = 0; i < length(modules); i++) {
log.console(" " + modules[i])
for (i = 0; i < length(args); i++) {
if (args[i] == '--all' || args[i] == '-a') {
show_all = true
} else if (args[i] == '--modules' || args[i] == '-m') {
filter_modules = true
} else if (args[i] == '--programs' || args[i] == '-p') {
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("")
log.console("Programs in " + pkg + ":")
programs = sort(programs)
if (length(programs) == 0) {
log.console(" (none)")
} else {
for (i = 0; i < length(programs); i++) {
log.console(" " + programs[i])
if (filter_modules || filter_programs) {
show_modules = filter_modules
show_programs = filter_programs
}
var list_one_package = function(pkg) {
var pkg_dir = null
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()

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 json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var filename = args[0]
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
print(json.encode(compiled))
var shop = use("internal/shop")
var show_pretty = false
var filename = null
var i = 0
for (i = 0; i < length(args); i++) {
if (args[i] == '--pretty') {
show_pretty = true
} else if (args[i] == '--help' || args[i] == '-h') {
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 modules = []
var i = 0
var stem = null
for (i = 0; i < length(files); i++) {
if (ends_with(files[i], '.cm')) {
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
}

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 tokenize = use("tokenize")
var parse = use("parse")
var shop = use("internal/shop")
var filename = args[0]
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
var ast = shop.parse_file(filename)
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 json = use("json")
var tokenize = use("tokenize")

View File

@@ -1,3 +1,5 @@
// cell qopconv - Convert QOP archive formats
var fd = use('fd')
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 mcode_json = null
var out_path = null
var build_dir = null
// Regenerate pipeline module seeds
for (i = 0; i < length(pipeline_modules); i++) {
@@ -103,7 +104,7 @@ if (fd.is_file(bootstrap_path)) {
print('\nRegenerated ' + text(generated) + ' seed(s)')
if (clean) {
var build_dir = shop.get_build_dir()
build_dir = shop.get_build_dir()
if (fd.is_dir(build_dir)) {
print('Clearing build cache: ' + 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)
{
crt->disrupt = 1;
JS_SetPauseFlag(crt->context, 2);
if (crt->state != ACTOR_RUNNING)
actor_free(crt);
}
@@ -265,11 +266,13 @@ void script_startup(cell_rt *prt)
g_runtime = JS_NewRuntime();
}
JSContext *js = JS_NewContext(g_runtime);
JS_SetInterruptHandler(js, (JSInterruptHandler *)actor_interrupt_cb, prt);
JS_SetContextOpaque(js, prt);
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. */
JS_AddGCRef(js, &prt->idx_buffer_ref);
JS_AddGCRef(js, &prt->on_exception_ref);
@@ -345,7 +348,8 @@ void script_startup(cell_rt *prt)
tmp = js_core_json_use(js);
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_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("\nRecompile after changes: make\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)
@@ -554,7 +559,6 @@ int cell_init(int argc, char **argv)
cli_rt->context = ctx;
JS_SetContextOpaque(ctx, cli_rt);
JS_SetInterruptHandler(ctx, (JSInterruptHandler *)actor_interrupt_cb, cli_rt);
JS_AddGCRef(ctx, &cli_rt->idx_buffer_ref);
JS_AddGCRef(ctx, &cli_rt->on_exception_ref);
@@ -565,7 +569,8 @@ int cell_init(int argc, char **argv)
cli_rt->on_exception_ref.val = JS_NULL;
cli_rt->message_handle_ref.val = JS_NULL;
cli_rt->unneeded_ref.val = JS_NULL;
cli_rt->actor_sym_ref.val = JS_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));
root_cell = cli_rt;
@@ -704,7 +709,6 @@ check_actors:
JS_DeleteGCRef(ctx, &cli_rt->message_handle_ref);
JS_DeleteGCRef(ctx, &cli_rt->unneeded_ref);
JS_DeleteGCRef(ctx, &cli_rt->actor_sym_ref);
JS_SetInterruptHandler(ctx, NULL, NULL);
pthread_mutex_destroy(cli_rt->mutex);
free(cli_rt->mutex);
@@ -757,11 +761,24 @@ void cell_trace_sethook(cell_hook)
int uncaught_exception(JSContext *js, JSValue v)
{
(void)v;
if (!JS_HasException(js))
int has_exc = JS_HasException(js);
int is_exc = JS_IsException(v);
if (!has_exc && !is_exc)
return 1;
/* Error message and backtrace were already printed to stderr
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;
}

View File

@@ -1,5 +1,6 @@
#include <pthread.h>
#include <stdatomic.h>
/* Letter type for unified message queue */
typedef enum {
@@ -21,6 +22,14 @@ typedef struct letter {
#define ACTOR_EXHAUSTED 3 // Actor waiting for GC
#define ACTOR_RECLAIMING 4 // Actor running GC
#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 {
JSContext *context;
@@ -58,6 +67,11 @@ typedef struct cell_rt {
int main_thread_only;
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
cell_hook trace_hook;
} cell_rt;
@@ -74,17 +88,19 @@ int uncaught_exception(JSContext *js, JSValue v);
int actor_exists(const char *id);
void set_actor_state(cell_rt *actor);
void enqueue_actor_priority(cell_rt *actor);
void actor_clock(cell_rt *actor, JSValue fn);
uint32_t actor_delay(cell_rt *actor, JSValue fn, double seconds);
JSValue actor_remove_timer(cell_rt *actor, uint32_t timer_id);
void exit_handler(void);
int actor_interrupt_cb(JSRuntime *rt, cell_rt *crt);
void actor_loop();
void actor_initialize(void);
void actor_free(cell_rt *actor);
int scheduler_actor_count(void);
void scheduler_enable_quiescence(void);
JSValue JS_ResumeRegisterVM(JSContext *ctx);
uint64_t cell_ns();
void cell_sleep(double seconds);
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");
}
/* 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
void __asan_on_error(void) {
@@ -757,6 +745,31 @@ void __asan_on_error(void) {
JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
JSValue this_obj, int argc, JSValue *argv,
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
collection which moves heap objects, invalidating stack-local copies */
JSGCRef env_gc, of_gc;
@@ -778,7 +791,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
}
/* Allocate initial frame */
JSFrameRegister *frame = alloc_frame_register(ctx, code->nr_slots);
frame = alloc_frame_register(ctx, code->nr_slots);
if (!frame) {
for (int i = nargs_copy - 1; i >= 0; i--) JS_PopGCRef(ctx, &arg_gcs[i]);
JS_PopGCRef(ctx, &this_gc);
@@ -788,7 +801,6 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
}
/* Protect frame from GC */
JSGCRef frame_ref;
JS_AddGCRef(ctx, &frame_ref);
frame_ref.val = JS_MKPTR(frame);
#ifdef HAVE_ASAN
@@ -812,9 +824,11 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
JS_PopGCRef(ctx, &of_gc);
JS_PopGCRef(ctx, &env_gc);
uint32_t pc = code->entry_point;
JSValue result = JS_NULL;
pc = code->entry_point;
result = JS_NULL;
} /* end normal path block */
vm_dispatch:
/* Execution loop — 32-bit instruction dispatch */
for (;;) {
#ifndef NDEBUG
@@ -1405,9 +1419,18 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
VM_CASE(MACH_JMP): {
int offset = MACH_GET_sJ(instr);
pc = (uint32_t)((int32_t)pc + offset);
if (offset < 0 && reg_vm_check_interrupt(ctx)) {
result = JS_ThrowInternalError(ctx, "interrupted");
goto done;
if (offset < 0) {
int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
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();
}
@@ -1421,9 +1444,18 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
if (cond) {
int offset = MACH_GET_sBx(instr);
pc = (uint32_t)((int32_t)pc + offset);
if (offset < 0 && reg_vm_check_interrupt(ctx)) {
result = JS_ThrowInternalError(ctx, "interrupted");
goto done;
if (offset < 0) {
int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
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();
@@ -1438,9 +1470,18 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
if (!cond) {
int offset = MACH_GET_sBx(instr);
pc = (uint32_t)((int32_t)pc + offset);
if (offset < 0 && reg_vm_check_interrupt(ctx)) {
result = JS_ThrowInternalError(ctx, "interrupted");
goto done;
if (offset < 0) {
int pf = atomic_load_explicit(&ctx->pause_flag, memory_order_relaxed);
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();
@@ -1953,6 +1994,7 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code,
/* C, native, or bytecode function */
ctx->reg_current_frame = frame_ref.val;
ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
ctx->vm_call_depth++;
JSValue ret;
if (fn->kind == JS_FUNC_KIND_C)
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]);
else
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);
ctx->reg_current_frame = JS_NULL;
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 */
ctx->reg_current_frame = frame_ref.val;
ctx->current_register_pc = pc > 0 ? pc - 1 : 0;
ctx->vm_call_depth++;
JSValue ret;
if (fn->kind == JS_FUNC_KIND_C)
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]);
else
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);
ctx->reg_current_frame = JS_NULL;
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:
#ifdef HAVE_ASAN
__asan_js_ctx = NULL;
@@ -2187,6 +2243,13 @@ done:
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
============================================================ */

View File

@@ -33,6 +33,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdatomic.h>
#include <sys/time.h>
#include <time.h>
#if defined(__APPLE__)
@@ -898,12 +899,12 @@ typedef struct JSArray {
JSValue values[]; /* inline flexible array member */
} JSArray;
/* JSBlob — not allocated on GC heap (blobs use JSRecord + opaque).
Struct kept for reference; gc_object_size/gc_scan_object do not handle OBJ_BLOB. */
/* JSBlob — inline bit data on the GC heap.
cap56 = capacity in bits, S bit = stone (immutable). */
typedef struct JSBlob {
objhdr_t mist_hdr;
word_t length;
uint8_t bits[];
objhdr_t mist_hdr; /* type=OBJ_BLOB, cap56=capacity_bits, S=stone */
word_t length; /* used bits */
word_t bits[]; /* inline bit data, ceil(cap56/64) words */
} JSBlob;
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) */
/* 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 */
typedef struct CCallRoot {
JSValue *argv; /* points to C-stack-local array */
@@ -1123,8 +1120,8 @@ struct JSContext {
uint64_t random_state;
/* when the counter reaches zero, JSRutime.interrupt_handler is called */
int interrupt_counter;
/* 0 = normal, 1 = suspend (fast timer), 2 = kill (slow timer) */
_Atomic int pause_flag;
/* if NULL, RegExp compilation is not supported */
JSValue (*compile_regexp) (JSContext *ctx, JSValue pattern, JSValue flags);
@@ -1138,8 +1135,12 @@ struct JSContext {
JSValue reg_current_frame; /* current JSFrameRegister being executed */
uint32_t current_register_pc; /* PC at exception time */
JSInterruptHandler *interrupt_handler;
void *interrupt_opaque;
/* VM suspend/resume state */
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;
@@ -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_text_format (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);
@@ -1545,25 +1546,14 @@ static inline void set_value (JSContext *ctx, JSValue *pval, JSValue new_val) {
void JS_ThrowInterrupted (JSContext *ctx);
static no_inline __exception int __js_poll_interrupts (JSContext *ctx) {
ctx->interrupt_counter = JS_INTERRUPT_COUNTER_INIT;
if (ctx->interrupt_handler) {
if (ctx->interrupt_handler (ctx->rt, ctx->interrupt_opaque)) {
JS_ThrowInterrupted (ctx);
return -1;
}
static inline __exception int js_poll_interrupts (JSContext *ctx) {
if (unlikely (atomic_load_explicit (&ctx->pause_flag, memory_order_relaxed) >= 2)) {
JS_ThrowInterrupted (ctx);
return -1;
}
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) === */
typedef struct PPretext {
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);
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 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);
@@ -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_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);
int reg_vm_check_interrupt(JSContext *ctx);
#endif /* QUICKJS_INTERNAL_H */

View File

@@ -294,6 +294,12 @@ JS_IsShortFloat (JSValue v) {
#define JS_FALSE ((JSValue)JS_TAG_BOOL)
#define JS_TRUE ((JSValue)(JS_TAG_BOOL | (1 << 5)))
#define JS_EXCEPTION ((JSValue)JS_TAG_EXCEPTION)
#define JS_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
#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. */
void JS_UpdateStackTop (JSContext *ctx);
/* return != 0 if the JS code needs to be interrupted */
typedef int JSInterruptHandler (JSRuntime *rt, void *opaque);
void JS_SetInterruptHandler (JSContext *ctx, JSInterruptHandler *cb,
void *opaque);
/* Returns the current VM call depth (0 = pure bytecode, >0 = C frames) */
int JS_GetVMCallDepth(JSContext *ctx);
/* 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);
@@ -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. */
MachCode *mach_compile_mcode(struct cJSON *mcode_json);
/* Get stack trace as cJSON array of frame objects.
Returns NULL if no register VM frame is active.
Caller must call cJSON_Delete() on the result. */
struct cJSON *JS_GetStack (JSContext *ctx);
/* Get stack trace as JS array of {fn, file, line, col} objects.
Returns an empty array if no register VM frame is active.
Does NOT clear reg_current_frame — caller is responsible if needed. */
JSValue JS_GetStack (JSContext *ctx);
#undef js_unlikely
#undef inline

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,9 @@ typedef struct actor_node {
typedef enum {
TIMER_JS,
TIMER_NATIVE_REMOVE
TIMER_NATIVE_REMOVE,
TIMER_PAUSE,
TIMER_KILL
} timer_type;
typedef struct {
@@ -30,23 +32,28 @@ typedef struct {
cell_rt *actor;
uint32_t timer_id;
timer_type type;
uint32_t turn_gen; /* generation at registration time */
} timer_node;
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 ---
static struct {
pthread_mutex_t lock; // Protects queue, shutdown flag, and timers
pthread_cond_t wake_cond; // Wakes up workers
pthread_cond_t timer_cond; // Wakes up the timer thread
pthread_cond_t main_cond; // Wakes up the main thread
actor_node *head; // Ready Queue Head
actor_node *tail; // Ready Queue Tail
actor_node *main_head; // Main Thread Queue Head
actor_node *main_tail; // Main Thread Queue Tail
actor_node *q_head[PQ_COUNT], *q_tail[PQ_COUNT]; // worker queues
actor_node *mq_head[PQ_COUNT], *mq_tail[PQ_COUNT]; // main-thread queues
int shutting_down;
int quiescence_enabled; // set after bootstrap, before actor_loop
_Atomic int quiescent_count; // actors idle with no messages and no timers
@@ -56,6 +63,33 @@ static struct {
pthread_t timer_thread;
} 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 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 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);
// Bubble up
@@ -161,8 +197,19 @@ void *timer_thread_func(void *arg) {
if (t.type == TIMER_NATIVE_REMOVE) {
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 {
pthread_mutex_lock(t.actor->msg_mutex);
pthread_mutex_lock(t.actor->msg_mutex);
int idx = hmgeti(t.actor->timers, t.timer_id);
if (idx != -1) {
JSValue cb = t.actor->timers[idx].value;
@@ -196,33 +243,48 @@ void *timer_thread_func(void *arg) {
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) {
while (1) {
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);
}
while (1) {
pthread_mutex_lock(&engine.lock);
if (engine.shutting_down && engine.head == NULL) {
pthread_mutex_unlock(&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);
}
while (!has_any_work(engine.q_head) && !engine.shutting_down) {
pthread_cond_wait(&engine.wake_cond, &engine.lock);
}
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) {
@@ -230,12 +292,14 @@ void actor_initialize(void) {
pthread_cond_init(&engine.wake_cond, NULL);
pthread_cond_init(&engine.timer_cond, NULL);
pthread_cond_init(&engine.main_cond, NULL);
engine.shutting_down = 0;
engine.head = NULL;
engine.tail = NULL;
engine.main_head = NULL;
engine.main_tail = NULL;
for (int i = 0; i < PQ_COUNT; i++) {
engine.q_head[i] = NULL;
engine.q_tail[i] = NULL;
engine.mq_head[i] = NULL;
engine.mq_tail[i] = NULL;
}
actors_mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(actors_mutex, NULL);
@@ -295,7 +359,6 @@ void actor_free(cell_rt *actor)
arrfree(actor->letters);
JS_SetInterruptHandler(js, NULL, NULL);
JS_FreeContext(js);
free(actor->id);
@@ -406,30 +469,7 @@ void set_actor_state(cell_rt *actor)
#endif
actor->state = ACTOR_READY;
actor->ar = 0;
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);
enqueue_actor_priority(actor);
} else if (!hmlen(actor->timers)) {
// No messages AND no timers
@@ -531,26 +571,23 @@ cell_rt *get_actor(char *id)
void actor_loop()
{
while (!engine.shutting_down) { // Direct read safe enough here or use lock
while (!engine.shutting_down) {
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);
}
if (engine.shutting_down && engine.main_head == NULL) {
pthread_mutex_unlock(&engine.lock);
break;
if (engine.shutting_down && !has_any_work(engine.mq_head)) {
pthread_mutex_unlock(&engine.lock);
break;
}
actor_node *node = engine.main_head;
engine.main_head = node->next;
if (engine.main_head == NULL) engine.main_tail = NULL;
actor_node *node = dequeue_priority(engine.mq_head, engine.mq_tail);
pthread_mutex_unlock(&engine.lock);
if (node) {
actor_turn(node->actor);
free(node);
actor_turn(node->actor);
free(node);
}
}
}
@@ -606,10 +643,6 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
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)
{
@@ -638,58 +671,131 @@ const char *send_message(const char *id, void *msg)
void actor_turn(cell_rt *actor)
{
pthread_mutex_lock(actor->mutex);
int prev_state = actor->state;
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)
actor->trace_hook(actor->name, CELL_HOOK_ENTER);
pthread_mutex_lock(actor->msg_mutex);
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);
if (!pending) {
pthread_mutex_unlock(actor->msg_mutex);
goto ENDTURN;
}
#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
letter l = actor->letters[0];
arrdel(actor->letters, 0);
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) {
// Create a JS blob from the C blob
size_t size = blob_length(l.blob_data) / 8; // Convert bits to bytes
#ifdef SCHEDULER_DEBUG
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);
size_t size = blob_length(l.blob_data) / 8;
JSValue arg = js_new_blob_stoned_copy(actor->context,
(void *)blob_data(l.blob_data), size);
blob_destroy(l.blob_data);
result = JS_Call(actor->context, actor->message_handle_ref.val, JS_NULL, 1, &arg);
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))
actor->disrupt = 1;
JS_FreeValue(actor->context, arg);
} else if (l.type == LETTER_CALLBACK) {
result = JS_Call(actor->context, l.callback, JS_NULL, 0, NULL);
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))
actor->disrupt = 1;
JS_FreeValue(actor->context, l.callback);
}
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;
if (actor->trace_hook)
actor->trace_hook(actor->name, CELL_HOOK_EXIT);
if (actor->disrupt) {
/* Actor must die. Unlock before freeing so actor_free can
lock/unlock/destroy the mutex without use-after-free. */
#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
pthread_mutex_unlock(actor->mutex);
actor_free(actor);
@@ -697,10 +803,21 @@ void actor_turn(cell_rt *actor)
}
#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
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);
}

View File

@@ -142,7 +142,6 @@ void actor_free(cell_rt *actor)
arrfree(actor->letters);
JS_SetInterruptHandler(js, NULL, NULL);
JS_FreeContext(js);
free(actor->id);
@@ -353,10 +352,6 @@ const char *register_actor(const char *id, cell_rt *actor, int mainthread, doubl
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)
{

View File

@@ -1,22 +1,20 @@
// streamline.ce — run the full compile + optimize pipeline
//
// Usage:
// pit streamline <file> Full optimized IR as JSON (default)
// pit streamline --stats <file> Summary stats per function
// pit streamline --ir <file> Human-readable IR
// pit streamline --check <file> Warnings only (e.g. high slot count)
// cell streamline <file> Full optimized IR as JSON (default)
// cell streamline --stats <file> Summary stats per function
// cell streamline --ir <file> Human-readable IR
// 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 json = use("json")
var tokenize = use("tokenize")
var parse = use("parse")
var fold = use("fold")
var mcode = use("mcode")
var streamline = use("streamline")
var shop = use("internal/shop")
var show_stats = false
var show_ir = false
var show_check = false
var show_types = false
var filename = null
var i = 0
@@ -27,40 +25,37 @@ for (i = 0; i < length(args); i++) {
show_ir = true
} else if (args[i] == '--check') {
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], '-')) {
filename = args[i]
}
}
if (!filename) {
print("usage: pit streamline [--stats] [--ir] [--check] <file>")
print("usage: cell streamline [--stats] [--ir] [--check] [--types] <file>")
$stop()
}
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var ast = parse(result.tokens, src, filename, tokenize)
var folded = fold(ast)
var compiled = mcode(folded)
// Deep copy for before snapshot (needed by --stats)
// Deep copy mcode for before snapshot (needed by --stats, streamline mutates)
var before = null
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 (!show_stats && !show_ir && !show_check) {
if (!show_stats && !show_ir && !show_check && !show_types) {
print(json.encode(optimized, true))
$stop()
}
// --- Helpers ---
var ir_stats = use("ir_stats")
var pad_right = function(s, w) {
var r = s
while (length(r) < w) {
@@ -69,6 +64,15 @@ var pad_right = function(s, w) {
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 instrs = func.instructions
var nops = 0
@@ -83,6 +87,13 @@ var count_nops = function(func) {
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 nr_args = func.nr_args != null ? func.nr_args : 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 ---
var main_name = optimized.name != null ? optimized.name : "<main>"
@@ -141,6 +310,9 @@ if (optimized.main != null) {
if (show_check) {
check_func(optimized.main, main_name)
}
if (show_types) {
dump_function_typed(optimized.main, main_name)
}
}
// Sub-functions
@@ -160,6 +332,9 @@ if (optimized.functions != null) {
if (show_check) {
check_func(func, fname)
}
if (show_types) {
dump_function_typed(func, fname)
}
fi = fi + 1
}
}

View File

@@ -336,6 +336,7 @@ var streamline = function(ir, log) {
var slot = 0
var typ = null
var rule = null
var cw_keys = null
if (instructions == null) {
return array(func.nr_slots)
@@ -362,6 +363,19 @@ var streamline = function(ir, log) {
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
k = 0
while (k < length(write_types)) {
@@ -1475,6 +1489,100 @@ var streamline = function(ir, log) {
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
// =========================================================
@@ -1530,6 +1638,12 @@ var streamline = function(ir, log) {
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
if (ir.main != null) {
optimize_function(ir.main, log)

View File

@@ -1,3 +1,5 @@
// cell test - Run tests for packages
var shop = use('internal/shop')
var pkg = use('package')
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 tokenize = use("tokenize")
var shop = use("internal/shop")
var filename = args[0]
var src = text(fd.slurp(filename))
var result = tokenize(src, filename)
var result = shop.tokenize_file(filename)
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 fd = use('fd')

View File

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

View File

@@ -4305,6 +4305,32 @@ run("closure set and get", function() {
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
// ============================================================================
@@ -5121,6 +5147,380 @@ run("json roundtrip preserves types", function() {
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
// ============================================================================

View File

@@ -7,6 +7,8 @@ sections:
url: "/docs/actors/"
- title: "Requestors"
url: "/docs/requestors/"
- title: "Logging"
url: "/docs/logging/"
- title: "Reference"
pages:
- 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 pkg = use('package')