From 511db25230c69b9484ece39ed4131d3920b16596 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Mon, 17 Aug 2026 11:03:59 -0500 Subject: [PATCH] rerun cycle 18 rather than reconstruct it The async/future measurements were produced by a C stub in /tmp, and that artifact was destroyed when the session worktrees were removed. The log then asserted results with nothing behind them -- a claim inside an evidence record, which is exactly what turns a chain of custody into a pile. Rerun, not reconstructed. Rebuilding the missing file would have been a fabrication with a fresh timestamp; rerunning produces new evidence with its own. lang/tests/integration/fixtures/future.c the future, as a tagged heap object lang/tests/integration/async_future.sh the harness, 6/6 ok unbound: synchronous, correct result ok unbound: el_await on a non-future passes through, no crash ok bound: does not crash ok bound: the awaited result is correct ok bound: the caller continues BEFORE the body finishes ok bound: wrap returns in <10ms while the body takes 50ms LABELLED AS A REPLICATION. The outcomes were already known when this harness was written, so its expectations are NOT predictions committed in advance. Its evidentiary value is that a third party can reproduce it, not that it was called ahead of time. Recording it as anything stronger would corrupt the record it is meant to repair. The fixture also carries the P5 defect and its fix in a comment: the first el_await dereferenced ->magic off an unvalidated slot and SIGSEGV'd on the unbound path, sixty seconds after the same defect was diagnosed elsewhere in the runtime. --- .../cycles/18-async-half-expressible.md | 17 ++++- lang/tests/integration/async_future.sh | 62 +++++++++++++++++ lang/tests/integration/fixtures/future.c | 66 +++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100755 lang/tests/integration/async_future.sh create mode 100644 lang/tests/integration/fixtures/future.c diff --git a/docs/v1/experiments/cycles/18-async-half-expressible.md b/docs/v1/experiments/cycles/18-async-half-expressible.md index 1cdc0c2..a7b2624 100644 --- a/docs/v1/experiments/cycles/18-async-half-expressible.md +++ b/docs/v1/experiments/cycles/18-async-half-expressible.md @@ -1,6 +1,21 @@ # async — half expressible, and the cycle that was dogma -**Status: measured on a branch, not merged. Two runs — the first was invalid.** +**Status: replicated and corroborated. Three runs — the first was invalid.** + +> **Chain of custody note, 2026-08-17.** The original measurements were produced +> by a C stub written in `/tmp`, and that artifact was destroyed when the session +> worktrees were removed. For a period this file asserted results with nothing +> behind them — a claim inside an evidence record, which is the defect that turns +> a chain into a pile. It was **rerun**, not reconstructed: reconstructing the +> missing file would have been a fabrication with a fresh timestamp. +> +> The fixture now lives at `lang/tests/integration/fixtures/future.c` and the +> harness at `lang/tests/integration/async_future.sh`, so a third party can +> reproduce this without taking my word for it. **6/6.** +> +> The replication is labelled as such: the outcomes were already known when the +> harness was written, so its expectations are not predictions committed in +> advance. Its value is reproducibility, not foresight. ## The first attempt was DOGMA, not science diff --git a/lang/tests/integration/async_future.sh b/lang/tests/integration/async_future.sh new file mode 100755 index 0000000..d7a1c6f --- /dev/null +++ b/lang/tests/integration/async_future.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# async_future.sh — REPLICATION of cycle 18. +# +# STATUS: replication, not a blind test. The outcomes were already observed on +# 2026-08-17 before this harness existed, so the expectations below are not +# predictions committed in advance. Its evidentiary value is that the artifact +# lives in the repository and a third party can run it — not that it was called +# ahead of time. The original run's artifact was written in /tmp and lost when +# the worktrees were removed, which broke the chain; this replaces the claim +# with something reproducible rather than reconstructing the missing file. +# +# CLAIM UNDER TEST: @async requires no compiler change. A future is one more +# magic-tagged heap object, and el_seam_wrap lets a construct bound AFTER the +# build decide whether and when to invoke the body. +set -uo pipefail +ELC="${1:?usage: async_future.sh }" +LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/..}" +LANG_DIR="$(cd "$LANG_DIR" && pwd)" +FIX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/fixtures/future.c" +W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; F=0 +chk(){ [ "$2" = "$3" ] && printf ' ok %s\n' "$1" || { printf ' FAIL %s\n expected %s got %s\n' "$1" "$2" "$3"; F=$((F+1)); }; } +cd "$LANG_DIR" +SRCS=$(../scripts/el-runtime-sources.sh runtime) +CF="-std=c11 -O2 -rdynamic -I runtime"; LF="" +for d in /opt/homebrew/opt/openssl@3 /usr/local/opt/openssl@3; do + [ -d "$d" ] && CF="$CF -I $d/include" && LF="-L $d/lib" +done +LF="$LF -lcurl -lssl -lcrypto -lpthread -lm" + +cat > "$W/p.el" <<'EOF' +extern fn el_await(h: Int) -> Int + +fn work(k: Int) -> Int { + return k * 2 +} + +fn main() { + let h: Int = work(21) + println("CALLER_CONTINUED") + let r: Int = el_await(h) + println("RESULT " + int_to_str(r)) +} +EOF +"$ELC" "$W/p.el" > "$W/p.c" 2>/dev/null +cc $CF -o "$W/p" "$W/p.c" "$FIX" $SRCS $LF 2>/dev/null || { echo " FAIL probe did not build"; exit 1; } + +out=$(cd "$W" && ./p 2>&1); rc=$? +chk "unbound: no construct, synchronous, correct result" "0" "$rc" +chk "unbound: el_await on a non-future passes through, no crash" "1" "$(echo "$out" | grep -c '^RESULT 42$')" + +printf 'work async wrap defer\n' > "$W/c.txt" +out=$(cd "$W" && EL_CONSTRUCTS=c.txt ./p 2>&1); rc=$? +chk "bound: does not crash" "0" "$rc" +chk "bound: the awaited result is correct" "1" "$(echo "$out" | grep -c '^RESULT 42$')" + +wrap=$(echo "$out" | awk '/^WRAP_RETURNED/{print $2}') +bend=$(echo "$out" | awk '/^BODY_END/{print $2}') +caller_before_body_end=$(echo "$out" | awk '/CALLER_CONTINUED/{c=NR} /^BODY_END/{b=NR} END{print (c +#include +#include +#include +#include +#include + +typedef int64_t el_val_t; + +#define EL_MAGIC_FUT 0xE1F07000u +typedef struct { uint32_t magic; pthread_t th; el_val_t result; int done; + el_val_t (*body)(void*); void* env; } ElFuture; + +static long t0_us; +static long now_us(void){ struct timespec ts; clock_gettime(CLOCK_MONOTONIC,&ts); + return ts.tv_sec*1000000L + ts.tv_nsec/1000; } + +static void* fut_runner(void* v){ + ElFuture* f = (ElFuture*)v; + printf("BODY_START %ld\n", now_us()-t0_us); + usleep(50000); /* 50ms, so interleaving is visible */ + f->result = f->body(f->env); + f->done = 1; + printf("BODY_END %ld\n", now_us()-t0_us); + return NULL; +} + +/* wraps_body target: returns the HANDLE immediately, never the result */ +el_val_t defer(el_val_t fn, el_val_t con, el_val_t (*b)(void*), void* e){ + (void)fn; (void)con; + t0_us = now_us(); + ElFuture* f = calloc(1,sizeof(ElFuture)); + f->magic = EL_MAGIC_FUT; f->body = b; f->env = e; + pthread_create(&f->th, NULL, fut_runner, f); + printf("WRAP_RETURNED %ld\n", now_us()-t0_us); + return (el_val_t)(intptr_t)f; +} + +/* el_await — block on the handle and yield the real result. + * + * NEVER dereference to decide whether a slot is a pointer. el_val_t carries + * integers too, so reading ->magic off an integer dereferences that integer AS + * AN ADDRESS. The first version of this function did exactly that and + * SIGSEGV'd on the unbound path -- sixty seconds after the same defect was + * diagnosed elsewhere in the runtime. Check the floor and alignment first. */ +el_val_t el_await(el_val_t h){ + if (h < 0x10000) return h; /* small ints / low addresses */ + if (h & 0x7) return h; /* malloc returns 8-aligned */ + ElFuture* f = (ElFuture*)(intptr_t)h; + if (f->magic != EL_MAGIC_FUT) return h; /* safe to read now */ + pthread_join(f->th, NULL); + el_val_t r = f->result; + free(f); + return r; +}