Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/agents/compiler-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).

Expand Down
59 changes: 59 additions & 0 deletions priv/defk_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. <T> is an element type (u8 i16 i32 i64 u64 f32 f64), <arg> an
* argument kind, <op> 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>_arg defk_make_arr defk_make_arr_view
* defk_make_bits defk_make_bool defk_make_f64
* arrays: defk_arr_<T> defk_alloc_<T> defk_alloc_raw_<T> defk_copy_<T>
* defk_copy_raw_<T> defk_get_<T> defk_put_<T> defk_to_<T> defk_to_i64_elem
* defk_rcopy_<T> defk_rcopy_back_<T> 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_<op> defk_nf defk_nf_sink
* defk_sink defk_nonfinite_lost
* interface:end
*
* The generated C defines these itself (<n> is a counter):
*
* kernel:start
* DEFK_SP DEFK_SAVE DEFK_SAVE<n> DEFK_SHORT DEFK_TICKS DEFK_RESUMING defk_rs
* defk_ticks defk_budget defk_clk defk_funcs defk_slot defk_z0 defk_a<n>
* defk_l<n> defk_halt<n> defk_r<n> defk_rp<n> defk_s<n>_<n> defk_lit_<hash>
* kernel:end
*/
#ifndef DEFK_RUNTIME_H
#define DEFK_RUNTIME_H
Expand Down
158 changes: 158 additions & 0 deletions test/runtime_interface_test.exs
Original file line number Diff line number Diff line change
@@ -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 <x> 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/(?<![A-Za-z0-9_])(?:defk|DEFK)_[A-Za-z0-9_*]*+(?!\.h\b)/

# the entries between "<marker>:start" and "<marker>:end", a label before
# ": " dropped, <x> 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/(?<![A-Za-z0-9_])(?:defk|DEFK)_(?:[A-Za-z0-9_]|##\w+?(?:##|\b))*/, code),
uniq: true,
do: Regex.replace(~r/##\w+?(?:##|\b)/, name, "*")
end

# two names with wildcards agree when either one matches the other with
# its wildcards filled in
defp agree?(a, b),
do: a == b or Regex.match?(re(a), sample(b)) or Regex.match?(re(b), sample(a))

defp re(name) do
body = name |> 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