From fcdb398f7998656746505a6f4fda8349509804b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20L=C3=BCning?= Date: Tue, 20 Oct 2026 10:24:37 +0200 Subject: [PATCH] defk-rt2.10: priv/defk_runtime.h lists the interface the compiler emits, checked by one test A comment block beside the header's Responsibilities index lists the runtime names the generated C uses (interface:start/end) and the names the generated C defines itself (kernel:start/end). test/runtime_interface_test.exs reads the names from the strings of compiler.ex, raise.ex and strip.ex (an interpolation as a wildcard) and checks the lists both ways, and that the interface names are the header's. The compiler map's line count is fixed. --- docs/agents/compiler-map.md | 9 +- priv/defk_runtime.h | 59 ++++++++++++ test/runtime_interface_test.exs | 158 ++++++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 test/runtime_interface_test.exs diff --git a/docs/agents/compiler-map.md b/docs/agents/compiler-map.md index 10188b72..5e6379c0 100644 --- a/docs/agents/compiler-map.md +++ b/docs/agents/compiler-map.md @@ -140,14 +140,19 @@ of rows the lowering declares as it emits; defk-ch4), not from the C text. key), warning once per use site. How to add an entry: `docs/agents/deprecations.md`. -## priv/defk_runtime.h (~1,900 lines) +## priv/defk_runtime.h (~2,500 lines) The C runtime included in every generated file: the `defk_ctx` (owned buffers, `owned_bytes` budget, stats), frames and safepoints (yield, resume, forced yields), input binding on every entry (binaries move across GC), `defk_make_arr` / `defk_make_arr_view`, strips (`DEFK_ZERO_STRIP`/`DEFK_COPY_STRIP`), range copies, crc32c. The -"Responsibilities" comment at the top is the index. Its raise sites use the +"Responsibilities" comment at the top is the index. Beside it, the +interface list names every runtime symbol the generated C uses, and a +second list names what the generated C defines itself; every other name is +internal. `test/runtime_interface_test.exs` checks both lists against the +strings of `compiler.ex`, `raise.ex` and `strip.ex`: a name the compiler +starts to emit goes into a list in the same change. The header's raise sites use the `DEFK_MSG_` defines of the generated `priv/defk_messages.h`, never a literal message (`Defk.Messages`). diff --git a/priv/defk_runtime.h b/priv/defk_runtime.h index cb4389b6..c8bcdc63 100644 --- a/priv/defk_runtime.h +++ b/priv/defk_runtime.h @@ -6,6 +6,65 @@ * - checked arithmetic / bounds checks that raise Elixir exceptions * - timeslice accounting so long kernels yield back to the scheduler * - the frame resource that holds a suspended kernel between yields + * + * Interface: the names the generated C uses (the strings of + * lib/defk/compiler.ex, raise.ex and strip.ex); every other name here is + * internal. is an element type (u8 i16 i32 i64 u64 f32 f64), an + * argument kind, a float operation or builtin. A test + * (test/runtime_interface_test.exs) checks this list against the + * compiler's strings both ways. + * + * interface:start + * results and raises: DEFK_DONE DEFK_YIELD DEFK_ERR DEFK_E_ARG DEFK_E_ARITH + * DEFK_E_RUNTIME defk_raise defk_fail defk_user_raise defk_user_raise_ints + * defk_no_clause defk_no_fun_clause defk_range_limit DEFK_LIKELY + * DEFK_UNLIKELY + * the context, frames and the library image: defk_ctx defk_cur_ctx + * defk_frame_of defk_promote defk_finish defk_slice_init defk_load + * defk_upgrade defk_unload DEFK_HEADER_HASH DEFK_MAX_OWNED DEFK_FAULT_HOOKS + * defk_stats_nif defk_force_yields_nif defk_inject_fault_nif + * defk_corrupt_tags_nif + * yields: DEFK_BUDGET0 DEFK_ZERO_TICKS defk_clock defk_clock_init defk_tick + * defk_lost_resume defk_stat_yields + * arguments and results: defk_get__arg defk_make_arr defk_make_arr_view + * defk_make_bits defk_make_bool defk_make_f64 + * arrays: defk_arr_ defk_alloc_ defk_alloc_raw_ defk_copy_ + * defk_copy_raw_ defk_get_ defk_put_ defk_to_ defk_to_i64_elem + * defk_rcopy_ defk_rcopy_back_ defk_bind defk_bind_strip + * defk_zero_strip defk_copy_strip defk_back_strip defk_move_strip + * DEFK_ZERO_STRIP DEFK_COPY_STRIP DEFK_STRIP + * bitstrings and word loads: defk_bits defk_bits_put defk_bits_put_signed + * defk_bits_put_u64 defk_u16_le defk_u16_be defk_u32_le defk_u32_be + * defk_u64_le defk_u64_be defk_u16_le_at defk_u16_be_at defk_u32_le_at + * defk_u32_be_at defk_u64_le_at defk_u64_be_at + * integers: defk_add_i64 defk_sub_i64 defk_mul_i64 defk_neg_i64 defk_abs_i64 + * defk_div_i64 defk_rem_i64 defk_bsl_i64 defk_bsr_i64 defk_add_i64_s + * defk_sub_i64_s defk_mul_i64_s defk_neg_i64_s defk_abs_i64_s + * defk_bsl_i64_s defk_bsr_i64_s defk_div_i64_g defk_rem_i64_g + * defk_add_u64 defk_sub_u64 defk_mul_u64 defk_div_u64 defk_rem_u64 + * defk_bsl_u64 defk_bsr_u64 defk_add_u64_s defk_sub_u64_s defk_div_u64_g + * defk_rem_u64_g defk_bsr_nn defk_wbsl defk_mulhi defk_u64_sat + * defk_i64_to_u64 defk_u64_to_i64 defk_f64_to_u64 defk_to_u64 + * defk_cmp_i64_f64 defk_cmp_u64_f64 defk_cmp_u64_i64 defk_min_i64 + * defk_max_i64 defk_min_u64 defk_max_u64 defk_clz defk_ctz defk_clz_i + * defk_clz_u defk_ctz_i defk_ctz_u defk_popcount defk_rotl_i defk_rotl_u + * defk_rotr_i defk_rotr_u defk_rotl64 defk_rotr64 defk_crc32c_ii + * defk_crc32c_iu defk_crc32c_uu + * floats and non-finite values: defk_fadd defk_fsub defk_fmul defk_fdiv + * defk_fadd_s defk_fmul_s defk_fdiv_s defk_min_f64 defk_max_f64 defk_sqrt + * defk_exp defk_log defk_log2 defk_log10 defk_pow defk_trunc defk_round + * defk_trunc_s defk_feq_exact defk_in_f64 defk_in_f64_later defk_get_in_f64 + * DEFK_FCHECK DEFK_NONFIN DEFK_NF_FCHECK DEFK_NF_ defk_nf defk_nf_sink + * defk_sink defk_nonfinite_lost + * interface:end + * + * The generated C defines these itself ( is a counter): + * + * kernel:start + * DEFK_SP DEFK_SAVE DEFK_SAVE DEFK_SHORT DEFK_TICKS DEFK_RESUMING defk_rs + * defk_ticks defk_budget defk_clk defk_funcs defk_slot defk_z0 defk_a + * defk_l defk_halt defk_r defk_rp defk_s_ defk_lit_ + * kernel:end */ #ifndef DEFK_RUNTIME_H #define DEFK_RUNTIME_H diff --git a/test/runtime_interface_test.exs b/test/runtime_interface_test.exs new file mode 100644 index 00000000..f6d224c4 --- /dev/null +++ b/test/runtime_interface_test.exs @@ -0,0 +1,158 @@ +# The interface list at the top of priv/defk_runtime.h against the names +# the compiler writes into a kernel's C, both ways. The names are read from +# the strings of the modules that write C (an interpolation in a name is a +# wildcard, `defk_get_#{e}` is defk_get_*), not from atoms, sigils or +# comments. A list entry's is a wildcard too. A name the compiler builds +# from variables only (`defk_#{f}_#{s}`) has no fixed part to check; an +# entry no other name covers passes when each of its pieces is an atom or +# a string piece of the sources (defk_clz_i by :clz and "i"), which is +# weaker. +defmodule Defk.RuntimeInterfaceTest do + use ExUnit.Case, async: true + + @sources Enum.map(~w(compiler.ex raise.ex strip.ex), &Path.expand("../lib/defk/#{&1}", __DIR__)) + + # where the builtins' names are, for the pieces + @words_sources [Path.expand("../lib/defk/builtins.ex", __DIR__) | @sources] + + @name ~r/(?:start" and ":end", a label before + # ": " dropped, as * + defp entries(marker) do + [_, block] = String.split(File.read!(header()), "#{marker}:start\n") + [block, _] = String.split(block, " * #{marker}:end") + + for line <- String.split(block, "\n"), + names = line |> String.trim_leading(" * ") |> String.split(": ") |> List.last(), + name <- String.split(names), + do: String.replace(name, ~r/<[A-Za-z]+>/, "*") + end + + # the names in the strings of the sources, an interpolation as * + defp forms do + for path <- @sources, + string <- strings(Code.string_to_quoted!(File.read!(path))), + [name] <- Regex.scan(@name, string), + uniq: true, + do: String.replace(name, ~r/\*+/, "*") + end + + defp strings(ast) do + {_, acc} = + Macro.prewalk(ast, [], fn + {sigil, _, _}, acc when sigil in [:sigil_r, :sigil_S, :sigil_s, :sigil_w] -> + {:skip, acc} + + {:<<>>, _, parts}, acc when is_list(parts) -> + {:skip, [Enum.map_join(parts, &if(is_binary(&1), do: &1, else: "*")) | acc]} + + string, acc when is_binary(string) -> + {string, [string | acc]} + + node, acc -> + {node, acc} + end) + + acc + end + + # the names the header defines or uses outside its comments, a pasted + # part (`defk_get_##T`) as * + defp header, do: Application.app_dir(:defk, "priv/defk_runtime.h") + + defp header_code, do: Regex.replace(~r{/\*.*?\*/|//[^\n]*}s, File.read!(header()), "") + + defp header_names(code) do + for [name] <- + Regex.scan(~r/(? String.split("*") |> Enum.map_join("[A-Za-z0-9_]*", &Regex.escape/1) + Regex.compile!("^#{body}$") + end + + defp sample(name), do: String.replace(name, "*", "x") + + # a form with no fixed part past the prefix (`defk_#{f}`, `defk_#{f}_#{s}`) + # matches any name + defp checkable?(form), do: Regex.match?(~r/^(?:defk|DEFK)_.*[A-Za-z0-9]/, form) + + # the pieces of the atoms and strings of the sources, word lists included + defp pieces do + for path <- @words_sources, + term <- terms(Code.string_to_quoted!(File.read!(path))), + [piece] <- Regex.scan(~r/[a-z0-9]+/, term), + into: MapSet.new(), + do: piece + end + + defp terms(ast) do + {_, acc} = + Macro.prewalk(ast, [], fn + t, acc when is_binary(t) -> {t, [t | acc]} + t, acc when is_atom(t) -> {t, [Atom.to_string(t) | acc]} + node, acc -> {node, acc} + end) + + acc + end + + defp pieces?(entry, pieces) do + entry + |> String.replace(~r/^(?:defk|DEFK)_/, "") + |> String.split(["_", "*"], trim: true) + |> Enum.all?(&MapSet.member?(pieces, &1)) + end + + test "every name the compiler writes is in a list" do + listed = entries("interface") ++ entries("kernel") + + missing = + for form <- forms(), checkable?(form), not Enum.any?(listed, &agree?(form, &1)), do: form + + assert missing == [], "add to the header's interface or kernel list: #{inspect(missing)}" + end + + test "every listed name is one the compiler writes" do + forms = Enum.filter(forms(), &checkable?/1) + pieces = pieces() + + unused = + for entry <- entries("interface") ++ entries("kernel"), + not Enum.any?(forms, &agree?(&1, entry)), + not pieces?(entry, pieces), + do: entry + + assert unused == [], + "the compiler writes no such name, drop it from the list: #{inspect(unused)}" + end + + # a pasted name with no fixed part (`defk_##NAME`) is found by its pieces, + # the macro's arguments + test "the interface names are the header's, the kernel's own names are not" do + code = header_code() + names = header_names(code) + forms = Enum.filter(names, &checkable?/1) + pieces = for [piece] <- Regex.scan(~r/[a-z0-9]+/, code), into: MapSet.new(), do: piece + + undefined = + for e <- entries("interface"), + not Enum.any?(forms, &agree?(&1, e)), + not pieces?(e, pieces), + do: e + + assert undefined == [], "not a name of the header: #{inspect(undefined)}" + + clashing = for e <- entries("kernel"), not String.contains?(e, "*"), e in names, do: e + assert clashing == [], "a header name in the kernel's list: #{inspect(clashing)}" + end +end