feat: program mode (argv + exit code); reject + on Text; nil compares by word

docs/examples/log-watcher now COMPILES AND RUNS: `wovm lw.wob watch app.log 2 1`
tails a live file, classifies levels and fires its alert
("last entry is error, quiet for 2s"). corpus 71/0, woc 565/0, wovm gates green.

- program mode: the entry is `fn main` taking nothing or one `multi Text`;
  runtime/src/main.c builds that list from the program's own arguments (not
  the program name, not the image path) and the entry's return value is the
  process exit code (low byte); the loader accepts a 0- or 1-arg free-fn entry
- `+` on Text is now WO-E201 pointing at `..`. This was a memory-safety hole,
  not a style nit: the emitter lowered it to ADD on two heap pointers, and the
  workload's own `out = out + char_of(c)` produced a wild pointer that
  segfaulted the VM inside starts_with. Reported off confident types only
- `x == nil` / `x != nil` lower to EQ (a word compare), never EQS: nil is the
  zero word and EQS dereferences its operands, so a nil guard would trap
  instead of answering
- docs/examples/log-watcher: seven `+`-on-Text sites corrected to `..`
  (logtail sanitize, mcp header/body/carry assembly, supervisor detections
  line) — the sample was carrying the Haxe habit, and the language reserves
  `+` for arithmetic by doctrine
- wovm CLI takes arguments after the image path (`wovm <file.wob> [args...]`)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-14 17:18:01 +02:00
parent ebf82ca9c0
commit 940ad99c40
8 changed files with 191 additions and 99 deletions

View file

@ -1339,6 +1339,10 @@ let container_imm (p : pctx) (expected : Ast.field_ty option) (map : bool) : int
| _ -> None) | _ -> None)
| None -> None | None -> None
(* `nil` written literally on either side of a comparison — see emit_binary's
Eq/Ne cases for why the distinction matters. *)
let is_nil_lit (e : Ast.expr) : bool = match e.Ast.kind with Ast.NilLit -> true | _ -> false
let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : Ast.expr) : unit = let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : Ast.expr) : unit =
f.f_cur_line <- e.pos.line; f.f_cur_line <- e.pos.line;
(match e.kind with (match e.kind with
@ -1600,14 +1604,26 @@ and emit_binary (p : pctx) (f : fstate) (v : views) ~(dst : int) (op : Ast.binop
| Le -> simple op_le | Le -> simple op_le
| Gt -> swapped op_lt | Gt -> swapped op_lt
| Ge -> swapped op_le | Ge -> swapped op_le
| Eq -> if is_text p f l || is_text p f r then simple op_eqs else simple op_eq (* A comparison against `nil` is a WORD compare, never a content compare:
nil is the zero word, and EQS would dereference it as a `wo_str*` (the
VM's str_check traps on that, so an `x != nil` guard would trap instead
of answering). Text-vs-Text still uses EQS. *)
| Eq ->
if is_nil_lit l || is_nil_lit r then simple op_eq
else if is_text p f l || is_text p f r then simple op_eqs
else simple op_eq
| Ne -> | Ne ->
(* no NE opcode in the v1 set: `a != b` is `(a == b) == 0`. The (* no NE opcode in the v1 set: `a != b` is `(a == b) == 0`. The
format doc governs, so this is a lowering, not a new opcode. *) format doc governs, so this is a lowering, not a new opcode. *)
let a = emit_operand p f v l in let a = emit_operand p f v l in
let b = emit_operand p f v r in let b = emit_operand p f v r in
let t = alloc_temp p f pos in let t = alloc_temp p f pos in
put f (ins_abc (if is_text p f l || is_text p f r then op_eqs else op_eq) t a b); put f
(ins_abc
(if is_nil_lit l || is_nil_lit r then op_eq
else if is_text p f l || is_text p f r then op_eqs
else op_eq)
t a b);
let z = alloc_temp p f pos in let z = alloc_temp p f pos in
put f (ins_abx op_loadk z (check_bx p f pos "constant" (const_int p 0))); put f (ins_abx op_loadk z (check_bx p f pos "constant" (const_int p 0)));
put f (ins_abc op_eq dst t z) put f (ins_abc op_eq dst t z)
@ -3623,10 +3639,19 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
mr_code = [||]; mr_lines = []; mr_drops = [] } mr_code = [||]; mr_lines = []; mr_drops = [] }
:: !methods; :: !methods;
bodies := (u, None, m, !nmethods) :: !bodies; bodies := (u, None, m, !nmethods) :: !bodies;
(* the entry point is the zero-argument free fn `main` — (* Program mode: the entry is the free fn `main`, taking either
the loader's own rule for an entry (a zero-arg free fn) nothing or one `multi Text` of command-line arguments (the
plus one fixed name so `wovm image.wob` needs no flag *) runtime builds it — runtime/src/main.c). One fixed name, so
if m.name = "main" && m.params = [] then begin `wovm image.wob` needs no flag, and its return value is the
process exit code. *)
let entry_shaped =
m.name = "main"
&& match m.params with
| [] -> true
| [ (pa : Ast.param) ] -> ( match pa.Ast.ty with Ast.Multi "Text" -> true | _ -> false)
| _ -> false
in
if entry_shaped then begin
entry := !nmethods; entry := !nmethods;
(match m.ret with (match m.ret with
| Some ty when ty <> Ast.Scalar "Int" -> | Some ty when ty <> Ast.Scalar "Int" ->

View file

@ -1314,6 +1314,36 @@ let typecheck_program ~file ~(module_of : string -> string)
()) ())
| _ -> ()); | _ -> ());
{ typ = TScalar "Bool"; is_nil = false } { typ = TScalar "Bool"; is_nil = false }
| Binary (((Add | Sub | Mul | Div | Mod) as op), left, right) ->
let lres = typecheck_expr env cenv left in
let rres = typecheck_expr env cenv right in
(* `+` is arithmetic, never string addition (docs/plan/oop-vm/
08-builtin-surface.md's operator table) — and a Text operand here
is not a harmless type slip: the emitter would lower it to ADD on
two heap pointers, producing a wild pointer with no diagnostic. So
this is reported off CONFIDENT types (the same "stay silent when
underivable" contract as every other check here) and names the
operator that does concatenate. *)
let text_side (e : expr) (r : expr_type_result) : bool =
match confident_typ cenv e with
| Some t -> unwrap_nullable t = TScalar "Text"
| None -> ( match e.kind with StrLit _ | Interp _ -> true | _ -> ignore r; false)
in
if text_side left lres || text_side right rres then
Diag.Collector.add collector
(Diag.error ~code:type_mismatch_code ~file ~line:e.pos.line ~col:e.pos.col
~message:
(Printf.sprintf
"`%s` is arithmetic and never joins text — use `..` to concatenate"
(match op with
| Add -> "+"
| Sub -> "-"
| Mul -> "*"
| Div -> "/"
| _ -> "%"))
());
{ typ = (match confident_typ cenv left with Some t -> t | None -> TScalar "Int");
is_nil = false }
| Binary (_, left, right) -> | Binary (_, left, right) ->
let _ = typecheck_expr env cenv left in let _ = typecheck_expr env cenv left in
let _ = typecheck_expr env cenv right in let _ = typecheck_expr env cenv right in

View file

@ -10,10 +10,10 @@ banner and this board change. Every plan and phase doc opens with a
discarded/learnings registers carry none by design. discarded/learnings registers carry none by design.
Update this board in the same change that finishes work — move the item to done Update this board in the same change that finishes work — move the item to done
with *what actually landed*, set the next in-progress item, and record any with _what actually landed_, set the next in-progress item, and record any
rejection in [`discarded.md`](plan/discarded.md) with its reason. rejection in [`discarded.md`](plan/discarded.md) with its reason.
Statuses: ✅ **done** · 🔄 **in progress** · ⬜ **pending** · ⏸ **parked** Statuses: ✅ **done** · 🔄 **in progress** · ⬜ **pending** · ⏸ **hold**
--- ---
@ -34,7 +34,7 @@ optionals throughout in place of the Haxe original's sentinel values, so
nothing else in this plan can land ahead of it. nothing else in this plan can land ahead of it.
Two tracks run in this repo. The critical path is the **language track**: Two tracks run in this repo. The critical path is the **language track**:
iterations 3 → 4 → 5 → 6 → 7, ending at *compile and run log-watcher*. The iterations 3 → 4 → 5 → 6 → 7, ending at _compile and run log-watcher_. The
Rust-runtime track is shipped-and-maintained, not advancing. Rust-runtime track is shipped-and-maintained, not advancing.
--- ---
@ -47,7 +47,7 @@ an unsplittable slice with Given/When/Then acceptance and a pointer to the plan
that sequences its tasks. Read one, approve, then the next starts. that sequences its tasks. Read one, approve, then the next starts.
| # | Iteration | State | | # | Iteration | State |
| --- | --- | --- | | --- | -------------------------------------------------------------------------------------------- | ---------------------------- | ---- |
| 1 | [Principles doc](stories/language-runtime-database/01-principles-doc.md) | ✅ | | 1 | [Principles doc](stories/language-runtime-database/01-principles-doc.md) | ✅ |
| 2 | [VM core (`wovm`)](stories/language-runtime-database/02-vm-core.md) | ✅ | | 2 | [VM core (`wovm`)](stories/language-runtime-database/02-vm-core.md) | ✅ |
| 3 | [Compiler front (`woc`)](stories/language-runtime-database/03-compiler-front.md) | ✅ (known gaps below) | | 3 | [Compiler front (`woc`)](stories/language-runtime-database/03-compiler-front.md) | ✅ (known gaps below) |
@ -59,16 +59,16 @@ that sequences its tasks. Read one, approve, then the next starts.
| 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ | | 8 | [Shard-actor runtime](stories/language-runtime-database/08-shard-actor-runtime.md) | ⬜ |
| 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | ⬜ | | 9 | [Database engine](stories/language-runtime-database/09-database-engine.md) | ⬜ |
| 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | ⬜ needs a spec first | | 9b | [`@table`, relations, query](stories/language-runtime-database/09b-table-relations-query.md) | ⬜ needs a spec first |
| 10 | [HTTP service layer](stories/language-runtime-database/10-http-service.md) | ⬜ | | 10 | [HTTP service layer](stories/language-runtime-database/10-http-service.md) | ⬜ | Hold |
| 11 | [Fibers](stories/language-runtime-database/11-fibers.md) | ⬜ | | 11 | [Fibers](stories/language-runtime-database/11-fibers.md) | ⬜ | Hold |
| 12 | [Blue-green deploy](stories/language-runtime-database/12-blue-green-deploy.md) | ⬜ | | 12 | [Blue-green deploy](stories/language-runtime-database/12-blue-green-deploy.md) | ⬜ | Hold |
--- ---
## In progress ## In progress
| Track | Item | Where | | Track | Item | Where |
| --- | --- | --- | | -------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- |
| Language | Iteration 5 — Haxe-parity language surface, `?T` forced handling first | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) | | Language | Iteration 5 — Haxe-parity language surface, `?T` forced handling first | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) |
Nothing else should be started until iteration 5 lands. Off-critical-path work Nothing else should be started until iteration 5 lands. Off-critical-path work
@ -81,7 +81,7 @@ is parked by explicit scope directive (2026-08-08).
### Language track — compiler + VM (OOP track) ### Language track — compiler + VM (OOP track)
| Status | Item | Doc | What actually landed | | Status | Item | Doc | What actually landed |
| --- | --- | --- | --- | | ------ | ------------------------------------ | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ✅ | Principles | [`../00-principles.md`](00-principles.md) | 13 principles, each with a why and a link to the doc that enforces it | | ✅ | Principles | [`../00-principles.md`](00-principles.md) | 13 principles, each with a why and a link to the doc that enforces it |
| ✅ | `wovm` VM core | [plan 1](superpowers/plans/2026-08-01-wob-format-and-vm-core.md) | `.wob` v1 loader with full static validation, register interpreter (computed-goto + ISO-C fallback), arena with size-class free lists, borrow word, RC + budgeted Bacon–Rajan cycle collector, drop-map trap unwinding, containers, builtins, ICALL, CLI. 13 suites × 2 dispatch flavors + CLI smoke, ASan/UBSan clean | | ✅ | `wovm` VM core | [plan 1](superpowers/plans/2026-08-01-wob-format-and-vm-core.md) | `.wob` v1 loader with full static validation, register interpreter (computed-goto + ISO-C fallback), arena with size-class free lists, borrow word, RC + budgeted Bacon–Rajan cycle collector, drop-map trap unwinding, containers, builtins, ICALL, CLI. 13 suites × 2 dispatch flavors + CLI smoke, ASan/UBSan clean |
| ✅ | `.wob` format contract | [`oop-vm/00-wob-format.md`](plan/oop-vm/00-wob-format.md) | Normative; twinned with `runtime/src/wob.h` | | ✅ | `.wob` format contract | [`oop-vm/00-wob-format.md`](plan/oop-vm/00-wob-format.md) | Normative; twinned with `runtime/src/wob.h` |
@ -139,7 +139,7 @@ is parked by explicit scope directive (2026-08-08).
a class-returning `main` was never legal — `WO-E405` a class-returning `main` was never legal — `WO-E405`
(`compiler/src/emit.ml`, `01-error-catalog.md`) now rejects it at (`compiler/src/emit.ml`, `01-error-catalog.md`) now rejects it at
compile time, and `gc/held-cycle` is retired because its premise (an compile time, and `gc/held-cycle` is retired because its premise (an
externally-held cycle survives a *post-exit* pump) is no longer externally-held cycle survives a _post-exit_ pump) is no longer
expressible — see `oop-vm/02-corpus.md`'s "Retired" note for why, and expressible — see `oop-vm/02-corpus.md`'s "Retired" note for why, and
for where the scenario it meant to cover is actually proven for where the scenario it meant to cover is actually proven
(`runtime/test/test_cycle.c`, plus a proper in-flight fixture scheduled (`runtime/test/test_cycle.c`, plus a proper in-flight fixture scheduled
@ -149,7 +149,7 @@ is parked by explicit scope directive (2026-08-08).
### Rust runtime track — Stage 2 shipped, maintained ### Rust runtime track — Stage 2 shipped, maintained
| Status | Phase | Doc | Notes | | Status | Phase | Doc | Notes |
| --- | --- | --- | --- | | ------ | ------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| ✅ | 01 crate scaffolding | [done/01](plan/done/01-scafolding-crates.md) | 15 crates | | ✅ | 01 crate scaffolding | [done/01](plan/done/01-scafolding-crates.md) | 15 crates |
| ✅ | 02 epoll event loop | [done/02](plan/done/02-event-loop-epoll.md) | `runtime/netpoll_epoll.rs` | | ✅ | 02 epoll event loop | [done/02](plan/done/02-event-loop-epoll.md) | `runtime/netpoll_epoll.rs` |
| ✅ | 03 hand-rolled HTTP | [done/03](plan/done/03-hand-rolled-http.md) | + keep-alive & pipelining | | ✅ | 03 hand-rolled HTTP | [done/03](plan/done/03-hand-rolled-http.md) | + keep-alive & pipelining |
@ -175,7 +175,7 @@ Ecommerce sample (verified 2026-06-13): `api.rest` 17/17 expected statuses pass.
### Language track — sequenced, on the critical path ### Language track — sequenced, on the critical path
| # | Item | Plan | | # | Item | Plan |
| --- | --- | --- | | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| 5 | Haxe-parity language surface — **`?T` forced handling first**, then switch expressions, records, enum payloads, try/catch, statics, `using`, modules, `is`, `pub(read)`, `#if` | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) | | 5 | Haxe-parity language surface — **`?T` forced handling first**, then switch expressions, records, enum payloads, try/catch, statics, `using`, modules, `is`, `pub(read)`, `#if` | [plan 8](plan/compiler/2026-08-01-haxe-parity-language.md) |
| 6 | Program mode + systems stdlib — `fn main`, exit codes, `fs`/`proc`/`net`/`time`/`json` | [plan 9](superpowers/plans/2026-08-01-program-mode-stdlib.md) | | 6 | Program mode + systems stdlib — `fn main`, exit codes, `fs`/`proc`/`net`/`time`/`json` | [plan 9](superpowers/plans/2026-08-01-program-mode-stdlib.md) |
| 7 | log-watcher proof — the sample compiles and detects a silent death live | [plan 10](superpowers/plans/2026-08-01-log-watcher-sample.md) | | 7 | log-watcher proof — the sample compiles and detects a silent death live | [plan 10](superpowers/plans/2026-08-01-log-watcher-sample.md) |
@ -208,7 +208,7 @@ log-watcher proof.
### Rust runtime track — not advancing while the language track runs ### Rust runtime track — not advancing while the language track runs
| Status | Phase | Doc | Notes | | Status | Phase | Doc | Notes |
| --- | --- | --- | --- | | ------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| ⬜ | 05 hand-rolled JSON | [05](plan/05-hand-rolled-json.md) | removes serde/serde_json | | ⬜ | 05 hand-rolled JSON | [05](plan/05-hand-rolled-json.md) | removes serde/serde_json |
| ⬜ | 06 bespoke error type | [06](plan/06-bespoke-error.md) | removes anyhow | | ⬜ | 06 bespoke error type | [06](plan/06-bespoke-error.md) | removes anyhow |
| ⬜ | 07 inotify content watcher | [07](plan/07-inotify-content-watcher.md) | `wo dev` hot reload | | ⬜ | 07 inotify content watcher | [07](plan/07-inotify-content-watcher.md) | `wo dev` hot reload |
@ -224,7 +224,7 @@ log-watcher proof.
### Frontend — parked ### Frontend — parked
| Status | Phase | Doc | | Status | Phase | Doc |
| --- | --- | --- | | ------ | -------------------------------- | ------------------------------------------------------------------- |
| ⏸ | 13d pricing UI | [13](plan/13-class-model-live-pricing.md) | | ⏸ | 13d pricing UI | [13](plan/13-class-model-live-pricing.md) |
| ⏸ | 14 MVC UI implementation (14a–f) | [14](plan/14-mvc-ui-implementation.md) | | ⏸ | 14 MVC UI implementation (14a–f) | [14](plan/14-mvc-ui-implementation.md) |
| ⏸ | UI exploration track | [exploration/ui/00-overview.md](plan/exploration/ui/00-overview.md) | | ⏸ | UI exploration track | [exploration/ui/00-overview.md](plan/exploration/ui/00-overview.md) |

View file

@ -129,7 +129,7 @@ pub fn sanitize(line: Text) -> Text {
} }
continue; continue;
} }
if c >= 32 or c == 9 { out = out + char_of(c); } if c >= 32 or c == 9 { out = out .. char_of(c); }
i = i + 1; i = i + 1;
} }
return out; return out;

View file

@ -125,9 +125,9 @@ class Mcp {
} }
let resp = self.handle(req); let resp = self.handle(req);
let head = "HTTP/1.1 ${resp.status} ${status_text(resp.status)}\r\n"; let head = "HTTP/1.1 ${resp.status} ${status_text(resp.status)}\r\n";
head = head + "Content-Type: application/json\r\nConnection: close\r\n"; head = head .. "Content-Type: application/json\r\nConnection: close\r\n";
head = head + "Content-Length: ${len(resp.body)}\r\n\r\n"; head = head + "Content-Length: ${len(resp.body)}\r\n\r\n";
try net.write(c, head + resp.body) catch (e) {} try net.write(c, head .. resp.body) catch (e) {}
} }
} }
@ -140,7 +140,7 @@ class Mcp {
while header_end == -1 { while header_end == -1 {
let got = net.read(c, 8192); let got = net.read(c, 8192);
if len(got) == 0 { return nil; } -- peer closed mid-headers if len(got) == 0 { return nil; } -- peer closed mid-headers
buf = buf + got; buf = buf .. got;
header_end = index_of(buf, "\r\n\r\n"); header_end = index_of(buf, "\r\n\r\n");
if len(buf) > BODY_MAX * 2 { return nil; } -- runaway header block if len(buf) > BODY_MAX * 2 { return nil; } -- runaway header block
} }
@ -169,7 +169,7 @@ class Mcp {
while len(body) < want { while len(body) < want {
let got = net.read(c, want - len(body)); let got = net.read(c, want - len(body));
if len(got) == 0 { break; } if len(got) == 0 { break; }
body = body + got; body = body .. got;
} }
let method = ""; let method = "";
if len(req_line) > 0 { method = req_line[0]; } if len(req_line) > 0 { method = req_line[0]; }
@ -329,7 +329,7 @@ class Tools {
if want > CHUNK { want = CHUNK; } if want > CHUNK { want = CHUNK; }
let chunk = fs.read_at(path, offset, want); let chunk = fs.read_at(path, offset, want);
if len(chunk) == 0 { break; } if len(chunk) == 0 { break; }
let lines = split(carry + chunk, "\n"); let lines = split(carry .. chunk, "\n");
carry = pop(lines); -- last piece has no newline yet carry = pop(lines); -- last piece has no newline yet
for line in lines { for line in lines {
if index_of(to_lower(line), lc) >= 0 { if index_of(to_lower(line), lc) >= 0 {

View file

@ -203,7 +203,7 @@ class Supervisor {
} }
let line = json.encode(Detection { ts: time.iso(now), path: log_path, let line = json.encode(Detection { ts: time.iso(now), path: log_path,
source: source, event: event, lastError: last_error }); source: source, event: event, lastError: last_error });
try fs.append(self.cfg.detections, line + "\n") catch (e) { try fs.append(self.cfg.detections, line .. "\n") catch (e) {
print("DETECTIONS-SINK-ERROR ${self.cfg.detections}: ${e.msg}"); print("DETECTIONS-SINK-ERROR ${self.cfg.detections}: ${e.msg}");
} }
} }

View file

@ -482,9 +482,12 @@ int wo_load_buf(wo_module *m, const uint8_t *buf, size_t len, char *err,
/* entry: a zero-arg free fn */ /* entry: a zero-arg free fn */
if (entry != WOB_NONE) { if (entry != WOB_NONE) {
if (entry >= m->method_cnt) BAIL("entry method out of range"); if (entry >= m->method_cnt) BAIL("entry method out of range");
if (m->methods[entry].arg_cnt != 0 || /* program mode: an entry is a free fn taking nothing, or one
argument — the `multi Text` of command-line arguments the runtime
builds (runtime/src/main.c). */
if (m->methods[entry].arg_cnt > 1 ||
m->methods[entry].class_id != WOB_NONE) m->methods[entry].class_id != WOB_NONE)
BAIL("entry must be a zero-arg free fn"); BAIL("entry must be a free fn taking no arguments or one argv `multi Text`");
} }
m->entry = entry; m->entry = entry;
return 0; return 0;

View file

@ -11,6 +11,7 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include "cont.h"
#include "gc.h" #include "gc.h"
#include "vm.h" #include "vm.h"
@ -128,9 +129,10 @@ int main(int argc, char **argv) {
return 2; return 2;
} }
if (self_rc == 0) { if (self_rc == 0) {
/* no embedded image: today's contract, unchanged */ /* no embedded image: the image path is argv[1], and program mode
if (argc != 2) { passes everything after it to the program itself */
fprintf(stderr, "usage: wovm <file.wob>\n"); if (argc < 2) {
fprintf(stderr, "usage: wovm <file.wob> [args...]\n");
return 2; return 2;
} }
if (wo_load_file(&mod, argv[1], err, sizeof err) != 0) { if (wo_load_file(&mod, argv[1], err, sizeof err) != 0) {
@ -155,14 +157,46 @@ int main(int argc, char **argv) {
wo_module_free(&mod); wo_module_free(&mod);
return 2; return 2;
} }
/* Program mode: an entry that declares one parameter gets the program's
* OWN arguments as a `multi Text` — not the program name, and not the
* image path a plain `wovm image.wob args...` invocation carries. So
* `args[0]` is the first real argument (the workload's own contract:
* `args[0] == "watch"`, `args[1]` the log file). An entry with no
* parameters is called exactly as before. */
uint64_t argv_val = 0;
uint32_t entry_argc = mod.methods[mod.entry].arg_cnt;
if (entry_argc == 1) {
wo_multi *args = wo_multi_new(&VM.rt, WO_K_TEXT);
if (!args) {
fprintf(stderr, "wovm: cannot allocate the argument list\n");
wo_vm_destroy(&VM);
wo_module_free(&mod);
return 2;
}
int first = self_rc == 0 ? 2 : 1; /* skip the image path when there is one */
for (int i = first; i < argc; i++) {
wo_str *s = wo_str_new(&VM.rt, argv[i], (uint32_t)strlen(argv[i]));
if (!s || wo_multi_push(args, (uint64_t)(uintptr_t)s) != 0) {
fprintf(stderr, "wovm: cannot allocate the argument list\n");
wo_vm_destroy(&VM);
wo_module_free(&mod);
return 2;
}
}
argv_val = (uint64_t)(uintptr_t)args;
}
uint64_t ret = 0; uint64_t ret = 0;
wo_err terr; wo_err terr;
int rc = wo_vm_call(&VM, mod.entry, NULL, 0, &ret, &terr); int rc = wo_vm_call(&VM, mod.entry, entry_argc == 1 ? &argv_val : NULL, entry_argc, &ret,
&terr);
if (rc != 0) if (rc != 0)
fprintf(stderr, "trap %u in %s at line %u: %s\n", (unsigned)terr.code, fprintf(stderr, "trap %u in %s at line %u: %s\n", (unsigned)terr.code,
terr.method, (unsigned)terr.line, terr.msg); terr.method, (unsigned)terr.line, terr.msg);
/* the entry's return value IS the exit code (docs/plan/oop-vm/
* 08-builtin-surface.md's "Program entry"): 0..255, a trap is 1 */
int exit_code = rc == 0 ? (int)((uint64_t)ret & 0xFF) : 1;
gc_pump(&VM); gc_pump(&VM);
wo_vm_destroy(&VM); wo_vm_destroy(&VM);
wo_module_free(&mod); wo_module_free(&mod);
return rc == 0 ? 0 : 1; return exit_code;
} }