diff --git a/compiler/src/emit.ml b/compiler/src/emit.ml index 8697ee1..96c76b8 100644 --- a/compiler/src/emit.ml +++ b/compiler/src/emit.ml @@ -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 diff --git a/compiler/src/types.ml b/compiler/src/types.ml index 69e52ca..f27adfa 100644 --- a/compiler/src/types.ml +++ b/compiler/src/types.ml @@ -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 diff --git a/runtime/src/builtin.c b/runtime/src/builtin.c index 45f8f9d..bfde742 100644 --- a/runtime/src/builtin.c +++ b/runtime/src/builtin.c @@ -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; diff --git a/runtime/src/builtin.h b/runtime/src/builtin.h index 7ce73f4..7717ce0 100644 --- a/runtime/src/builtin.h +++ b/runtime/src/builtin.h @@ -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 */ diff --git a/runtime/src/loader.c b/runtime/src/loader.c index ef9a1b7..78f8c38 100644 --- a/runtime/src/loader.c +++ b/runtime/src/loader.c @@ -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) { diff --git a/runtime/src/sysio.c b/runtime/src/sysio.c new file mode 100644 index 0000000..2ad9e20 --- /dev/null +++ b/runtime/src/sysio.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; + } +} diff --git a/runtime/src/wob.h b/runtime/src/wob.h index 461423f..904d9da 100644 --- a/runtime/src/wob.h +++ b/runtime/src/wob.h @@ -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) { diff --git a/tests/corpus/compile-fail/lang-use-stdlib-not-linked/fixture.wo b/tests/corpus/compile-fail/lang-use-stdlib-not-linked/fixture.wo index bb82b24..79119a8 100644 --- a/tests/corpus/compile-fail/lang-use-stdlib-not-linked/fixture.wo +++ b/tests/corpus/compile-fail/lang-use-stdlib-not-linked/fixture.wo @@ -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")) }