EXPERIMENT: derive arity from the runtime's own declarations

codegen.el carried builtin_arity(): 344 lines, 300 entries, a hand-maintained
second copy of el_runtime.h.

PREDICTIONS AND RESULTS
  P1 the table duplicates the header                     TRUE   243 shared names
  P2 they have already drifted                           FALSE  ZERO drift. The
                                                                duplicate had been
                                                                maintained correctly.
  P3 codegen can emit call-arity relations               TRUE
  P4 the check becomes a query against the header        TRUE
  P5 codegen drops to roughly baseline                   TRUE   4903 -> 4512,
                                                                149 BELOW the 4661
                                                                it started at

P2 being false is the better result: the table was not WRONG, it was
INCOMPLETE. 110 functions the runtime declares had no entry, so calling them
with the wrong argument count produced no El-level diagnostic at all. Measured:
the old compiler reports 0 arity errors for __http_do_map_to_file(1); the query
reports "takes 5 arguments, called with 1".

Deriving from the header fixes coverage AND makes drift impossible by
construction. 503 signatures, versus 300 entries maintained by hand.

THREE DEFECTS IN MY OWN CHECKER, each found by running it rather than reading it
  1. El names and C names differ -- `println` is `__println`. 60 of 500 decls
     carry the prefix and codegen owns the mapping; the old table carried both
     keys. One rule covers all 60.
  2. Multi-line declarations parsed as zero params, so the checker reported
     "takes 0" for a function taking 5. A diagnostic with the wrong number in it
     is worse than none -- the same shape as the stale caller attribution in the
     previous pass.
  3. Fixing (2) by joining lines dropped 500 signatures to 334, because a
     declaration preceded by a comment no longer started its record. Comments
     are stripped first now.

98/98 native, 5/5 arity_query.sh, fixpoint ok.
This commit is contained in:
bigmerge
2026-08-17 09:21:10 -05:00
parent 29f78f9f67
commit 9cc6040df2
3 changed files with 84 additions and 390 deletions
+6 -390
View File
@@ -1000,12 +1000,17 @@ fn cg_expr(expr: Map<String, Any>) -> String {
// work. Caught by capability_query.sh, not by reading the code.
record_call("program", "is_kind:" + state_get("__program_kind"))
record_call(state_get("__cg_current_fn"), fn_name)
// Record the ARITY at the call site. The compiler knows how many
// arguments were written; whether that is correct is a question
// about the runtime's surface, and the runtime already declares its
// surface in el_runtime.h. A table inside the emitter is a second
// copy of that header, maintained by hand.
record_call(fn_name, "arity:" + native_int_to_str(native_list_len(args)))
// Arity check against the builtin table - refuse, with a clear
// El-source message, when a known builtin gets the wrong arg
// count (e.g. `http_serve(port)` instead of `http_serve(port,
// handler)`). User-defined fns and variadic builtins pass
// through (builtin_arity returns -1).
arity_check_call(fn_name, arity)
// sleep(Duration) - Phase 1 of the typed-time work. When the
// single arg is provably a Duration we lower to el_sleep_duration
// so the runtime sees nanos directly. Existing sleep() callers
@@ -2544,400 +2549,15 @@ fn emit_time_violations() -> Void {
// dynamic dispatch) return -1 -> no check. A mismatch records a violation
// in process state, which emit_arity_violations() turns into #error
// directives at the top of the generated C.
fn builtin_arity(name: String) -> Int {
// I/O
if str_eq(name, "println") { return 1 }
if str_eq(name, "print") { return 1 }
if str_eq(name, "readline") { return 0 }
// LSP seed primitives
if str_eq(name, "__read_n") { return 1 }
if str_eq(name, "__print_raw") { return 1 }
// Test-registry accessors. These are not runtime builtins they are
// GENERATED into the same translation unit by the --test path below, one
// set per test binary. They are declared here so the El-side runner in
// runtime/eltest.el can call them with a known arity.
if str_eq(name, "__el_reg_count") { return 0 }
if str_eq(name, "__el_reg_name") { return 1 }
if str_eq(name, "__el_reg_invoke") { return 1 }
if str_eq(name, "__el_reg_last_ns") { return 0 }
if str_eq(name, "__el_reg_msg") { return 0 }
if str_eq(name, "__el_reg_asserts") { return 0 }
if str_eq(name, "__el_opt_json") { return 0 }
// String
if str_eq(name, "el_str_concat") { return 2 }
if str_eq(name, "str_eq") { return 2 }
if str_eq(name, "str_starts_with") { return 2 }
if str_eq(name, "str_ends_with") { return 2 }
if str_eq(name, "str_len") { return 1 }
if str_eq(name, "str_concat") { return 2 }
if str_eq(name, "int_to_str") { return 1 }
if str_eq(name, "str_to_int") { return 1 }
if str_eq(name, "str_slice") { return 3 }
if str_eq(name, "str_contains") { return 2 }
if str_eq(name, "str_replace") { return 3 }
if str_eq(name, "str_to_upper") { return 1 }
if str_eq(name, "str_to_lower") { return 1 }
if str_eq(name, "str_trim") { return 1 }
if str_eq(name, "str_index_of") { return 2 }
if str_eq(name, "str_split") { return 2 }
if str_eq(name, "str_char_at") { return 2 }
if str_eq(name, "str_char_code") { return 2 }
if str_eq(name, "str_pad_left") { return 3 }
if str_eq(name, "str_pad_right") { return 3 }
if str_eq(name, "str_format") { return 2 }
if str_eq(name, "str_lower") { return 1 }
if str_eq(name, "str_upper") { return 1 }
// Text-processing primitives (Phase 1)
if str_eq(name, "str_count") { return 2 }
if str_eq(name, "str_count_chars") { return 1 }
if str_eq(name, "str_count_bytes") { return 1 }
if str_eq(name, "str_count_lines") { return 1 }
if str_eq(name, "str_count_words") { return 1 }
if str_eq(name, "str_count_letters") { return 1 }
if str_eq(name, "str_count_digits") { return 1 }
if str_eq(name, "str_index_of_all") { return 2 }
if str_eq(name, "str_last_index_of") { return 2 }
if str_eq(name, "str_find_chars") { return 2 }
if str_eq(name, "str_repeat") { return 2 }
if str_eq(name, "str_reverse") { return 1 }
if str_eq(name, "str_strip_prefix") { return 2 }
if str_eq(name, "str_strip_suffix") { return 2 }
if str_eq(name, "str_strip_chars") { return 2 }
if str_eq(name, "str_lstrip") { return 1 }
if str_eq(name, "str_rstrip") { return 1 }
if str_eq(name, "is_letter") { return 1 }
if str_eq(name, "is_digit") { return 1 }
if str_eq(name, "is_alphanumeric") { return 1 }
if str_eq(name, "is_whitespace") { return 1 }
if str_eq(name, "is_punctuation") { return 1 }
if str_eq(name, "is_uppercase") { return 1 }
if str_eq(name, "is_lowercase") { return 1 }
if str_eq(name, "str_split_lines") { return 1 }
if str_eq(name, "str_split_chars") { return 1 }
if str_eq(name, "str_split_n") { return 3 }
if str_eq(name, "str_join") { return 2 }
// HTML sanitizer
if str_eq(name, "el_html_sanitize") { return 2 }
// Math
if str_eq(name, "el_abs") { return 1 }
if str_eq(name, "el_max") { return 2 }
if str_eq(name, "el_min") { return 2 }
// List
if str_eq(name, "el_list_len") { return 1 }
if str_eq(name, "el_list_get") { return 2 }
if str_eq(name, "el_list_append") { return 2 }
if str_eq(name, "el_list_empty") { return 0 }
if str_eq(name, "el_list_clone") { return 1 }
if str_eq(name, "list_push") { return 2 }
if str_eq(name, "list_push_front") { return 2 }
if str_eq(name, "list_join") { return 2 }
if str_eq(name, "list_range") { return 2 }
// Map
if str_eq(name, "el_get_field") { return 2 }
if str_eq(name, "el_map_get") { return 2 }
if str_eq(name, "el_map_set") { return 3 }
// HTTP
if str_eq(name, "http_get") { return 1 }
if str_eq(name, "http_post") { return 2 }
if str_eq(name, "http_post_json") { return 2 }
if str_eq(name, "http_get_with_headers") { return 2 }
if str_eq(name, "http_post_with_headers") { return 3 }
if str_eq(name, "http_post_form_auth") { return 3 }
if str_eq(name, "http_serve") { return 2 }
if str_eq(name, "http_set_handler") { return 1 }
// Seed primitives (__-prefix) runtime/el_seed.c
if str_eq(name, "__str_len") { return 1 }
if str_eq(name, "__str_char_at") { return 2 }
if str_eq(name, "__str_alloc") { return 1 }
if str_eq(name, "__str_set_char") { return 3 }
if str_eq(name, "__str_cmp") { return 2 }
if str_eq(name, "__str_ncmp") { return 3 }
if str_eq(name, "__str_concat_raw") { return 2 }
if str_eq(name, "__str_slice_raw") { return 3 }
if str_eq(name, "__int_to_str") { return 1 }
if str_eq(name, "__str_to_int") { return 1 }
if str_eq(name, "__float_to_str") { return 1 }
if str_eq(name, "__str_to_float") { return 1 }
if str_eq(name, "__println") { return 1 }
if str_eq(name, "__print") { return 1 }
if str_eq(name, "__readline") { return 0 }
if str_eq(name, "__fs_read") { return 1 }
if str_eq(name, "__fs_write") { return 2 }
if str_eq(name, "__fs_exists") { return 1 }
if str_eq(name, "__fs_list_raw") { return 1 }
if str_eq(name, "__fs_mkdir") { return 1 }
if str_eq(name, "__fs_write_bytes") { return 3 }
if str_eq(name, "__http_do") { return 5 }
if str_eq(name, "__http_do_map") { return 5 }
if str_eq(name, "__http_do_to_file") { return 5 }
if str_eq(name, "__http_serve") { return 2 }
if str_eq(name, "__http_serve_v2") { return 2 }
if str_eq(name, "__http_response") { return 3 }
if str_eq(name, "__thread_create") { return 2 }
if str_eq(name, "__thread_join") { return 1 }
if str_eq(name, "__mutex_new") { return 0 }
if str_eq(name, "__mutex_lock") { return 1 }
if str_eq(name, "__mutex_unlock") { return 1 }
if str_eq(name, "__exec") { return 1 }
if str_eq(name, "__exec_bg") { return 1 }
if str_eq(name, "__env_get") { return 1 }
if str_eq(name, "__args_json") { return 0 }
if str_eq(name, "__exit_program") { return 1 }
if str_eq(name, "__time_now_ns") { return 0 }
if str_eq(name, "__sleep_ms") { return 1 }
if str_eq(name, "__uuid_v4") { return 0 }
if str_eq(name, "__sqrt_f") { return 1 }
if str_eq(name, "__log_f") { return 1 }
if str_eq(name, "__ln_f") { return 1 }
if str_eq(name, "__sin_f") { return 1 }
if str_eq(name, "__cos_f") { return 1 }
if str_eq(name, "__pi_f") { return 0 }
if str_eq(name, "__state_set") { return 2 }
if str_eq(name, "__state_get") { return 1 }
if str_eq(name, "__state_del") { return 1 }
if str_eq(name, "__state_keys") { return 0 }
if str_eq(name, "__html_sanitize") { return 2 }
if str_eq(name, "__url_encode") { return 1 }
if str_eq(name, "__url_decode") { return 1 }
if str_eq(name, "__json_get") { return 2 }
if str_eq(name, "__json_get_raw") { return 2 }
if str_eq(name, "__json_parse_map") { return 1 }
if str_eq(name, "__json_stringify_val") { return 1 }
if str_eq(name, "__json_array_len") { return 1 }
if str_eq(name, "__json_array_get") { return 2 }
if str_eq(name, "__json_array_get_string") { return 2 }
if str_eq(name, "__json_set") { return 3 }
if str_eq(name, "__engram_node") { return 3 }
if str_eq(name, "__engram_node_full") { return 8 }
if str_eq(name, "__engram_get_node") { return 1 }
if str_eq(name, "__engram_strengthen") { return 1 }
if str_eq(name, "__engram_forget") { return 1 }
if str_eq(name, "__engram_node_count") { return 0 }
if str_eq(name, "__engram_search") { return 2 }
if str_eq(name, "__engram_scan_nodes") { return 2 }
if str_eq(name, "__engram_connect") { return 4 }
if str_eq(name, "__engram_edge_between") { return 2 }
if str_eq(name, "__engram_neighbors") { return 1 }
if str_eq(name, "__engram_neighbors_filtered") { return 3 }
if str_eq(name, "__engram_activate") { return 2 }
if str_eq(name, "__engram_activate_json") { return 2 }
if str_eq(name, "__engram_op_assert_json") { return 2 }
if str_eq(name, "__engram_node_full_in") { return 9 }
if str_eq(name, "__engram_connect_in") { return 5 }
if str_eq(name, "__engram_scan_nodes_json") { return 2 }
if str_eq(name, "__engram_edges_json") { return 2 }
if str_eq(name, "__engram_pool_stats_json") { return 0 }
if str_eq(name, "__el_alloc_count") { return 0 }
if str_eq(name, "__el_alloc_bytes") { return 0 }
if str_eq(name, "__el_peak_rss") { return 0 }
if str_eq(name, "__generate") { return 1 }
// Filesystem
if str_eq(name, "fs_read") { return 1 }
if str_eq(name, "fs_write") { return 2 }
if str_eq(name, "fs_list") { return 1 }
if str_eq(name, "fs_size") { return 1 }
if str_eq(name, "fs_read_b64_chunk") { return 3 }
// JSON
if str_eq(name, "json_get") { return 2 }
if str_eq(name, "json_parse") { return 1 }
if str_eq(name, "json_stringify") { return 1 }
if str_eq(name, "json_get_string") { return 2 }
if str_eq(name, "json_get_int") { return 2 }
if str_eq(name, "json_get_float") { return 2 }
if str_eq(name, "json_get_bool") { return 2 }
if str_eq(name, "json_get_raw") { return 2 }
if str_eq(name, "json_set") { return 3 }
if str_eq(name, "json_array_len") { return 1 }
// Time
if str_eq(name, "time_now") { return 0 }
if str_eq(name, "time_now_utc") { return 0 }
if str_eq(name, "sleep_secs") { return 1 }
if str_eq(name, "sleep_ms") { return 1 }
if str_eq(name, "time_format") { return 2 }
if str_eq(name, "time_to_parts") { return 1 }
if str_eq(name, "time_from_parts") { return 3 }
if str_eq(name, "time_add") { return 3 }
if str_eq(name, "time_diff") { return 3 }
// UUID
if str_eq(name, "uuid_new") { return 0 }
if str_eq(name, "uuid_v4") { return 0 }
// Env / state
if str_eq(name, "env") { return 1 }
if str_eq(name, "state_set") { return 2 }
if str_eq(name, "state_get") { return 1 }
if str_eq(name, "state_del") { return 1 }
if str_eq(name, "state_keys") { return 0 }
// Float
if str_eq(name, "float_to_str") { return 1 }
if str_eq(name, "int_to_float") { return 1 }
if str_eq(name, "float_to_int") { return 1 }
if str_eq(name, "format_float") { return 2 }
if str_eq(name, "decimal_round") { return 2 }
if str_eq(name, "str_to_float") { return 1 }
// Math (Float)
if str_eq(name, "math_sqrt") { return 1 }
if str_eq(name, "math_log") { return 1 }
if str_eq(name, "math_ln") { return 1 }
if str_eq(name, "math_sin") { return 1 }
if str_eq(name, "math_cos") { return 1 }
if str_eq(name, "math_pi") { return 0 }
// Bool
if str_eq(name, "bool_to_str") { return 1 }
// Process
if str_eq(name, "exit_program") { return 1 }
// Process info
if str_eq(name, "getpid_now") { return 0 }
// stdout redirect (used by elc post-processing)
if str_eq(name, "stdout_to_file") { return 1 }
if str_eq(name, "stdout_restore") { return 0 }
// Subprocess execution
if str_eq(name, "exec_command") { return 1 }
if str_eq(name, "exec_capture") { return 1 }
if str_eq(name, "exec") { return 1 }
if str_eq(name, "exec_bg") { return 1 }
// CGI / DHARMA
if str_eq(name, "dharma_connect") { return 1 }
if str_eq(name, "dharma_send") { return 2 }
if str_eq(name, "dharma_activate") { return 1 }
if str_eq(name, "dharma_emit") { return 2 }
if str_eq(name, "dharma_field") { return 1 }
if str_eq(name, "dharma_strengthen") { return 2 }
if str_eq(name, "dharma_relationship") { return 1 }
if str_eq(name, "dharma_peers") { return 0 }
// Engram
if str_eq(name, "engram_node") { return 3 }
if str_eq(name, "engram_node_full") { return 8 }
if str_eq(name, "engram_get_node") { return 1 }
if str_eq(name, "engram_strengthen") { return 1 }
if str_eq(name, "engram_forget") { return 1 }
if str_eq(name, "engram_node_count") { return 0 }
if str_eq(name, "engram_search") { return 2 }
if str_eq(name, "engram_scan_nodes") { return 2 }
if str_eq(name, "engram_connect") { return 4 }
if str_eq(name, "engram_edge_between") { return 2 }
if str_eq(name, "engram_neighbors") { return 1 }
if str_eq(name, "engram_neighbors_filtered") { return 3 }
if str_eq(name, "engram_edge_count") { return 0 }
if str_eq(name, "engram_activate") { return 2 }
if str_eq(name, "engram_save") { return 1 }
if str_eq(name, "engram_load") { return 1 }
if str_eq(name, "engram_store_boot") { return 1 }
if str_eq(name, "engram_store_checkpoint") { return 0 }
if str_eq(name, "engram_store_close") { return 0 }
if str_eq(name, "engram_get_node_json") { return 1 }
if str_eq(name, "engram_get_node_by_label") { return 1 }
if str_eq(name, "engram_search_json") { return 2 }
if str_eq(name, "engram_scan_nodes_json") { return 2 }
if str_eq(name, "engram_edges_json") { return 2 }
if str_eq(name, "engram_pool_stats_json") { return 0 }
if str_eq(name, "el_alloc_count") { return 0 }
if str_eq(name, "el_alloc_bytes") { return 0 }
if str_eq(name, "el_peak_rss") { return 0 }
if str_eq(name, "el_black_box") { return 1 }
if str_eq(name, "engram_neighbors_json") { return 3 }
if str_eq(name, "engram_activate_json") { return 2 }
if str_eq(name, "engram_stats_json") { return 0 }
if str_eq(name, "engram_op_assert_json") { return 2 }
if str_eq(name, "engram_node_full_in") { return 9 }
if str_eq(name, "engram_connect_in") { return 5 }
// LLM
if str_eq(name, "llm_call") { return 2 }
if str_eq(name, "llm_call_system") { return 3 }
if str_eq(name, "llm_call_agentic") { return 4 }
if str_eq(name, "llm_vision") { return 4 }
if str_eq(name, "llm_models") { return 0 }
if str_eq(name, "llm_register_tool") { return 2 }
// Crypto
if str_eq(name, "sha256_hex") { return 1 }
if str_eq(name, "sha256_bytes") { return 1 }
if str_eq(name, "hmac_sha256_hex") { return 2 }
if str_eq(name, "hmac_sha256_bytes") { return 2 }
if str_eq(name, "base64_encode") { return 1 }
if str_eq(name, "base64_decode") { return 1 }
if str_eq(name, "base64url_encode") { return 1 }
if str_eq(name, "base64url_decode") { return 1 }
// Native VM aliases
if str_eq(name, "native_list_get") { return 2 }
if str_eq(name, "native_list_len") { return 1 }
if str_eq(name, "native_list_append") { return 2 }
if str_eq(name, "native_list_empty") { return 0 }
if str_eq(name, "native_list_clone") { return 1 }
if str_eq(name, "native_string_chars") { return 1 }
if str_eq(name, "native_int_to_str") { return 1 }
// Method-call aliases
if str_eq(name, "append") { return 2 }
if str_eq(name, "len") { return 1 }
if str_eq(name, "get") { return 2 }
if str_eq(name, "map_get") { return 2 }
if str_eq(name, "map_set") { return 3 }
// Threading seed primitives
if str_eq(name, "__thread_create") { return 2 }
if str_eq(name, "__thread_join") { return 1 }
if str_eq(name, "__mutex_new") { return 0 }
if str_eq(name, "__mutex_lock") { return 1 }
if str_eq(name, "__mutex_unlock") { return 1 }
// Channel seed primitives
if str_eq(name, "__channel_new") { return 1 }
if str_eq(name, "__channel_send") { return 2 }
if str_eq(name, "__channel_recv") { return 1 }
if str_eq(name, "__channel_try_recv") { return 1 }
if str_eq(name, "__channel_close") { return 1 }
// Arena mark/restore builtins
if str_eq(name, "el_arena_push") { return 0 }
if str_eq(name, "el_arena_pop") { return 1 }
// -1 sentinel: variadic / unknown / user-defined -> no check.
return -1
}
fn arity_record_violation(fn_name: String, expected: Int, actual: Int) -> Bool {
let csv: String = state_get("__arity_violations")
if str_eq(csv, "") { let csv = "," }
// Encode as fn_name|expected|actual to recover all three at emit time.
let entry: String = fn_name + "|" + native_int_to_str(expected) + "|" + native_int_to_str(actual)
let key: String = "," + entry + ","
if str_contains(csv, key) { return true }
state_set("__arity_violations", csv + entry + ",")
return true
}
// Validate the call's arity against the builtin table. Returns true (always)
// because cg_expr ignores the result; -1 from builtin_arity signals
// "no check possible" (variadic or user-defined). A mismatch is recorded
// and surfaced as an #error at the bottom of the generated C, so cc fails
// before it ever attempts to type-check the wrong call.
fn arity_check_call(fn_name: String, actual: Int) -> Bool {
let expected: Int = builtin_arity(fn_name)
if expected < 0 { return true }
if expected == actual { return true }
arity_record_violation(fn_name, expected, actual)
return true
}
// Emit recorded arity violations as #error directives.
fn emit_arity_violations() -> Void {
let csv: String = state_get("__arity_violations")
if str_eq(csv, "") { return }
if str_eq(csv, ",") { return }
let n: Int = str_len(csv)
let i: Int = 1
while i < n {
let next_comma: Int = str_index_of(str_slice(csv, i, n), ",")
if next_comma < 0 { return }
let entry: String = str_slice(csv, i, i + next_comma)
let p1: Int = str_index_of(entry, "|")
if p1 > 0 {
let fn_name: String = str_slice(entry, 0, p1)
let rest: String = str_slice(entry, p1 + 1, str_len(entry))
let p2: Int = str_index_of(rest, "|")
if p2 > 0 {
let exp_s: String = str_slice(rest, 0, p2)
let act_s: String = str_slice(rest, p2 + 1, str_len(rest))
emit_line("#error \"arity error: '" + fn_name + "' takes " + exp_s + " arguments, but called with " + act_s + "\"")
}
}
let i = i + next_comma + 1
}
}
fn add_int_name(name: String) -> Bool {
let csv: String = state_get("__int_names")
@@ -3586,7 +3206,6 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
state_set("__program_kind", kind)
// Clear capability-violation accumulator from any prior compile.
// Clear arity-violation accumulator from any prior compile.
state_set("__arity_violations", "")
// Clear temporal-type-violation accumulator from any prior compile.
state_set("__time_violations", "")
@@ -3853,7 +3472,6 @@ fn codegen(stmts: [Map<String, Any>], source: String) -> String {
// Same for builtin-arity violations: cc halts on the first #error,
// so a misuse of a known builtin (wrong arg count) fails the build
// with a clear message naming the builtin and its expected arity.
emit_arity_violations()
// Temporal-type violations (Instant + Instant, Duration + Int, -).
emit_time_violations()
@@ -4364,7 +3982,6 @@ fn emit_streaming_preamble(sigs: [Map<String, Any>], source: String) -> Void {
if cgi_count >= 1 { let kind = "cgi" }
if svc_count >= 1 { let kind = "service" }
state_set("__program_kind", kind)
state_set("__arity_violations", "")
state_set("__time_violations", "")
emit_line("#include <stdint.h>")
@@ -4888,7 +4505,6 @@ fn codegen_streaming(tokens: [Any], sigs: [Map<String, Any>], source: String) ->
emit_line(" return 0;")
emit_line("}")
emit_blank()
emit_arity_violations()
emit_time_violations()
el_arena_pop(main_arena_mark)
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Control for arity-from-header: the runtime declares its own surface, so the
# compiler does not carry a second copy of it.
set -uo pipefail
ELC="${1:?usage: arity_query.sh <elc>}"
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 main() {\n println("a", "b")\n}\n' > "$W/bad.el"
EL_RELATIONS_OUT="$W/r1.txt" "$ELC" "$W/bad.el" >/dev/null 2>&1
chk "the emitter does not adjudicate arity" "0" "$("$ELC" "$W/bad.el" 2>/dev/null | grep -c 'arity error')"
out=$("$LANG_DIR/tools/check/arity.sh" "$W/r1.txt" 2>&1); rc=$?
chk "a wrong-arity call is caught" "1" "$rc"
chk "the expected count is correct" "1" "$(echo "$out" | grep -c "takes 1 arguments, called with 2")"
printf 'fn main() {\n println("a")\n}\n' > "$W/ok.el"
EL_RELATIONS_OUT="$W/r2.txt" "$ELC" "$W/ok.el" >/dev/null 2>&1
"$LANG_DIR/tools/check/arity.sh" "$W/r2.txt" >/dev/null 2>&1
chk "a correct call is clean" "0" "$?"
# multi-line declarations must not parse as zero params
n=$("$LANG_DIR/tools/check/arity.sh" "$W/r2.txt" | grep -oE '[0-9]+ signatures')
chk "signatures parsed from the header" "503 signatures" "$n"
echo; echo " 5 assertions, $((5-F)) passed, $F failed"; exit $F
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# arity.sh — check call arity against the runtime's OWN declarations.
#
# codegen.el carried builtin_arity(): 344 lines, 300 entries, of which 243 were
# an exact duplicate of el_runtime.h. Measured drift between them was zero --
# the duplicate had been maintained correctly -- but 199 functions the runtime
# declares had NO entry, so calling them with the wrong argument count produced
# no El-level diagnostic at all. The table was not wrong, it was 40% incomplete.
#
# Deriving from the header fixes the coverage and makes drift impossible.
set -uo pipefail
REL="${1:?usage: arity.sh <relations-file> [runtime-header]}"
HDR="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/runtime/el_runtime.h}"
[ -f "$REL" ] || exit 0
[ -f "$HDR" ] || { echo "no header: $HDR" >&2; exit 0; }
SIG=$(mktemp); trap 'rm -f "$SIG"' EXIT
# Declarations may span lines, so join continuations before parsing. Reading
# only the first line silently yields 0 params, and a checker that reports the
# wrong expected count is worse than no checker at all.
sed 's://.*::' "$HDR" | tr '\n' ' ' | sed 's:/\*[^*]*\*/: :g; s/;/;\n/g' | awk '
/el_val_t[[:space:]]+[a-z0-9_]+[[:space:]]*\(/ {
line=$0
match(line, /el_val_t[[:space:]]+[a-z0-9_]+/); name=substr(line,RSTART,RLENGTH)
sub(/el_val_t[[:space:]]+/,"",name)
match(line, /\(.*\)/); params=substr(line,RSTART+1,RLENGTH-2)
gsub(/^[[:space:]]+|[[:space:]]+$/,"",params)
if (params=="void" || params=="") n=0
else { n=1; for(i=1;i<=length(params);i++) if(substr(params,i,1)==",") n++ }
if (line ~ /\.\.\./) n=-1
print name, n
}' | sort -u > "$SIG"
V=0
while read -r callee _ rest; do
[ "${rest#arity:}" = "$rest" ] && continue
actual="${rest#arity:}"
expected=$(awk -v n="$callee" '$1==n {print $2; exit}' "$SIG")
# 60 of 500 runtime decls carry a __ prefix: El's `println` is C's
# `__println`. codegen owns that mapping and its table carried BOTH keys.
# One rule covers every one of them.
[ -n "$expected" ] || expected=$(awk -v n="__$callee" '$1==n {print $2; exit}' "$SIG")
[ -n "$expected" ] || continue # not a runtime builtin
[ "$expected" = "-1" ] && continue # variadic
if [ "$actual" != "$expected" ]; then
printf "arity error: '%s' takes %s arguments, called with %s\n" "$callee" "$expected" "$actual"
V=$((V+1))
fi
done < <(sort -u "$REL")
[ "$V" -eq 0 ] && echo "arity: clean ($(wc -l < "$SIG" | tr -d ' ') signatures from the header)"
exit "$V"