feat: systems stdlib OS half — fs, time, env, net, proc

log-watcher diagnostics 129 -> 55 (json is what is left: 13 encode sites,
3 `as` parses and their cascade). corpus 71/0, woc 565/0, wovm gates green.

- runtime/src/sysio.c (new): 17 builtins behind the reserved module names —
  fs.exists/list/stat/read_all/read_at/append, time.sleep/local/iso,
  env.get/stopping, net.listen/accept/read/write/close, proc.run. Thin
  blocking libc calls; a failed syscall traps the new WO_T_IO with errno's
  own message, which `try ... catch` is how a program handles
  - record-returning members (fs.stat, time.local, proc.run) take their
    result record's CLASS ID as the last argument, so the VM allocates what
    it fills without knowing any source type name (the err_fill pattern)
  - absence is the zero word: a missing path from fs.stat and an unset
    env.get are nil, not traps
  - env.stopping installs SIGTERM/SIGINT handlers on first use only
- types.ml: predeclared Stat/TimeParts/Proc records (field order is the
  contract with sysio.c) + the stdlib member table (arity, builtin id,
  return type, result record) + stdlib return types in confident_typ
- emit.ml: stdlib member calls lower to their builtin with the record class
  id appended; WO-E406 now means "no such member", not "not linked";
  predeclared records enter the class table only when a program needs them
- emit.ml: fstate carries the method's declared return type, so a tail
  `return []` / `return {}` gets its element kinds; a non-empty list literal
  falls back to its own element type when there is no declared destination
- types.ml: confident_typ chases a container read (`c[i]`), which is what
  makes a switch over a value pulled out of a map resolve; a void `try` arm
  no longer demands its catch arm agree
- corpus: lang-use-stdlib-not-linked now pins WO-E406 for an unknown MEMBER
  (fs.slurp) — the "not linked" premise is gone now that fs is linked

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-14 16:54:31 +02:00
parent ea77fc9d5c
commit fb91085bdb
8 changed files with 787 additions and 50 deletions

View file

@ -463,6 +463,10 @@ type loop_frame = {
type fstate = {
f_file : string;
f_fn : string;
(* the enclosing method's declared return type — what gives a contextual
value in tail position its type (`return []` / `return nil` /
`return {}`), the same role a `let`'s annotation plays *)
f_ret : Ast.field_ty option;
f_code : code;
mutable f_cur_line : int; (* line of the construct being lowered *)
mutable f_line : int; (* last line written to the table *)
@ -845,6 +849,21 @@ let iface_method (p : pctx) (iname : string) (m : string) : (int * Types.method_
| None -> None
| Some sg -> Some (slot, sg))))
(* Types.typ -> this file's own Ast.field_ty view. Needed for the one table
that is stated in the typechecker's language and consumed here: the
systems stdlib's declared return shapes (Types.stdlib_members). Container
element types beyond one scalar level cannot be spelled as a field_ty
(`Multi of string`), so a nested container yields None — nothing in the
stdlib returns one. *)
let rec field_ty_of_typ (t : Types.typ) : Ast.field_ty option =
match t with
| Types.TScalar n -> Some (Scalar n)
| Types.TRef n -> Some (Ref n)
| Types.TMulti (Types.TScalar n) -> Some (Multi n)
| Types.TMap (Types.TScalar k, Types.TScalar v) -> Some (Map (k, v))
| Types.TNullable inner -> ( match field_ty_of_typ inner with Some ft -> Some (Nullable ft) | None -> None)
| Types.TMulti _ | Types.TMap _ | Types.TVoid -> None
let builtin_ret (name : string) (argty : Ast.field_ty option) : Ast.field_ty option =
match name with
| "int_to_text" -> Some (Scalar "Text")
@ -1004,7 +1023,12 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option
match static_method p cls_name mname with Some mi -> mi.Types.ret | None -> None)
| Ident alias -> (
match use_edge_for p ~file:f.f_file alias with
| Some u when u.Types.ue_is_stdlib -> None (* nothing to infer a return type from yet *)
(* the systems stdlib's own declared return shapes (Types.
stdlib_members) — the source of `st.size` resolving at all *)
| Some u when u.Types.ue_is_stdlib -> (
match Types.stdlib_member alias mname with
| Some sm -> ( match sm.Types.sm_ret with Some t -> field_ty_of_typ t | None -> None)
| None -> None)
| Some u -> (
let target_mid = Types.path_str u.Types.ue_segments in
match Hashtbl.find_opt p.p_module_syms target_mid with
@ -1295,7 +1319,15 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
(docs/plan/oop-vm/08-builtin-surface.md) — plus one `multi_push` per
element, in source order. *)
| ListLit items -> (
match container_imm p expected false with
(* the destination decides the element kinds; when there is no declared
destination, a non-empty literal knows its own element type and a
tail-position literal (`return []`) takes the method's return type *)
let dest =
match expected with
| Some _ -> expected
| None -> ( match ty_of_expr p f e with Some t -> Some t | None -> f.f_ret)
in
match container_imm p dest false with
| None ->
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
~message:
@ -1306,7 +1338,7 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
sync_mask p f v e.id;
put f (ins_abc op_builtin dst imm b_multi_new);
let elem_ty =
match expected with
match dest with
| Some t -> ( match unwrap t with Multi en -> Some (Scalar en) | _ -> None)
| None -> None
in
@ -1327,7 +1359,8 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
items;
f.f_temp <- outer)
| MapLit -> (
match container_imm p expected true with
let dest = match expected with Some _ -> expected | None -> f.f_ret in
match container_imm p dest true with
| None ->
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
~message:
@ -2223,12 +2256,52 @@ and emit_call (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e : As
| None -> ())
| Ident alias -> (
match use_edge_for p ~file:f.f_file alias with
| Some u when u.Types.ue_is_stdlib ->
| Some u when u.Types.ue_is_stdlib -> (
(* the systems stdlib: one builtin per member. A member whose
result is a record takes that record's class id as its last
argument, so the VM allocates what it fills (sysio.c). *)
match Types.stdlib_member alias mname with
| None ->
err p ~code:stdlib_not_linked_code ~file:f.f_file ~pos:e.pos
~message:
(Printf.sprintf "stdlib module `%s` is not linked in this milestone (called as `%s.%s`)"
alias alias mname);
(Printf.sprintf "stdlib module `%s` has no member `%s`" alias mname);
put f (ins_abx op_loadk dst (const_int p 0))
| Some sm ->
if List.length args <> sm.Types.sm_arity then begin
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
~message:
(Printf.sprintf "`%s.%s` takes %d argument(s), given %d" alias mname
sm.Types.sm_arity (List.length args));
put f (ins_abx op_loadk dst (const_int p 0))
end
else begin
let extra = match sm.Types.sm_record with Some _ -> 1 | None -> 0 in
let n = sm.Types.sm_arity + extra in
let base = alloc_temps p f e.pos (max n 1) in
List.iteri
(fun i (a : Ast.expr) ->
let save = f.f_temp in
emit_expr p f v ~dst:(base + i) a;
f.f_temp <- save)
args;
(match sm.Types.sm_record with
| Some rec_name -> (
match class_of_name p rec_name with
| Some cid ->
put f
(ins_abx op_loadk (base + sm.Types.sm_arity)
(check_bx p f e.pos "constant" (const_int p cid)))
| None ->
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
~message:
(Printf.sprintf "no class-table entry for the `%s` record — `%s.%s` cannot \
build its result"
rec_name alias mname))
| None -> ());
sync_mask p f v e.id;
f.f_cur_line <- e.pos.line;
put f (ins_abc op_builtin dst base sm.Types.sm_builtin)
end)
| Some u -> (
let target_mid = Types.path_str u.Types.ue_segments in
let target_fi =
@ -3129,7 +3202,7 @@ and emit_do_while (p : pctx) (f : fstate) (v : views) (s : Ast.stmt) (body : Ast
let emit_method (p : pctx) (v : views) ~(file : string) ~(self_class : (int * string) option)
(m : Ast.method_decl) (rec_ : methrec) : unit =
let f =
{ f_file = file; f_fn = m.name; f_code = code_create (); f_cur_line = m.pos.line;
{ f_file = file; f_fn = m.name; f_ret = m.ret; f_code = code_create (); f_cur_line = m.pos.line;
f_line = -1; f_lines = []; f_owned = 0L; f_gc = 0L; f_last_owned = 0L; f_last_gc = 0L;
f_drops = []; f_nlocals = 0; f_temp = 0; f_max = 0; f_env = []; f_decl = Hashtbl.create 16;
f_node = Hashtbl.create 64; f_kind = Hashtbl.create 16; f_declared = []; f_div = false; f_maxjmp = 0;
@ -3207,6 +3280,25 @@ let program_uses_try (prog : Ast.program) : bool =
prog;
!found
(* Which predeclared records does this program actually need a class-table
entry for? `Error` when it catches; a stdlib result record when it calls
the member that fills one. Nothing else gains a class it never uses, so
every image that predates this surface keeps its exact class table. *)
let needed_records (prog : Ast.program) : string list =
let want = ref [] in
let add n = if not (List.mem n !want) then want := n :: !want in
Types.walk_program
(fun _ (e : Ast.expr) ->
match e.Ast.kind with
| Ast.Try _ -> add Types.error_record_name
| Ast.Call ({ Ast.kind = Ast.Field ({ Ast.kind = Ast.Ident head; _ }, mname); _ }, _) -> (
match Types.stdlib_member head mname with
| Some { Types.sm_record = Some r; _ } -> add r
| _ -> ())
| _ -> ())
prog;
!want
(* Structural satisfaction, Go-style (spec section 2): a class satisfies
an interface when it has a method of the same name and parameter count
for every method the interface declares. There is no `implements`
@ -3349,18 +3441,20 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
and only when nothing already claims the name. Its field order is
Types.error_record_fields, which is the same order the VM's
WO_B_ERR_FILL builtin writes. *)
List.iter
(fun (name, fields) ->
if
(not (SM.mem Types.error_record_name !class_id))
&& List.exists (fun u -> program_uses_try u.prog) units
(not (SM.mem name !class_id))
&& List.exists (fun u -> List.mem name (needed_records u.prog)) units
then begin
let cid = !nclasses in
class_id := SM.add Types.error_record_name cid !class_id;
class_id := SM.add name cid !class_id;
incr nclasses;
classes :=
{ cr_name = Types.error_record_name; cr_gc = false;
cr_fields = Array.of_list Types.error_record_fields; cr_methods = [] }
{ cr_name = name; cr_gc = false; cr_fields = Array.of_list fields; cr_methods = [] }
:: !classes
end;
end)
Types.predeclared_records;
let class_id = !class_id in
let p_classes = Array.of_list (List.rev !classes) in
let p_ifaces = Array.of_list (List.rev !ifaces) in

View file

@ -184,6 +184,79 @@ let error_record_fields : (string * field_ty) list =
[ ("code", Scalar "Int"); ("line", Scalar "Int"); ("method", Scalar "Text");
("msg", Scalar "Text") ]
(* The other predeclared records: the results the systems stdlib's
record-returning members fill. Field ORDER is the contract with
runtime/src/sysio.c, which writes them by index (the compiler passes the
record's class id as the member's last argument, so the VM allocates what
it fills without knowing any source type name). *)
let stat_record_name = "Stat"
let stat_record_fields : (string * field_ty) list =
[ ("size", Scalar "Int"); ("mtime", Scalar "Int"); ("inode", Scalar "Int");
("dir", Scalar "Bool") ]
let time_record_name = "TimeParts"
let time_record_fields : (string * field_ty) list =
[ ("year", Scalar "Int"); ("month", Scalar "Int"); ("day", Scalar "Int");
("hour", Scalar "Int"); ("minute", Scalar "Int"); ("second", Scalar "Int");
("dow", Scalar "Int") ]
let proc_record_name = "Proc"
let proc_record_fields : (string * field_ty) list =
[ ("code", Scalar "Int"); ("out", Scalar "Text"); ("err", Scalar "Text") ]
let predeclared_records : (string * (string * field_ty) list) list =
[ (error_record_name, error_record_fields); (stat_record_name, stat_record_fields);
(time_record_name, time_record_fields); (proc_record_name, proc_record_fields) ]
(* One member of a reserved stdlib module (`fs.stat`, `net.write`, ...).
[sm_builtin] is its .wob builtin id (runtime/src/wob.h); [sm_record] names
the predeclared record whose class id the emitter appends as the call's
last argument, so [sm_arity] is the SOURCE-visible argument count, not the
builtin's. `time.now` deliberately reuses the existing `now` builtin
rather than adding a second clock. *)
type stdlib_member = {
sm_module : string;
sm_name : string;
sm_arity : int;
sm_builtin : int;
sm_ret : typ option; (* None = yields no value *)
sm_record : string option;
}
let stdlib_members : stdlib_member list =
let m sm_module sm_name sm_arity sm_builtin sm_ret sm_record =
{ sm_module; sm_name; sm_arity; sm_builtin; sm_ret; sm_record }
in
[ (* fs *)
m "fs" "exists" 1 40 (Some (TScalar "Bool")) None;
m "fs" "list" 1 41 (Some (TMulti (TScalar "Text"))) None;
m "fs" "stat" 1 42 (Some (TNullable (TScalar stat_record_name))) (Some stat_record_name);
m "fs" "read_all" 2 43 (Some (TScalar "Text")) None;
m "fs" "read_at" 3 44 (Some (TScalar "Text")) None;
m "fs" "append" 2 45 None None;
(* time *)
m "time" "now" 0 0 (Some (TScalar "Int")) None;
m "time" "sleep" 1 46 None None;
m "time" "local" 1 47 (Some (TScalar time_record_name)) (Some time_record_name);
m "time" "iso" 1 48 (Some (TScalar "Text")) None;
(* env *)
m "env" "get" 1 49 (Some (TNullable (TScalar "Text"))) None;
m "env" "stopping" 0 50 (Some (TScalar "Bool")) None;
(* net *)
m "net" "listen" 2 51 (Some (TScalar "Int")) None;
m "net" "accept" 1 52 (Some (TScalar "Int")) None;
m "net" "read" 2 53 (Some (TScalar "Text")) None;
m "net" "write" 2 54 None None;
m "net" "close" 1 55 None None;
(* proc *)
m "proc" "run" 2 56 (Some (TNullable (TScalar proc_record_name))) (Some proc_record_name) ]
let stdlib_member (m : string) (name : string) : stdlib_member option =
List.find_opt (fun s -> s.sm_module = m && s.sm_name = name) stdlib_members
(* Adds the predeclared records to a symbol table. Applied to the MERGED
table only (bin/main.ml), never to a per-file one: one entry per file
would read as a cross-file duplicate declaration (WO-E214). A source
@ -191,14 +264,17 @@ let error_record_fields : (string * field_ty) list =
ones `catch (e)` binds, which is either what it wanted or a type error
it will hear about at the use site. *)
let with_builtin_records (syms : symbols) : symbols =
if StringMap.mem error_record_name syms.classes then syms
List.fold_left
(fun (acc : symbols) ((name : string), (fields : (string * field_ty) list)) ->
if StringMap.mem name acc.classes then acc
else
let info =
{ name = error_record_name; is_class = false; is_record = true; is_gc = false; table = None;
fields = List.map (fun (n, t) -> (n, t, None, [])) error_record_fields; methods = [];
id = -1; pos = { line = 0; col = 0 }; pub = true }
{ name; is_class = false; is_record = true; is_gc = false; table = None;
fields = List.map (fun (n, t) -> (n, t, None, [])) fields; methods = []; id = -1;
pos = { line = 0; col = 0 }; pub = true }
in
{ syms with classes = StringMap.add error_record_name info syms.classes }
{ acc with classes = StringMap.add name info acc.classes })
syms predeclared_records
let rec has_recursive_structure (cls : class_info) : bool =
List.exists (fun (_, ty, _, _) ->
@ -922,6 +998,14 @@ let typecheck_program ~file ~(module_of : string -> string)
would answer, and the try arm is the one that always has a value. *)
| Try { body; _ } -> confident_typ cenv body
| Ident name -> StringMap.find_opt name cenv
(* `c[i]` — a container read is exactly as confident as the container
itself (a `switch` over `w.result()` where `w` came out of a map
depends on this chain resolving). *)
| Index (base, _) -> (
match Option.map unwrap_nullable (confident_typ cenv base) with
| Some (TMulti e') -> Some e'
| Some (TMap (_, v)) -> Some v
| _ -> None)
| Field (base, field_name) -> (
match confident_typ cenv base with
| Some (TScalar class_name) -> (
@ -990,13 +1074,18 @@ let typecheck_program ~file ~(module_of : string -> string)
(* haxe-parity Task 7: a static call (`Flock.held(path)`).
The base names a class, so it has no confident *value*
type above — only this shape reaches here with a
resolvable member. *)
resolvable member. A reserved stdlib module's member
(`fs.stat(path)`) has the same shape and is resolved from
the stdlib table. *)
match base.kind with
| Ident cls_name -> (
match static_method_of syms cls_name mname with
| Ident head -> (
match static_method_of syms head mname with
| Some m -> (
match m.ret with Some ft -> Some (resolve_field_ty ft) | None -> Some TVoid)
| None -> None)
| None -> (
match stdlib_member head mname with
| Some sm -> ( match sm.sm_ret with Some t -> Some t | None -> Some TVoid)
| None -> None))
| _ -> None))
| _ -> None)
| Ctor (class_name, _fields) ->
@ -1027,12 +1116,14 @@ let typecheck_program ~file ~(module_of : string -> string)
needs `int_to_text` first) -- unlike the placeholders below,
this is a fact, not a guess. *)
Some (TScalar "Text")
| Index _ | Unary _ | Binary _ | DbStub _ ->
(* Not chased: `Index`/the arithmetic-ladder `Binary` ops have no
reliable per-node type in this pass at all (see above);
`Unary`/`DbStub` would be cheap to add but nothing in this
task's fixtures or the log-watcher sample needs them, and a
narrower deriver is the safer default. *)
| Unary _ | Binary _ | DbStub _ ->
(* Not chased: the arithmetic-ladder `Binary` ops have no reliable
per-node type in this pass at all (see above); `Unary`/`DbStub`
would be cheap to add but nothing in this task's fixtures or the
log-watcher sample needs them, and a narrower deriver is the
safer default. (`Index` IS chased now — a container read is as
confident as its container, which is what lets a `switch` over a
value pulled out of a map resolve.) *)
None
| Switch _ ->
(* haxe-parity Task 3: same "not chased" call as `Index`/`Binary`
@ -1273,7 +1364,13 @@ let typecheck_program ~file ~(module_of : string -> string)
underivable" contract every other confident-type consumer here
follows, which also keeps a `catch (e) nil` arm quiet until
optionals land. *)
(* A try arm that yields nothing is statement position (`try
fs.append(...) catch (e) { ... }`): there is no value to agree
about, so whatever the catch arm's last statement evaluates to is
discarded exactly like the try arm's own result. *)
(match (handler_res, confident_typ cenv body) with
| _, Some TVoid -> ()
| _ when body_res.typ = TVoid -> ()
| Some (Some ht, hpos), Some bt when not (typ_equal syms ht bt) ->
Diag.Collector.add collector
(Diag.error ~code:type_mismatch_code ~file ~line:hpos.line ~col:hpos.col

View file

@ -63,6 +63,8 @@ static int elem_cmp(uint8_t kind, uint64_t a, uint64_t b) {
int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
wo_rt *rt = &vm->rt;
uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins);
/* the OS half lives in its own translation unit — see sysio.c */
if (C >= WO_B_SYS_FIRST) return wo_builtin_sys(vm, R, ins, msg);
switch (C) {
case WO_B_NOW: { /* wall-clock milliseconds */
struct timespec ts;

View file

@ -9,4 +9,9 @@
int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg);
/* The systems stdlib's OS half (runtime/src/sysio.c): same contract as
* wo_builtin above — 0 on success, a WO_T_* code with *msg set on failure.
* wo_builtin dispatches every id at or above WO_B_SYS_FIRST here. */
int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg);
#endif /* WO_BUILTIN_H */

View file

@ -51,6 +51,14 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
[WO_B_SLICE] = 3, [WO_B_POP] = 1, [WO_B_SHIFT] = 1,
[WO_B_SORT] = 1, [WO_B_REVERSE] = 1, [WO_B_MAP_REMOVE] = 2,
[WO_B_MAP_KEY_AT] = 2, [WO_B_MAP_VAL_AT] = 2, [WO_B_MULTI_SET] = 3,
/* systems stdlib, OS half (sysio.c). Record-returning members count
their result record's class id as an argument. */
[WO_B_FS_EXISTS] = 1, [WO_B_FS_LIST] = 1, [WO_B_FS_STAT] = 2,
[WO_B_FS_READ_ALL] = 2, [WO_B_FS_READ_AT] = 3, [WO_B_FS_APPEND] = 2,
[WO_B_TIME_SLEEP] = 1, [WO_B_TIME_LOCAL] = 2, [WO_B_TIME_ISO] = 1,
[WO_B_ENV_GET] = 1, [WO_B_ENV_STOPPING] = 0, [WO_B_NET_LISTEN] = 2,
[WO_B_NET_ACCEPT] = 1, [WO_B_NET_READ] = 2, [WO_B_NET_WRITE] = 2,
[WO_B_NET_CLOSE] = 1, [WO_B_PROC_RUN] = 3,
};
static int vtab_cmp(const void *a, const void *b) {

500
runtime/src/sysio.c Normal file
View file

@ -0,0 +1,500 @@
/* sysio.c — the systems stdlib's operating-system half: `fs`, `time`,
* `env`, `net` and `proc` (docs/plan/oop-vm/08-builtin-surface.md's
* "Modules" section). Split out of builtin.c because this is the only
* part of the runtime that talks to the kernel: everything here is a
* thin, blocking libc call, so the failure surface is uniform — a
* syscall that fails traps WO_T_IO with errno's own message, and the
* source decides whether that is fatal or a `try ... catch` away.
*
* Record-returning members (fs.stat, time.local, proc.run) take the
* class id of their result record as their LAST argument: the compiler
* predeclares the record (Types.stdlib_records) and passes the id, so
* the VM allocates the object it fills without knowing anything about
* the source's type names. Field order per record is the contract
* documented beside each case below. Absence is the zero word, like
* every other `?T`.
*/
#define _POSIX_C_SOURCE 200809L
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include "builtin.h"
#include "cont.h"
#include "gc.h"
/* ---- shared helpers -------------------------------------------------- */
/* A Text argument as a NUL-terminated C string in a caller-owned buffer:
* every path/name/host the kernel takes needs one, and `wo_str` carries no
* terminator. Returns -1 when the value is not a Text or does not fit. */
static int cstr_of(uint64_t v, char *buf, size_t cap, const char **msg) {
if (!v) {
*msg = "null text";
return -1;
}
const wo_str *s = (const wo_str *)(uintptr_t)v;
if (s->h.class_id != WO_CLS_STR) {
*msg = "not a text value";
return -1;
}
if (s->len + 1 > cap) {
*msg = "text too long for a path";
return -1;
}
memcpy(buf, s->data, s->len);
buf[s->len] = '\0';
return 0;
}
/* Allocate the record a stdlib member fills, given the class id the
* compiler passed and the field count that member's contract needs. */
static wo_hdr *record_of(wo_vm *vm, uint64_t class_id, uint32_t need, const char **msg) {
if (class_id >= vm->mod->class_cnt ||
vm->mod->classes[class_id].field_cnt < need) {
*msg = "stdlib result record has the wrong shape";
return NULL;
}
wo_hdr *o = wo_obj_new(&vm->rt, (uint32_t)class_id);
if (!o) *msg = "out of memory";
return o;
}
/* SIGTERM/SIGINT flag behind `env.stopping()`. Installed on first use, so a
* program that never asks keeps the default disposition. */
static volatile sig_atomic_t stop_flag = 0;
static int stop_installed = 0;
static void on_stop(int sig) {
(void)sig;
stop_flag = 1;
}
static void install_stop_handlers(void) {
if (stop_installed) return;
stop_installed = 1;
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_stop;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
}
/* Read a whole (or capped) byte range out of an open fd into a fresh Text.
* [want] is the byte ceiling; a short read is not an error (a growing log
* file is the normal case). */
static wo_str *read_range(wo_rt *rt, int fd, off_t off, size_t want, const char **msg,
uint32_t *tcode) {
wo_str *s = wo_str_alloc(rt, (uint32_t)want);
if (!s) {
*msg = "out of memory";
*tcode = WO_T_OOM;
return NULL;
}
size_t got = 0;
while (got < want) {
ssize_t n = off < 0 ? read(fd, s->data + got, want - got)
: pread(fd, s->data + got, want - got, off + (off_t)got);
if (n < 0) {
if (errno == EINTR) continue;
wo_str_free(rt, s);
*msg = strerror(errno);
*tcode = WO_T_IO;
return NULL;
}
if (n == 0) break; /* EOF */
got += (size_t)n;
}
s->len = (uint32_t)got; /* the allocation may be longer; length is truth */
return s;
}
int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
wo_rt *rt = &vm->rt;
uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins);
char path[4096];
switch (C) {
/* ---- fs ---------------------------------------------------------- */
case WO_B_FS_EXISTS: {
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
struct stat sb;
R[A] = stat(path, &sb) == 0 ? 1 : 0;
return 0;
}
case WO_B_FS_LIST: { /* names only, no "." / ".."; unsorted (the source
* sorts when order matters) */
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
DIR *d = opendir(path);
if (!d) {
*msg = strerror(errno);
return WO_T_IO;
}
wo_multi *out = wo_multi_new(rt, WO_K_TEXT);
if (!out) {
closedir(d);
*msg = "out of memory";
return WO_T_OOM;
}
struct dirent *e;
while ((e = readdir(d))) {
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
wo_str *nm = wo_str_new(rt, e->d_name, (uint32_t)strlen(e->d_name));
if (!nm || wo_multi_push(out, (uint64_t)(uintptr_t)nm) != 0) {
if (nm) wo_str_free(rt, nm);
wo_drop_obj(rt, &out->h);
closedir(d);
*msg = "out of memory";
return WO_T_OOM;
}
}
closedir(d);
R[A] = (uint64_t)(uintptr_t)out;
return 0;
}
case WO_B_FS_STAT: { /* Stat: 0 size, 1 mtime (ms), 2 inode, 3 dir */
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
struct stat sb;
if (stat(path, &sb) != 0) {
R[A] = 0; /* absent, not a failure: `?Stat`'s own nil */
return 0;
}
wo_hdr *o = record_of(vm, R[B + 1], 4, msg);
if (!o) return R[B + 1] >= vm->mod->class_cnt ? WO_T_BOUNDS : WO_T_OOM;
uint64_t *fs_ = wo_fields(o);
fs_[0] = (uint64_t)sb.st_size;
fs_[1] = (uint64_t)((int64_t)sb.st_mtime * 1000);
fs_[2] = (uint64_t)sb.st_ino;
fs_[3] = S_ISDIR(sb.st_mode) ? 1 : 0;
R[A] = (uint64_t)(uintptr_t)o;
return 0;
}
case WO_B_FS_READ_ALL: { /* up to `cap` bytes; a bigger file is truncated,
* which is what every caller's cap argument is
* there to bound */
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
int64_t cap = (int64_t)R[B + 1];
if (cap < 0) cap = 0;
int fd = open(path, O_RDONLY);
if (fd < 0) {
*msg = strerror(errno);
return WO_T_IO;
}
uint32_t tcode = 0;
wo_str *s = read_range(rt, fd, -1, (size_t)cap, msg, &tcode);
close(fd);
if (!s) return tcode;
R[A] = (uint64_t)(uintptr_t)s;
return 0;
}
case WO_B_FS_READ_AT: {
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
int64_t off = (int64_t)R[B + 1], want = (int64_t)R[B + 2];
if (off < 0 || want < 0) {
*msg = "negative offset or length";
return WO_T_BOUNDS;
}
int fd = open(path, O_RDONLY);
if (fd < 0) {
*msg = strerror(errno);
return WO_T_IO;
}
uint32_t tcode = 0;
wo_str *s = read_range(rt, fd, off, (size_t)want, msg, &tcode);
close(fd);
if (!s) return tcode;
R[A] = (uint64_t)(uintptr_t)s;
return 0;
}
case WO_B_FS_APPEND: {
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
const wo_str *body = (const wo_str *)(uintptr_t)R[B + 1];
if (!body || body->h.class_id != WO_CLS_STR) {
*msg = "not a text value";
return WO_T_BOUNDS;
}
int fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) {
*msg = strerror(errno);
return WO_T_IO;
}
uint32_t at = 0;
while (at < body->len) {
ssize_t n = write(fd, body->data + at, body->len - at);
if (n < 0) {
if (errno == EINTR) continue;
close(fd);
*msg = strerror(errno);
return WO_T_IO;
}
at += (uint32_t)n;
}
close(fd);
R[A] = 0;
return 0;
}
/* ---- time -------------------------------------------------------- */
case WO_B_TIME_SLEEP: {
int64_t ms = (int64_t)R[B];
if (ms > 0) {
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L}, rem;
while (nanosleep(&ts, &rem) != 0 && errno == EINTR) ts = rem;
}
R[A] = 0;
return 0;
}
case WO_B_TIME_LOCAL: { /* Parts: 0 year, 1 month (1..12), 2 day, 3 hour,
* 4 minute, 5 second, 6 dow (0 = Sunday) */
time_t secs = (time_t)((int64_t)R[B] / 1000);
struct tm tmv;
if (!localtime_r(&secs, &tmv)) {
*msg = "cannot convert that instant to local time";
return WO_T_BOUNDS;
}
wo_hdr *o = record_of(vm, R[B + 1], 7, msg);
if (!o) return R[B + 1] >= vm->mod->class_cnt ? WO_T_BOUNDS : WO_T_OOM;
uint64_t *fl = wo_fields(o);
fl[0] = (uint64_t)(tmv.tm_year + 1900);
fl[1] = (uint64_t)(tmv.tm_mon + 1);
fl[2] = (uint64_t)tmv.tm_mday;
fl[3] = (uint64_t)tmv.tm_hour;
fl[4] = (uint64_t)tmv.tm_min;
fl[5] = (uint64_t)tmv.tm_sec;
fl[6] = (uint64_t)tmv.tm_wday;
R[A] = (uint64_t)(uintptr_t)o;
return 0;
}
case WO_B_TIME_ISO: { /* UTC, second resolution: 1970-01-01T00:00:00Z */
time_t secs = (time_t)((int64_t)R[B] / 1000);
struct tm tmv;
char buf[32];
if (!gmtime_r(&secs, &tmv)) {
*msg = "cannot convert that instant to UTC";
return WO_T_BOUNDS;
}
int n = snprintf(buf, sizeof buf, "%04d-%02d-%02dT%02d:%02d:%02dZ", tmv.tm_year + 1900,
tmv.tm_mon + 1, tmv.tm_mday, tmv.tm_hour, tmv.tm_min, tmv.tm_sec);
wo_str *s = wo_str_new(rt, buf, (uint32_t)n);
if (!s) {
*msg = "out of memory";
return WO_T_OOM;
}
R[A] = (uint64_t)(uintptr_t)s;
return 0;
}
/* ---- env --------------------------------------------------------- */
case WO_B_ENV_GET: { /* unset is nil, the zero word */
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
const char *val = getenv(path);
if (!val) {
R[A] = 0;
return 0;
}
wo_str *s = wo_str_new(rt, val, (uint32_t)strlen(val));
if (!s) {
*msg = "out of memory";
return WO_T_OOM;
}
R[A] = (uint64_t)(uintptr_t)s;
return 0;
}
case WO_B_ENV_STOPPING: {
install_stop_handlers();
R[A] = stop_flag ? 1 : 0;
return 0;
}
/* ---- net --------------------------------------------------------- */
case WO_B_NET_LISTEN: { /* IPv4, SO_REUSEADDR, backlog 64 */
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
int64_t port = (int64_t)R[B + 1];
if (port < 0 || port > 65535) {
*msg = "port out of range";
return WO_T_BOUNDS;
}
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
*msg = strerror(errno);
return WO_T_IO;
}
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
addr.sin_addr.s_addr =
!strcmp(path, "0.0.0.0") ? (in_addr_t)INADDR_ANY : inet_addr(path);
if (bind(fd, (struct sockaddr *)&addr, sizeof addr) != 0 || listen(fd, 64) != 0) {
*msg = strerror(errno);
close(fd);
return WO_T_IO;
}
R[A] = (uint64_t)fd;
return 0;
}
case WO_B_NET_ACCEPT: {
int fd;
do {
fd = accept((int)R[B], NULL, NULL);
} while (fd < 0 && errno == EINTR);
if (fd < 0) {
*msg = strerror(errno);
return WO_T_IO;
}
R[A] = (uint64_t)fd;
return 0;
}
case WO_B_NET_READ: { /* one read, up to `max` bytes; EOF is the empty
* Text, which is how the source detects it */
int64_t max = (int64_t)R[B + 1];
if (max < 0) max = 0;
wo_str *s = wo_str_alloc(rt, (uint32_t)max);
if (!s) {
*msg = "out of memory";
return WO_T_OOM;
}
ssize_t n;
do {
n = read((int)R[B], s->data, (size_t)max);
} while (n < 0 && errno == EINTR);
if (n < 0) {
wo_str_free(rt, s);
*msg = strerror(errno);
return WO_T_IO;
}
s->len = (uint32_t)n;
R[A] = (uint64_t)(uintptr_t)s;
return 0;
}
case WO_B_NET_WRITE: {
const wo_str *body = (const wo_str *)(uintptr_t)R[B + 1];
if (!body || body->h.class_id != WO_CLS_STR) {
*msg = "not a text value";
return WO_T_BOUNDS;
}
uint32_t at = 0;
while (at < body->len) {
ssize_t n = write((int)R[B], body->data + at, body->len - at);
if (n < 0) {
if (errno == EINTR) continue;
*msg = strerror(errno);
return WO_T_IO;
}
at += (uint32_t)n;
}
R[A] = 0;
return 0;
}
case WO_B_NET_CLOSE: {
close((int)R[B]);
R[A] = 0;
return 0;
}
/* ---- proc -------------------------------------------------------- */
case WO_B_PROC_RUN: { /* Proc: 0 code, 1 out, 2 err. argv[0] is the
* command itself; the `multi Text` argument
* supplies the rest. stdout and stderr are
* captured through one pipe each, capped. */
if (cstr_of(R[B], path, sizeof path, msg)) return WO_T_BOUNDS;
wo_multi *argv_m = (wo_multi *)(uintptr_t)R[B + 1];
if (!argv_m || argv_m->h.class_id != WO_CLS_MULTI || argv_m->elem_kind != WO_K_TEXT) {
*msg = "`proc.run` needs a `multi Text` of arguments";
return WO_T_BOUNDS;
}
if (argv_m->len > 62) {
*msg = "too many process arguments";
return WO_T_BOUNDS;
}
char *argv[64];
char argbuf[62][512];
argv[0] = path;
for (uint32_t i = 0; i < argv_m->len; i++) {
const wo_str *a = (const wo_str *)(uintptr_t)argv_m->items[i];
if (!a || a->h.class_id != WO_CLS_STR || a->len + 1 > sizeof argbuf[0]) {
*msg = "process argument is not a short text";
return WO_T_BOUNDS;
}
memcpy(argbuf[i], a->data, a->len);
argbuf[i][a->len] = '\0';
argv[i + 1] = argbuf[i];
}
argv[argv_m->len + 1] = NULL;
int op[2], ep[2];
if (pipe(op) != 0) {
*msg = strerror(errno);
return WO_T_IO;
}
if (pipe(ep) != 0) {
close(op[0]);
close(op[1]);
*msg = strerror(errno);
return WO_T_IO;
}
pid_t pid = fork();
if (pid < 0) {
close(op[0]);
close(op[1]);
close(ep[0]);
close(ep[1]);
*msg = strerror(errno);
return WO_T_IO;
}
if (pid == 0) {
dup2(op[1], STDOUT_FILENO);
dup2(ep[1], STDERR_FILENO);
close(op[0]);
close(op[1]);
close(ep[0]);
close(ep[1]);
execvp(path, argv);
_exit(127); /* exec failed: the same code a shell reports */
}
close(op[1]);
close(ep[1]);
char obuf[8192], ebuf[4096];
size_t olen = 0, elen = 0;
ssize_t n;
while (olen < sizeof obuf && (n = read(op[0], obuf + olen, sizeof obuf - olen)) > 0)
olen += (size_t)n;
while (elen < sizeof ebuf && (n = read(ep[0], ebuf + elen, sizeof ebuf - elen)) > 0)
elen += (size_t)n;
close(op[0]);
close(ep[0]);
int status = 0;
while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {
}
wo_hdr *o = record_of(vm, R[B + 2], 3, msg);
if (!o) return R[B + 2] >= vm->mod->class_cnt ? WO_T_BOUNDS : WO_T_OOM;
wo_str *out = wo_str_new(rt, obuf, (uint32_t)olen);
wo_str *errs = wo_str_new(rt, ebuf, (uint32_t)elen);
if (!out || !errs) {
if (out) wo_str_free(rt, out);
if (errs) wo_str_free(rt, errs);
wo_drop_obj(rt, o);
*msg = "out of memory";
return WO_T_OOM;
}
uint64_t *fp = wo_fields(o);
fp[0] = (uint64_t)(int64_t)(WIFEXITED(status) ? WEXITSTATUS(status) : -1);
fp[1] = (uint64_t)(uintptr_t)out;
fp[2] = (uint64_t)(uintptr_t)errs;
R[A] = (uint64_t)(uintptr_t)o;
return 0;
}
default:
*msg = "unknown stdlib builtin";
return WO_T_EXPLICIT;
}
}

View file

@ -84,6 +84,11 @@ enum {
WO_T_BOUNDS = 6,
WO_T_KEY = 7,
WO_T_EXPLICIT = 8,
/* systems stdlib: a syscall the source cannot prevent said no (a
* missing directory, a closed socket, a failed exec). errno's own
* message rides along in the error record, and `try ... catch` is how
* a program that expects the failure handles it. */
WO_T_IO = 9,
};
/* ---- opcodes (spec section 5; semantics in the format doc) ---- */
@ -208,8 +213,32 @@ enum {
WO_B_MULTI_SET = 39, /* (multi, i, v) -> 0; in-place element write,
* dropping the element it replaces. `m[i] = v`
* for a multi, the mirror of map_set. */
/* ---- systems stdlib: the OS half (runtime/src/sysio.c). Members that
* return a record take their result record's CLASS ID as their last
* argument — the compiler predeclares the record and passes the id, so
* the VM allocates what it fills without knowing source type names.
* Field orders are the contract, documented per case in sysio.c. ---- */
WO_B_FS_EXISTS = 40, /* (path) -> 1/0 */
WO_B_FS_LIST = 41, /* (dir) -> multi Text; unreadable dir traps IO */
WO_B_FS_STAT = 42, /* (path, cls) -> ?Stat {size, mtime, inode, dir} */
WO_B_FS_READ_ALL = 43, /* (path, cap) -> Text, truncated at cap */
WO_B_FS_READ_AT = 44, /* (path, off, len) -> Text, short read allowed */
WO_B_FS_APPEND = 45, /* (path, text) -> 0; creates the file if absent */
WO_B_TIME_SLEEP = 46, /* (ms) -> 0 */
WO_B_TIME_LOCAL = 47, /* (ms, cls) -> Parts {year..second, dow} */
WO_B_TIME_ISO = 48, /* (ms) -> Text, UTC seconds precision */
WO_B_ENV_GET = 49, /* (name) -> ?Text; unset is nil */
WO_B_ENV_STOPPING = 50, /* () -> 1/0; SIGTERM/SIGINT latch */
WO_B_NET_LISTEN = 51, /* (host, port) -> fd */
WO_B_NET_ACCEPT = 52, /* (fd) -> fd */
WO_B_NET_READ = 53, /* (fd, max) -> Text; empty = EOF */
WO_B_NET_WRITE = 54, /* (fd, text) -> 0 */
WO_B_NET_CLOSE = 55, /* (fd) -> 0 */
WO_B_PROC_RUN = 56, /* (cmd, multi Text args, cls) -> Proc {code, out, err} */
};
#define WO_B_MAX 39u
#define WO_B_MAX 56u
/* ids at or above this one live in sysio.c, not builtin.c */
#define WO_B_SYS_FIRST WO_B_FS_EXISTS
/* ---- instruction encode/decode: op:8 A:8 then B:8 C:8 or Bx:16 ---- */
static inline uint32_t wo_ins_abc(uint8_t op, uint8_t a, uint8_t b, uint8_t c) {

View file

@ -1,12 +1,14 @@
-- haxe-parity Task 1 (modules): `fs` is a reserved stdlib namespace —
-- `use fs` resolves and `fs.stat(...)` typechecks as UNKNOWN-BUT-
-- RESERVED (no E207/E225/arity error; the six namespaces' members
-- arrive in plan 9). A call through it that survives all the way to
-- emission is WO-E406, not silently accepted and not a generic
-- WO-E403 "cannot resolve the receiver" -- there is nothing wrong with
-- the reference, only nothing to lower it to yet.
-- haxe-parity Task 1 (modules) + systems stdlib: `fs` is a reserved
-- stdlib namespace, so `use fs` resolves and a qualified call through it
-- typechecks without any module-level complaint (no E207/E225). What
-- WO-E406 now marks is the one thing left that a reserved namespace
-- cannot answer: a member it does not have. `fs.stat`/`fs.read_all`/...
-- are linked (runtime/src/sysio.c); `fs.slurp` is not any member of any
-- version of `fs`, and saying so at the call site is the whole point of
-- keeping a dedicated code for it rather than a generic WO-E403
-- "cannot resolve the receiver".
use fs
fn main() {
print_int(fs.stat("x"))
print(fs.slurp("x"))
}