From 8e525758172687491c250c76e61e707a3e15031d Mon Sep 17 00:00:00 2001 From: deepfates Date: Mon, 28 Sep 2026 07:12:14 -0700 Subject: [PATCH] Signature fields named nil, true or false keep their names --- lib/imp/signature/field.ex | 17 +++++++++++++++-- test/property_invariants_test.exs | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/imp/signature/field.ex b/lib/imp/signature/field.ex index db4ef274..b853dd00 100644 --- a/lib/imp/signature/field.ex +++ b/lib/imp/signature/field.ex @@ -69,6 +69,7 @@ defmodule Imp.Signature.Field do end def new(name, kind) when is_atom(name) do + name = normalize_name(name) %__MODULE__{name: name, kind: normalize_kind(kind), prefix: infer_prefix(name)} end @@ -172,10 +173,22 @@ defmodule Imp.Signature.Field do end end + # `nil`, `true` and `false` are atoms but not names: a field given one of + # them as an atom is refused, and one written as text keeps its text, so + # `"nil: string -> a"` has a field named "nil" that saves and loads as "nil". + defp normalize_name(name) when name in [nil, true, false] do + raise ArgumentError, + "Imp.Signature.Field.new/2 expects a field name, got: #{inspect(name)}" + end + defp normalize_name(name) when is_atom(name), do: name - defp normalize_name(name) when is_binary(name), - do: name |> String.trim() |> existing_atom_or_string() + defp normalize_name(name) when is_binary(name) do + case String.trim(name) do + literal when literal in ["nil", "true", "false"] -> literal + trimmed -> existing_atom_or_string(trimmed) + end + end defp normalize_name(name) do raise ArgumentError, diff --git a/test/property_invariants_test.exs b/test/property_invariants_test.exs index 47b95c14..80675e98 100644 --- a/test/property_invariants_test.exs +++ b/test/property_invariants_test.exs @@ -43,6 +43,22 @@ defmodule PropertyInvariantsTest do end end + test "fields named nil, true and false keep their names through dump and load" do + signature = Imp.signature("nil: string, true: string -> false: string") + loaded = signature |> Imp.Signature.dump() |> Jason.encode!() |> Jason.decode!() + loaded = Imp.Signature.load!(loaded) + + assert Imp.Signature.input_names(signature) == ["nil", "true"] + assert Imp.Signature.input_names(loaded) == ["nil", "true"] + assert Imp.Signature.output_names(loaded) == ["false"] + end + + test "a field name given as the atom nil is refused" do + assert_raise ArgumentError, ~r/expects a field name, got: nil/, fn -> + Imp.Signature.Field.new(nil, :input) + end + end + property "signature dump/load round-trips the serialized contract" do check all(spec <- signature_spec(), max_runs: 100) do signature = Imp.signature(spec)