diff --git a/lang/tests/integration/seam_binding.sh b/lang/tests/integration/seam_binding.sh new file mode 100755 index 0000000..27bf13b --- /dev/null +++ b/lang/tests/integration/seam_binding.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# seam_binding.sh — integration control for the runtime construct seam. +# +# The seam's whole claim is that a construct declared AFTER a binary exists +# applies to that already-built program. That cannot be checked by +# compile_capture, which only sees emitted text: it needs a built binary, a +# linked target, and an environment. Hence a harness rather than a unit test. +# +# usage: seam_binding.sh [lang-dir] +# exit 0 = all assertions held; non-zero = number of failures +set -uo pipefail +ELC="${1:?usage: seam_binding.sh [lang-dir]}" +LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT +FAILS=0 + +ok() { printf ' ok %s\n' "$1"; } +fail() { printf ' FAIL %s\n expected: %s\n actual: %s\n' "$1" "$2" "$3"; FAILS=$((FAILS+1)); } +check(){ [ "$2" = "$3" ] && ok "$1" || fail "$1" "$2" "$3"; } + +SRCS=$("$LANG_DIR/../scripts/el-runtime-sources.sh" "$LANG_DIR/runtime") +CFLAGS="-std=c11 -O2 -rdynamic -I $LANG_DIR/runtime" +for d in /opt/homebrew/opt/openssl@3 /usr/local/opt/openssl@3; do + [ -d "$d" ] && CFLAGS="$CFLAGS -I $d/include" && LDFLAGS="-L $d/lib" +done +LDFLAGS="${LDFLAGS:-} -lcurl -lssl -lcrypto -lpthread -lm" + +# A construct target that is LINKED but never referenced from El source. +cat > "$WORK/targets.c" <<'EOF' +#include +#include +typedef int64_t el_val_t; +el_val_t observe(el_val_t fn, el_val_t con, el_val_t r){ + printf("SEEN %s/%s\n", (const char*)(intptr_t)fn, (const char*)(intptr_t)con); + return r; /* zero = do not refuse */ +} +el_val_t refuse(el_val_t fn, el_val_t con, el_val_t r){ + (void)fn; (void)con; (void)r; return 42; /* non-zero = short-circuit */ +} +EOF + +# A program with NO construct anywhere in its source. +cat > "$WORK/prog.el" <<'EOF' +fn work() -> Int { + return 7 +} + +fn main() { + println(int_to_str(work())) +} +EOF + +"$ELC" "$WORK/prog.el" > "$WORK/prog.c" 2>/dev/null +cc $CFLAGS -o "$WORK/prog" "$WORK/prog.c" "$WORK/targets.c" $SRCS $LDFLAGS 2>/dev/null \ + || { echo " FAIL probe did not build"; exit 1; } + +check "unbound program is unaffected" \ + "7" "$(cd "$WORK" && ./prog 2>&1)" + +printf 'work audited entry observe\n' > "$WORK/observe.txt" +check "a construct declared AFTER the build applies" \ + "SEEN work/audited +7" "$(cd "$WORK" && EL_CONSTRUCTS=observe.txt ./prog 2>&1)" + +printf 'work denied entry refuse\n' > "$WORK/refuse.txt" +check "a construct declared after the build can REFUSE" \ + "42" "$(cd "$WORK" && EL_CONSTRUCTS=refuse.txt ./prog 2>&1)" + +printf 'work ghost entry no_such_symbol_anywhere\n' > "$WORK/ghost.txt" +check "an unlinked target is skipped, not fatal" \ + "7" "$(cd "$WORK" && EL_CONSTRUCTS=ghost.txt ./prog 2>&1)" + +printf 'other_fn x entry refuse\n' > "$WORK/other.txt" +check "a binding for a different fn does not fire" \ + "7" "$(cd "$WORK" && EL_CONSTRUCTS=other.txt ./prog 2>&1)" + +printf 'work a entry observe\nwork b entry observe\n' > "$WORK/two.txt" +check "two constructs compose on one crossing" \ + "SEEN work/a +SEEN work/b +7" "$(cd "$WORK" && EL_CONSTRUCTS=two.txt ./prog 2>&1)" + +echo +echo " 6 assertions, $((6-FAILS)) passed, $FAILS failed" +exit $FAILS diff --git a/lang/tests/native/test_compiler.el b/lang/tests/native/test_compiler.el index 03988ec..7ce68af 100644 --- a/lang/tests/native/test_compiler.el +++ b/lang/tests/native/test_compiler.el @@ -735,19 +735,7 @@ test "compiler-stdint-include" { // be measured and "is this decorator earning its keep" stays an argument // instead of a query. -test "decorator-manager-beat-carries-construct" { - let src: String = "@manager\nfn f() -> Int { return 1 }" - let out: String = compile_capture(src) - assert str_contains(out, "engram_boundary_beat"), "@manager injects the beat" - assert str_contains(out, "EL_STR(\"manager\")"), "beat carries the construct that caused it" -} -test "decorator-accessor-beat-carries-construct" { - let src: String = "@accessor\nfn f() -> Int { return 1 }" - let out: String = compile_capture(src) - assert str_contains(out, "engram_boundary_beat"), "@accessor injects the beat" - assert str_contains(out, "EL_STR(\"accessor\")"), "beat carries the construct that caused it" -} test "decorator-undecorated-fn-has-no-beat" { let src: String = "fn f() -> Int { return 1 }" @@ -778,12 +766,6 @@ test "decorator-authenticate-compiles-to-nothing" { // A construct declares its own meaning and codegen reads it. Adding a // construct is a declaration in the program; it does not touch the compiler. -test "declared-construct-injects-without-compiler-knowledge" { - let src: String = "@decorator(\"injects_at_entry\", \"engram_boundary_beat\")\nfn audited() {}\n@audited\nfn risky() -> Int { return 7 }" - let out: String = compile_capture(src) - assert str_contains(out, "engram_boundary_beat(EL_STR(\"risky\")"), "a program-declared construct injects" - assert str_contains(out, "EL_STR(\"audited\")"), "the beat is attributed to the declared construct" -} test "declared-construct-name-unknown-to-codegen" { // The name is arbitrary. Nothing in the compiler mentions it. @@ -798,13 +780,6 @@ test "undeclared-construct-still-injects-nothing" { assert !str_contains(out, "engram_boundary_beat"), "an undeclared construct injects nothing" } -test "builtin-constructs-still-inject" { - // manager/accessor are the compiled-in core, seeded not branched. - let src: String = "@manager\nfn m() -> Int { return 1 }\n@accessor\nfn a() -> Int { return 2 }" - let out: String = compile_capture(src) - assert str_contains(out, "EL_STR(\"manager\")"), "seeded manager still injects" - assert str_contains(out, "EL_STR(\"accessor\")"), "seeded accessor still injects" -} // ── Declared constructs: guards ────────────────────────────────────────────── // @@ -813,29 +788,8 @@ test "builtin-constructs-still-inject" { // needed and never had — fourteen applications that read as protection and // emitted no instruction. -test "declared-guard-emits-refusable-check" { - let src: String = "@decorator(\"guards_at_entry\", \"my_auth\")\nfn authenticate() {}\n@authenticate\nfn handler() -> Int { return 7 }" - let out: String = compile_capture(src) - assert str_contains(out, "my_auth(EL_STR(\"handler\")"), "the guard is called at entry" - assert str_contains(out, "if (__g) return __g;"), "a non-zero guard result short-circuits the fn" -} -test "declared-guards-stack-in-order" { - let src: String = "@decorator(\"guards_at_entry\", \"my_auth\")\nfn authenticate() {}\n@decorator(\"guards_at_entry\", \"my_roles\")\nfn authorize() {}\n@authenticate\n@authorize\nfn handler() -> Int { return 7 }" - let out: String = compile_capture(src) - assert str_contains(out, "my_auth("), "first guard runs" - assert str_contains(out, "my_roles("), "second guard runs — every guard applies, not just the topmost" -} -test "guard-precedes-injection" { - // A refused call must not report a boundary crossing. - let src: String = "@decorator(\"guards_at_entry\", \"my_auth\")\nfn authenticate() {}\n@authenticate\n@manager\nfn handler() -> Int { return 7 }" - let out: String = compile_capture(src) - let g: Int = str_index_of(out, "my_auth(") - let b: Int = str_index_of(out, "engram_boundary_beat(EL_STR(\"handler\")") - assert g < b, "the guard is emitted before the beat" - assert g >= 0, "guard present" -} test "undeclared-guard-emits-nothing" { let src: String = "@not_a_declared_guard\nfn handler() -> Int { return 7 }" @@ -865,16 +819,6 @@ test "no-exit-construct-emits-no-wrapper" { assert !str_contains(out, "__el_body_"), "fns without an exit construct are unwrapped, byte for byte as before" } -test "constructs-compose-guard-entry-exit" { - let src: String = "@decorator(\"guards_at_entry\", \"my_auth\")\nfn authenticate() {}\n@decorator(\"injects_at_exit\", \"persist_now\")\nfn durable() {}\n@authenticate\n@durable\n@manager\nfn op() -> Int { return 1 }" - let out: String = compile_capture(src) - let g: Int = str_index_of(out, "my_auth(") - let b: Int = str_index_of(out, "engram_boundary_beat(EL_STR(\"op\")") - let x: Int = str_index_of(out, "persist_now(") - assert g < b, "guard before entry injection" - assert b < x, "entry injection before exit injection" - assert x >= 0, "three independent constructs compose on one fn" -} // ── Declared constructs: wraps and prohibitions ────────────────────────────── @@ -924,3 +868,31 @@ test "seam-emitted-without-any-decorator" { assert str_contains(out, "el_seam_run"), "an undecorated fn is still bindable at runtime" assert !str_contains(out, "engram_boundary_beat"), "and nothing is inlined for it" } + + +// ── Runtime seam: what replaced the compile-time entry mechanism ───────────── +// +// Entry injection and refusal moved from emission to execution. These assert +// the emitted shape; the BEHAVIOUR — that a construct declared after the build +// applies, refuses, composes, and that an unlinked target is skipped — is +// covered by tests/integration/seam_binding.sh, which needs a built binary and +// an environment and therefore cannot be a compile_capture test. + +test "seam-replaces-inlined-entry-injection" { + let src: String = "@manager\nfn m() -> Int { return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "el_seam_run(EL_STR(\"m\")"), "the crossing goes through the seam" + assert !str_contains(out, "engram_boundary_beat(EL_STR(\"m\")"), "nothing is inlined at the crossing any more" +} + +test "seam-entry-is-refusable" { + let src: String = "fn f() -> Int { return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "if (__s) return __s;"), "a bound construct can short-circuit the fn" +} + +test "seam-is-emitted-for-undecorated-fns" { + let src: String = "fn plain() -> Int { return 1 }" + let out: String = compile_capture(src) + assert str_contains(out, "el_seam_run(EL_STR(\"plain\")"), "any fn is bindable later, decorated or not" +}