From 9cc6040df26eef5ad1718671792a2bcc87d43ec0 Mon Sep 17 00:00:00 2001 From: bigmerge Date: Mon, 17 Aug 2026 09:21:10 -0500 Subject: [PATCH] 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. --- lang/el-compiler/src/codegen.el | 396 +------------------------- lang/tests/integration/arity_query.sh | 26 ++ lang/tools/check/arity.sh | 52 ++++ 3 files changed, 84 insertions(+), 390 deletions(-) create mode 100755 lang/tests/integration/arity_query.sh create mode 100755 lang/tools/check/arity.sh diff --git a/lang/el-compiler/src/codegen.el b/lang/el-compiler/src/codegen.el index 01a9346..6a0aa3b 100644 --- a/lang/el-compiler/src/codegen.el +++ b/lang/el-compiler/src/codegen.el @@ -1000,12 +1000,17 @@ fn cg_expr(expr: Map) -> 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], 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], 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], 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 ") @@ -4888,7 +4505,6 @@ fn codegen_streaming(tokens: [Any], sigs: [Map], source: String) -> emit_line(" return 0;") emit_line("}") emit_blank() - emit_arity_violations() emit_time_violations() el_arena_pop(main_arena_mark) diff --git a/lang/tests/integration/arity_query.sh b/lang/tests/integration/arity_query.sh new file mode 100755 index 0000000..6a9e9b9 --- /dev/null +++ b/lang/tests/integration/arity_query.sh @@ -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 }" +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 diff --git a/lang/tools/check/arity.sh b/lang/tools/check/arity.sh new file mode 100755 index 0000000..c0d7a75 --- /dev/null +++ b/lang/tools/check/arity.sh @@ -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 [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"