Merge pull request 'promote dev → stage: chain-of-custody repair' (#168) from dev into stage
El SDK Release / build-and-release (pull_request) Failing after 23s
El SDK CI - stage / build-and-test (push) Failing after 12m50s

This commit was merged in pull request #168.
This commit is contained in:
2026-08-17 16:05:31 +00:00
3 changed files with 144 additions and 1 deletions
@@ -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
+62
View File
@@ -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 <elc>}"
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<b)?1:0}')
chk "bound: the caller continues BEFORE the body finishes" "1" "$caller_before_body_end"
chk "bound: the wrap returns in under 10ms while the body takes 50ms" "1" "$([ "${wrap:-999999}" -lt 10000 ] && [ "${bend:-0}" -gt 40000 ] && echo 1 || echo 0)"
echo; echo " 6 assertions, $((6-F)) passed, $F failed"; exit $F
+66
View File
@@ -0,0 +1,66 @@
/* future.c — a FUTURE as one more magic-tagged heap object.
*
* Fixture for tests/integration/async_future.sh. Linked into the probe but
* never referenced from El source: everything here is reached only by binding
* a construct AFTER the binary exists.
*
* The claim under test: @async needs no compiler change. el_val_t already
* carries List, Map, Geometry, Manifold and Bin as magic-tagged heap pointers;
* a future is one more, and el_seam_wrap hands the target the body so it can
* decide whether and when to invoke it.
*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
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;
}