land arity-from-header: the runtime declares its own surface

This commit is contained in:
bigmerge
2026-08-17 09:21:10 -05:00
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"