feat: spawn / send / actor M — the unified actor surface (arc T3)
- language: `spawn Cls { fields }` expression (ctor semantics — fields
MOVE; result is the address); `actor M` parametric field type
(contextual like multi/map — actor stays a legal identifier); `send`
is a builtin free-fn name, not a keyword (shadowing rule applies)
- typing: M inferred from Cls's receive(msg: M); WO-E221 when receive
is missing, mis-armed, or M is not a class/record/union; send checks
addr is `actor M` and the message IS an M (silent when underivable);
ctor half of spawn delegates to the Ctor arm (completeness, ?T, E207)
- ownership: send's message TRANSFERS (sender's later use = WO-E301,
corpus-pinned); spawn's fields move via the ctor machinery; an
address is Copy
- emit: spawn lowers to ctor + LOADK receive's method index + BUILTIN
68; send is BUILTIN 69 with the message excluded from fresh-arg drops
(the runtime owns it now)
- runtime: wo_actor (moved-in instance, receive idx, growable FIFO
mailbox, one delivery fiber at a time); delivery reuses the fiber
context across messages and re-queues per message (fairness — an
actor never monopolizes); the runtime drops each message after its
receive returns; actor state/queued/in-flight messages are GC roots;
teardown drops everything (main-return reap included); loader knows
the two arities
- corpus: run/actor-echo (typed spawn/send, one-at-a-time delivery
interleaved with main by budget — output exact, ASan-clean),
compile-fail/spawn-no-receive (WO-E221), send-after-move (WO-E301)
- battery green: oop-e2e 92/0, woc-test, wovm-test, log-watcher 7/0,
employee 8/0, web-app 21/0, deps-accept 8/0
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4f1b04d02d
commit
77c9090ce5
20 changed files with 454 additions and 23 deletions
|
|
@ -73,6 +73,9 @@ type field_ty =
|
|||
`ref` — NOT a stored column; reading it
|
||||
scans C's index on f. Types as multi C. *)
|
||||
| Nullable of field_ty (* ?T wrapper *)
|
||||
| Actor of string (* actor M: a typed actor address (arc, 8+11);
|
||||
M is the receive-message class. A copyable
|
||||
scalar word at runtime. *)
|
||||
|
||||
(* Parameter passing convention (spec section 3, rule 2): default is an
|
||||
immutable borrow; `mut` is an exclusive borrow; `take` moves
|
||||
|
|
@ -252,6 +255,10 @@ and expr_kind =
|
|||
uses either, so neither is grammar here (YAGNI, recorded in the
|
||||
task report). *)
|
||||
| Switch of expr * switch_arm list
|
||||
(* the concurrency arc: `spawn Cls { fields }` — construct the actor's
|
||||
state (exactly a ctor literal, fields MOVE in) and start it; the
|
||||
result is an `actor M` address, M inferred from Cls's receive. *)
|
||||
| Spawn of string * (string * expr) list
|
||||
(* Container literals, the driving workload's own spelling for a fresh
|
||||
container: `[]` / `[a, b, c]` for a `multi T`, `{}` for an empty
|
||||
`map<K, V>`. They lower to exactly what `multi_new()`/`map_new()`
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ let kind_label (k : Token.kind) : string =
|
|||
| Token.KwInsert -> "KW_INSERT"
|
||||
| Token.KwSelect -> "KW_SELECT"
|
||||
| Token.KwUse -> "KW_USE"
|
||||
| Token.KwSpawn -> "KW_SPAWN"
|
||||
| Token.KwPub -> "KW_PUB"
|
||||
| Token.KwBreak -> "KW_BREAK"
|
||||
| Token.KwContinue -> "KW_CONTINUE"
|
||||
|
|
@ -151,6 +152,7 @@ let rec field_ty_str : Ast.field_ty -> string = function
|
|||
| Ast.Multi s -> Printf.sprintf "multi %s" s
|
||||
| Ast.Map (k, v) -> Printf.sprintf "map<%s, %s>" k v
|
||||
| Ast.Backlink (c, f) -> Printf.sprintf "backlink %s.%s" c f
|
||||
| Ast.Actor m -> Printf.sprintf "actor %s" m
|
||||
| Ast.Nullable t -> "?" ^ field_ty_str t
|
||||
|
||||
let param_str (p : Ast.param) : string = Printf.sprintf "%s%s: %s" (conv_str p.conv) p.name (field_ty_str p.ty)
|
||||
|
|
@ -242,6 +244,9 @@ let rec expr_str (e : Ast.expr) : string =
|
|||
| Ast.MapLit -> "{}"
|
||||
| Ast.NilLit -> "nil"
|
||||
| Ast.As (inner, ty) -> Printf.sprintf "%s as %s" (expr_str inner) (field_ty_str ty)
|
||||
| Ast.Spawn (cn, fields) ->
|
||||
Printf.sprintf "spawn %s{%s}" cn
|
||||
(String.concat ", " (List.map (fun (n, v) -> n ^ ": " ^ expr_str v) fields))
|
||||
(* Like SWITCH above: a one-line summary, not a full unparse of the
|
||||
catch arm's statements. *)
|
||||
| Ast.Try { body; ename; handler } ->
|
||||
|
|
|
|||
|
|
@ -786,6 +786,8 @@ let b_db_delete = 63
|
|||
let b_db_scan = 64
|
||||
let b_db_get_field = 65
|
||||
let b_db_probe = 66
|
||||
let b_spawn = 68 (* arc: spawn(instance, receive_method_idx) -> actor address *)
|
||||
let b_send = 69 (* arc: send(addr, msg) — msg moves to the runtime *)
|
||||
|
||||
(* iteration 9b: `d.staff` where staff is `backlink Employee.dept` reads by
|
||||
probing Employee's index on its `dept` column. Resolve to (source cid,
|
||||
|
|
@ -950,7 +952,9 @@ let is_builtin_name (n : string) =
|
|||
(* systems stdlib *)
|
||||
"len"; "byte_at"; "print_err"; "starts_with"; "ends_with"; "index_of"; "last_index_of";
|
||||
"substr"; "trim"; "to_lower"; "char_of"; "parse_int"; "split"; "split_ws"; "join"; "slice";
|
||||
"pop"; "shift"; "sort"; "reverse"; "remove"; "key_at"; "val_at" ]
|
||||
"pop"; "shift"; "sort"; "reverse"; "remove"; "key_at"; "val_at";
|
||||
(* the concurrency arc *)
|
||||
"send" ]
|
||||
|
||||
(* ---- unions and variants (haxe-parity Task 4) ------------------------
|
||||
|
||||
|
|
@ -1118,6 +1122,17 @@ let rec ty_of_expr (p : pctx) (f : fstate) (e : Ast.expr) : Ast.field_ty option
|
|||
| Eq | Ne | Lt | Le | Gt | Ge | And | Or -> Some (Scalar "Bool")
|
||||
| Add | Sub | Mul | Div | Mod -> ( match ty_of_expr p f l with Some t -> Some t | None -> Some (Scalar "Int")))
|
||||
| Ctor (cn, _) -> Some (Scalar cn)
|
||||
(* arc: a spawn's value is the typed actor address (a scalar word) *)
|
||||
| Spawn (cn, _) -> (
|
||||
match Types.StringMap.find_opt cn p.p_syms.Types.classes with
|
||||
| Some cls -> (
|
||||
match
|
||||
List.find_opt (fun (m : Types.method_info) -> m.Types.name = "receive") cls.Types.methods
|
||||
with
|
||||
| Some { Types.params = [ (_, pty, _) ]; _ } -> (
|
||||
match pty with Ast.Scalar mname -> Some (Ast.Actor mname) | _ -> None)
|
||||
| _ -> None)
|
||||
| None -> None)
|
||||
| Insert _ -> Some (Scalar "Int")
|
||||
| Delete _ -> Some (Scalar "Int")
|
||||
| Query q ->
|
||||
|
|
@ -1395,6 +1410,7 @@ let field_class_meta (p : pctx) (ty : Ast.field_ty) : int =
|
|||
| Some n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
||||
| None -> wob_none)
|
||||
| Ast.Ref n -> ( match class_of_name p n with Some cid -> cid | None -> wob_none)
|
||||
| Ast.Actor _ -> wob_none (* an address word: no per-class drop metadata *)
|
||||
| Ast.Backlink _ | Ast.Nullable _ -> wob_none
|
||||
|
||||
let field_elem_meta (p : pctx) (ty : Ast.field_ty) : int =
|
||||
|
|
@ -1730,6 +1746,26 @@ let rec emit_expr (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e
|
|||
put f (ins_abc op_neg dst b 0)
|
||||
| Binary (op, l, r) -> emit_binary p f v ~dst op l r
|
||||
| Ctor (cn, fields) -> emit_ctor p f v ~dst e cn fields
|
||||
| Spawn (cn, fields) ->
|
||||
(* build the actor's state exactly as a ctor, then hand instance +
|
||||
receive's method index to the runtime; dst gets the address word.
|
||||
Reserve dst like emit_ctor does — in tail position dst can sit at
|
||||
f_temp and the two-slot argument window would clobber it. *)
|
||||
let outer = f.f_temp in
|
||||
if f.f_temp <= dst then f.f_temp <- dst + 1;
|
||||
let t = alloc_temps p f e.pos 2 in
|
||||
emit_ctor p f v ~dst:t e cn fields;
|
||||
(match SM.find_opt (cn ^ ".receive") p.p_method_id with
|
||||
| Some midx ->
|
||||
put f (ins_abx op_loadk (t + 1) (check_bx p f e.pos "constant" (const_int p midx)));
|
||||
sync_mask p f v e.id;
|
||||
f.f_cur_line <- e.pos.line;
|
||||
put f (ins_abc op_builtin dst t b_spawn)
|
||||
| None ->
|
||||
err p ~code:cannot_lower_code ~file:f.f_file ~pos:e.pos
|
||||
~message:(Printf.sprintf "`spawn %s`: no `receive` method (typecheck should have refused)" cn);
|
||||
put f (ins_abx op_loadk dst (const_int p 0)));
|
||||
f.f_temp <- outer
|
||||
| Insert (cn, fields) -> emit_insert p f v ~dst e cn fields
|
||||
| Delete target -> (
|
||||
match ty_of_expr p f target with
|
||||
|
|
@ -3320,6 +3356,7 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e :
|
|||
then 1
|
||||
else if
|
||||
id = b_multi_push || id = b_multi_get || id = b_map_get || id = b_map_has
|
||||
|| id = b_send
|
||||
(* systems stdlib, two arguments *)
|
||||
|| id = b_byte_at || id = b_starts_with || id = b_ends_with || id = b_index_of
|
||||
|| id = b_last_index_of || id = b_split || id = b_join || id = b_map_remove
|
||||
|
|
@ -3359,7 +3396,7 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e :
|
|||
dangle the value just read) and the stores, which either copy (Text,
|
||||
handled by copied_container_call) or take ownership (OWNED/GCREF). *)
|
||||
let reader = List.mem name [ "get"; "latest"; "key_at"; "val_at" ] in
|
||||
(if not (List.mem name [ "push"; "set" ]) then
|
||||
(if not (List.mem name [ "push"; "set"; "send" ]) then
|
||||
List.iteri
|
||||
(fun i (a : Ast.expr) ->
|
||||
(* a reader's result points into arg0 (the container) — dropping
|
||||
|
|
@ -3389,6 +3426,7 @@ and emit_builtin (p : pctx) (f : fstate) (v : views) ~(dst : int) ?expected (e :
|
|||
List.iteri (fun i (a : Ast.expr) -> if i > 0 then drop_fresh_text p f (base + i) a) args
|
||||
in
|
||||
match name with
|
||||
| "send" -> fixed b_send (* arc: msg (arg1) moved to the runtime — never dropped here *)
|
||||
| "now" -> fixed b_now
|
||||
| "print" -> fixed b_print
|
||||
| "print_int" -> fixed b_print_int
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ let rec refs_of_ty (classes : Types.class_info SMap.t) (t : Ast.field_ty) :
|
|||
| Ast.Map (k, v) -> List.filter (fun n -> SMap.mem n classes) [ k; v ]
|
||||
| Ast.Nullable ft -> refs_of_ty classes ft
|
||||
| Ast.Ref _ | Ast.Backlink _ -> [] (* id / virtual inverse: no pointer edge *)
|
||||
| Ast.Actor _ -> [] (* an address word — the runtime owns actors, never a pointer edge *)
|
||||
|
||||
let edges (classes : Types.class_info SMap.t) (ci : Types.class_info) :
|
||||
string list =
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ let keyword_kind = function
|
|||
| "true" -> Some Token.KwTrue
|
||||
| "false" -> Some Token.KwFalse
|
||||
| "use" -> Some Token.KwUse
|
||||
| "spawn" -> Some Token.KwSpawn
|
||||
| "pub" -> Some Token.KwPub
|
||||
| "break" -> Some Token.KwBreak
|
||||
| "continue" -> Some Token.KwContinue
|
||||
|
|
|
|||
|
|
@ -421,6 +421,7 @@ let oclass_of (ctx : ctx) (ft : Ast.field_ty) : oclass =
|
|||
| Some u -> if u.Types.u_has_payload then Owned else Copy
|
||||
| None -> Copy (* unknown type: WO-E225 already reported by types.ml *))
|
||||
| Ref _ -> Copy
|
||||
| Actor _ -> Copy (* an address is a copyable word; the runtime owns actors *)
|
||||
| Backlink _ -> Copy (* a virtual collection of row ids read on demand *)
|
||||
| Multi _ | Map _ -> Owned
|
||||
| Nullable _ -> Copy (* unreachable: unwrapped above *)
|
||||
|
|
@ -505,6 +506,17 @@ let rec expr_ty (ctx : ctx) (e : Ast.expr) : Ast.field_ty option =
|
|||
field/parameter it is built into) decides. *)
|
||||
| ListLit (first :: _) -> (
|
||||
match expr_ty ctx first with Some (Scalar n) -> Some (Multi n) | _ -> None)
|
||||
(* arc: a spawn's value is a typed address — a copyable scalar word *)
|
||||
| Spawn (cn, _) -> (
|
||||
match Types.StringMap.find_opt cn ctx.syms.Types.classes with
|
||||
| Some cls -> (
|
||||
match
|
||||
List.find_opt (fun (m : Types.method_info) -> m.Types.name = "receive") cls.Types.methods
|
||||
with
|
||||
| Some { Types.params = [ (_, pty, _) ]; _ } -> (
|
||||
match pty with Ast.Scalar mname -> Some (Ast.Actor mname) | _ -> None)
|
||||
| _ -> None)
|
||||
| None -> None)
|
||||
| ListLit [] | MapLit -> None
|
||||
(* haxe-parity Task 6: `nil` is the zero word — contextual on its
|
||||
destination, and never something this frame owns. *)
|
||||
|
|
@ -1124,6 +1136,9 @@ let rec read_expr (ctx : ctx) (e : Ast.expr) : unit =
|
|||
read_place_parts ctx e
|
||||
| Call (callee, args) -> analyze_call ctx e callee args
|
||||
| Ctor (cn, fields) -> analyze_ctor ctx cn fields
|
||||
(* arc: spawn's ctor half moves fields exactly as a ctor literal does;
|
||||
the result (an address) is Copy, so no drop for the spawn itself *)
|
||||
| Spawn (cn, fields) -> analyze_ctor ctx cn fields
|
||||
| Insert (_, fields) ->
|
||||
(* iteration 9 Task 3: the engine copies every field value at the row
|
||||
API (the two-worlds bulkhead), so an insert BORROWS its values —
|
||||
|
|
@ -1323,6 +1338,9 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
|||
| Ident "push" -> i = 1 && Types.StringMap.find_opt "push" ctx.syms.Types.free_fns = None
|
||||
| Ident "set" ->
|
||||
(i = 1 || i = 2) && Types.StringMap.find_opt "set" ctx.syms.Types.free_fns = None
|
||||
(* arc: send(addr, msg) MOVES the message to the runtime — the sender's
|
||||
binding dies (compile-time move, iteration 8's criterion) *)
|
||||
| Ident "send" -> i = 1 && Types.StringMap.find_opt "send" ctx.syms.Types.free_fns = None
|
||||
| _ -> false
|
||||
in
|
||||
List.iteri
|
||||
|
|
@ -1336,8 +1354,13 @@ and analyze_call (ctx : ctx) (call_e : Ast.expr) (callee : Ast.expr) (args : Ast
|
|||
record_move ctx p (MvArg pname))
|
||||
else if container_store_slot i && place_class ctx p = Owned
|
||||
&& not (stores_by_copy ctx p) then
|
||||
(if transfer ctx p ~what:"cannot be stored in a container" then
|
||||
record_move ctx p (MvArg "element"))
|
||||
(if
|
||||
transfer ctx p
|
||||
~what:
|
||||
(match callee.kind with
|
||||
| Ident "send" -> "cannot be sent — a message moves to the receiver"
|
||||
| _ -> "cannot be stored in a container")
|
||||
then record_move ctx p (MvArg "element"))
|
||||
)
|
||||
args;
|
||||
record_drop ctx ~node:call_e.id ~pos:call_e.pos ~kind:DLiveMask
|
||||
|
|
|
|||
|
|
@ -395,6 +395,11 @@ let parse_field_ty (st : state) : Ast.field_ty =
|
|||
expect st Token.Dot "'.'";
|
||||
let fld = expect_ident st "backlink source field" in
|
||||
Ast.Backlink (cls, fld)
|
||||
| Token.Ident "actor" ->
|
||||
(* the concurrency arc: `actor M` — a typed actor address,
|
||||
contextual like multi/map (actor stays a legal identifier) *)
|
||||
ignore (advance st);
|
||||
Ast.Actor (expect_ident st "actor message type")
|
||||
| Token.Ident "map" ->
|
||||
ignore (advance st);
|
||||
expect st Token.Lt "'<'";
|
||||
|
|
@ -1239,6 +1244,16 @@ and parse_primary (st : state) : Ast.expr =
|
|||
"only the empty map literal `{}` is an expression — build entries with `set(m, k, v)`";
|
||||
ignore (advance st);
|
||||
{ Ast.id; pos; kind = Ast.MapLit }
|
||||
| Token.KwSpawn ->
|
||||
(* arc: `spawn Cls { fields }` — exactly a ctor literal behind the
|
||||
keyword; fields MOVE in, result is the actor address *)
|
||||
let pos = peek_pos st in
|
||||
let id = fresh_id st in
|
||||
ignore (advance st);
|
||||
let lit = parse_ctor_literal st in
|
||||
(match lit.Ast.kind with
|
||||
| Ast.Ctor (cn, fields) -> { Ast.id; pos; kind = Ast.Spawn (cn, fields) }
|
||||
| _ -> unexpected st "a class constructor after `spawn`")
|
||||
| Token.Ident _ when looks_like_ctor st -> parse_ctor_literal st
|
||||
| Token.Ident s ->
|
||||
let pos = peek_pos st in
|
||||
|
|
@ -1966,6 +1981,8 @@ let rec subst_expr (consts : Ast.expr StringMap.t) (bound : StringSet.t) (e : As
|
|||
{ e with Ast.kind = Ast.Binary (op, subst_expr consts bound l, subst_expr consts bound r) }
|
||||
| Ast.Ctor (cn, fields) ->
|
||||
{ e with Ast.kind = Ast.Ctor (cn, List.map (fun (n, v) -> (n, subst_expr consts bound v)) fields) }
|
||||
| Ast.Spawn (cn, fields) ->
|
||||
{ e with Ast.kind = Ast.Spawn (cn, List.map (fun (n, v) -> (n, subst_expr consts bound v)) fields) }
|
||||
| Ast.Insert (cn, fields) ->
|
||||
{ e with Ast.kind = Ast.Insert (cn, List.map (fun (n, v) -> (n, subst_expr consts bound v)) fields) }
|
||||
| Ast.Delete t -> { e with Ast.kind = Ast.Delete (subst_expr consts bound t) }
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ type kind =
|
|||
field-name collision to dodge (see lexer.ml's module doc for why
|
||||
those other names stayed idents). *)
|
||||
| KwUse
|
||||
(* the concurrency arc (iterations 8+11): `spawn Cls { ... }`. `send`
|
||||
is deliberately NOT a keyword — it is a builtin free-fn name. *)
|
||||
| KwSpawn
|
||||
| KwPub
|
||||
(* haxe-parity Task 2 (small control surface): break/continue/do-while,
|
||||
const values, and/or booleans, and inline-fn rejection (the haxe
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type typ =
|
|||
| TMulti of typ (* multi T *)
|
||||
| TMap of typ * typ (* map<K, V> *)
|
||||
| TRef of string (* ref T — ID link *)
|
||||
| TActor of string (* actor M — a typed actor address (arc 8+11); a copyable scalar word *)
|
||||
| TVoid (* no return *)
|
||||
|
||||
(* .wob field kinds (docs/plan/oop-vm/00-wob-format.md) *)
|
||||
|
|
@ -325,6 +326,7 @@ let rec field_ty_of_typ (t : typ) : field_ty option =
|
|||
| TMulti (TScalar n) -> Some (Multi n)
|
||||
| TMap (TScalar k, TScalar v) -> Some (Map (k, v))
|
||||
| TNullable inner -> ( match field_ty_of_typ inner with Some ft -> Some (Nullable ft) | None -> None)
|
||||
| TActor m -> Some (Actor m)
|
||||
| TMulti _ | TMap _ | TVoid -> None
|
||||
|
||||
let rec typ_of_field_ty (ft : field_ty) : typ =
|
||||
|
|
@ -334,6 +336,7 @@ let rec typ_of_field_ty (ft : field_ty) : typ =
|
|||
| Multi inner_name -> TMulti (TScalar inner_name)
|
||||
| Map (k_name, v_name) -> TMap (TScalar k_name, TScalar v_name)
|
||||
| Backlink (c, _) -> TMulti (TScalar c) (* reads as a collection of C *)
|
||||
| Actor m -> TActor m
|
||||
| Nullable inner -> TNullable (typ_of_field_ty inner)
|
||||
|
||||
(* wob_kind_of_typ: maps internal typ to .wob field kind *)
|
||||
|
|
@ -368,6 +371,7 @@ let wob_kind_of_typ (syms : symbols) (t : typ) : wob_kind =
|
|||
| TMulti _ -> WO_K_MULTI
|
||||
| TMap _ -> WO_K_MAP
|
||||
| TRef _ -> WO_K_SCALAR
|
||||
| TActor _ -> WO_K_SCALAR (* an address is a copyable word; the runtime owns actors *)
|
||||
| TVoid -> WO_K_SCALAR
|
||||
in
|
||||
kind_of t
|
||||
|
|
@ -389,6 +393,7 @@ let invalid_builtin_code = Diag.types_prefix ^ "09"
|
|||
let module_not_imported_code = Diag.types_prefix ^ "10"
|
||||
let nullable_used_without_check_code = Diag.types_prefix ^ "11"
|
||||
let nullable_assign_mismatch_code = Diag.types_prefix ^ "12"
|
||||
let spawn_no_receive_code = Diag.types_prefix ^ "21" (* WO-E221: spawn target lacks fn receive(msg: M); E219/E220 are taken on the language-surface-strictness branch *)
|
||||
let missing_nil_check_code = Diag.types_prefix ^ "13"
|
||||
|
||||
(* haxe-parity Task 1 (modules). module_not_imported_code (WO-E210,
|
||||
|
|
@ -603,7 +608,7 @@ let rec scalar_name_of (ft : field_ty) : string option =
|
|||
match ft with
|
||||
| Scalar name -> Some name
|
||||
| Nullable inner -> scalar_name_of inner
|
||||
| Ref _ | Multi _ | Map _ | Backlink _ -> None
|
||||
| Ref _ | Multi _ | Map _ | Backlink _ | Actor _ -> None
|
||||
|
||||
(* Checked once per field declaration (not at every access/use site), so
|
||||
the diagnostic lands at the field's own declaration position and
|
||||
|
|
@ -854,6 +859,7 @@ let rec typ_label (t : typ) : string =
|
|||
| TMulti _ -> "multi"
|
||||
| TMap _ -> "map"
|
||||
| TRef name -> "ref " ^ name
|
||||
| TActor m -> "actor " ^ m
|
||||
| TVoid -> "void"
|
||||
|
||||
(* A builtin call's own confident return type, for when it appears as
|
||||
|
|
@ -1173,6 +1179,16 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
| None -> None))
|
||||
| _ -> None))
|
||||
| _ -> None)
|
||||
| Spawn (cn, _fields) -> (
|
||||
match StringMap.find_opt cn syms.classes with
|
||||
| Some cls -> (
|
||||
match List.find_opt (fun (m : method_info) -> m.name = "receive") cls.methods with
|
||||
| Some { params = [ (_, pty, _) ]; _ } -> (
|
||||
match typ_of_field_ty pty with
|
||||
| TScalar mname -> Some (TActor mname)
|
||||
| _ -> None)
|
||||
| _ -> None)
|
||||
| None -> None)
|
||||
| Ctor (class_name, _fields) ->
|
||||
(* `Ctor`'s class name is never a placeholder -- unlike
|
||||
`Ident`/`Field`/`Index`/`Call`, there is no fallback path
|
||||
|
|
@ -1433,6 +1449,40 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
"variant `%s` of `%s` takes %d payload argument(s), given %d"
|
||||
vi.vi_name u.u_name want got)
|
||||
())
|
||||
| None when name = "send" ->
|
||||
(* arc: send(addr, msg) — bespoke shape: addr is an
|
||||
`actor M`, msg must BE an M (exactly; moves are checked
|
||||
by the owner pass). Silent when underivable, the
|
||||
standing contract. *)
|
||||
(if List.length args <> 2 then
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:bad_arity_code ~file ~line:e.pos.line ~col:e.pos.col
|
||||
~message:
|
||||
(Printf.sprintf "`send` takes 2 arguments (address, message), given %d"
|
||||
(List.length args))
|
||||
())
|
||||
else
|
||||
match args with
|
||||
| [ a; m ] -> (
|
||||
match confident_typ cenv a with
|
||||
| Some (TActor want) -> (
|
||||
match confident_typ cenv m with
|
||||
| Some (TScalar got) when got <> want ->
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:type_mismatch_code ~file ~line:m.pos.line
|
||||
~col:m.pos.col
|
||||
~message:
|
||||
(Printf.sprintf
|
||||
"this actor receives `%s` — the message is a `%s`" want got)
|
||||
())
|
||||
| _ -> ())
|
||||
| Some _ ->
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:type_mismatch_code ~file ~line:a.pos.line
|
||||
~col:a.pos.col
|
||||
~message:"`send`'s first argument must be an `actor M` address" ())
|
||||
| None -> ())
|
||||
| _ -> ())
|
||||
| None ->
|
||||
let confident_types = List.map (confident_typ cenv) args in
|
||||
check_builtin_call ~file collector name e.pos args confident_types)
|
||||
|
|
@ -1563,6 +1613,36 @@ let typecheck_program ~file ~(module_of : string -> string)
|
|||
let ir = typecheck_expr env cenv inner in
|
||||
if is_nullable ir.typ || ir.is_nil then e211 inner.pos (expr_label inner);
|
||||
{ typ = TScalar "Text"; is_nil = false }
|
||||
| Spawn (cn, fields) ->
|
||||
(* the ctor half checks exactly as a ctor literal (completeness,
|
||||
field types, ?T boundaries) — delegate, then type the address *)
|
||||
let _ = typecheck_expr env cenv { e with kind = Ctor (cn, fields) } in
|
||||
let bad why =
|
||||
Diag.Collector.add collector
|
||||
(Diag.error ~code:spawn_no_receive_code ~file ~line:e.pos.line ~col:e.pos.col
|
||||
~message:
|
||||
(Printf.sprintf
|
||||
"`spawn %s { ... }`: %s — an actor is a class with `fn receive(msg: M)` where M is a class, record, or union"
|
||||
cn why)
|
||||
());
|
||||
{ typ = TScalar "Int"; is_nil = false }
|
||||
in
|
||||
(match StringMap.find_opt cn syms.classes with
|
||||
| Some cls -> (
|
||||
match List.find_opt (fun (m : method_info) -> m.name = "receive") cls.methods with
|
||||
| Some { params = [ (_, pty, _) ]; _ } -> (
|
||||
match typ_of_field_ty pty with
|
||||
| TScalar mname
|
||||
when StringMap.mem mname syms.classes
|
||||
|| StringMap.mem mname syms.unions ->
|
||||
{ typ = TActor mname; is_nil = false }
|
||||
| TScalar mname -> bad (Printf.sprintf "receive's message type `%s` is not a declared class, record, or union" mname)
|
||||
| _ -> bad "receive's parameter must be a plain class, record, or union type")
|
||||
| Some _ -> bad "its `receive` must take exactly one parameter"
|
||||
| None -> bad (Printf.sprintf "`%s` has no `receive` method" cn))
|
||||
| None ->
|
||||
(* unknown class: the delegated Ctor check already reported E207 *)
|
||||
{ typ = TScalar "Int"; is_nil = false })
|
||||
| Ctor (class_name, fields) ->
|
||||
(try
|
||||
let cls = StringMap.find class_name syms.classes in
|
||||
|
|
@ -2448,7 +2528,7 @@ and walk_expr (bound : StringSet.t) (visit : StringSet.t -> expr -> unit) (e : e
|
|||
| Binary (_, l, r) ->
|
||||
walk_expr bound visit l;
|
||||
walk_expr bound visit r
|
||||
| Ctor (_, fields) | Insert (_, fields) ->
|
||||
| Ctor (_, fields) | Insert (_, fields) | Spawn (_, fields) ->
|
||||
List.iter (fun (_, v) -> walk_expr bound visit v) fields
|
||||
| Interp inner -> walk_expr bound visit inner
|
||||
| ListLit items -> List.iter (walk_expr bound visit) items
|
||||
|
|
@ -2728,6 +2808,7 @@ let rec field_ty_str (ft : field_ty) : string =
|
|||
| Multi s -> "multi " ^ s
|
||||
| Map (k, v) -> "map<" ^ k ^ ", " ^ v ^ ">"
|
||||
| Backlink (c, f) -> "backlink " ^ c ^ "." ^ f
|
||||
| Actor m -> "actor " ^ m
|
||||
| Nullable t -> "?" ^ field_ty_str t
|
||||
|
||||
let dump_symbols (syms : symbols) : string =
|
||||
|
|
|
|||
|
|
@ -72,6 +72,18 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
|
|||
if (C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) return wo_builtin_sys(vm, R, ins, msg);
|
||||
if (C >= WO_B_DB_INSERT && C <= WO_B_DB_PROBE) return wo_builtin_db(vm, R, ins, msg);
|
||||
switch (C) {
|
||||
case WO_B_SPAWN: {
|
||||
/* arc: R[B] = the moved-in instance, R[B+1] = receive's method
|
||||
* index (a compile-time constant); R[A] = the actor address */
|
||||
int rc = wo_vm_actor_spawn(vm, R[B], (uint32_t)R[B + 1], &R[A], msg);
|
||||
return rc;
|
||||
}
|
||||
case WO_B_SEND: {
|
||||
int rc = wo_vm_actor_send(vm, R[B], R[B + 1], msg);
|
||||
if (rc) return rc;
|
||||
R[A] = 0;
|
||||
return 0;
|
||||
}
|
||||
case WO_B_NOW: { /* wall-clock milliseconds */
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
|
|||
[WO_B_DB_GET_FIELD] = 3,
|
||||
[WO_B_DB_PROBE] = 3,
|
||||
[WO_B_STR_LT] = 2,
|
||||
[WO_B_SPAWN] = 2, [WO_B_SEND] = 2, /* arc: (instance, midx) / (addr, msg) */
|
||||
[WO_B_NOW] = 0, [WO_B_PRINT] = 1, [WO_B_PRINT_INT] = 1,
|
||||
[WO_B_WORDS] = 1, [WO_B_MULTI_NEW] = 0, [WO_B_MULTI_PUSH] = 2,
|
||||
[WO_B_MULTI_GET] = 2, [WO_B_COUNT] = 1, [WO_B_LATEST] = 1,
|
||||
|
|
|
|||
176
runtime/src/vm.c
176
runtime/src/vm.c
|
|
@ -28,7 +28,24 @@ int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap) {
|
|||
return wo_rt_init(&vm->rt, heap_cap, mod->classes, mod->class_cnt);
|
||||
}
|
||||
|
||||
void wo_vm_destroy(wo_vm *vm) { wo_rt_destroy(&vm->rt); }
|
||||
void wo_vm_destroy(wo_vm *vm) {
|
||||
/* actors first — dropping their state and queued messages needs the
|
||||
* runtime alive */
|
||||
wo_actor *a = vm->actors;
|
||||
while (a) {
|
||||
wo_actor *nx = a->next_all;
|
||||
if (a->instance) wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)a->instance);
|
||||
for (uint32_t i = 0; i < a->mlen; i++) {
|
||||
uint64_t m = a->msgs[(a->mhead + i) % a->mcap];
|
||||
if (m) wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)m);
|
||||
}
|
||||
free(a->msgs);
|
||||
free(a);
|
||||
a = nx;
|
||||
}
|
||||
vm->actors = NULL;
|
||||
wo_rt_destroy(&vm->rt);
|
||||
}
|
||||
|
||||
/* ---- the run queue (stage 1 Task 2) ---------------------------------- */
|
||||
|
||||
|
|
@ -78,6 +95,12 @@ static void fib_reap(wo_vm *vm, wo_fiber *fb) {
|
|||
vm->cur = fb;
|
||||
vm_unwind(vm, 0);
|
||||
vm->cur = save;
|
||||
if (fb->actor) {
|
||||
/* the in-flight message is the runtime's to drop */
|
||||
if (fb->cur_msg) wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)fb->cur_msg);
|
||||
fb->cur_msg = 0;
|
||||
fb->actor->active = NULL;
|
||||
}
|
||||
if (fb != &vm->f0) {
|
||||
vm->nfibers--;
|
||||
free(fb);
|
||||
|
|
@ -90,6 +113,90 @@ static void fib_reap_all(wo_vm *vm) {
|
|||
while ((fb = fib_dequeue(vm)) != NULL) fib_reap(vm, fb);
|
||||
}
|
||||
|
||||
/* ---- actors (arc stage 1 Task 3) -------------------------------------- */
|
||||
|
||||
static uint64_t actor_pop(wo_actor *a) {
|
||||
uint64_t m = a->msgs[a->mhead];
|
||||
a->mhead = (a->mhead + 1) % a->mcap;
|
||||
a->mlen--;
|
||||
return m;
|
||||
}
|
||||
|
||||
static int actor_push(wo_actor *a, uint64_t m) {
|
||||
if (a->mlen == a->mcap) {
|
||||
uint32_t ncap = a->mcap ? a->mcap * 2 : 8;
|
||||
uint64_t *nm = malloc((size_t)ncap * 8u);
|
||||
if (!nm) return -1;
|
||||
for (uint32_t i = 0; i < a->mlen; i++) nm[i] = a->msgs[(a->mhead + i) % a->mcap];
|
||||
free(a->msgs);
|
||||
a->msgs = nm;
|
||||
a->mhead = 0;
|
||||
a->mcap = ncap;
|
||||
}
|
||||
a->msgs[(a->mhead + a->mlen) % a->mcap] = m;
|
||||
a->mlen++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Mailbox nonempty, no delivery fiber: start one on the next message.
|
||||
* receive borrows both self and the message; the runtime keeps ownership
|
||||
* of the message (fiber->cur_msg) and drops it when the call returns. */
|
||||
static int actor_activate(wo_vm *vm, wo_actor *a) {
|
||||
uint64_t m = actor_pop(a);
|
||||
uint64_t args[2] = { a->instance, m };
|
||||
wo_fiber *fb = wo_vm_spawn_fiber(vm, a->method, args, 2);
|
||||
if (!fb) return -1;
|
||||
fb->actor = a;
|
||||
fb->cur_msg = m;
|
||||
a->active = fb;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int wo_vm_actor_spawn(wo_vm *vm, uint64_t instance, uint32_t method_idx,
|
||||
uint64_t *out_addr, const char **msg) {
|
||||
if (method_idx >= vm->mod->method_cnt
|
||||
|| vm->mod->methods[method_idx].arg_cnt != 2) {
|
||||
*msg = "spawn: receive must take (self, msg)";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
if (!instance) {
|
||||
*msg = "spawn: nil instance";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
wo_actor *a = calloc(1, sizeof(*a));
|
||||
if (!a) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
a->instance = instance;
|
||||
a->method = method_idx;
|
||||
a->next_all = vm->actors;
|
||||
vm->actors = a;
|
||||
*out_addr = (uint64_t)(uintptr_t)a;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int wo_vm_actor_send(wo_vm *vm, uint64_t addr, uint64_t msg_val, const char **msg) {
|
||||
wo_actor *a = (wo_actor *)(uintptr_t)addr;
|
||||
if (!a) {
|
||||
*msg = "send: nil actor address";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
if (!msg_val) {
|
||||
*msg = "send: nil message";
|
||||
return WO_T_BOUNDS;
|
||||
}
|
||||
if (actor_push(a, msg_val) != 0) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
if (!a->active && actor_activate(vm, a) != 0) {
|
||||
*msg = "out of memory";
|
||||
return WO_T_OOM;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The drop-table entry governing instruction [pc]: the last one recorded
|
||||
* at or before it. NULL = nothing live there. */
|
||||
static const wo_dropent *vm_dropent(const wo_methodrec *me, uint32_t pc) {
|
||||
|
|
@ -165,6 +272,17 @@ static void vm_gc_roots(wo_vm *vm) {
|
|||
vm_gc_roots_fiber(vm, vm->cur);
|
||||
for (const wo_fiber *fb = vm->qhead; fb; fb = fb->next)
|
||||
vm_gc_roots_fiber(vm, fb);
|
||||
/* actors: moved-in state, queued messages, and the in-flight message
|
||||
* are runtime-owned — none sits in any frame's masks */
|
||||
for (const wo_actor *a = vm->actors; a; a = a->next_all) {
|
||||
if (a->instance) wo_gc_scan_root(&vm->rt, (wo_hdr *)(uintptr_t)a->instance);
|
||||
for (uint32_t i = 0; i < a->mlen; i++) {
|
||||
uint64_t m = a->msgs[(a->mhead + i) % a->mcap];
|
||||
if (m) wo_gc_scan_root(&vm->rt, (wo_hdr *)(uintptr_t)m);
|
||||
}
|
||||
if (a->active && a->active->cur_msg)
|
||||
wo_gc_scan_root(&vm->rt, (wo_hdr *)(uintptr_t)a->active->cur_msg);
|
||||
}
|
||||
}
|
||||
|
||||
/* One safepoint: start a cycle when the trigger says so (snapshot the
|
||||
|
|
@ -514,20 +632,48 @@ dispatch:
|
|||
* wrapper returns nothing owned — Task 3's contract). The queue cannot be
|
||||
* empty when a spawned fiber ends: main never parks in stage 1, so it is
|
||||
* either live or queued. */
|
||||
#define FIBER_DONE(rv) \
|
||||
do { \
|
||||
if (vm->cur == &vm->f0) { \
|
||||
fib_reap_all(vm); \
|
||||
*ret = (rv); \
|
||||
return 0; \
|
||||
} \
|
||||
wo_fiber *dead = vm->cur; \
|
||||
vm->cur = fib_dequeue(vm); \
|
||||
vm->nfibers--; \
|
||||
free(dead); \
|
||||
vm->budget = vm->budget0; \
|
||||
RELOAD(); \
|
||||
NEXT(); \
|
||||
#define FIBER_DONE(rv) \
|
||||
do { \
|
||||
if (vm->cur == &vm->f0) { \
|
||||
fib_reap_all(vm); \
|
||||
*ret = (rv); \
|
||||
return 0; \
|
||||
} \
|
||||
wo_fiber *dead = vm->cur; \
|
||||
if (dead->actor) { \
|
||||
wo_actor *a = dead->actor; \
|
||||
if (dead->cur_msg) { \
|
||||
wo_drop_obj(&vm->rt, (wo_hdr *)(uintptr_t)dead->cur_msg); \
|
||||
dead->cur_msg = 0; \
|
||||
} \
|
||||
if (a->mlen) { \
|
||||
/* next message: REUSE this context, re-queued for \
|
||||
* fairness (one message per turn, never a monopolist) */ \
|
||||
uint64_t m_ = actor_pop(a); \
|
||||
const wo_methodrec *sme_ = &vm->mod->methods[a->method]; \
|
||||
dead->depth = 1; \
|
||||
dead->ncatch = 0; \
|
||||
dead->frames[0].method = a->method; \
|
||||
dead->frames[0].pc = 0; \
|
||||
dead->frames[0].base = 0; \
|
||||
dead->regs[0] = a->instance; \
|
||||
dead->regs[1] = m_; \
|
||||
memset(dead->regs + 2, 0, (size_t)(sme_->reg_cnt - 2) * 8u); \
|
||||
dead->cur_msg = m_; \
|
||||
fib_enqueue(vm, dead); \
|
||||
vm->cur = fib_dequeue(vm); \
|
||||
vm->budget = vm->budget0; \
|
||||
RELOAD(); \
|
||||
NEXT(); \
|
||||
} \
|
||||
a->active = NULL; \
|
||||
} \
|
||||
vm->cur = fib_dequeue(vm); \
|
||||
vm->nfibers--; \
|
||||
free(dead); \
|
||||
vm->budget = vm->budget0; \
|
||||
RELOAD(); \
|
||||
NEXT(); \
|
||||
} while (0)
|
||||
|
||||
CASE(RET) : {
|
||||
|
|
|
|||
|
|
@ -59,8 +59,25 @@ typedef struct wo_fiber {
|
|||
wo_err caught;
|
||||
wo_fib_state state;
|
||||
struct wo_fiber *next; /* intrusive FIFO link (run queue) */
|
||||
/* arc actors: when this fiber is an actor's delivery fiber, `actor`
|
||||
* points at it and `cur_msg` is the message the current receive call
|
||||
* borrows — the RUNTIME owns it and drops it after the call returns. */
|
||||
struct wo_actor *actor;
|
||||
uint64_t cur_msg;
|
||||
} wo_fiber;
|
||||
|
||||
/* An actor: moved-in state, its receive method, a FIFO mailbox, and at
|
||||
* most one delivery fiber at a time (one message at a time — the actor
|
||||
* guarantee). Actors live until program end (v1: no actor death). */
|
||||
typedef struct wo_actor {
|
||||
uint64_t instance; /* the moved-in state object (runtime-owned) */
|
||||
uint32_t method; /* receive's method index (self + msg = 2 args) */
|
||||
uint64_t *msgs; /* FIFO ring, growable */
|
||||
uint32_t mhead, mlen, mcap;
|
||||
wo_fiber *active; /* the delivery fiber, NULL when idle */
|
||||
struct wo_actor *next_all; /* the vm's all-actors list */
|
||||
} wo_actor;
|
||||
|
||||
typedef struct wo_vm {
|
||||
const wo_module *mod;
|
||||
wo_rt rt;
|
||||
|
|
@ -70,8 +87,14 @@ typedef struct wo_vm {
|
|||
uint32_t nfibers; /* live fibers besides main */
|
||||
int64_t budget0; /* reductions per slice (WO_REDUCTIONS, default 4000) */
|
||||
int64_t budget; /* countdown for the live fiber */
|
||||
wo_actor *actors; /* every spawned actor (torn down at destroy) */
|
||||
} wo_vm;
|
||||
|
||||
/* arc: the spawn/send builtins' runtime halves (vm.c owns the scheduler). */
|
||||
int wo_vm_actor_spawn(wo_vm *vm, uint64_t instance, uint32_t method_idx,
|
||||
uint64_t *out_addr, const char **msg);
|
||||
int wo_vm_actor_send(wo_vm *vm, uint64_t addr, uint64_t msg_val, const char **msg);
|
||||
|
||||
/* Spawn a fiber that will run method_idx(args) — the runtime half the
|
||||
* `spawn` expression lowers onto (stage 1 Task 3); Task 2's tests drive it
|
||||
* directly. The fiber is RUNNABLE and queued; it runs when the scheduler
|
||||
|
|
|
|||
|
|
@ -337,10 +337,13 @@ enum {
|
|||
WO_B_DB_SCAN = 64,
|
||||
WO_B_DB_GET_FIELD = 65,
|
||||
WO_B_DB_PROBE = 66,
|
||||
WO_B_STR_LT = 67, /* (a, b) text -> 1 if a < b by content, else 0 (query
|
||||
WO_B_STR_LT = 67, /* (a, b) text -> 1 if a < b by content, else 0 (query
|
||||
* order-by on a Text key; scalars use the LT opcode) */
|
||||
WO_B_SPAWN = 68, /* (instance, receive_method_idx) -> actor address (arc) */
|
||||
WO_B_SEND = 69, /* (address, msg) — msg moves to the runtime (arc) */
|
||||
};
|
||||
#define WO_B_MAX 67u
|
||||
|
||||
#define WO_B_MAX 69u
|
||||
/* ids at or above this one live in sysio.c, not builtin.c */
|
||||
#define WO_B_SYS_FIRST WO_B_FS_EXISTS
|
||||
|
||||
|
|
|
|||
1
tests/corpus/compile-fail/send-after-move/fixture.code
Normal file
1
tests/corpus/compile-fail/send-after-move/fixture.code
Normal file
|
|
@ -0,0 +1 @@
|
|||
WO-E301
|
||||
20
tests/corpus/compile-fail/send-after-move/fixture.wo
Normal file
20
tests/corpus/compile-fail/send-after-move/fixture.wo
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
-- a message MOVES on send: the sender's binding is dead afterwards
|
||||
|
||||
class Note {
|
||||
text: Text
|
||||
}
|
||||
|
||||
class Sink {
|
||||
pad: Int
|
||||
fn receive(msg: Note) {
|
||||
print(msg.text);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let a = spawn Sink { pad: 0 };
|
||||
let n = Note { text: "gone" };
|
||||
send(a, n);
|
||||
print(n.text);
|
||||
return 0;
|
||||
}
|
||||
1
tests/corpus/compile-fail/spawn-no-receive/fixture.code
Normal file
1
tests/corpus/compile-fail/spawn-no-receive/fixture.code
Normal file
|
|
@ -0,0 +1 @@
|
|||
WO-E221
|
||||
10
tests/corpus/compile-fail/spawn-no-receive/fixture.wo
Normal file
10
tests/corpus/compile-fail/spawn-no-receive/fixture.wo
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-- spawn's target must be an actor: a class with fn receive(msg: M)
|
||||
|
||||
class Silent {
|
||||
n: Int
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let a = spawn Silent { n: 1 };
|
||||
return 0;
|
||||
}
|
||||
4
tests/corpus/run/actor-echo/fixture.out
Normal file
4
tests/corpus/run/actor-echo/fixture.out
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
log: first #1
|
||||
log: second #2
|
||||
log: third #3
|
||||
main done
|
||||
34
tests/corpus/run/actor-echo/fixture.wo
Normal file
34
tests/corpus/run/actor-echo/fixture.wo
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
-- the arc: spawn/send/actor M — an actor is a class with receive(msg: M);
|
||||
-- messages MOVE; delivery is one message at a time; main's return reaps
|
||||
-- idle actors clean (ASan gate covers this fixture too).
|
||||
|
||||
class Note {
|
||||
text: Text
|
||||
}
|
||||
|
||||
class Logger {
|
||||
prefix: Text
|
||||
seen: Int
|
||||
fn receive(msg: Note) {
|
||||
self.seen = self.seen + 1;
|
||||
print("${self.prefix}${msg.text} #${self.seen}");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let a: actor Note = spawn Logger { prefix: "log: ", seen: 0 };
|
||||
send(a, Note { text: "first" });
|
||||
send(a, Note { text: "second" });
|
||||
let n = Note { text: "third" };
|
||||
send(a, n);
|
||||
-- the scheduler drains actor messages when main yields or ends; a
|
||||
-- backward-jump loop hands turns over under the default budget? No:
|
||||
-- default budget is 4000 — force turns with a tiny spin so delivery
|
||||
-- interleaves before main returns
|
||||
let i = 0;
|
||||
while i < 20000 {
|
||||
i = i + 1;
|
||||
}
|
||||
print("main done");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue