Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,16 @@ 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. 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
formation, control, ownership, and whole/per-unit rows must land together. Am-v requires a
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
Expand Down
43 changes: 39 additions & 4 deletions crates/align_driver/tests/task_group.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
//! `task_group` structured concurrency (slice ④a — walking skeleton). `spawn(fn { … })` returns
//! a `Task<R>`; `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::*;

Expand Down Expand Up @@ -202,6 +200,37 @@ 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 task_handle_through_branch_and_loop_join() {
if !backend_available() {
return;
}
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), "4\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
Expand Down Expand Up @@ -324,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}"
);
}
}

Expand Down
17 changes: 4 additions & 13 deletions crates/align_sema/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading