Merge branch 'master' into fix_aot
This commit is contained in:
20
CLAUDE.md
20
CLAUDE.md
@@ -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:
|
||||
|
||||
2
bench.ce
2
bench.ce
@@ -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
45466
boot/fold.cm.mcode
45466
boot/fold.cm.mcode
File diff suppressed because it is too large
Load Diff
57423
boot/mcode.cm.mcode
57423
boot/mcode.cm.mcode
File diff suppressed because it is too large
Load Diff
69093
boot/parse.cm.mcode
69093
boot/parse.cm.mcode
File diff suppressed because it is too large
Load Diff
45232
boot/streamline.cm.mcode
45232
boot/streamline.cm.mcode
File diff suppressed because it is too large
Load Diff
25219
boot/tokenize.cm.mcode
25219
boot/tokenize.cm.mcode
File diff suppressed because it is too large
Load Diff
31
build.cm
31
build.cm
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
78
docs/cli.md
78
docs/cli.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
233
docs/logging.md
Normal 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
|
||||
```
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
16
dump_ast.cm
16
dump_ast.cm
@@ -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))
|
||||
19
dump_ir.ce
19
dump_ir.ce
@@ -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
|
||||
|
||||
117
dump_mcode.cm
117
dump_mcode.cm
@@ -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
|
||||
}
|
||||
}
|
||||
237
dump_types.cm
237
dump_types.cm
@@ -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
|
||||
}
|
||||
}
|
||||
43
explain.ce
43
explain.ce
@@ -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
14
fold.ce
@@ -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
150
help.ce
@@ -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()
|
||||
|
||||
32
index.ce
32
index.ce
@@ -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) {
|
||||
|
||||
59
index.cm
59
index.cm
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
165
internal/shop.cm
165
internal/shop.cm
@@ -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
343
log.ce
Normal 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
140
ls.ce
@@ -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
131
mcode.ce
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
10
parse.ce
10
parse.ce
@@ -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
2
qbe.ce
@@ -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")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cell qopconv - Convert QOP archive formats
|
||||
|
||||
var fd = use('fd')
|
||||
var qop = use('qop')
|
||||
|
||||
|
||||
123
query.ce
Normal file
123
query.ce
Normal 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
62
query.cm
Normal 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
|
||||
3
seed.ce
3
seed.ce
@@ -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 + '"')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
113
source/mach.c
113
source/mach.c
@@ -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
|
||||
============================================================ */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
900
source/runtime.c
900
source/runtime.c
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
219
streamline.ce
219
streamline.ce
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
114
streamline.cm
114
streamline.cm
@@ -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)
|
||||
|
||||
2
test.ce
2
test.ce
@@ -1,3 +1,5 @@
|
||||
// cell test - Run tests for packages
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var pkg = use('package')
|
||||
var fd = use('fd')
|
||||
|
||||
12
tests/actor_memory_abuse.ce
Normal file
12
tests/actor_memory_abuse.ce
Normal 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')
|
||||
13
tests/actor_slow_killed.ce
Normal file
13
tests/actor_slow_killed.ce
Normal 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')
|
||||
20
tests/actor_suspend_resume.ce
Normal file
20
tests/actor_suspend_resume.ce
Normal 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')
|
||||
5
tests/hang_actor_memory.ce
Normal file
5
tests/hang_actor_memory.ce
Normal file
@@ -0,0 +1,5 @@
|
||||
// Actor that allocates memory until killed by memory limit
|
||||
var big = []
|
||||
while (1) {
|
||||
big[] = array(10000, 0)
|
||||
}
|
||||
11
tests/slow_compute_actor.ce
Normal file
11
tests/slow_compute_actor.ce
Normal 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})
|
||||
})
|
||||
@@ -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}))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// cell upgrade - Upgrade the cell installation
|
||||
|
||||
var shop = use('internal/shop')
|
||||
var fd = use('fd')
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
// cell version - Show cell version
|
||||
|
||||
log.console("0.1.0")
|
||||
$stop()
|
||||
400
vm_suite.ce
400
vm_suite.ce
@@ -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
|
||||
// ============================================================================
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user