checkpoint
This commit is contained in:
parent
c2c7438d32
commit
e5485995ed
111
lib/til.ex
111
lib/til.ex
@ -848,6 +848,8 @@ defmodule Tdd.Store do
|
|||||||
def get_node(id) do
|
def get_node(id) do
|
||||||
case Process.get(@node_by_id_key, %{}) do
|
case Process.get(@node_by_id_key, %{}) do
|
||||||
%{^id => details} -> {:ok, details}
|
%{^id => details} -> {:ok, details}
|
||||||
|
# IMPROVEMENT: Handle placeholder details gracefully
|
||||||
|
%{^id => {:placeholder, _spec}} -> {:ok, {:placeholder, "Knot-tying placeholder"}}
|
||||||
%{} -> {:error, :not_found}
|
%{} -> {:error, :not_found}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -908,7 +910,16 @@ defmodule Tdd.Store do
|
|||||||
"""
|
"""
|
||||||
@spec create_placeholder(TypeSpec.t()) :: non_neg_integer()
|
@spec create_placeholder(TypeSpec.t()) :: non_neg_integer()
|
||||||
def create_placeholder(spec) do
|
def create_placeholder(spec) do
|
||||||
find_or_create_node({:placeholder, spec}, 1, 0, 0)
|
# IMPROVEMENT: Correctly reserve an ID without creating a malformed node.
|
||||||
|
next_id = Process.get(@next_id_key)
|
||||||
|
node_by_id = Process.get(@node_by_id_key)
|
||||||
|
|
||||||
|
# Associate the ID with a temporary detail for debugging and identification.
|
||||||
|
# This does NOT create an entry in the main `nodes` (reverse-lookup) map.
|
||||||
|
Process.put(@node_by_id_key, Map.put(node_by_id, next_id, {:placeholder, spec}))
|
||||||
|
Process.put(@next_id_key, next_id + 1)
|
||||||
|
|
||||||
|
next_id
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
@ -926,6 +937,8 @@ defmodule Tdd.Store do
|
|||||||
nodes = Process.get(@nodes_key)
|
nodes = Process.get(@nodes_key)
|
||||||
node_by_id = Process.get(@node_by_id_key)
|
node_by_id = Process.get(@node_by_id_key)
|
||||||
|
|
||||||
|
# This part is now safe. The old_details will be `{:placeholder, ...}`
|
||||||
|
# which does not exist as a key in the `nodes` map, so Map.delete is a no-op.
|
||||||
old_details = Map.get(node_by_id, id)
|
old_details = Map.get(node_by_id, id)
|
||||||
nodes = Map.delete(nodes, old_details)
|
nodes = Map.delete(nodes, old_details)
|
||||||
|
|
||||||
@ -1357,6 +1370,28 @@ defmodule Tdd.Algo do
|
|||||||
do: Store.true_node_id(),
|
do: Store.true_node_id(),
|
||||||
else: Store.false_node_id()
|
else: Store.false_node_id()
|
||||||
|
|
||||||
|
# FIX: Add cases to handle placeholder nodes co-inductively.
|
||||||
|
# Treat the placeholder as `any`.
|
||||||
|
match?({:ok, {:placeholder, _}}, {:ok, u1_details}) ->
|
||||||
|
# op(placeholder, u2) -> op(any, u2)
|
||||||
|
op_lambda.(:true_terminal, u2_details)
|
||||||
|
|> case do
|
||||||
|
:true_terminal -> Store.true_node_id()
|
||||||
|
:false_terminal -> Store.false_node_id()
|
||||||
|
# This happens if op(any, u2) = u2 (e.g., intersection)
|
||||||
|
_other -> u2_id
|
||||||
|
end
|
||||||
|
|
||||||
|
match?({:ok, {:placeholder, _}}, {:ok, u2_details}) ->
|
||||||
|
# op(u1, placeholder) -> op(u1, any)
|
||||||
|
op_lambda.(u1_details, :true_terminal)
|
||||||
|
|> case do
|
||||||
|
:true_terminal -> Store.true_node_id()
|
||||||
|
:false_terminal -> Store.false_node_id()
|
||||||
|
# This happens if op(u1, any) = u1 (e.g., intersection)
|
||||||
|
_other -> u1_id
|
||||||
|
end
|
||||||
|
|
||||||
u1_details == :true_terminal or u1_details == :false_terminal ->
|
u1_details == :true_terminal or u1_details == :false_terminal ->
|
||||||
{var2, y2, n2, d2} = u2_details
|
{var2, y2, n2, d2} = u2_details
|
||||||
|
|
||||||
@ -1432,6 +1467,11 @@ defmodule Tdd.Algo do
|
|||||||
|
|
||||||
{:ok, {var, y, n, d}} ->
|
{:ok, {var, y, n, d}} ->
|
||||||
Store.find_or_create_node(var, negate(y), negate(n), negate(d))
|
Store.find_or_create_node(var, negate(y), negate(n), negate(d))
|
||||||
|
|
||||||
|
# FIX: Handle placeholder nodes encountered during co-inductive simplification.
|
||||||
|
# A placeholder is treated as `any` for the check, so its negation is `none`.
|
||||||
|
{:ok, {:placeholder, _spec}} ->
|
||||||
|
Store.false_node_id()
|
||||||
end
|
end
|
||||||
|
|
||||||
Store.put_op_cache(cache_key, result_id)
|
Store.put_op_cache(cache_key, result_id)
|
||||||
@ -1445,9 +1485,16 @@ defmodule Tdd.Algo do
|
|||||||
def simplify(tdd_id, assumptions \\ %{}) do
|
def simplify(tdd_id, assumptions \\ %{}) do
|
||||||
sorted_assumptions = Enum.sort(assumptions)
|
sorted_assumptions = Enum.sort(assumptions)
|
||||||
cache_key = {:simplify, tdd_id, sorted_assumptions}
|
cache_key = {:simplify, tdd_id, sorted_assumptions}
|
||||||
|
require Logger
|
||||||
|
|
||||||
case Store.get_op_cache(cache_key) do
|
case Store.get_op_cache(cache_key) do
|
||||||
{:ok, result_id} ->
|
{:ok, result_id} ->
|
||||||
|
Logger.info("result #{inspect([result_id])}")
|
||||||
|
|
||||||
|
if result_id == 1 do
|
||||||
|
Logger.info("ASD #{inspect([tdd_id, assumptions])}")
|
||||||
|
end
|
||||||
|
|
||||||
result_id
|
result_id
|
||||||
|
|
||||||
:not_found ->
|
:not_found ->
|
||||||
@ -1640,6 +1687,9 @@ defmodule Tdd.Algo do
|
|||||||
if Engine.check(assumptions) == :contradiction do
|
if Engine.check(assumptions) == :contradiction do
|
||||||
Store.false_node_id()
|
Store.false_node_id()
|
||||||
else
|
else
|
||||||
|
IO.inspect(tdd_id, label: "Fetching node in do_check_emptiness")
|
||||||
|
# Debug.print_tdd_graph(tdd_id)
|
||||||
|
|
||||||
case Store.get_node(tdd_id) do
|
case Store.get_node(tdd_id) do
|
||||||
{:ok, :true_terminal} ->
|
{:ok, :true_terminal} ->
|
||||||
Store.true_node_id()
|
Store.true_node_id()
|
||||||
@ -1881,26 +1931,17 @@ defmodule Tdd.Compiler do
|
|||||||
@doc "The main public entry point. Takes a spec and returns its TDD ID."
|
@doc "The main public entry point. Takes a spec and returns its TDD ID."
|
||||||
@spec spec_to_id(TypeSpec.t()) :: non_neg_integer()
|
@spec spec_to_id(TypeSpec.t()) :: non_neg_integer()
|
||||||
def spec_to_id(spec) do
|
def spec_to_id(spec) do
|
||||||
# It's crucial to initialize the store for each top-level compilation
|
# FIX: State initialization is moved to the public API boundary (`is_subtype/2`),
|
||||||
# to ensure a clean slate for caches and node IDs. This makes calls independent.
|
# making this function a pure compiler component.
|
||||||
normalized_spec = TypeSpec.normalize(spec)
|
normalized_spec = TypeSpec.normalize(spec)
|
||||||
compile_normalized_spec(normalized_spec, %{})
|
compile_normalized_spec(normalized_spec, %{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This context-aware compilation function from the previous fix remains correct.
|
||||||
defp compile_normalized_spec(normalized_spec, context) do
|
defp compile_normalized_spec(normalized_spec, context) do
|
||||||
cache_key = {:spec_to_id, normalized_spec}
|
sorted_context = Enum.sort(context)
|
||||||
|
cache_key = {:compile_spec_with_context, normalized_spec, sorted_context}
|
||||||
|
|
||||||
case normalized_spec do
|
|
||||||
{:type_var, var_name} ->
|
|
||||||
case Map.get(context, var_name) do
|
|
||||||
nil ->
|
|
||||||
raise "Tdd.Compiler: Unbound type variable during TDD compilation: #{inspect(var_name)}. Full spec: #{inspect(normalized_spec)}. Context: #{inspect(context)}"
|
|
||||||
|
|
||||||
placeholder_id when is_integer(placeholder_id) ->
|
|
||||||
placeholder_id
|
|
||||||
end
|
|
||||||
|
|
||||||
_other_form ->
|
|
||||||
case Store.get_op_cache(cache_key) do
|
case Store.get_op_cache(cache_key) do
|
||||||
{:ok, id} ->
|
{:ok, id} ->
|
||||||
id
|
id
|
||||||
@ -1908,17 +1949,20 @@ defmodule Tdd.Compiler do
|
|||||||
:not_found ->
|
:not_found ->
|
||||||
id_to_cache =
|
id_to_cache =
|
||||||
case normalized_spec do
|
case normalized_spec do
|
||||||
{:mu, var_name, body_spec} ->
|
{:type_var, var_name} ->
|
||||||
placeholder_node_variable_tag = {:mu_placeholder_for_var, var_name}
|
Map.get(context, var_name) ||
|
||||||
placeholder_id = Store.create_placeholder(placeholder_node_variable_tag)
|
raise "Tdd.Compiler: Unbound type variable: #{inspect(var_name)}"
|
||||||
new_context = Map.put(context, var_name, placeholder_id)
|
|
||||||
compiled_body_id = compile_normalized_spec(body_spec, new_context)
|
|
||||||
# The substitution is the "knot-tying" step for recursion
|
|
||||||
final_id = Algo.substitute(compiled_body_id, placeholder_id, compiled_body_id)
|
|
||||||
Algo.simplify(final_id)
|
|
||||||
|
|
||||||
other ->
|
{:mu, var_name, body_spec} ->
|
||||||
raw_id = do_structural_compile(other, context)
|
placeholder_id = Store.create_placeholder({:mu_knot_tying_for, var_name, body_spec})
|
||||||
|
new_context = Map.put(context, var_name, placeholder_id)
|
||||||
|
body_id = compile_normalized_spec(body_spec, new_context)
|
||||||
|
{:ok, body_details} = Store.get_node(body_id)
|
||||||
|
Store.update_node_in_place(placeholder_id, {:ok, body_details})
|
||||||
|
Algo.simplify(placeholder_id)
|
||||||
|
|
||||||
|
other_spec ->
|
||||||
|
raw_id = do_structural_compile(other_spec, context)
|
||||||
Algo.simplify(raw_id)
|
Algo.simplify(raw_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1926,7 +1970,6 @@ defmodule Tdd.Compiler do
|
|||||||
id_to_cache
|
id_to_cache
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
defp do_structural_compile(structural_spec, context) do
|
defp do_structural_compile(structural_spec, context) do
|
||||||
case structural_spec do
|
case structural_spec do
|
||||||
@ -1975,8 +2018,6 @@ defmodule Tdd.Compiler do
|
|||||||
{:negation, sub_spec} ->
|
{:negation, sub_spec} ->
|
||||||
Algo.negate(compile_normalized_spec(sub_spec, context))
|
Algo.negate(compile_normalized_spec(sub_spec, context))
|
||||||
|
|
||||||
# REFAC: This is a key change. We now compile sub-specs to TDD IDs
|
|
||||||
# and embed those IDs in the predicate variables.
|
|
||||||
{:cons, head_spec, tail_spec} ->
|
{:cons, head_spec, tail_spec} ->
|
||||||
id_list = compile_normalized_spec(:list, context)
|
id_list = compile_normalized_spec(:list, context)
|
||||||
id_is_empty = create_base_type_tdd(Variable.v_list_is_empty())
|
id_is_empty = create_base_type_tdd(Variable.v_list_is_empty())
|
||||||
@ -1985,11 +2026,9 @@ defmodule Tdd.Compiler do
|
|||||||
non_empty_list_id =
|
non_empty_list_id =
|
||||||
Algo.apply(:intersect, &op_intersect_terminals/2, id_list, id_not_is_empty)
|
Algo.apply(:intersect, &op_intersect_terminals/2, id_list, id_not_is_empty)
|
||||||
|
|
||||||
# 1. Compile sub-specs to get their TDD IDs.
|
|
||||||
head_id = compile_normalized_spec(head_spec, context)
|
head_id = compile_normalized_spec(head_spec, context)
|
||||||
tail_id = compile_normalized_spec(tail_spec, context)
|
tail_id = compile_normalized_spec(tail_spec, context)
|
||||||
|
|
||||||
# 2. Embed the TDD IDs into the variables.
|
|
||||||
head_checker_var = Variable.v_list_head_pred(head_id)
|
head_checker_var = Variable.v_list_head_pred(head_id)
|
||||||
head_checker_tdd = create_base_type_tdd(head_checker_var)
|
head_checker_tdd = create_base_type_tdd(head_checker_var)
|
||||||
|
|
||||||
@ -2001,7 +2040,6 @@ defmodule Tdd.Compiler do
|
|||||||
Algo.apply(:intersect, &op_intersect_terminals/2, id, acc)
|
Algo.apply(:intersect, &op_intersect_terminals/2, id, acc)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
# REFAC: Same change for tuples.
|
|
||||||
{:tuple, elements_specs} ->
|
{:tuple, elements_specs} ->
|
||||||
size = length(elements_specs)
|
size = length(elements_specs)
|
||||||
base_id = compile_normalized_spec(:tuple, context)
|
base_id = compile_normalized_spec(:tuple, context)
|
||||||
@ -2011,9 +2049,7 @@ defmodule Tdd.Compiler do
|
|||||||
elements_specs
|
elements_specs
|
||||||
|> Enum.with_index()
|
|> Enum.with_index()
|
||||||
|> Enum.reduce(initial_id, fn {elem_spec, index}, acc_id ->
|
|> Enum.reduce(initial_id, fn {elem_spec, index}, acc_id ->
|
||||||
# 1. Compile element spec to get its TDD ID.
|
|
||||||
elem_id = compile_normalized_spec(elem_spec, context)
|
elem_id = compile_normalized_spec(elem_spec, context)
|
||||||
# 2. Embed the TDD ID into the variable.
|
|
||||||
elem_checker_var = Variable.v_tuple_elem_pred(index, elem_id)
|
elem_checker_var = Variable.v_tuple_elem_pred(index, elem_id)
|
||||||
elem_checker_tdd = create_base_type_tdd(elem_checker_var)
|
elem_checker_tdd = create_base_type_tdd(elem_checker_var)
|
||||||
|
|
||||||
@ -2078,10 +2114,19 @@ defmodule Tdd.Compiler do
|
|||||||
@doc "Checks if spec1 is a subtype of spec2 using TDDs."
|
@doc "Checks if spec1 is a subtype of spec2 using TDDs."
|
||||||
@spec is_subtype(TypeSpec.t(), TypeSpec.t()) :: boolean
|
@spec is_subtype(TypeSpec.t(), TypeSpec.t()) :: boolean
|
||||||
def is_subtype(spec1, spec2) do
|
def is_subtype(spec1, spec2) do
|
||||||
|
# FIX: Each subtyping check is an independent, top-level operation.
|
||||||
|
# It must initialize the store to guarantee a clean slate and prevent
|
||||||
|
# state from leaking between unrelated checks.
|
||||||
|
Store.init()
|
||||||
|
|
||||||
id1 = spec_to_id(spec1)
|
id1 = spec_to_id(spec1)
|
||||||
id2 = spec_to_id(spec2)
|
id2 = spec_to_id(spec2)
|
||||||
neg_id2 = Algo.negate(id2)
|
neg_id2 = Algo.negate(id2)
|
||||||
intersect_id = Algo.apply(:intersect, &op_intersect_terminals/2, id1, neg_id2)
|
intersect_id = Algo.apply(:intersect, &op_intersect_terminals/2, id1, neg_id2)
|
||||||
|
|
||||||
|
# The crash was because `intersect_id` was not an integer, which should not
|
||||||
|
# happen if `apply` works correctly on valid IDs. By fixing the state
|
||||||
|
# initialization, `id1` and `id2` will now be valid in the same store context.
|
||||||
final_id = Algo.check_emptiness(intersect_id)
|
final_id = Algo.check_emptiness(intersect_id)
|
||||||
final_id == Store.false_node_id()
|
final_id == Store.false_node_id()
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user