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"