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
14 changes: 10 additions & 4 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ per-PR journals are preserved in
[`docs/archive/HANDOFF-2026-07-25.md`](docs/archive/HANDOFF-2026-07-25.md);
neither is a source of current status.

_Last updated: 2026-08-25._ Request 18's retained-root regular-file constructors are implemented
_Last updated: 2026-08-26._ Request 19's shared recursive-Drop codegen is implemented against
`docs/impl/21-build-perf-plan.md` item 3a. On its real-client fixture, raw IR fell from 1,517,324
lines / 113.6 MB to 109,992 lines / 5.96 MB, while the cold three-unit release build fell from
471.074 seconds and more than 832,704 KiB observed RSS to 13.555 seconds and 266,400 KiB peak RSS;
the executable prints the exact required PASS line. Merge and align-llm's consumer-owned pin/lane
restoration remain. Request 18's retained-root regular-file constructors are implemented
against the accepted design in `docs/impl/29-fs-retained-root-plan.md` and real-client verified by
align-llm PR #99 at `78eae459fd1f88bad1c3c3ca7b86921a08ecf168`, pinned to Align merge
`19c3db144c462bf7d6784f88d64cc124229b7ec2`; C6d is complete. Request 16's sum-payload projection and
Expand Down Expand Up @@ -359,11 +364,12 @@ facts must live in this repository.
surface, ownership model, limit, exact pin, focused adoption owner, and final capable integration
evidence is recorded there. The latest closure wave covers Request 14 through align-llm PR #100,
Requests 16–17 through #98, and Request 18 through #99.
Request 20 is merged in PR #887: the required `macos-15` PR leg now runs the existing
Request 20 is merged in PR #887 and real-client verified; its align-llm publication record is
merged in align-llm PR #107, so the register can close it. The required `macos-15` PR leg runs the existing
`m5_owned_json` owner, and the discovered storage-generation regression no longer falsely retains
a `JsonOwnedDecode` input/arena fact. The complete owner passed locally on Apple Silicon and in the
required macOS CI leg. Request 19's large-by-value-struct code-generation cost is now the
highest-priority align-llm request and should start on the other development machine.
required macOS CI leg. Request 19's Align-side implementation is complete; its merge and the
consumer-owned pin, hosted-lane restoration, and fresh-worker proof remain.

Consumer-gated deferrals that remain intentional:

Expand Down
30 changes: 30 additions & 0 deletions bench/large_drop_codegen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Shared recursive-Drop codegen benchmark

This local, non-gating benchmark owns the resource promise in
`docs/impl/21-build-perf-plan.md` item 3a. It compares optimized compiler
binaries from the pre-change and candidate revisions on two inputs:

- `control.align`, a one-shot small Move record whose instrumented runtime must
print its value and report exactly one allocation and one destructor free; and
- align-llm Request 19's `prompt_verifier_smoke.align`, the pathological wide
Move-record and early-exit client fixture.

Build each revision's `alignc` and then rebuild the adjacent runtime archive
with `align_runtime`'s `alloc-count` feature. Keep each binary and archive in a
separate directory, then run:

```text
BASELINE_ALIGNC=/path/to/baseline/alignc \
CANDIDATE_ALIGNC=/path/to/candidate/alignc \
REQUEST19_SOURCE=/path/to/align-llm/src/prompt_verifier_smoke.align \
bench/large_drop_codegen/run.sh
```

The harness uses fresh caches to report actual frontend/codegen miss counts,
then reports raw-IR lines, release object bytes, build wall time and peak
compiler RSS, and generated-program cleanup wall time and peak RSS. It requires
byte-identical program output, the exact `1 / 1` control allocation/free result,
and Request 19's PASS line. Both arms use `--no-rt-lto` so the allocation
counters come from the instrumented archive rather than the ordinary runtime
bitcode. Run the real consumer build separately with its default runtime-LTO
policy before accepting the item.
27 changes: 27 additions & 0 deletions bench/large_drop_codegen/control.align
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
extern "C" {
fn align_rt_alloc_count() -> i64
fn align_rt_free_count() -> i64
}

SmallDrop { text: string, number: i64 }

fn allocations() -> i64 {
unsafe { return align_rt_alloc_count() }
}

fn frees() -> i64 {
unsafe { return align_rt_free_count() }
}

fn one_drop() {
value := SmallDrop { text: "control".clone(), number: 7 }
print(value.number)
}

fn main() {
allocations_before := allocations()
frees_before := frees()
one_drop()
print(allocations() - allocations_before)
print(frees() - frees_before)
}
39 changes: 39 additions & 0 deletions bench/large_drop_codegen/measure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Run one benchmark command and report wall time plus child peak RSS as JSON."""

import json
import os
import resource
import subprocess
import sys
import time


def main() -> int:
if len(sys.argv) < 4 or sys.argv[2] != "--":
print("usage: measure.py WORKDIR -- COMMAND [ARG ...]", file=sys.stderr)
return 2
workdir = sys.argv[1]
command = sys.argv[3:]
start = time.monotonic()
completed = subprocess.run(
command,
cwd=workdir,
env=os.environ.copy(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
elapsed = time.monotonic() - start
rss = resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss
if sys.platform == "darwin":
rss //= 1024
if completed.returncode != 0:
sys.stdout.buffer.write(completed.stdout)
sys.stderr.buffer.write(completed.stderr)
return completed.returncode
print(json.dumps({"wall_seconds": round(elapsed, 3), "peak_rss_kib": rss}, sort_keys=True))
return 0


if __name__ == "__main__":
raise SystemExit(main())
90 changes: 90 additions & 0 deletions bench/large_drop_codegen/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Local Request-19 compile-cost comparison. This is a measurement, not a CI gate.
set -euo pipefail

here="$(cd "$(dirname "$0")" && pwd)"
baseline="${BASELINE_ALIGNC:-}"
candidate="${CANDIDATE_ALIGNC:-}"
request_source="${REQUEST19_SOURCE:-$here/../../../align-llm/src/prompt_verifier_smoke.align}"

if [ -z "$baseline" ] || [ -z "$candidate" ]; then
echo "set BASELINE_ALIGNC and CANDIDATE_ALIGNC to optimized compiler binaries" >&2
exit 2
fi
for compiler in "$baseline" "$candidate"; do
if [ ! -x "$compiler" ]; then
echo "compiler is not executable: $compiler" >&2
exit 2
fi
if [ ! -f "$(dirname "$compiler")/libalign_runtime.a" ]; then
echo "instrumented libalign_runtime.a is missing beside $compiler" >&2
exit 2
fi
done
if [ ! -f "$request_source" ]; then
echo "Request 19 source is missing; set REQUEST19_SOURCE explicitly" >&2
exit 2
fi

work="$(mktemp -d)"
cleanup() {
rm -rf "$work"
}
trap cleanup EXIT

measure_case() {
revision="$1"
compiler="$2"
case_name="$3"
source="$4"
dir="$work/$revision-$case_name"
mkdir -p "$dir"
# Preserve sibling modules imported by the entry fixture. The Request 19 owner imports
# prompt_artifacts.align and prompt_score.align from its source directory.
cp "$(dirname "$source")"/*.align "$dir/"
cp "$source" "$dir/main.align"

(
cd "$dir"
ALIGNC_CACHE="$dir/cache" "$compiler" build main.align --profile release --no-rt-lto \
--cache-stats >build.stdout 2>build.stderr
)
frontend_misses="$(awk '/^alignc: cache: .* frontend miss \(/ { count++ } END { print count + 0 }' "$dir/build.stderr")"
frontend_hits="$(awk '/^alignc: cache: .* frontend hit$/ { count++ } END { print count + 0 }' "$dir/build.stderr")"
codegen_misses="$(awk '/^alignc: cache: .* miss \(/ && $0 !~ / frontend miss \(/ { count++ } END { print count + 0 }' "$dir/build.stderr")"
codegen_hits="$(awk '/^alignc: cache: .* hit$/ && $0 !~ / frontend hit$/ { count++ } END { print count + 0 }' "$dir/build.stderr")"
if [ "$frontend_hits" -ne 0 ] || [ "$codegen_hits" -ne 0 ]; then
echo "$revision/$case_name unexpectedly hit a fresh cache" >&2
sed -n '/^alignc: cache:/p' "$dir/build.stderr" >&2
exit 1
fi

(
cd "$dir"
"$compiler" emit-llvm main.align --stage raw --no-rt-lto >raw.ll
"$compiler" emit-obj main.align main.o --profile release --no-rt-lto
)
build_timing="$(ALIGNC_CACHE=off python3 "$here/measure.py" "$dir" -- \
"$compiler" build main.align --profile release --no-rt-lto)"
"$dir/main" >"$dir/program.stdout"
cleanup_timing="$(python3 "$here/measure.py" "$dir" -- "$dir/main")"

raw_lines="$(wc -l <"$dir/raw.ll" | tr -d ' ')"
object_bytes="$(wc -c <"$dir/main.o" | tr -d ' ')"
echo "$revision/$case_name: frontend_misses=$frontend_misses codegen_misses=$codegen_misses raw_ir_lines=$raw_lines object_bytes=$object_bytes build_timing=$build_timing cleanup_timing=$cleanup_timing"
}

measure_case baseline "$baseline" control "$here/control.align"
measure_case candidate "$candidate" control "$here/control.align"
cmp "$work/baseline-control/program.stdout" "$work/candidate-control/program.stdout"
if [ "$(tr '\n' ' ' <"$work/candidate-control/program.stdout")" != "7 1 1 " ]; then
echo "small control must report value 7, one allocation, and one destructor free" >&2
exit 1
fi

measure_case baseline "$baseline" request19 "$request_source"
measure_case candidate "$candidate" request19 "$request_source"
cmp "$work/baseline-request19/program.stdout" "$work/candidate-request19/program.stdout"
grep -Fx "prompt verifier smoke: complete, incomplete, compact, and tamper cases PASS" \
"$work/candidate-request19/program.stdout" >/dev/null
echo "program output: identical; control destructor count: 1/1; Request 19: PASS"
37 changes: 27 additions & 10 deletions crates/align_codegen_llvm/src/drop_codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ impl<'c, 'a> FnGen<'c, 'a> {
&self,
base: inkwell::values::PointerValue<'c>,
ty: Ty,
) -> Result<(), CodegenError> {
self.emit_drop_at_iterative_in(self.func, base, ty)
}

/// The target-selectable form used to define a private struct destructor while an ordinary
/// function body is being emitted. Cleanup itself remains one iterative CFG inside `function`;
/// it never calls another generated struct helper, so runtime stack depth does not follow the
/// nominal type depth.
pub(super) fn emit_drop_at_iterative_in(
&self,
function: FunctionValue<'c>,
base: inkwell::values::PointerValue<'c>,
ty: Ty,
) -> Result<(), CodegenError> {
enum Work<'c> {
Drop {
Expand Down Expand Up @@ -148,8 +161,8 @@ impl<'c, 'a> FnGen<'c, 'a> {
.build_load(self.ctx.i8_type(), tag_ptr, "dropopttag")
.map_err(|error| self.err(error))?
.into_int_value();
let some = self.ctx.append_basic_block(self.func, "drop.opt.some");
let cont = self.ctx.append_basic_block(self.func, "drop.opt.cont");
let some = self.ctx.append_basic_block(function, "drop.opt.some");
let cont = self.ctx.append_basic_block(function, "drop.opt.cont");
let is_some = self
.builder
.build_int_compare(
Expand Down Expand Up @@ -189,7 +202,7 @@ impl<'c, 'a> FnGen<'c, 'a> {
.build_load(self.ctx.i8_type(), tag_ptr, "droprestag")
.map_err(|error| self.err(error))?
.into_int_value();
let cont = self.ctx.append_basic_block(self.func, "drop.result.cont");
let cont = self.ctx.append_basic_block(function, "drop.result.cont");
let candidates = [
(0u64, 1u32, scalar_to_ty(ok), "drop.result.ok"),
(1u64, 2u32, scalar_to_ty(err), "drop.result.err"),
Expand All @@ -206,7 +219,7 @@ impl<'c, 'a> FnGen<'c, 'a> {
)
})
.map(|(tag, field, ty, name)| {
(tag, field, ty, self.ctx.append_basic_block(self.func, name))
(tag, field, ty, self.ctx.append_basic_block(function, name))
})
.collect::<Vec<_>>();
let cases = branches
Expand Down Expand Up @@ -287,14 +300,14 @@ impl<'c, 'a> FnGen<'c, 'a> {
.build_load(self.ctx.i32_type(), tag_ptr, "droptagv")
.map_err(|error| self.err(error))?
.into_int_value();
let cont = self.ctx.append_basic_block(self.func, "drop.enum.cont");
let cont = self.ctx.append_basic_block(function, "drop.enum.cont");
let branches = owned
.into_iter()
.map(|(tag, fields)| {
(
tag,
fields,
self.ctx.append_basic_block(self.func, "drop.enum.v"),
self.ctx.append_basic_block(function, "drop.enum.v"),
)
})
.collect::<Vec<_>>();
Expand Down Expand Up @@ -341,9 +354,9 @@ impl<'c, 'a> FnGen<'c, 'a> {
.get(id as usize)
.ok_or_else(|| self.err(format!("struct type id {id} is missing")))?;
let i64_type = self.ctx.i64_type();
let head = self.ctx.append_basic_block(self.func, "dropdeep.head");
let body = self.ctx.append_basic_block(self.func, "dropdeep.body");
let done = self.ctx.append_basic_block(self.func, "dropdeep.done");
let head = self.ctx.append_basic_block(function, "dropdeep.head");
let body = self.ctx.append_basic_block(function, "dropdeep.body");
let done = self.ctx.append_basic_block(function, "dropdeep.done");
let predecessor = self
.builder
.get_insert_block()
Expand Down Expand Up @@ -472,7 +485,11 @@ impl<'c, 'a> FnGen<'c, 'a> {
Ty::String
| Ty::DynArray(_)
| Ty::DynStructArray(..)
| Ty::DynSliceArray(_) => {
| Ty::DynSliceArray(_)
| Ty::DynVecArray(..)
| Ty::DynMaskArray(..)
| Ty::DynFixedArray(..)
| Ty::DynFixedStructArray(..) => {
let aggregate = self
.builder
.build_load(slice_struct_type(self.ctx), base, "dropslicev")
Expand Down
Loading
Loading