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
27 changes: 13 additions & 14 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ about the present state, the next decision, and operational facts. The former
per-PR journal is preserved in
[`docs/archive/HANDOFF-2026-07-25.md`](docs/archive/HANDOFF-2026-07-25.md).

_Last updated: 2026-08-05._ The C-A canonical callable capability is complete
through am-c3. MIR now separates typed program and runtime direct targets, LLVM
program identities are encoded consistently across whole-program, per-unit,
export, main-wrapper, and ThinLTO paths, and every generated callable family uses
canonical collected identity plus deterministic collision probing. No public
`pkg.db` surface exists yet.
_Last updated: 2026-08-05._ The C-B borrow/ownership capability is complete
through L2e. Direct, captured, imported, and function-value returns preserve
exact owner provenance; recursively Move returns carry a path-selected cleanup
bit; and shared/exclusive parameters preserve caller ownership, replacement,
generation invalidation, and whole/per-unit ABI parity. No public `pkg.db`
surface exists yet.

The remaining compiler plan uses consumer-complete capability waves rather
than one PR per dormant acceptance cell:

```text
C-A canonical callable closure complete through c3
C-B borrow/ownership closure af/ar/ap/t/b + L2c/L2d/L2e
C-B borrow/ownership closure complete through L2e

after C-B, in parallel:
next independent waves:
F-A native resources L3
F-B region materialization L4 + L6
F-C static artifacts L5
Expand Down Expand Up @@ -924,12 +924,11 @@ was rerun after #636. #637-#644 passed their focused and PR CI gates.

## Next work

C-A closes the canonical callable plan from c2a2b through c3 as one consumer-complete wave. The
next implementation is C-B: direct, captured, and imported return-provenance closure through
L2b-b, followed in the same ownership capability by Move-return cleanup and shared/exclusive
borrow consumers through L2e. Reopen the C-B closure matrix before coding and preserve the exact
control-flow/type-reconciliation matrix in the repository instructions. F-A/F-B/F-C begin only
after C-B.
C-B closes direct, captured, imported, and function-value return provenance together with
path-selected Move-return cleanup and shared/exclusive borrow consumers through L2e. The next
independent implementation waves are F-A native resources (L3), F-B region materialization
(L4 + L6), and F-C static artifacts (L5). Each reopens its owning closure matrix before coding;
F-D package integration begins only after F-A/F-B and the complete F-C prerequisite gate.

The query-centered `pkg.db` design and its general library-boundary prerequisites are specified in
`docs/impl/pkg-design/db.md` and `docs/impl/17-library-boundary-prerequisites.md`; the feasibility
Expand Down
1 change: 1 addition & 0 deletions bench/library_boundary/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bench/library_boundary/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ publish = false
align_driver = { path = "../../crates/align_driver" }
align_interface = { path = "../../crates/align_interface" }
align_mir = { path = "../../crates/align_mir" }
align_sema = { path = "../../crates/align_sema" }
align_span = { path = "../../crates/align_span" }

[workspace]
16 changes: 16 additions & 0 deletions bench/library_boundary/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ This cumulative harness owns the measurements named by

```text
bench/library_boundary/run.sh interface
bench/library_boundary/run.sh provenance
bench/library_boundary/run.sh move-return
bench/library_boundary/run.sh shared-borrow
bench/library_boundary/run.sh exclusive-borrow
```

`interface` builds a deterministic 512-function exported surface with explicit L2a parameter modes
Expand Down Expand Up @@ -44,5 +48,17 @@ a 256-function, high-CFG fixture with three expression-valued branches per funct
L2b-b adds the `indirect-return` row to the same group after target-relative function-value
provenance lands.

`move-return` builds four optimized executables around the same 100,000-call loop and reports
nanoseconds per call. `copy-return-control` is the value-only control; `move-return-none`,
`move-return-some`, and `move-return-err` exercise the dynamic cleanup-bit ABI with the clear
`Option` path, owned `Option` payload, and owned `Result` error payload respectively. Process launch
cost is amortized across the inner loop; the rows are comparative evidence, not timing assertions.

`shared-borrow` compares the `by-value-call-control` row with repeated zero-allocation
`shared-borrow-call` inspection of one owned string. `exclusive-borrow` compares a by-value Copy
update (`exclusive-copy-control`) with an in-place Copy update (`exclusive-copy-call`) and records
the allocation-and-Drop cost of replacing a Move string through the caller cleanup-bit ABI
(`exclusive-move-replace`). Each row uses the same 100,000-call inner loop.

The benchmark is a regression tracker, not a timing assertion. Record the command, compiler commit,
host, and output when comparing changes.
7 changes: 7 additions & 0 deletions bench/library_boundary/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
set -euo pipefail

repo_root="$(cd "$(dirname "$0")/../.." && pwd)"
if [[ "${1:-}" == "move-return" || "${1:-}" == "shared-borrow" || "${1:-}" == "exclusive-borrow" ]]; then
"$repo_root/scripts/cargo.sh" build \
--quiet \
--release \
--manifest-path "$repo_root/Cargo.toml" \
-p align_runtime
fi
exec "$repo_root/scripts/cargo.sh" run \
--quiet \
--release \
Expand Down
170 changes: 169 additions & 1 deletion bench/library_boundary/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ fn interface_fixture() -> InterfaceSummary {
ret: named("i64"),
return_borrow: ReturnBorrowSummary::None,
return_region: ReturnRegionSummary::None,
return_cleanup: align_sema::hir::ReturnCleanupAbi::None,
effect: Effect::Pure,
generic_body: None,
});
Expand Down Expand Up @@ -212,6 +213,7 @@ fn import_validation_fixture() -> InterfaceSummary {
params: vec![0],
captures: Vec::new(),
},
return_cleanup: align_sema::hir::ReturnCleanupAbi::None,
effect: Effect::Pure,
generic_body: None,
})
Expand Down Expand Up @@ -493,16 +495,182 @@ fn run_provenance() {
println!("mir-continuation-lowering\t{milliseconds:.3}\tms/lower\t{block_count}\tblocks");
}

fn return_fixture(kind: &str, calls: u64) -> String {
let (producer, consume) = match kind {
"copy-return-control" => (
"fn produce(value: i64) -> i64 = value\n",
"fn consume(value: i64) -> i64 = value\n",
),
"move-return-none" => (
"fn produce(_: i64) -> Option<string> = None\n",
"fn consume(value: Option<string>) -> i64 = match value { Some(text) => text.len() None => 0 }\n",
),
"move-return-some" => (
"fn produce(_: i64) -> Option<string> = Some(\"owned\".clone())\n",
"fn consume(value: Option<string>) -> i64 = match value { Some(text) => text.len() None => 0 }\n",
),
"move-return-err" => (
"fn produce(_: i64) -> Result<string, string> = Err(\"owned\".clone())\n",
"fn consume(value: Result<string, string>) -> i64 = match value { Ok(text) => text.len() Err(text) => text.len() }\n",
),
_ => unreachable!("closed benchmark row"),
};
format!(
"{producer}{consume}\
fn main() -> i32 {{\n\
\u{20}\u{20}mut index: i64 := 0\n\
\u{20}\u{20}mut total: i64 := 0\n\
\u{20}\u{20}loop {{\n\
\u{20}\u{20}\u{20}\u{20}if index == {calls} {{ break }}\n\
\u{20}\u{20}\u{20}\u{20}total = total + consume(produce(index))\n\
\u{20}\u{20}\u{20}\u{20}index = index + 1\n\
\u{20}\u{20}}}\n\
\u{20}\u{20}if total < 0 {{ return 1 }}\n\
\u{20}\u{20}return 0\n\
}}\n"
)
}

fn compile_call_fixture(
directory: &std::path::Path,
row: &str,
source: &str,
) -> std::path::PathBuf {
let mut source_map = align_span::SourceMap::new();
let checked = align_driver::check(&mut source_map, &format!("{row}.align"), source);
assert!(
!checked.diags.has_errors(),
"{row} fixture must check:\n{}",
align_driver::format_diagnostics(&source_map, &checked.diags)
);
let mir = align_driver::lower_to_mir(&checked.hir);
let object = directory.join(format!("{row}.o"));
let executable = directory.join(row);
align_driver::emit_object_file(
&mir,
&object,
align_driver::BuildTarget::Baseline,
align_driver::Profile::Release,
&[],
false,
)
.unwrap_or_else(|error| panic!("{row} object emission failed: {error}"));
align_driver::link_executable(
&object,
&executable,
&mir.link_libs,
align_driver::Profile::Release,
)
.unwrap_or_else(|error| panic!("{row} link failed: {error}"));
executable
}

fn run_call_rows(
group: &str,
rows: &[&str],
calls: u64,
fixture: fn(&str, u64) -> String,
) {
assert!(align_driver::backend_available(), "{group} benchmark requires the LLVM backend");
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock")
.as_nanos();
let directory = std::env::temp_dir().join(format!(
"align-library-boundary-{group}-{}-{unique}", std::process::id()
));
std::fs::create_dir(&directory).expect("create benchmark directory");
struct Cleanup(std::path::PathBuf);
impl Drop for Cleanup {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
let _cleanup = Cleanup(directory.clone());

for &row in rows {
let source = fixture(row, calls);
let executable = compile_call_fixture(&directory, row, &source);
let minimum = Duration::from_millis(750);
let start = Instant::now();
let mut processes = 0_u64;
while start.elapsed() < minimum {
let status = std::process::Command::new(&executable)
.status()
.unwrap_or_else(|error| panic!("{row} execution failed: {error}"));
assert!(status.success(), "{row} returned {status}");
processes += 1;
}
let elapsed = start.elapsed();
let nanoseconds = elapsed.as_secs_f64() * 1_000_000_000.0
/ (calls * processes) as f64;
println!("{row}\t{nanoseconds:.3}\tns/call\t{processes}\tprocesses");
}
}

fn run_move_return() {
const ROWS: [&str; 4] = [
"copy-return-control",
"move-return-none",
"move-return-some",
"move-return-err",
];
run_call_rows("move-return", &ROWS, 100_000, return_fixture);
}

fn borrow_fixture(kind: &str, calls: u64) -> String {
match kind {
"by-value-call-control" => format!(
"fn inspect(value: i64) -> i64 = value + 1\n\
fn main() -> i32 {{ mut index: i64 := 0; mut total: i64 := 0; loop {{ if index == {calls} {{ break }}; total = total + inspect(index); index = index + 1 }}; if total < 0 {{ return 1 }}; return 0 }}\n"
),
"shared-borrow-call" => format!(
"fn inspect(borrow value: string) -> i64 = value.len()\n\
fn main() -> i32 {{ value := \"shared\".clone(); mut index: i64 := 0; mut total: i64 := 0; loop {{ if index == {calls} {{ break }}; total = total + inspect(value); index = index + 1 }}; if total < 0 {{ return 1 }}; return 0 }}\n"
),
"exclusive-copy-control" => format!(
"fn increment(value: i64) -> i64 = value + 1\n\
fn main() -> i32 {{ mut index: i64 := 0; mut value: i64 := 0; loop {{ if index == {calls} {{ break }}; value = increment(value); index = index + 1 }}; if value != {calls} {{ return 1 }}; return 0 }}\n"
),
"exclusive-copy-call" => format!(
"fn increment(borrow mut value: i64) {{ value = value + 1 }}\n\
fn main() -> i32 {{ mut index: i64 := 0; mut value: i64 := 0; loop {{ if index == {calls} {{ break }}; increment(value); index = index + 1 }}; if value != {calls} {{ return 1 }}; return 0 }}\n"
),
"exclusive-move-replace" => format!(
"fn replace(borrow mut value: string) {{ value = \"replacement\".clone() }}\n\
fn main() -> i32 {{ mut index: i64 := 0; mut value := \"initial\".clone(); loop {{ if index == {calls} {{ break }}; replace(value); index = index + 1 }}; if value.len() != 11 {{ return 1 }}; return 0 }}\n"
),
_ => unreachable!("closed borrowed-call benchmark row"),
}
}

fn run_shared_borrow() {
const ROWS: [&str; 2] = ["by-value-call-control", "shared-borrow-call"];
run_call_rows("shared-borrow", &ROWS, 100_000, borrow_fixture);
}

fn run_exclusive_borrow() {
const ROWS: [&str; 3] = [
"exclusive-copy-control",
"exclusive-copy-call",
"exclusive-move-replace",
];
run_call_rows("exclusive-borrow", &ROWS, 100_000, borrow_fixture);
}

fn main() {
match std::env::args().nth(1).as_deref() {
Some("interface") => run_interface(),
Some("provenance") => run_provenance(),
Some("move-return") => run_move_return(),
Some("shared-borrow") => run_shared_borrow(),
Some("exclusive-borrow") => run_exclusive_borrow(),
Some(other) => {
eprintln!("unknown library-boundary benchmark group `{other}`");
std::process::exit(2);
}
None => {
eprintln!("usage: run.sh interface|provenance");
eprintln!("usage: run.sh interface|provenance|move-return|shared-borrow|exclusive-borrow");
std::process::exit(2);
}
}
Expand Down
6 changes: 2 additions & 4 deletions crates/align_ast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,8 @@ pub struct Param {
pub ty: Type,
}

/// Function parameter ownership/access mode. L2a represents every settled mode end to end while
/// the parser still produces only [`ByValue`](ParamMode::ByValue) and the already-shipped
/// [`Out`](ParamMode::Out). `Borrow` and `BorrowMut` remain reserved until their complete semantic
/// slices land.
/// Function parameter ownership/access mode. The parser treats `out`, `borrow`, and `borrow mut`
/// contextually so the same words remain available as ordinary identifiers and type names.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ParamMode {
ByValue,
Expand Down
Loading
Loading