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:
parent
b6d7d4c940
commit
fb9d60fc77
8 changed files with 787 additions and 50 deletions
|
|
@ -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 ->
|
||||
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);
|
||||
put f (ins_abx op_loadk dst (const_int p 0))
|
||||
| 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` 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. *)
|
||||
if
|
||||
(not (SM.mem Types.error_record_name !class_id))
|
||||
&& List.exists (fun u -> program_uses_try u.prog) units
|
||||
then begin
|
||||
let cid = !nclasses in
|
||||
class_id := SM.add Types.error_record_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 = [] }
|
||||
:: !classes
|
||||
end;
|
||||
List.iter
|
||||
(fun (name, fields) ->
|
||||
if
|
||||
(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 name cid !class_id;
|
||||
incr nclasses;
|
||||
classes :=
|
||||
{ cr_name = name; cr_gc = false; cr_fields = Array.of_list fields; cr_methods = [] }
|
||||
:: !classes
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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 }
|
||||
in
|
||||
{ syms with classes = StringMap.add error_record_name info syms.classes }
|
||||
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; 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
|
||||
{ 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
500
runtime/src/sysio.c
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue