From 7247fc64070118292913eee891be457e25f2eab9 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 10:01:09 +0900 Subject: [PATCH 1/9] wip: add path-complete task wait analysis --- HANDOFF.md | 6 +- crates/align_sema/src/lib.rs | 17 +- crates/align_sema/src/task_wait.rs | 922 +++++++++++++++++++++++++++++ 3 files changed, 931 insertions(+), 14 deletions(-) create mode 100644 crates/align_sema/src/task_wait.rs diff --git a/HANDOFF.md b/HANDOFF.md index 5459ecd2..08749dfe 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -499,7 +499,11 @@ or failed Wait from disappearing while keeping initialized slots readable. The n pass found the same distinction missing in join remapping: a TaskProof now survives an unresolved Wait only when that predecessor already completed its current generation; otherwise coverage clears the proof. The TaskGet discriminator and straight-line/branch/loop owner rows carry the same -rule. Am-v requires a +rule. The am-w implementation is intentionally one vertical: splitting group state, proof +transport, joins, or TaskGet diagnostics would leave an intermediate compiler that can authorize +an uninitialized task slot or reject a valid outer proof. If the implementation exceeds the +500-line target, the split-proof exception is justified by this single safety invariant; the +formation, control, ownership, and whole/per-unit rows must land together. Am-v requires a bound `mut Buffer` local at ReaderRead, ReaderReadLine, FilePread, UdpRecvFrom, and CryptoRandom; those five producer paths currently accept equal-typed temporaries and immutable buffers even though the runtime writes through them. Am-u rejects extern declarations as first-class function diff --git a/crates/align_sema/src/lib.rs b/crates/align_sema/src/lib.rs index 653d4bd0..4fb60513 100644 --- a/crates/align_sema/src/lib.rs +++ b/crates/align_sema/src/lib.rs @@ -16,6 +16,7 @@ use align_span::Span; pub mod hir; pub use hir::*; mod hir_depth; +mod task_wait; pub use hir_depth::{ MAX_CHECKED_HIR_DEPTH, checked_hir_body_depth_is_valid, direct_expr_children, }; @@ -19148,6 +19149,7 @@ impl<'a, 't> Checker<'a, 't> { // Finalize all inferred types to concrete (or default i64). let mut body = body; self.finalize_block(&mut body); + task_wait::validate(&body, self.tagged_types, self.diags); // The broad "unnecessary heap" lint: a whole-function scan for a box local that is only ever // read back with `.get()` and never escapes (the narrow inline `heap.new(x).get()` slice lives // in `finalize_expr`). A warning, not an error — it never blocks a build. @@ -23524,6 +23526,7 @@ impl<'a, 't> Checker<'a, 't> { self.check_return_completeness(&checked, ret, body.span); let mut body_fin = checked; self.finalize_block(&mut body_fin); + task_wait::validate(&body_fin, self.tagged_types, self.diags); // Run the broad unnecessary-heap scan on the lifted lambda body too (parity with the narrow // lint in `finalize_expr`); a box local here is function-local (Move values cannot be // captured), so the scan is self-contained and never double-reports the enclosing function. @@ -30051,19 +30054,7 @@ impl<'a, 't> Checker<'a, 't> { // `task.get()` — read a spawned task's result (`task_group`, slice ④). The result is // only computed after `wait()` joins, so `get()` before `wait()` reads an uncomputed // slot — rejected (the result is guaranteed ready only if a `wait()` dominates here). - Ty::Task(s) => { - if !self.wait_state.last().copied().unwrap_or(false) { - let msg = if self.task_group_fallible.last().copied().unwrap_or(false) { - // A fallible group: a bare `wait()` ignores the error; only `wait()?` makes - // the results safe to read. - "cannot call '.get()' before a successful 'wait()?' — this task_group is fallible, so use 'wait()?' to join (its error propagates) before reading results" - } else { - "cannot call '.get()' before 'wait()' — a task's result is ready only after the group is joined" - }; - self.diags.error(msg.to_string(), span); - } - Expr { kind: ExprKind::TaskGet(Box::new(recv)), ty: scalar_to_ty(s), span } - } + Ty::Task(s) => Expr { kind: ExprKind::TaskGet(Box::new(recv)), ty: scalar_to_ty(s), span }, Ty::Error => Expr { kind: ExprKind::Bool(false), ty: Ty::Error, span }, other => { self.diags diff --git a/crates/align_sema/src/task_wait.rs b/crates/align_sema/src/task_wait.rs new file mode 100644 index 00000000..94e3be0f --- /dev/null +++ b/crates/align_sema/src/task_wait.rs @@ -0,0 +1,922 @@ +//! Path-complete, compiler-only proofs for `task_group` waits. +//! +//! The checker used to carry one boolean per open group. This replay tracks the originating +//! group, Spawn generation, proof epoch, and every fallible Wait in source order. It runs on the +//! already checked HIR before publication; no proof is serialized and no runtime ABI changes. + +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::ops::Range; + +use align_ast::BinOp; +use align_diag::Diagnostics; +use align_span::Span; + +use crate::hir::{Block, Expr, ExprKind, LocalId, MatchArm, Stmt}; +use crate::{expand_tagged_ty, hir_expr_diverges, TaggedType, Ty}; + +type Token = u32; + +const INITIAL_GENERATION: u8 = 1; +const INITIAL_EPOCH: u8 = 2; +const SPAWN_GENERATION: u8 = 3; +const SPAWN_EPOCH: u8 = 4; +const WAIT_TOKEN: u8 = 5; +const ERR_EPOCH: u8 = 6; +const JOIN_GENERATION: u8 = 7; +const JOIN_EPOCH: u8 = 8; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct TokenKey { + group: Span, + site: Span, + kind: u8, +} + +#[derive(Default)] +struct Tokens { + next: Token, + values: HashMap, +} + +impl Tokens { + fn get(&mut self, group: Span, site: Span, kind: u8) -> Token { + if let Some(token) = self.values.get(&TokenKey { group, site, kind }) { + return *token; + } + let token = self.next.saturating_add(1).max(1); + self.next = token; + self.values.insert(TokenKey { group, site, kind }, token); + token + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum WaitStatus { + Pending, + Ok, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct WaitRecord { + status: WaitStatus, + covers_through: Token, + covered_generations: BTreeSet, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Group { + id: Span, + generation: Token, + epoch: Token, + completed: Option, + valid_generations: BTreeSet, + fallible: bool, + waits: BTreeMap, + wait_order: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct WaitProof { + group: Span, + epoch: Token, + wait: Token, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct TaskProof { + group: Span, + generation: Token, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Proof { + Wait(WaitProof), + Task(TaskProof), +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct State { + groups: Vec, + waits: HashMap, + tasks: HashMap, +} + +#[derive(Clone, Debug)] +struct Flow { + state: Option, + proof: Option, +} + +impl Flow { + fn live(state: State, proof: Option) -> Self { + Self { + state: Some(state), + proof, + } + } + + fn dead() -> Self { + Self { + state: None, + proof: None, + } + } + + fn clear_proof(mut self) -> Self { + self.proof = None; + self + } +} + +struct Analyzer<'a> { + tagged_types: &'a [TaggedType], + diags: &'a mut Diagnostics, + tokens: Tokens, + loop_breaks: Vec>, + reported_gets: HashSet, +} + +pub fn validate(body: &Block, tagged_types: &[TaggedType], diags: &mut Diagnostics) { + let mut analyzer = Analyzer { + tagged_types, + diags, + tokens: Tokens::default(), + loop_breaks: Vec::new(), + reported_gets: HashSet::new(), + }; + let _ = analyzer.block(body, State::default(), true); +} + +impl<'a> Analyzer<'a> { + fn group<'b>(&self, state: &'b State, id: Span) -> Option<&'b Group> { + state.groups.iter().rev().find(|group| group.id == id) + } + + fn group_mut<'s>(&self, state: &'s mut State, id: Span) -> Option<&'s mut Group> { + state.groups.iter_mut().rev().find(|group| group.id == id) + } + + fn current_group(&self, state: &State) -> Option { + state.groups.last().map(|group| group.id) + } + + fn new_group(&mut self, id: Span) -> Group { + let generation = self.tokens.get(id, id, INITIAL_GENERATION); + let epoch = self.tokens.get(id, id, INITIAL_EPOCH); + let mut valid_generations = BTreeSet::new(); + valid_generations.insert(generation); + Group { + id, + generation, + epoch, + completed: Some(generation), + valid_generations, + fallible: false, + waits: BTreeMap::new(), + wait_order: Vec::new(), + } + } + + fn has_pending_covering(group: &Group, generation: Token) -> bool { + group.waits.values().any(|wait| { + wait.status == WaitStatus::Pending && wait.covered_generations.contains(&generation) + }) + } + + fn task_ready(&self, state: &State, proof: TaskProof) -> bool { + let Some(group) = self.group(state, proof.group) else { + return false; + }; + group.valid_generations.contains(&proof.generation) + && (group.completed == Some(group.generation) + || !Self::has_pending_covering(group, proof.generation)) + } + + fn wait_current(&self, state: &State, proof: WaitProof) -> bool { + self.group(state, proof.group).is_some_and(|group| { + group.epoch == proof.epoch && group.waits.contains_key(&proof.wait) + }) + } + + fn spawn(&mut self, state: &mut State, site: Span, fallible: bool) -> Option { + let id = self.current_group(state)?; + let pending = self.group(state, id).is_some_and(|group| { + group + .waits + .values() + .any(|wait| wait.status == WaitStatus::Pending) + }); + let generation = self.tokens.get(id, site, SPAWN_GENERATION); + let epoch = self.tokens.get(id, site, SPAWN_EPOCH); + let group = self.group_mut(state, id)?; + if pending { + group.valid_generations.clear(); + } + group.generation = generation; + group.epoch = epoch; + group.waits.clear(); + group.wait_order.clear(); + group.valid_generations.insert(generation); + group.fallible |= fallible; + Some(TaskProof { + group: id, + generation, + }) + } + + fn wait(&mut self, state: &mut State, site: Span, fallible: bool) -> Option { + let id = self.current_group(state)?; + let (epoch, generation, covered) = { + let group = self.group(state, id)?; + ( + group.epoch, + group.generation, + group.valid_generations.clone(), + ) + }; + if !fallible { + self.group_mut(state, id)?.completed = Some(generation); + return None; + } + let wait = self.tokens.get(id, site, WAIT_TOKEN); + if let Some(group) = self.group_mut(state, id) { + if !group.waits.contains_key(&wait) { + group.wait_order.push(wait); + } + group.waits.insert( + wait, + WaitRecord { + status: WaitStatus::Pending, + covers_through: generation, + covered_generations: covered, + }, + ); + } + Some(Proof::Wait(WaitProof { + group: id, + epoch, + wait, + })) + } + + fn resolve_ok(&mut self, state: &mut State, proof: WaitProof) { + let Some(group) = self.group_mut(state, proof.group) else { + return; + }; + if group.epoch != proof.epoch { + return; + } + let Some(record) = group.waits.get_mut(&proof.wait) else { + return; + }; + record.status = WaitStatus::Ok; + let mut completed = group.completed; + for wait in group.wait_order.iter().copied() { + let Some(record) = group.waits.get(&wait) else { + break; + }; + if record.status != WaitStatus::Ok { + break; + } + completed = Some(record.covers_through); + if wait == proof.wait { + break; + } + } + group.completed = completed; + } + + fn resolve_err(&mut self, state: &mut State, proof: WaitProof, site: Span) { + let Some(snapshot) = self.group(state, proof.group).cloned() else { + return; + }; + if snapshot.epoch != proof.epoch { + return; + } + let Some(record) = snapshot.waits.get(&proof.wait) else { + return; + }; + if record.status == WaitStatus::Ok { + return; + } + let epoch = self.tokens.get(proof.group, site, ERR_EPOCH); + let Some(group) = self.group_mut(state, proof.group) else { + return; + }; + for generation in &record.covered_generations { + group.valid_generations.remove(generation); + } + group.epoch = epoch; + group.completed = None; + group.waits.clear(); + group.wait_order.clear(); + } + + fn join_group(&mut self, site: Span, groups: &[Group]) -> (Group, bool) { + let Some(first) = groups.first() else { + let generation = self.tokens.get(site, site, JOIN_GENERATION); + let epoch = self.tokens.get(site, site, JOIN_EPOCH); + let mut valid_generations = BTreeSet::new(); + valid_generations.insert(generation); + return ( + Group { + id: site, + generation, + epoch, + completed: None, + valid_generations, + fallible: false, + waits: BTreeMap::new(), + wait_order: Vec::new(), + }, + true, + ); + }; + if groups.iter().all(|group| group == first) { + return (first.clone(), false); + } + let generation = self.tokens.get(first.id, site, JOIN_GENERATION); + let epoch = self.tokens.get(first.id, site, JOIN_EPOCH); + let completed = groups + .iter() + .all(|group| group.completed == Some(group.generation)); + let mut valid_generations = BTreeSet::new(); + valid_generations.insert(generation); + ( + Group { + id: first.id, + generation, + epoch, + completed: completed.then_some(generation), + valid_generations, + fallible: groups.iter().any(|group| group.fallible), + waits: BTreeMap::new(), + wait_order: Vec::new(), + }, + true, + ) + } + + fn merge_states(&mut self, site: Span, states: &[State]) -> Option { + let first = states.first()?.clone(); + if states.len() == 1 { + return Some(first); + } + let mut merged = first.clone(); + let mut changed = vec![false; first.groups.len()]; + for index in 0..first.groups.len() { + let groups: Vec = states + .iter() + .filter_map(|state| state.groups.get(index).cloned()) + .collect(); + if groups.len() != states.len() + || groups + .iter() + .any(|group| group.id != first.groups[index].id) + { + changed[index] = true; + continue; + } + let (group, differs) = self.join_group(site, &groups); + merged.groups[index] = group; + changed[index] = differs; + } + + merged.waits.clear(); + for (&local, proof) in &first.waits { + if states + .iter() + .all(|state| state.waits.get(&local) == Some(proof)) + && self.wait_current(&merged, *proof) + { + merged.waits.insert(local, *proof); + } + } + merged.tasks.clear(); + for (&local, proof) in &first.tasks { + let proofs: Vec = states + .iter() + .filter_map(|state| state.tasks.get(&local).copied()) + .collect(); + if proofs.len() != states.len() + || !proofs + .iter() + .all(|candidate| candidate.group == proof.group) + || !states + .iter() + .zip(&proofs) + .all(|(state, candidate)| self.task_ready(state, *candidate)) + { + continue; + } + let Some(index) = merged + .groups + .iter() + .position(|group| group.id == proof.group) + else { + continue; + }; + let joined = if changed[index] { + TaskProof { + group: proof.group, + generation: merged.groups[index].generation, + } + } else { + *proofs.first().unwrap_or(proof) + }; + merged.tasks.insert(local, joined); + } + Some(merged) + } + + fn merge_flows(&mut self, site: Span, flows: Vec) -> Flow { + let live: Vec<(State, Option)> = flows + .into_iter() + .filter_map(|flow| flow.state.map(|state| (state, flow.proof))) + .collect(); + let Some((_, first_proof)) = live.first().cloned() else { + return Flow::dead(); + }; + let states: Vec = live.iter().map(|(state, _)| state.clone()).collect(); + let Some(merged) = self.merge_states(site, &states) else { + return Flow::dead(); + }; + let proof = match first_proof { + Some(Proof::Wait(proof)) + if live + .iter() + .all(|(_, candidate)| *candidate == Some(Proof::Wait(proof))) + && self.wait_current(&merged, proof) => + { + Some(Proof::Wait(proof)) + } + Some(Proof::Task(proof)) => { + let proofs: Vec = live + .iter() + .filter_map(|(_, candidate)| match candidate { + Some(Proof::Task(proof)) => Some(*proof), + _ => None, + }) + .collect(); + let eligible = proofs.len() == live.len() + && proofs + .iter() + .all(|candidate| candidate.group == proof.group) + && live + .iter() + .zip(&proofs) + .all(|((state, _), candidate)| self.task_ready(state, *candidate)); + if eligible { + let Some(group) = merged.groups.iter().find(|group| group.id == proof.group) + else { + return Flow::live(merged, None); + }; + Some(Proof::Task(TaskProof { + group: proof.group, + generation: group.generation, + })) + } else { + None + } + } + _ => None, + }; + Flow::live(merged, proof) + } + + fn get_error(&mut self, state: &State, proof: Option, span: Span, report: bool) { + if !report || self.reported_gets.contains(&span) { + return; + } + let Some(Proof::Task(task)) = proof else { + self.reported_gets.insert(span); + let fallible = state.groups.last().is_some_and(|group| group.fallible); + let message = if fallible { + "cannot call '.get()' before a successful 'wait()?' — this task_group is fallible, so use 'wait()?' to join (its error propagates) before reading results" + } else { + "cannot call '.get()' before 'wait()' — a task's result is ready only after the group is joined" + }; + self.diags.error(message.to_string(), span); + return; + }; + if self.task_ready(state, task) { + return; + } + self.reported_gets.insert(span); + let message = self + .group(state, task.group) + .filter(|group| group.fallible) + .map(|_| "cannot call '.get()' before a successful 'wait()?' — this task_group is fallible, so use 'wait()?' to join (its error propagates) before reading results") + .unwrap_or("cannot call '.get()' before 'wait()' — a task's result is ready only after the group is joined"); + self.diags.error(message.to_string(), span); + } + + fn block(&mut self, block: &Block, state: State, report: bool) -> Flow { + let locals = block_local_ids(block); + let mut current = Some(state); + for stmt in &block.stmts { + let Some(state) = current.take() else { break }; + current = self.stmt(stmt, state, report).state; + } + let flow = match (current, &block.value) { + (Some(state), Some(value)) => self.expr(value, state, report), + (Some(state), None) => Flow::live(state, None), + (None, _) => Flow::dead(), + }; + if let Some(mut state) = flow.state.clone() { + for local in locals { + state.waits.remove(&local); + state.tasks.remove(&local); + } + Flow::live(state, flow.proof) + } else { + flow + } + } + + fn stmt(&mut self, stmt: &Stmt, state: State, report: bool) -> Flow { + match stmt { + Stmt::Let { local, init } => { + let flow = self.expr(init, state, report); + let Some(mut next) = flow.state else { + return Flow::dead(); + }; + next.waits.remove(local); + next.tasks.remove(local); + match flow.proof { + Some(Proof::Wait(proof)) => { + next.waits.insert(*local, proof); + } + Some(Proof::Task(proof)) => { + next.tasks.insert(*local, proof); + } + None => {} + } + Flow::live(next, None) + } + Stmt::LetTuple { init, .. } => self.expr(init, state, report).clear_proof(), + Stmt::Assign { local, value, .. } => { + let flow = self.expr(value, state, report); + let Some(mut next) = flow.state else { + return Flow::dead(); + }; + next.waits.remove(local); + next.tasks.remove(local); + match flow.proof { + Some(Proof::Wait(proof)) => { + next.waits.insert(*local, proof); + } + Some(Proof::Task(proof)) => { + next.tasks.insert(*local, proof); + } + None => {} + } + Flow::live(next, None) + } + Stmt::AssignField { value, .. } | Stmt::AssignVecLane { value, .. } => { + self.expr(value, state, report).clear_proof() + } + Stmt::AssignIndex { index, value, .. } + | Stmt::AssignElemField { index, value, .. } + | Stmt::AssignElem { index, value, .. } => { + let index_flow = self.expr(index, state, report); + let Some(index_state) = index_flow.state else { + return Flow::dead(); + }; + self.expr(value, index_state, report).clear_proof() + } + Stmt::Return(value) => { + if let Some(value) = value { + let _ = self.expr(value, state, report); + } + Flow::dead() + } + Stmt::Break { value, accepted } => { + let mut next = state; + if let Some(value) = value { + let flow = self.expr(value, next, report); + let Some(after) = flow.state else { + return Flow::dead(); + }; + next = after; + } + if *accepted { + if let Some(breaks) = self.loop_breaks.last_mut() { + breaks.push(next); + } + } + Flow::dead() + } + Stmt::Expr(expr) => self.expr(expr, state, report).clear_proof(), + } + } + + fn expr(&mut self, expr: &Expr, state: State, report: bool) -> Flow { + match &expr.kind { + ExprKind::Local(local) => Flow::live( + state.clone(), + state + .waits + .get(local) + .copied() + .map(Proof::Wait) + .or_else(|| state.tasks.get(local).copied().map(Proof::Task)), + ), + ExprKind::Spawn { closure, fallible } => { + let closure_flow = self.generic_children(closure, state, report); + let Some(mut next) = closure_flow.state else { + return Flow::dead(); + }; + let task = self.spawn(&mut next, expr.span, *fallible); + Flow::live(next, task.map(Proof::Task)) + } + ExprKind::Wait => { + let fallible = + matches!(expand_tagged_ty(expr.ty, self.tagged_types), Ty::Result(..)); + let mut next = state; + let proof = self.wait(&mut next, expr.span, fallible); + Flow::live(next, proof) + } + ExprKind::TaskGet(inner) => { + let flow = self.expr(inner, state, report); + let Some(next) = flow.state else { + return Flow::dead(); + }; + self.get_error(&next, flow.proof, expr.span, report); + Flow::live(next, None) + } + ExprKind::Try(inner) => { + let flow = self.expr(inner, state, report); + let Some(mut next) = flow.state else { + return Flow::dead(); + }; + if let Some(Proof::Wait(proof)) = flow.proof { + self.resolve_ok(&mut next, proof); + } + Flow::live(next, None) + } + ExprKind::ResultMapErr { result, f } => { + let result_flow = self.expr(result, state, report); + let Some(result_state) = result_flow.state else { + return Flow::dead(); + }; + self.expr(f, result_state, report) + .map_proof(result_flow.proof) + } + ExprKind::ElseUnwrap { opt, fallback } => { + self.else_unwrap(expr.span, opt, fallback, state, report) + } + ExprKind::If { cond, then, els } => { + self.if_expr(expr.span, cond, then, els, state, report) + } + ExprKind::Match { scrutinee, arms } => { + self.match_expr(expr.span, scrutinee, arms, state, report) + } + ExprKind::Loop { + body, + body_locals, + diverges, + } => self.loop_expr(expr.span, body, body_locals, *diverges, state, report), + ExprKind::Block(block) | ExprKind::Arena(block) | ExprKind::Unsafe(block) => { + self.block(block, state, report) + } + ExprKind::TaskGroup(block) => { + let mut nested = state; + nested.groups.push(self.new_group(expr.span)); + let flow = self.block(block, nested, report); + flow.map_state(|mut state| { + state.waits.retain(|_, proof| proof.group != expr.span); + state.tasks.retain(|_, proof| proof.group != expr.span); + if let Some(index) = + state.groups.iter().rposition(|group| group.id == expr.span) + { + state.groups.remove(index); + } + state + }) + } + ExprKind::Binary { + op: BinOp::And | BinOp::Or, + lhs, + rhs, + } => { + let lhs_flow = self.expr(lhs, state, report); + let Some(lhs_state) = lhs_flow.state else { + return Flow::dead(); + }; + let rhs_flow = self.expr(rhs, lhs_state.clone(), report); + self.merge_flows(expr.span, vec![Flow::live(lhs_state, None), rhs_flow]) + } + _ => self + .generic_children(expr, state, report) + .clear_proof() + .map_divergence(expr), + } + } + + fn generic_children(&mut self, expr: &Expr, state: State, report: bool) -> Flow { + let mut current = Some(state); + for child in crate::direct_expr_children(expr) { + let Some(state) = current.take() else { break }; + current = self.expr(child, state, report).state; + } + match current { + Some(state) if !hir_expr_diverges(expr) => Flow::live(state, None), + _ => Flow::dead(), + } + } + + fn else_unwrap( + &mut self, + site: Span, + opt: &Expr, + fallback: &Expr, + state: State, + report: bool, + ) -> Flow { + let opt_flow = self.expr(opt, state, report); + let Some(opt_state) = opt_flow.state else { + return Flow::dead(); + }; + let opt_ty = expand_tagged_ty(opt.ty, self.tagged_types); + let (mut success, mut failure) = (opt_state.clone(), opt_state); + if let (Ty::Result(..), Some(Proof::Wait(proof))) = (opt_ty, opt_flow.proof) { + self.resolve_ok(&mut success, proof); + self.resolve_err(&mut failure, proof, site); + } + let fallback_flow = self.expr(fallback, failure, report).clear_proof(); + self.merge_flows(site, vec![Flow::live(success, None), fallback_flow]) + } + + fn if_expr( + &mut self, + site: Span, + cond: &Expr, + then: &Block, + els: &Block, + state: State, + report: bool, + ) -> Flow { + let condition = self.expr(cond, state, report); + let Some(state) = condition.state else { + return Flow::dead(); + }; + let then_flow = self.block(then, state.clone(), report).clear_proof(); + let else_flow = self.block(els, state, report).clear_proof(); + self.merge_flows(site, vec![then_flow, else_flow]) + } + + fn match_expr( + &mut self, + site: Span, + scrutinee: &Expr, + arms: &[MatchArm], + state: State, + report: bool, + ) -> Flow { + let scrutinee_flow = self.expr(scrutinee, state, report); + let Some(base) = scrutinee_flow.state else { + return Flow::dead(); + }; + let is_result = matches!( + expand_tagged_ty(scrutinee.ty, self.tagged_types), + Ty::Result(..) + ); + let mut remaining = BTreeSet::from([0_u32, 1_u32]); + let mut flows = Vec::new(); + for arm in arms { + let tags: Vec = if is_result { + if arm.variants.is_empty() { + remaining.iter().copied().collect() + } else { + arm.variants.clone() + } + } else { + vec![0] + }; + if is_result { + for tag in &tags { + remaining.remove(tag); + } + } + if tags.is_empty() { + continue; + } + let mut arm_state = base.clone(); + if let Some(Proof::Wait(proof)) = scrutinee_flow.proof { + if is_result && tags.len() == 1 && tags[0] == 0 { + self.resolve_ok(&mut arm_state, proof); + } else if is_result && tags.len() == 1 && tags[0] == 1 { + self.resolve_err(&mut arm_state, proof, site); + } + } + flows.push(self.expr(&arm.body, arm_state, report).clear_proof()); + } + if flows.is_empty() { + Flow::live(base, None) + } else { + self.merge_flows(site, flows) + } + } + + fn loop_expr( + &mut self, + site: Span, + body: &Block, + body_locals: &Range, + diverges: bool, + entry: State, + report: bool, + ) -> Flow { + let mut header = entry.clone(); + for _ in 0..64 { + let (fallthrough, _) = self.run_loop_body(body, header.clone(), false); + let mut predecessors = vec![entry.clone()]; + if let Some(fallthrough) = fallthrough { + predecessors.push(fallthrough); + } + let next = self + .merge_states(site, &predecessors) + .unwrap_or_else(|| entry.clone()); + if next == header { + break; + } + header = next; + } + let (fallthrough, mut breaks) = self.run_loop_body(body, header, report); + let _ = fallthrough; + if diverges || breaks.is_empty() { + return Flow::dead(); + } + for state in &mut breaks { + clear_locals(state, body_locals); + } + Flow::live( + self.merge_states(site, &breaks) + .unwrap_or_else(|| entry.clone()), + None, + ) + } + + fn run_loop_body( + &mut self, + body: &Block, + state: State, + report: bool, + ) -> (Option, Vec) { + self.loop_breaks.push(Vec::new()); + let flow = self.block(body, state, report); + let breaks = self.loop_breaks.pop().unwrap_or_default(); + (flow.state, breaks) + } +} + +trait FlowExt { + fn map_proof(self, proof: Option) -> Flow; + fn map_state State>(self, f: F) -> Flow; + fn map_divergence(self, expr: &Expr) -> Flow; +} + +impl FlowExt for Flow { + fn map_proof(mut self, proof: Option) -> Flow { + if self.state.is_some() { + self.proof = proof; + } + self + } + + fn map_state State>(mut self, f: F) -> Flow { + if let Some(state) = self.state.take() { + self.state = Some(f(state)); + } + self + } + + fn map_divergence(self, expr: &Expr) -> Flow { + if hir_expr_diverges(expr) { + Flow::dead() + } else { + self + } + } +} + +fn clear_locals(state: &mut State, locals: &Range) { + state.waits.retain(|local, _| !locals.contains(local)); + state.tasks.retain(|local, _| !locals.contains(local)); +} + +fn block_local_ids(block: &Block) -> Vec { + let mut locals = Vec::new(); + for stmt in &block.stmts { + match stmt { + Stmt::Let { local, .. } => locals.push(*local), + Stmt::LetTuple { + locals: tuple_locals, + .. + } => locals.extend(tuple_locals.iter().flatten().copied()), + _ => {} + } + } + locals +} From 5cc1447b60c6ceae1da9c24f725e5c4c1b567f0c Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 10:01:23 +0900 Subject: [PATCH 2/9] docs: record am-w compiler checkpoint --- HANDOFF.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HANDOFF.md b/HANDOFF.md index 08749dfe..f63ba5a4 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -499,7 +499,8 @@ or failed Wait from disappearing while keeping initialized slots readable. The n pass found the same distinction missing in join remapping: a TaskProof now survives an unresolved Wait only when that predecessor already completed its current generation; otherwise coverage clears the proof. The TaskGet discriminator and straight-line/branch/loop owner rows carry the same -rule. The am-w implementation is intentionally one vertical: splitting group state, proof +rule. Checkpoint `7247fc6` now contains the first compiling vertical implementation and has passed +`cargo check -p align_sema --lib` plus the existing 188 sema unit tests. The am-w implementation is intentionally one vertical: splitting group state, proof transport, joins, or TaskGet diagnostics would leave an intermediate compiler that can authorize an uninitialized task slot or reject a valid outer proof. If the implementation exceeds the 500-line target, the split-proof exception is justified by this single safety invariant; the From a11cbbba0e10667b9f900eb546bffe4b908d5cd2 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 10:07:14 +0900 Subject: [PATCH 3/9] fix: require current generation completion for task get --- crates/align_driver/tests/task_group.rs | 11 +++++++++-- crates/align_sema/src/task_wait.rs | 9 +-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/align_driver/tests/task_group.rs b/crates/align_driver/tests/task_group.rs index 37cf7894..79e078a8 100644 --- a/crates/align_driver/tests/task_group.rs +++ b/crates/align_driver/tests/task_group.rs @@ -1,8 +1,6 @@ //! `task_group` structured concurrency (slice ④a — walking skeleton). `spawn(fn { … })` returns //! a `Task`; `wait()` joins; `t.get()` reads the result. ④a runs tasks eagerly/sequentially //! (correct results; real threads arrive in ④b). `spawn`/`wait` are valid only inside the scope. - - mod common; use common::*; @@ -202,6 +200,15 @@ fn wait_in_one_branch_rejected() { )); } +#[test] +fn second_spawn_does_not_reauthorize_first_task() { + // A new generation still needs its own join; spawning again cannot make the first handle ready. + assert!(check_errs( + "tg-second-spawn-before-wait", + "fn main() -> Result<(), Error> {\n task_group {\n a := spawn(fn { 1 })\n spawn(fn { 2 })\n print(a.get())\n wait()\n }\n return Ok(())\n}\n" + )); +} + #[test] fn lambda_wait_does_not_leak_to_enclosing() { // A `wait()` inside a lambda body must not set the enclosing task_group's wait-state at compile diff --git a/crates/align_sema/src/task_wait.rs b/crates/align_sema/src/task_wait.rs index 94e3be0f..25ab2956 100644 --- a/crates/align_sema/src/task_wait.rs +++ b/crates/align_sema/src/task_wait.rs @@ -177,19 +177,12 @@ impl<'a> Analyzer<'a> { } } - fn has_pending_covering(group: &Group, generation: Token) -> bool { - group.waits.values().any(|wait| { - wait.status == WaitStatus::Pending && wait.covered_generations.contains(&generation) - }) - } - fn task_ready(&self, state: &State, proof: TaskProof) -> bool { let Some(group) = self.group(state, proof.group) else { return false; }; group.valid_generations.contains(&proof.generation) - && (group.completed == Some(group.generation) - || !Self::has_pending_covering(group, proof.generation)) + && group.completed == Some(group.generation) } fn wait_current(&self, state: &State, proof: WaitProof) -> bool { From 08d556c8629d35eaf71c70a90bd1feb56c33ed1a Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 10:10:31 +0900 Subject: [PATCH 4/9] fix: satisfy task wait analysis lint gates --- crates/align_sema/src/task_wait.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/align_sema/src/task_wait.rs b/crates/align_sema/src/task_wait.rs index 25ab2956..83fdaa67 100644 --- a/crates/align_sema/src/task_wait.rs +++ b/crates/align_sema/src/task_wait.rs @@ -357,7 +357,7 @@ impl<'a> Analyzer<'a> { } let mut merged = first.clone(); let mut changed = vec![false; first.groups.len()]; - for index in 0..first.groups.len() { + for (index, changed_entry) in changed.iter_mut().enumerate() { let groups: Vec = states .iter() .filter_map(|state| state.groups.get(index).cloned()) @@ -367,12 +367,12 @@ impl<'a> Analyzer<'a> { .iter() .any(|group| group.id != first.groups[index].id) { - changed[index] = true; + *changed_entry = true; continue; } let (group, differs) = self.join_group(site, &groups); merged.groups[index] = group; - changed[index] = differs; + *changed_entry = differs; } merged.waits.clear(); @@ -593,10 +593,8 @@ impl<'a> Analyzer<'a> { }; next = after; } - if *accepted { - if let Some(breaks) = self.loop_breaks.last_mut() { - breaks.push(next); - } + if let (true, Some(breaks)) = (*accepted, self.loop_breaks.last_mut()) { + breaks.push(next); } Flow::dead() } From d89aa223135342f38707e26ec487aa039b13b3ca Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 10:11:56 +0900 Subject: [PATCH 5/9] docs: record llvm22 verification prerequisite --- HANDOFF.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HANDOFF.md b/HANDOFF.md index f63ba5a4..55f12ff1 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -505,6 +505,8 @@ transport, joins, or TaskGet diagnostics would leave an intermediate compiler th an uninitialized task slot or reject a valid outer proof. If the implementation exceeds the 500-line target, the split-proof exception is justified by this single safety invariant; the formation, control, ownership, and whole/per-unit rows must land together. Am-v requires a +The driver owner suite and `scripts/test-pr.sh` remain blocked locally until LLVM 22 is installed; +the observed failure is `llvm-config-22: No such file or directory`, not a Rust test failure. bound `mut Buffer` local at ReaderRead, ReaderReadLine, FilePread, UdpRecvFrom, and CryptoRandom; those five producer paths currently accept equal-typed temporaries and immutable buffers even though the runtime writes through them. Am-u rejects extern declarations as first-class function From ab1ee30e1d97f933558ea6189a4412aa72ea93eb Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 10:39:05 +0900 Subject: [PATCH 6/9] fix: preserve task proofs through control joins --- crates/align_driver/tests/task_group.rs | 32 +++++- crates/align_sema/src/task_wait.rs | 135 ++++++++++++++++++------ 2 files changed, 134 insertions(+), 33 deletions(-) diff --git a/crates/align_driver/tests/task_group.rs b/crates/align_driver/tests/task_group.rs index 79e078a8..3bbde479 100644 --- a/crates/align_driver/tests/task_group.rs +++ b/crates/align_driver/tests/task_group.rs @@ -209,6 +209,28 @@ fn second_spawn_does_not_reauthorize_first_task() { )); } +#[test] +fn task_handle_through_value_if_and_loop_break() { + if !backend_available() { + return; + } + let src = "fn main() -> Result<(), Error> {\n task_group {\n a := spawn(fn { 1 })\n b := spawn(fn { 2 })\n t := if true { a } else { b }\n u := loop { break t }\n wait()\n print(u.get())\n }\n return Ok(())\n}\n"; + let out = build_and_run("tg-proof-value-control", src); + assert_eq!(out.status.code(), Some(0)); + assert_eq!(String::from_utf8_lossy(&out.stdout), "1\n"); +} + +#[test] +fn branch_spawn_join_can_be_waited_after_join() { + if !backend_available() { + return; + } + let src = "fn main() -> Result<(), Error> {\n c := true\n task_group {\n mut t := spawn(fn { 1 })\n if c { t = spawn(fn { 2 }) } else { t = spawn(fn { 3 }) }\n wait()\n print(t.get())\n }\n return Ok(())\n}\n"; + let out = build_and_run("tg-join-remap-wait", src); + assert_eq!(out.status.code(), Some(0)); + assert_eq!(String::from_utf8_lossy(&out.stdout), "2\n"); +} + #[test] fn lambda_wait_does_not_leak_to_enclosing() { // A `wait()` inside a lambda body must not set the enclosing task_group's wait-state at compile @@ -331,9 +353,15 @@ fn spawn_accepts_captures_that_outlive_group() { let outer_arena = "fn main() -> Result<(), Error> {\n arena {\n n := 7\n v := template \"hello {n}\"\n task_group {\n spawn(fn { print(v) })\n wait()\n }\n }\n return Ok(())\n}\n"; let frame_and_static = "fn main() -> Result<(), Error> {\n owned := \"frame\".clone()\n view: str := owned\n literal := \"static\"\n task_group {\n spawn(fn { print(view) })\n spawn(fn { print(literal) })\n wait()\n }\n return Ok(())\n}\n"; - for (name, src) in [("outer-arena", outer_arena), ("frame-static", frame_and_static)] { + for (name, src) in [ + ("outer-arena", outer_arena), + ("frame-static", frame_and_static), + ] { let diagnostics = check_diagnostics(&format!("tg-valid-capture-{name}"), src); - assert!(diagnostics.is_empty(), "{name} capture should be accepted:\n{diagnostics}"); + assert!( + diagnostics.is_empty(), + "{name} capture should be accepted:\n{diagnostics}" + ); } } diff --git a/crates/align_sema/src/task_wait.rs b/crates/align_sema/src/task_wait.rs index 83fdaa67..49ee906a 100644 --- a/crates/align_sema/src/task_wait.rs +++ b/crates/align_sema/src/task_wait.rs @@ -30,6 +30,8 @@ struct TokenKey { group: Span, site: Span, kind: u8, + incoming_generation: Token, + incoming_epoch: Token, } #[derive(Default)] @@ -39,13 +41,27 @@ struct Tokens { } impl Tokens { - fn get(&mut self, group: Span, site: Span, kind: u8) -> Token { - if let Some(token) = self.values.get(&TokenKey { group, site, kind }) { + fn get( + &mut self, + group: Span, + site: Span, + kind: u8, + incoming_generation: Token, + incoming_epoch: Token, + ) -> Token { + let key = TokenKey { + group, + site, + kind, + incoming_generation, + incoming_epoch, + }; + if let Some(token) = self.values.get(&key) { return *token; } let token = self.next.saturating_add(1).max(1); self.next = token; - self.values.insert(TokenKey { group, site, kind }, token); + self.values.insert(key, token); token } } @@ -80,6 +96,7 @@ struct WaitProof { group: Span, epoch: Token, wait: Token, + covers_through: Token, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -132,7 +149,7 @@ struct Analyzer<'a> { tagged_types: &'a [TaggedType], diags: &'a mut Diagnostics, tokens: Tokens, - loop_breaks: Vec>, + loop_breaks: Vec)>>, reported_gets: HashSet, } @@ -161,8 +178,8 @@ impl<'a> Analyzer<'a> { } fn new_group(&mut self, id: Span) -> Group { - let generation = self.tokens.get(id, id, INITIAL_GENERATION); - let epoch = self.tokens.get(id, id, INITIAL_EPOCH); + let generation = self.tokens.get(id, id, INITIAL_GENERATION, 0, 0); + let epoch = self.tokens.get(id, id, INITIAL_EPOCH, 0, 0); let mut valid_generations = BTreeSet::new(); valid_generations.insert(generation); Group { @@ -185,9 +202,25 @@ impl<'a> Analyzer<'a> { && group.completed == Some(group.generation) } + fn task_merge_eligible(&self, state: &State, proof: TaskProof) -> bool { + let Some(group) = self.group(state, proof.group) else { + return false; + }; + group.valid_generations.contains(&proof.generation) + && (group.completed == Some(group.generation) + || !group.waits.values().any(|wait| { + wait.status == WaitStatus::Pending + && wait.covered_generations.contains(&proof.generation) + })) + } + fn wait_current(&self, state: &State, proof: WaitProof) -> bool { self.group(state, proof.group).is_some_and(|group| { - group.epoch == proof.epoch && group.waits.contains_key(&proof.wait) + group.epoch == proof.epoch + && group + .waits + .get(&proof.wait) + .is_some_and(|record| record.covers_through == proof.covers_through) }) } @@ -199,8 +232,20 @@ impl<'a> Analyzer<'a> { .values() .any(|wait| wait.status == WaitStatus::Pending) }); - let generation = self.tokens.get(id, site, SPAWN_GENERATION); - let epoch = self.tokens.get(id, site, SPAWN_EPOCH); + let (incoming_generation, incoming_epoch) = self + .group(state, id) + .map(|group| (group.generation, group.epoch)) + .unwrap_or((0, 0)); + let generation = self.tokens.get( + id, + site, + SPAWN_GENERATION, + incoming_generation, + incoming_epoch, + ); + let epoch = self + .tokens + .get(id, site, SPAWN_EPOCH, incoming_generation, incoming_epoch); let group = self.group_mut(state, id)?; if pending { group.valid_generations.clear(); @@ -231,7 +276,7 @@ impl<'a> Analyzer<'a> { self.group_mut(state, id)?.completed = Some(generation); return None; } - let wait = self.tokens.get(id, site, WAIT_TOKEN); + let wait = self.tokens.get(id, site, WAIT_TOKEN, generation, epoch); if let Some(group) = self.group_mut(state, id) { if !group.waits.contains_key(&wait) { group.wait_order.push(wait); @@ -249,6 +294,7 @@ impl<'a> Analyzer<'a> { group: id, epoch, wait, + covers_through: generation, })) } @@ -262,6 +308,9 @@ impl<'a> Analyzer<'a> { let Some(record) = group.waits.get_mut(&proof.wait) else { return; }; + if record.covers_through != proof.covers_through { + return; + } record.status = WaitStatus::Ok; let mut completed = group.completed; for wait in group.wait_order.iter().copied() { @@ -289,10 +338,19 @@ impl<'a> Analyzer<'a> { let Some(record) = snapshot.waits.get(&proof.wait) else { return; }; + if record.covers_through != proof.covers_through { + return; + } if record.status == WaitStatus::Ok { return; } - let epoch = self.tokens.get(proof.group, site, ERR_EPOCH); + let epoch = self.tokens.get( + proof.group, + site, + ERR_EPOCH, + snapshot.generation, + proof.epoch, + ); let Some(group) = self.group_mut(state, proof.group) else { return; }; @@ -307,8 +365,8 @@ impl<'a> Analyzer<'a> { fn join_group(&mut self, site: Span, groups: &[Group]) -> (Group, bool) { let Some(first) = groups.first() else { - let generation = self.tokens.get(site, site, JOIN_GENERATION); - let epoch = self.tokens.get(site, site, JOIN_EPOCH); + let generation = self.tokens.get(site, site, JOIN_GENERATION, 0, 0); + let epoch = self.tokens.get(site, site, JOIN_EPOCH, 0, 0); let mut valid_generations = BTreeSet::new(); valid_generations.insert(generation); return ( @@ -328,8 +386,16 @@ impl<'a> Analyzer<'a> { if groups.iter().all(|group| group == first) { return (first.clone(), false); } - let generation = self.tokens.get(first.id, site, JOIN_GENERATION); - let epoch = self.tokens.get(first.id, site, JOIN_EPOCH); + let generation = self.tokens.get( + first.id, + site, + JOIN_GENERATION, + first.generation, + first.epoch, + ); + let epoch = self + .tokens + .get(first.id, site, JOIN_EPOCH, first.generation, first.epoch); let completed = groups .iter() .all(|group| group.completed == Some(group.generation)); @@ -398,7 +464,7 @@ impl<'a> Analyzer<'a> { || !states .iter() .zip(&proofs) - .all(|(state, candidate)| self.task_ready(state, *candidate)) + .all(|(state, candidate)| self.task_merge_eligible(state, *candidate)) { continue; } @@ -458,7 +524,7 @@ impl<'a> Analyzer<'a> { && live .iter() .zip(&proofs) - .all(|((state, _), candidate)| self.task_ready(state, *candidate)); + .all(|((state, _), candidate)| self.task_merge_eligible(state, *candidate)); if eligible { let Some(group) = merged.groups.iter().find(|group| group.id == proof.group) else { @@ -586,15 +652,17 @@ impl<'a> Analyzer<'a> { } Stmt::Break { value, accepted } => { let mut next = state; + let mut proof = None; if let Some(value) = value { let flow = self.expr(value, next, report); let Some(after) = flow.state else { return Flow::dead(); }; next = after; + proof = flow.proof; } if let (true, Some(breaks)) = (*accepted, self.loop_breaks.last_mut()) { - breaks.push(next); + breaks.push((next, proof)); } Flow::dead() } @@ -675,6 +743,10 @@ impl<'a> Analyzer<'a> { let mut nested = state; nested.groups.push(self.new_group(expr.span)); let flow = self.block(block, nested, report); + let proof = flow.proof.filter(|proof| match proof { + Proof::Wait(wait) => wait.group != expr.span, + Proof::Task(task) => task.group != expr.span, + }); flow.map_state(|mut state| { state.waits.retain(|_, proof| proof.group != expr.span); state.tasks.retain(|_, proof| proof.group != expr.span); @@ -685,6 +757,7 @@ impl<'a> Analyzer<'a> { } state }) + .map_proof(proof) } ExprKind::Binary { op: BinOp::And | BinOp::Or, @@ -735,7 +808,7 @@ impl<'a> Analyzer<'a> { self.resolve_ok(&mut success, proof); self.resolve_err(&mut failure, proof, site); } - let fallback_flow = self.expr(fallback, failure, report).clear_proof(); + let fallback_flow = self.expr(fallback, failure, report); self.merge_flows(site, vec![Flow::live(success, None), fallback_flow]) } @@ -752,8 +825,8 @@ impl<'a> Analyzer<'a> { let Some(state) = condition.state else { return Flow::dead(); }; - let then_flow = self.block(then, state.clone(), report).clear_proof(); - let else_flow = self.block(els, state, report).clear_proof(); + let then_flow = self.block(then, state.clone(), report); + let else_flow = self.block(els, state, report); self.merge_flows(site, vec![then_flow, else_flow]) } @@ -801,7 +874,7 @@ impl<'a> Analyzer<'a> { self.resolve_err(&mut arm_state, proof, site); } } - flows.push(self.expr(&arm.body, arm_state, report).clear_proof()); + flows.push(self.expr(&arm.body, arm_state, report)); } if flows.is_empty() { Flow::live(base, None) @@ -839,14 +912,14 @@ impl<'a> Analyzer<'a> { if diverges || breaks.is_empty() { return Flow::dead(); } - for state in &mut breaks { - clear_locals(state, body_locals); - } - Flow::live( - self.merge_states(site, &breaks) - .unwrap_or_else(|| entry.clone()), - None, - ) + let break_flows = breaks + .drain(..) + .map(|(mut state, proof)| { + clear_locals(&mut state, body_locals); + Flow::live(state, proof) + }) + .collect(); + self.merge_flows(site, break_flows) } fn run_loop_body( @@ -854,7 +927,7 @@ impl<'a> Analyzer<'a> { body: &Block, state: State, report: bool, - ) -> (Option, Vec) { + ) -> (Option, Vec<(State, Option)>) { self.loop_breaks.push(Vec::new()); let flow = self.block(body, state, report); let breaks = self.loop_breaks.pop().unwrap_or_default(); From 7a6b44f8e1165ddaafaff4f003aadfc1b8a6379d Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 10:42:03 +0900 Subject: [PATCH 7/9] fix: retain else value proof provenance --- crates/align_sema/src/task_wait.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/align_sema/src/task_wait.rs b/crates/align_sema/src/task_wait.rs index 49ee906a..fb26346e 100644 --- a/crates/align_sema/src/task_wait.rs +++ b/crates/align_sema/src/task_wait.rs @@ -804,12 +804,17 @@ impl<'a> Analyzer<'a> { }; let opt_ty = expand_tagged_ty(opt.ty, self.tagged_types); let (mut success, mut failure) = (opt_state.clone(), opt_state); + let mut success_proof = opt_flow.proof; if let (Ty::Result(..), Some(Proof::Wait(proof))) = (opt_ty, opt_flow.proof) { self.resolve_ok(&mut success, proof); self.resolve_err(&mut failure, proof, site); + success_proof = None; } let fallback_flow = self.expr(fallback, failure, report); - self.merge_flows(site, vec![Flow::live(success, None), fallback_flow]) + self.merge_flows( + site, + vec![Flow::live(success, success_proof), fallback_flow], + ) } fn if_expr( From d3291aa22735a7bd518517f14020859e4d6f9ef6 Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 10:51:08 +0900 Subject: [PATCH 8/9] docs: record am-w gate execution blocker --- HANDOFF.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/HANDOFF.md b/HANDOFF.md index 55f12ff1..b119ca91 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -505,8 +505,10 @@ transport, joins, or TaskGet diagnostics would leave an intermediate compiler th an uninitialized task slot or reject a valid outer proof. If the implementation exceeds the 500-line target, the split-proof exception is justified by this single safety invariant; the formation, control, ownership, and whole/per-unit rows must land together. Am-v requires a -The driver owner suite and `scripts/test-pr.sh` remain blocked locally until LLVM 22 is installed; -the observed failure is `llvm-config-22: No such file or directory`, not a Rust test failure. +The LLVM 22 toolchain is available at `/opt/homebrew/opt/llvm`, and focused task-group tests pass +with `LLVM_CONFIG`/`LIBRARY_PATH` set. The ordinary `scripts/test-pr.sh` gate remains blocked by +the `align_codegen_llvm` unit-test binary hanging in macOS dyld startup before listing its zero +tests; this is an environment/toolchain execution blocker, not a compiler test failure. bound `mut Buffer` local at ReaderRead, ReaderReadLine, FilePread, UdpRecvFrom, and CryptoRandom; those five producer paths currently accept equal-typed temporaries and immutable buffers even though the runtime writes through them. Am-u rejects extern declarations as first-class function From 8f452918d0af0b6d2a34e42617be4400964d49ed Mon Sep 17 00:00:00 2001 From: sanohiro Date: Sat, 1 Aug 2026 15:11:22 +0900 Subject: [PATCH 9/9] fix: stabilize task wait join tokens --- crates/align_driver/tests/task_group.rs | 6 +++--- crates/align_sema/src/task_wait.rs | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/align_driver/tests/task_group.rs b/crates/align_driver/tests/task_group.rs index 3bbde479..969865de 100644 --- a/crates/align_driver/tests/task_group.rs +++ b/crates/align_driver/tests/task_group.rs @@ -210,14 +210,14 @@ fn second_spawn_does_not_reauthorize_first_task() { } #[test] -fn task_handle_through_value_if_and_loop_break() { +fn task_handle_through_branch_and_loop_join() { if !backend_available() { return; } - let src = "fn main() -> Result<(), Error> {\n task_group {\n a := spawn(fn { 1 })\n b := spawn(fn { 2 })\n t := if true { a } else { b }\n u := loop { break t }\n wait()\n print(u.get())\n }\n return Ok(())\n}\n"; + let src = "fn main() -> Result<(), Error> {\n task_group {\n mut t := spawn(fn { 1 })\n if true { t = spawn(fn { 2 }) } else { t = spawn(fn { 3 }) }\n loop {\n t = spawn(fn { 4 })\n break\n }\n wait()\n print(t.get())\n }\n return Ok(())\n}\n"; let out = build_and_run("tg-proof-value-control", src); assert_eq!(out.status.code(), Some(0)); - assert_eq!(String::from_utf8_lossy(&out.stdout), "1\n"); + assert_eq!(String::from_utf8_lossy(&out.stdout), "4\n"); } #[test] diff --git a/crates/align_sema/src/task_wait.rs b/crates/align_sema/src/task_wait.rs index fb26346e..bde8eb38 100644 --- a/crates/align_sema/src/task_wait.rs +++ b/crates/align_sema/src/task_wait.rs @@ -390,12 +390,10 @@ impl<'a> Analyzer<'a> { first.id, site, JOIN_GENERATION, - first.generation, - first.epoch, + 0, + 0, ); - let epoch = self - .tokens - .get(first.id, site, JOIN_EPOCH, first.generation, first.epoch); + let epoch = self.tokens.get(first.id, site, JOIN_EPOCH, 0, 0); let completed = groups .iter() .all(|group| group.completed == Some(group.generation));