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
|
||||
case Process.get(@node_by_id_key, %{}) do
|
||||
%{^id => details} -> {:ok, details}
|
||||
# IMPROVEMENT: Handle placeholder details gracefully
|
||||
%{^id => {:placeholder, _spec}} -> {:ok, {:placeholder, "Knot-tying placeholder"}}
|
||||
%{} -> {:error, :not_found}
|
||||
end
|
||||
end
|
||||
@ -908,7 +910,16 @@ defmodule Tdd.Store do
|
||||
"""
|
||||
@spec create_placeholder(TypeSpec.t()) :: non_neg_integer()
|
||||
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
|
||||
|
||||
@doc """
|
||||
@ -926,6 +937,8 @@ defmodule Tdd.Store do
|
||||
nodes = Process.get(@nodes_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)
|
||||
nodes = Map.delete(nodes, old_details)
|
||||
|
||||
@ -1357,6 +1370,28 @@ defmodule Tdd.Algo do
|
||||
do: Store.true_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 ->
|
||||
{var2, y2, n2, d2} = u2_details
|
||||
|
||||
@ -1432,6 +1467,11 @@ defmodule Tdd.Algo do
|
||||
|
||||
{:ok, {var, y, n, 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
|
||||
|
||||
Store.put_op_cache(cache_key, result_id)
|
||||
@ -1445,9 +1485,16 @@ defmodule Tdd.Algo do
|
||||
def simplify(tdd_id, assumptions \\ %{}) do
|
||||
sorted_assumptions = Enum.sort(assumptions)
|
||||
cache_key = {:simplify, tdd_id, sorted_assumptions}
|
||||
require Logger
|
||||
|
||||
case Store.get_op_cache(cache_key) do
|
||||
{:ok, result_id} ->
|
||||
Logger.info("result #{inspect([result_id])}")
|
||||
|
||||
if result_id == 1 do
|
||||
Logger.info("ASD #{inspect([tdd_id, assumptions])}")
|
||||
end
|
||||
|
||||
result_id
|
||||
|
||||
:not_found ->
|
||||
@ -1640,6 +1687,9 @@ defmodule Tdd.Algo do
|
||||
if Engine.check(assumptions) == :contradiction do
|
||||
Store.false_node_id()
|
||||
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
|
||||
{:ok, :true_terminal} ->
|
||||
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."
|
||||
@spec spec_to_id(TypeSpec.t()) :: non_neg_integer()
|
||||
def spec_to_id(spec) do
|
||||
# It's crucial to initialize the store for each top-level compilation
|
||||
# to ensure a clean slate for caches and node IDs. This makes calls independent.
|
||||
# FIX: State initialization is moved to the public API boundary (`is_subtype/2`),
|
||||
# making this function a pure compiler component.
|
||||
normalized_spec = TypeSpec.normalize(spec)
|
||||
compile_normalized_spec(normalized_spec, %{})
|
||||
end
|
||||
|
||||
# This context-aware compilation function from the previous fix remains correct.
|
||||
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
|
||||
{:ok, id} ->
|
||||
id
|
||||
@ -1908,17 +1949,20 @@ defmodule Tdd.Compiler do
|
||||
:not_found ->
|
||||
id_to_cache =
|
||||
case normalized_spec do
|
||||
{:mu, var_name, body_spec} ->
|
||||
placeholder_node_variable_tag = {:mu_placeholder_for_var, var_name}
|
||||
placeholder_id = Store.create_placeholder(placeholder_node_variable_tag)
|
||||
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)
|
||||
{:type_var, var_name} ->
|
||||
Map.get(context, var_name) ||
|
||||
raise "Tdd.Compiler: Unbound type variable: #{inspect(var_name)}"
|
||||
|
||||
other ->
|
||||
raw_id = do_structural_compile(other, context)
|
||||
{:mu, var_name, body_spec} ->
|
||||
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)
|
||||
end
|
||||
|
||||
@ -1926,7 +1970,6 @@ defmodule Tdd.Compiler do
|
||||
id_to_cache
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_structural_compile(structural_spec, context) do
|
||||
case structural_spec do
|
||||
@ -1975,8 +2018,6 @@ defmodule Tdd.Compiler do
|
||||
{:negation, sub_spec} ->
|
||||
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} ->
|
||||
id_list = compile_normalized_spec(:list, context)
|
||||
id_is_empty = create_base_type_tdd(Variable.v_list_is_empty())
|
||||
@ -1985,11 +2026,9 @@ defmodule Tdd.Compiler do
|
||||
non_empty_list_id =
|
||||
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)
|
||||
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_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)
|
||||
end)
|
||||
|
||||
# REFAC: Same change for tuples.
|
||||
{:tuple, elements_specs} ->
|
||||
size = length(elements_specs)
|
||||
base_id = compile_normalized_spec(:tuple, context)
|
||||
@ -2011,9 +2049,7 @@ defmodule Tdd.Compiler do
|
||||
elements_specs
|
||||
|> Enum.with_index()
|
||||
|> 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)
|
||||
# 2. Embed the TDD ID into the variable.
|
||||
elem_checker_var = Variable.v_tuple_elem_pred(index, elem_id)
|
||||
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."
|
||||
@spec is_subtype(TypeSpec.t(), TypeSpec.t()) :: boolean
|
||||
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)
|
||||
id2 = spec_to_id(spec2)
|
||||
neg_id2 = Algo.negate(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 == Store.false_node_id()
|
||||
end
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user