feat: true cancellation for the fiber Io layer (PLAN-IO-UNIFY Phase 3)
A cancelled async worker now abandons its body at its next suspend instead
of running to completion.
- Cancel-flag back-ref (D4): SpawnOpts.cancel_flag (core.sx) + Fiber.cancel_flag
(sched.sx), set from opts.cancel_flag in Scheduler.spawn_raw; async passes
xx @f.canceled (the Future.canceled Atomic(bool) erased to *void).
- Delivery: Scheduler.suspend_raw consults fiber_canceled(self.current) PRE-park
(raise without parking — no deadlock if cancel landed before the worker ran)
and POST-resume (cancel landed while parked), raising error.Canceled.
cancel(f) flips the sticky flag, marks .canceled, and wakes the worker.
- async worker is failable Closure() -> ($R, !); the completion closure
f.value = worker() catch {…} marks .canceled/.failed and wakes the awaiter,
so post-suspend side effects never run. New failable io.sleep(ms) is the
cancellation point.
- Compiler: a -> ! fn whose only error source is try-ing a protocol method
(io.suspend_raw) was wrongly flagged 'declared ! but never errors';
collectErrorSites now marks a try of a non-identifier callee as a dynamic
(opaque) error source, suppressing the warning.
- Two UAFs found by adversarial review and fixed: (1) cancel-before-park
orphaned io.sleep's armed timer — suspend_raw's pre-park raise now evicts the
current fiber's timer/waiter first; (2) cancel(f) could wake a reaped worker —
now only wakes when was_pending.
Migrated 1805/1806/1824 to failable workers. Lock: example 1825 (seq: 1 -99,
post-suspend line never runs); byte-identical on aarch64-macOS + aarch64-linux.
.ir churn is the SpawnOpts layout change (type-table string renumbering).
This commit is contained in:
@@ -38,79 +38,90 @@ pub const ErrorAnalysis = struct {
|
||||
|
||||
/// Collect the error TAGS raised + the `try`-call EDGES of a function body,
|
||||
/// for the inferred-set fix-point. Stops at nested function boundaries.
|
||||
pub fn collectErrorSites(self: ErrorAnalysis, node: *const Node, tags: *std.ArrayList(u32), edges: *std.ArrayList([]const u8)) void {
|
||||
pub fn collectErrorSites(self: ErrorAnalysis, node: *const Node, tags: *std.ArrayList(u32), edges: *std.ArrayList([]const u8), dyn: *bool) void {
|
||||
switch (node.data) {
|
||||
.raise_stmt => |rs| {
|
||||
if (Lowering.isErrorTagLiteralNode(rs.tag)) {
|
||||
tags.append(self.l.alloc, self.l.module.types.internTag(rs.tag.data.field_access.field)) catch {};
|
||||
}
|
||||
self.collectErrorSites(rs.tag, tags, edges);
|
||||
self.collectErrorSites(rs.tag, tags, edges, dyn);
|
||||
},
|
||||
.try_expr => |te| {
|
||||
if (Lowering.callTargetName(te.operand)) |nm| edges.append(self.l.alloc, nm) catch {};
|
||||
self.collectErrorSites(te.operand, tags, edges);
|
||||
if (Lowering.callTargetName(te.operand)) |nm| {
|
||||
edges.append(self.l.alloc, nm) catch {};
|
||||
} else if (te.operand.data == .call) {
|
||||
// A `try` whose callee is NOT a plain identifier — a protocol
|
||||
// method (`io.suspend_raw`), a UFCS / instance method, a
|
||||
// closure / fn-pointer value. Its error channel is OPAQUE to
|
||||
// this static convergence (no free-fn name to resolve a set
|
||||
// from), so the function genuinely propagates a dynamic error.
|
||||
// Mark it so the "declared `!` but never errors" warning is
|
||||
// suppressed — the `!` is load-bearing, not droppable.
|
||||
dyn.* = true;
|
||||
}
|
||||
self.collectErrorSites(te.operand, tags, edges, dyn);
|
||||
},
|
||||
.block => |b| for (b.stmts) |s| self.collectErrorSites(s, tags, edges),
|
||||
.block => |b| for (b.stmts) |s| self.collectErrorSites(s, tags, edges, dyn),
|
||||
.if_expr => |ie| {
|
||||
self.collectErrorSites(ie.condition, tags, edges);
|
||||
self.collectErrorSites(ie.then_branch, tags, edges);
|
||||
if (ie.else_branch) |eb| self.collectErrorSites(eb, tags, edges);
|
||||
self.collectErrorSites(ie.condition, tags, edges, dyn);
|
||||
self.collectErrorSites(ie.then_branch, tags, edges, dyn);
|
||||
if (ie.else_branch) |eb| self.collectErrorSites(eb, tags, edges, dyn);
|
||||
},
|
||||
.while_expr => |w| {
|
||||
self.collectErrorSites(w.condition, tags, edges);
|
||||
self.collectErrorSites(w.body, tags, edges);
|
||||
self.collectErrorSites(w.condition, tags, edges, dyn);
|
||||
self.collectErrorSites(w.body, tags, edges, dyn);
|
||||
},
|
||||
.for_expr => |f| {
|
||||
for (f.iterables) |it| {
|
||||
self.collectErrorSites(it.expr, tags, edges);
|
||||
if (it.range_end) |re| self.collectErrorSites(re, tags, edges);
|
||||
self.collectErrorSites(it.expr, tags, edges, dyn);
|
||||
if (it.range_end) |re| self.collectErrorSites(re, tags, edges, dyn);
|
||||
}
|
||||
self.collectErrorSites(f.body, tags, edges);
|
||||
self.collectErrorSites(f.body, tags, edges, dyn);
|
||||
},
|
||||
.return_stmt => |r| if (r.value) |v| self.collectErrorSites(v, tags, edges),
|
||||
.var_decl => |v| if (v.value) |val| self.collectErrorSites(val, tags, edges),
|
||||
.const_decl => |c| self.collectErrorSites(c.value, tags, edges),
|
||||
.destructure_decl => |d| self.collectErrorSites(d.value, tags, edges),
|
||||
.return_stmt => |r| if (r.value) |v| self.collectErrorSites(v, tags, edges, dyn),
|
||||
.var_decl => |v| if (v.value) |val| self.collectErrorSites(val, tags, edges, dyn),
|
||||
.const_decl => |c| self.collectErrorSites(c.value, tags, edges, dyn),
|
||||
.destructure_decl => |d| self.collectErrorSites(d.value, tags, edges, dyn),
|
||||
.assignment => |a| {
|
||||
self.collectErrorSites(a.target, tags, edges);
|
||||
self.collectErrorSites(a.value, tags, edges);
|
||||
self.collectErrorSites(a.target, tags, edges, dyn);
|
||||
self.collectErrorSites(a.value, tags, edges, dyn);
|
||||
},
|
||||
.multi_assign => |m| {
|
||||
for (m.targets) |t| self.collectErrorSites(t, tags, edges);
|
||||
for (m.values) |v| self.collectErrorSites(v, tags, edges);
|
||||
for (m.targets) |t| self.collectErrorSites(t, tags, edges, dyn);
|
||||
for (m.values) |v| self.collectErrorSites(v, tags, edges, dyn);
|
||||
},
|
||||
.call => |c| {
|
||||
self.collectErrorSites(c.callee, tags, edges);
|
||||
for (c.args) |a| self.collectErrorSites(a, tags, edges);
|
||||
self.collectErrorSites(c.callee, tags, edges, dyn);
|
||||
for (c.args) |a| self.collectErrorSites(a, tags, edges, dyn);
|
||||
},
|
||||
.binary_op => |b| {
|
||||
self.collectErrorSites(b.lhs, tags, edges);
|
||||
self.collectErrorSites(b.rhs, tags, edges);
|
||||
self.collectErrorSites(b.lhs, tags, edges, dyn);
|
||||
self.collectErrorSites(b.rhs, tags, edges, dyn);
|
||||
},
|
||||
.unary_op => |u| self.collectErrorSites(u.operand, tags, edges),
|
||||
.deref_expr => |d| self.collectErrorSites(d.operand, tags, edges),
|
||||
.force_unwrap => |fu| self.collectErrorSites(fu.operand, tags, edges),
|
||||
.unary_op => |u| self.collectErrorSites(u.operand, tags, edges, dyn),
|
||||
.deref_expr => |d| self.collectErrorSites(d.operand, tags, edges, dyn),
|
||||
.force_unwrap => |fu| self.collectErrorSites(fu.operand, tags, edges, dyn),
|
||||
.null_coalesce => |nc| {
|
||||
self.collectErrorSites(nc.lhs, tags, edges);
|
||||
self.collectErrorSites(nc.rhs, tags, edges);
|
||||
self.collectErrorSites(nc.lhs, tags, edges, dyn);
|
||||
self.collectErrorSites(nc.rhs, tags, edges, dyn);
|
||||
},
|
||||
.field_access => |fa| self.collectErrorSites(fa.object, tags, edges),
|
||||
.field_access => |fa| self.collectErrorSites(fa.object, tags, edges, dyn),
|
||||
.index_expr => |ix| {
|
||||
self.collectErrorSites(ix.object, tags, edges);
|
||||
self.collectErrorSites(ix.index, tags, edges);
|
||||
self.collectErrorSites(ix.object, tags, edges, dyn);
|
||||
self.collectErrorSites(ix.index, tags, edges, dyn);
|
||||
},
|
||||
.spread_expr => |s| self.collectErrorSites(s.operand, tags, edges),
|
||||
.spread_expr => |s| self.collectErrorSites(s.operand, tags, edges, dyn),
|
||||
.catch_expr => |ce| {
|
||||
self.collectErrorSites(ce.operand, tags, edges);
|
||||
self.collectErrorSites(ce.body, tags, edges);
|
||||
self.collectErrorSites(ce.operand, tags, edges, dyn);
|
||||
self.collectErrorSites(ce.body, tags, edges, dyn);
|
||||
},
|
||||
.defer_stmt => |d| self.collectErrorSites(d.expr, tags, edges),
|
||||
.defer_stmt => |d| self.collectErrorSites(d.expr, tags, edges, dyn),
|
||||
.push_stmt => |p| {
|
||||
self.collectErrorSites(p.context_expr, tags, edges);
|
||||
self.collectErrorSites(p.body, tags, edges);
|
||||
self.collectErrorSites(p.context_expr, tags, edges, dyn);
|
||||
self.collectErrorSites(p.body, tags, edges, dyn);
|
||||
},
|
||||
.array_literal => |al| for (al.elements) |el| self.collectErrorSites(el, tags, edges),
|
||||
.tuple_literal => |tl| for (tl.elements) |el| self.collectErrorSites(el.value, tags, edges),
|
||||
.array_literal => |al| for (al.elements) |el| self.collectErrorSites(el, tags, edges, dyn),
|
||||
.tuple_literal => |tl| for (tl.elements) |el| self.collectErrorSites(el.value, tags, edges, dyn),
|
||||
// Stop at nested function boundaries; leaves contribute nothing.
|
||||
else => {},
|
||||
}
|
||||
@@ -127,6 +138,11 @@ pub const ErrorAnalysis = struct {
|
||||
tags: std.ArrayList(u32),
|
||||
edges: std.ArrayList([]const u8),
|
||||
rt: ?*const Node,
|
||||
// The body `try`s a callee with an OPAQUE error channel (a protocol
|
||||
// method / UFCS-method / closure call) — so it genuinely propagates a
|
||||
// dynamic error even when no concrete tag converges. Suppresses the
|
||||
// empty-set "drop the `!`" warning.
|
||||
dyn: bool,
|
||||
};
|
||||
var work = std.StringHashMap(Node_).init(self.l.alloc);
|
||||
defer work.deinit();
|
||||
@@ -138,8 +154,9 @@ pub const ErrorAnalysis = struct {
|
||||
if (!Lowering.astIsPureBareInferred(fd.return_type)) continue;
|
||||
var tags = std.ArrayList(u32).empty;
|
||||
var edges = std.ArrayList([]const u8).empty;
|
||||
self.collectErrorSites(fd.body, &tags, &edges);
|
||||
work.put(e.key_ptr.*, .{ .tags = tags, .edges = edges, .rt = fd.return_type }) catch {};
|
||||
var dyn = false;
|
||||
self.collectErrorSites(fd.body, &tags, &edges, &dyn);
|
||||
work.put(e.key_ptr.*, .{ .tags = tags, .edges = edges, .rt = fd.return_type, .dyn = dyn }) catch {};
|
||||
}
|
||||
|
||||
// Union edge contributions until no set grows (monotone → terminates).
|
||||
@@ -178,7 +195,7 @@ pub const ErrorAnalysis = struct {
|
||||
// protocol-impl method (its `!` is dictated by the protocol
|
||||
// contract — e.g. `Io.suspend_raw` — so a non-raising impl body
|
||||
// is not a "drop the `!`" case; see `impl_method_names`).
|
||||
if (sorted.len == 0 and !std.mem.eql(u8, se.key_ptr.*, "main") and !self.l.impl_method_names.contains(se.key_ptr.*)) {
|
||||
if (sorted.len == 0 and !se.value_ptr.dyn and !std.mem.eql(u8, se.key_ptr.*, "main") and !self.l.impl_method_names.contains(se.key_ptr.*)) {
|
||||
if (self.l.diagnostics) |diags| {
|
||||
if (se.value_ptr.rt) |rt| {
|
||||
diags.addFmt(.warn, rt.span, "function '{s}' is declared `!` but never errors — drop the `!`", .{se.key_ptr.*});
|
||||
|
||||
@@ -1049,7 +1049,10 @@ pub fn recordClosureShape(self: *Lowering, lam: *const ast.Lambda) void {
|
||||
defer tags.deinit(self.alloc);
|
||||
var edges = std.ArrayList([]const u8).empty;
|
||||
defer edges.deinit(self.alloc);
|
||||
self.errorAnalysis().collectErrorSites(lam.body, &tags, &edges);
|
||||
// `dyn` (opaque-error-channel `try`) is irrelevant to closure-shape set
|
||||
// widening — that signal only gates the top-level "drop the `!`" warning.
|
||||
var dyn_unused = false;
|
||||
self.errorAnalysis().collectErrorSites(lam.body, &tags, &edges, &dyn_unused);
|
||||
for (edges.items) |callee| {
|
||||
for (self.calleeEscapeTags(callee)) |t| {
|
||||
if (!containsTag(tags.items, t)) tags.append(self.alloc, t) catch {};
|
||||
|
||||
Reference in New Issue
Block a user