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; +}