diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 380e32a..288e1c2 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -2797,6 +2797,12 @@ fn params_to_env_args(params: [Any]) -> String { fn cg_fn(stmt: Map) -> Void { let fn_name: String = stmt["name"] state_set("__cg_current_fn", fn_name) + // Record every top-level definition. El has NO namespacing -- imports are + // textual inlining, so two modules defining the same name emit two C + // functions into one translation unit. cc catches it, but names the + // generated helpers first and the user's fn fourth. Recording it lets the + // collision be reported at El level, in El terms. + record_call(fn_name, "defines") // Emit which constructs this fn carries, so the prohibition check can be a // query over relations instead of a walk inside the emitter. let cdl = stmt["decorators"] diff --git a/lang/tests/integration/definitions_query.sh b/lang/tests/integration/definitions_query.sh new file mode 100755 index 0000000..2f31c3c --- /dev/null +++ b/lang/tests/integration/definitions_query.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Control for duplicate-definition detection. +# +# El has no namespacing: import is textual inlining, so two modules defining the +# same name emit two C functions into one translation unit. +set -uo pipefail +ELC="${1:?usage: definitions_query.sh }" +LANG_DIR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +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)); }; } + +printf 'fn helper() -> Int { return 1 }\n' > "$W/a.el" +printf 'fn helper() -> Int { return 2 }\n' > "$W/b.el" +printf 'import "a.el"\nimport "b.el"\nfn main() { println(int_to_str(helper())) }\n' > "$W/m.el" +EL_RELATIONS_OUT="$W/r.txt" "$ELC" "$W/m.el" >/dev/null 2>&1 +out=$("$LANG_DIR/tools/check/definitions.sh" "$W/r.txt" 2>&1); rc=$? +chk "a collision across modules is caught at El level" "1" "$rc" +chk "the colliding name is reported" "1" "$(echo "$out" | grep -c "'helper' is defined 2 times")" +chk "and the reason is given" "1" "$(echo "$out" | grep -c 'no namespacing')" + +printf 'fn only_once() -> Int { return 1 }\nfn main() { println(int_to_str(only_once())) }\n' > "$W/c.el" +EL_RELATIONS_OUT="$W/r2.txt" "$ELC" "$W/c.el" >/dev/null 2>&1 +"$LANG_DIR/tools/check/definitions.sh" "$W/r2.txt" >/dev/null 2>&1 +chk "a clean program exits 0" "0" "$?" +echo; echo " 4 assertions, $((4-F)) passed, $F failed"; exit $F diff --git a/lang/tools/check/definitions.sh b/lang/tools/check/definitions.sh new file mode 100755 index 0000000..b97c7f2 --- /dev/null +++ b/lang/tools/check/definitions.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# definitions.sh — catch duplicate top-level definitions at El level. +# +# El has no namespacing. `import` is textual inlining, so two modules defining +# the same name emit two C functions into one translation unit. cc does catch +# it, but reports the generated helpers (__el_body_f, __env_f, __thunk_f) before +# the user's own function, so the first three errors name symbols the user never +# wrote. +# +# LIMIT, stated rather than hidden: textual inlining destroys file provenance. +# By the time codegen runs there is one source string and no record of which +# file a definition came from, so this can say WHICH name collides but not which +# files. Naming the files needs provenance threaded through resolve_imports. +set -uo pipefail +REL="${1:?usage: definitions.sh }" +[ -f "$REL" ] || exit 0 +V=0 +while read -r name count; do + [ "$count" -gt 1 ] || continue + printf "duplicate definition: '%s' is defined %s times — El has no namespacing, so imported modules share one global scope\n" "$name" "$count" + V=$((V+1)) +done < <(grep ' calls defines$' "$REL" | awk '{print $1}' | sort | uniq -c | awk '{print $2, $1}') +[ "$V" -eq 0 ] && echo "definitions: clean" +exit "$V"