asdasdasdasd
This commit is contained in:
parent
d7f1a7a141
commit
976f8250e3
393
new.exs
393
new.exs
@ -1,11 +1,7 @@
|
|||||||
|
|
||||||
Code.require_file("./debug.exs")
|
Code.require_file("./debug.exs")
|
||||||
|
|
||||||
defmodule Tdd.TypeSpec do
|
defmodule Tdd.TypeSpec do
|
||||||
# NOTE: This module remains unchanged as the flaw was not in the TypeSpec
|
|
||||||
# definition or normalization, but in how the TDD system used them.
|
|
||||||
# The original provided code for this module is correct and complete.
|
|
||||||
# I am including it here for completeness of the single-code-block response.
|
|
||||||
|
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Defines the `TypeSpec` structure and functions for its manipulation.
|
Defines the `TypeSpec` structure and functions for its manipulation.
|
||||||
Normalization includes alpha-conversion, beta-reduction, and a final
|
Normalization includes alpha-conversion, beta-reduction, and a final
|
||||||
@ -692,25 +688,26 @@ defmodule Tdd.TypeSpec do
|
|||||||
(min == :neg_inf or val >= min) and (max == :pos_inf or val <= max)
|
(min == :neg_inf or val >= min) and (max == :pos_inf or val <= max)
|
||||||
|
|
||||||
{{:mu, v1, b1_body}, {:mu, v2, b2_body}} ->
|
{{:mu, v1, b1_body}, {:mu, v2, b2_body}} ->
|
||||||
|
# This logic is from the original file, which is correct in principle
|
||||||
|
# but was failing due to the TDD layer bug.
|
||||||
cond do
|
cond do
|
||||||
is_list_mu_form(b1_body, v1) and is_list_mu_form(b2_body, v2) ->
|
is_list_mu_form(b1_body, v1) and is_list_mu_form(b2_body, v2) ->
|
||||||
e1 = extract_list_mu_element(b1_body, v1)
|
e1 = extract_list_mu_element(b1_body, v1)
|
||||||
e2 = extract_list_mu_element(b2_body, v2)
|
e2 = extract_list_mu_element(b2_body, v2)
|
||||||
do_is_subtype_structural?(e1, e2, new_visited)
|
do_is_subtype_structural?(e1, e2, new_visited)
|
||||||
|
|
||||||
v1 == v2 ->
|
|
||||||
true
|
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
false
|
unfolded_b1 = substitute_vars_canonical(b1_body, %{v1 => spec1})
|
||||||
|
do_is_subtype_structural?(unfolded_b1, spec2, new_visited)
|
||||||
end
|
end
|
||||||
|
|
||||||
{_non_mu_spec, {:mu, v2, b2_body} = mu_spec2} ->
|
{_non_mu_spec, {:mu, v2, b2_body} = mu_spec2} ->
|
||||||
unfolded_b2 = substitute_vars_canonical(b2_body, %{v2 => mu_spec2})
|
unfolded_b2 = substitute_vars_canonical(b2_body, %{v2 => mu_spec2})
|
||||||
do_is_subtype_structural?(spec1, unfolded_b2, new_visited)
|
do_is_subtype_structural?(spec1, unfolded_b2, new_visited)
|
||||||
|
|
||||||
{{:mu, _, _}, _non_mu_spec} ->
|
{{:mu, v1, b1_body} = mu_spec1, _non_mu_spec} ->
|
||||||
false
|
unfolded_b1 = substitute_vars_canonical(b1_body, %{v1 => mu_spec1})
|
||||||
|
do_is_subtype_structural?(unfolded_b1, spec2, new_visited)
|
||||||
|
|
||||||
{{:negation, n_body1}, {:negation, n_body2}} ->
|
{{:negation, n_body1}, {:negation, n_body2}} ->
|
||||||
do_is_subtype_structural?(n_body2, n_body1, new_visited)
|
do_is_subtype_structural?(n_body2, n_body1, new_visited)
|
||||||
@ -1323,22 +1320,18 @@ defmodule Tdd.Consistency.Engine do
|
|||||||
defp safe_min(a, b), do: :erlang.min(a, b)
|
defp safe_min(a, b), do: :erlang.min(a, b)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
defmodule Tdd.Algo do
|
defmodule Tdd.Algo do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Implements the core, stateless algorithms for TDD manipulation.
|
Implements the core, stateless algorithms for TDD manipulation.
|
||||||
REFAC: This module is now cleaner. `handle_recursive_subproblem` no longer
|
|
||||||
calls the compiler with a bad context. It now uses `Tdd.TypeReconstructor`
|
|
||||||
and TDD-native operations to perform its check, decoupling it from the
|
|
||||||
compiler's internal state.
|
|
||||||
"""
|
"""
|
||||||
use Tdd.Debug
|
use Tdd.Debug
|
||||||
alias Tdd.Store
|
alias Tdd.Store
|
||||||
alias Tdd.Consistency.Engine
|
alias Tdd.Consistency.Engine
|
||||||
alias Tdd.Debug
|
alias Tdd.Debug
|
||||||
alias Tdd.TypeReconstructor
|
|
||||||
alias Tdd.Compiler
|
|
||||||
|
|
||||||
# --- Binary Operation: Apply ---
|
# --- Binary Operation: Apply ---
|
||||||
|
# This function is correct and does not need to be changed.
|
||||||
@spec apply(atom, (atom, atom -> atom), non_neg_integer, non_neg_integer) :: non_neg_integer
|
@spec apply(atom, (atom, atom -> atom), non_neg_integer, non_neg_integer) :: non_neg_integer
|
||||||
def apply(op_name, op_lambda, u1_id, u2_id) do
|
def apply(op_name, op_lambda, u1_id, u2_id) do
|
||||||
cache_key = {:apply, op_name, Enum.sort([u1_id, u2_id])}
|
cache_key = {:apply, op_name, Enum.sort([u1_id, u2_id])}
|
||||||
@ -1354,6 +1347,7 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This function is correct and does not need to be changed.
|
||||||
defp do_apply(op_name, op_lambda, u1_id, u2_id) do
|
defp do_apply(op_name, op_lambda, u1_id, u2_id) do
|
||||||
with {:ok, u1_details} <- Store.get_node(u1_id),
|
with {:ok, u1_details} <- Store.get_node(u1_id),
|
||||||
{:ok, u2_details} <- Store.get_node(u2_id) do
|
{:ok, u2_details} <- Store.get_node(u2_id) do
|
||||||
@ -1419,6 +1413,7 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
|
|
||||||
# --- Unary Operation: Negation ---
|
# --- Unary Operation: Negation ---
|
||||||
|
# This function is correct and does not need to be changed.
|
||||||
@spec negate(non_neg_integer) :: non_neg_integer
|
@spec negate(non_neg_integer) :: non_neg_integer
|
||||||
def negate(tdd_id) do
|
def negate(tdd_id) do
|
||||||
cache_key = {:negate, tdd_id}
|
cache_key = {:negate, tdd_id}
|
||||||
@ -1446,6 +1441,7 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
|
|
||||||
# --- Unary Operation: Semantic Simplification ---
|
# --- Unary Operation: Semantic Simplification ---
|
||||||
|
# This function is correct and does not need to be changed.
|
||||||
@spec simplify(non_neg_integer(), map()) :: non_neg_integer
|
@spec simplify(non_neg_integer(), map()) :: non_neg_integer
|
||||||
def simplify(tdd_id, assumptions \\ %{}) do
|
def simplify(tdd_id, assumptions \\ %{}) do
|
||||||
sorted_assumptions = Enum.sort(assumptions)
|
sorted_assumptions = Enum.sort(assumptions)
|
||||||
@ -1462,6 +1458,7 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This function is correct and does not need to be changed.
|
||||||
defp do_simplify(tdd_id, sorted_assumptions, context) do
|
defp do_simplify(tdd_id, sorted_assumptions, context) do
|
||||||
current_state = {tdd_id, sorted_assumptions}
|
current_state = {tdd_id, sorted_assumptions}
|
||||||
|
|
||||||
@ -1482,8 +1479,7 @@ defmodule Tdd.Algo do
|
|||||||
Store.false_node_id()
|
Store.false_node_id()
|
||||||
|
|
||||||
{:ok, {var, y, n, d}} ->
|
{:ok, {var, y, n, d}} ->
|
||||||
# REFAC: Dispatch to the new handler for recursive variables.
|
# Dispatch to the handler for recursive variables.
|
||||||
# The variable now contains a TDD ID, not a spec.
|
|
||||||
case var do
|
case var do
|
||||||
{5, :c_head, constraint_id, _} ->
|
{5, :c_head, constraint_id, _} ->
|
||||||
handle_recursive_subproblem(
|
handle_recursive_subproblem(
|
||||||
@ -1564,11 +1560,23 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
|
|
||||||
# --- Unary Operation: Substitute ---
|
# --- Unary Operation: Substitute ---
|
||||||
|
# FIX: The implementation of substitute needs to change.
|
||||||
|
|
||||||
@spec substitute(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: non_neg_integer()
|
@spec substitute(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: non_neg_integer()
|
||||||
def substitute(root_id, from_id, to_id) do
|
def substitute(root_id, from_id, to_id) do
|
||||||
if root_id == from_id, do: to_id, else: do_substitute(root_id, from_id, to_id)
|
if root_id == from_id, do: to_id, else: do_substitute(root_id, from_id, to_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This helper inspects and replaces TDD IDs embedded in predicate variables.
|
||||||
|
defp substitute_in_var(var, from_id, to_id) do
|
||||||
|
case var do
|
||||||
|
{4, :b_element, index, ^from_id} -> {4, :b_element, index, to_id}
|
||||||
|
{5, :c_head, ^from_id, nil} -> {5, :c_head, to_id, nil}
|
||||||
|
{5, :d_tail, ^from_id, nil} -> {5, :d_tail, to_id, nil}
|
||||||
|
_other -> var
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp do_substitute(root_id, from_id, to_id) do
|
defp do_substitute(root_id, from_id, to_id) do
|
||||||
cache_key = {:substitute, root_id, from_id, to_id}
|
cache_key = {:substitute, root_id, from_id, to_id}
|
||||||
|
|
||||||
@ -1586,10 +1594,12 @@ defmodule Tdd.Algo do
|
|||||||
Store.false_node_id()
|
Store.false_node_id()
|
||||||
|
|
||||||
{:ok, {var, y, n, d}} ->
|
{:ok, {var, y, n, d}} ->
|
||||||
|
# FIX: Substitute within the variable term itself.
|
||||||
|
new_var = substitute_in_var(var, from_id, to_id)
|
||||||
new_y = substitute(y, from_id, to_id)
|
new_y = substitute(y, from_id, to_id)
|
||||||
new_n = substitute(n, from_id, to_id)
|
new_n = substitute(n, from_id, to_id)
|
||||||
new_d = substitute(d, from_id, to_id)
|
new_d = substitute(d, from_id, to_id)
|
||||||
Store.find_or_create_node(var, new_y, new_n, new_d)
|
Store.find_or_create_node(new_var, new_y, new_n, new_d)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
raise "substitute encountered an error getting node #{root_id}: #{reason}"
|
raise "substitute encountered an error getting node #{root_id}: #{reason}"
|
||||||
@ -1600,7 +1610,9 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# --- Coinductive Emptiness Check ---
|
# --- Coinductive Emptiness Check ---
|
||||||
|
# This function is correct and does not need to be changed.
|
||||||
@spec check_emptiness(non_neg_integer()) :: non_neg_integer()
|
@spec check_emptiness(non_neg_integer()) :: non_neg_integer()
|
||||||
def check_emptiness(tdd_id) do
|
def check_emptiness(tdd_id) do
|
||||||
cache_key = {:check_emptiness, tdd_id}
|
cache_key = {:check_emptiness, tdd_id}
|
||||||
@ -1617,6 +1629,7 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# This function is correct and does not need to be changed.
|
||||||
defp do_check_emptiness(tdd_id, sorted_assumptions, context) do
|
defp do_check_emptiness(tdd_id, sorted_assumptions, context) do
|
||||||
current_state = {tdd_id, sorted_assumptions}
|
current_state = {tdd_id, sorted_assumptions}
|
||||||
|
|
||||||
@ -1637,7 +1650,7 @@ defmodule Tdd.Algo do
|
|||||||
Store.false_node_id()
|
Store.false_node_id()
|
||||||
|
|
||||||
{:ok, {var, y, n, d}} ->
|
{:ok, {var, y, n, d}} ->
|
||||||
# REFAC: Dispatch to the new handler for recursive variables.
|
# Dispatch to the handler for recursive variables.
|
||||||
case var do
|
case var do
|
||||||
{5, :c_head, constraint_id, _} ->
|
{5, :c_head, constraint_id, _} ->
|
||||||
handle_recursive_subproblem(
|
handle_recursive_subproblem(
|
||||||
@ -1737,162 +1750,110 @@ defmodule Tdd.Algo do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# REFAC: This is the completely rewritten function. It is the heart of the fix.
|
# This function, containing our previous fix, is correct and does not need to be changed.
|
||||||
@doc """
|
|
||||||
Handles recursive simplification by setting up and solving a sub-problem.
|
|
||||||
It is now decoupled from the compiler's internal state and context types.
|
|
||||||
"""
|
|
||||||
defp handle_recursive_subproblem(
|
defp handle_recursive_subproblem(
|
||||||
algo_type,
|
algo_type,
|
||||||
sub_key,
|
sub_key,
|
||||||
constraint_id, # This is now a TDD ID, not a TypeSpec.
|
constraint_id, # This is a TDD ID for the constraint on the sub-problem.
|
||||||
node_details,
|
node_details,
|
||||||
sorted_assumptions,
|
sorted_assumptions,
|
||||||
context # This is the coinductive context (a MapSet).
|
context # This is the coinductive context (a MapSet).
|
||||||
) do
|
) do
|
||||||
{var, y, n, _d} = node_details
|
{var, y, n, d} = node_details
|
||||||
assumptions = Map.new(sorted_assumptions)
|
assumptions = Map.new(sorted_assumptions)
|
||||||
|
|
||||||
# 1. Partition assumptions to get those relevant to the sub-problem.
|
# 1. Build the TDD for the sub-problem's effective type by intersecting all
|
||||||
{sub_assumptions_raw, _other_assumptions} =
|
# its constraints from the current assumption set.
|
||||||
Enum.partition(assumptions, fn {v, _} ->
|
op_intersect = fn
|
||||||
(Tdd.Predicate.Info.get_traits(v) || %{})[:sub_key] == sub_key
|
|
||||||
end)
|
|
||||||
|
|
||||||
# 2. Reconstruct the TypeSpec for the sub-problem from its assumptions.
|
|
||||||
# First, unwrap the variables from their scoped form to their base form.
|
|
||||||
sub_assumptions_map = Tdd.Consistency.Engine.remap_sub_problem_vars(sub_assumptions_raw)
|
|
||||||
reconstructed_sub_spec = Tdd.TypeReconstructor.spec_from_assumptions(sub_assumptions_map)
|
|
||||||
|
|
||||||
# 3. Compile the reconstructed spec to a TDD. This call to the compiler
|
|
||||||
# uses a fresh context, which is correct for a ground type spec, fixing the bug.
|
|
||||||
sub_problem_tdd_id = Tdd.Compiler.spec_to_id(reconstructed_sub_spec)
|
|
||||||
|
|
||||||
# 4. Check if the reconstructed TDD is a subtype of the constraint TDD.
|
|
||||||
# `A <: B` is equivalent to `(A & ~B) == none`.
|
|
||||||
neg_constraint_id = negate(constraint_id)
|
|
||||||
|
|
||||||
op_intersect_terminals = fn
|
|
||||||
:false_terminal, _ -> :false_terminal
|
:false_terminal, _ -> :false_terminal
|
||||||
_, :false_terminal -> :false_terminal
|
_, :false_terminal -> :false_terminal
|
||||||
t, :true_terminal -> t
|
t, :true_terminal -> t
|
||||||
:true_terminal, t -> t
|
:true_terminal, t -> t
|
||||||
end
|
end
|
||||||
|
|
||||||
intersect_id =
|
sub_problem_constraints =
|
||||||
apply(:intersect, op_intersect_terminals, sub_problem_tdd_id, neg_constraint_id)
|
Enum.filter(assumptions, fn {v, _} ->
|
||||||
|
(Tdd.Predicate.Info.get_traits(v) || %{})[:sub_key] == sub_key
|
||||||
|
end)
|
||||||
|
|
||||||
# We must use the coinductive `check_emptiness` for this check, as the types
|
sub_problem_tdd_id =
|
||||||
# involved may themselves be recursive.
|
Enum.reduce(sub_problem_constraints, Store.true_node_id(), fn {var, val}, acc_id ->
|
||||||
is_sub = check_emptiness(intersect_id) == Store.false_node_id()
|
constraint_for_this_assumption = Engine.unwrap_var(var)
|
||||||
|
id_to_intersect = if val, do: constraint_for_this_assumption, else: negate(constraint_for_this_assumption)
|
||||||
|
apply(:intersect, op_intersect, acc_id, id_to_intersect)
|
||||||
|
end)
|
||||||
|
|
||||||
# 5. Branch based on the subtyping result.
|
# 2. Check for the three logical outcomes:
|
||||||
if is_sub do
|
# - Does the path imply the constraint is satisfied?
|
||||||
# The constraint is satisfied, so the predicate `var` is effectively true.
|
# - Does the path imply the constraint is violated?
|
||||||
# We follow the 'yes' branch. The original assumption set already contains
|
# - Or are both outcomes still possible?
|
||||||
# the information that implies this, so we don't need to add `{var, true}`.
|
|
||||||
|
# Implies satisfied: `sub_problem_tdd_id <: constraint_id`
|
||||||
|
# This is equivalent to `(sub_problem_tdd_id & !constraint_id)` being empty.
|
||||||
|
neg_constraint_id = negate(constraint_id)
|
||||||
|
intersect_sub_with_neg_constraint = apply(:intersect, op_intersect, sub_problem_tdd_id, neg_constraint_id)
|
||||||
|
implies_satisfied = check_emptiness(intersect_sub_with_neg_constraint) == Store.false_node_id()
|
||||||
|
|
||||||
|
# Implies violated: `sub_problem_tdd_id` and `constraint_id` are disjoint.
|
||||||
|
# This is equivalent to `(sub_problem_tdd_id & constraint_id)` being empty.
|
||||||
|
intersect_sub_with_constraint = apply(:intersect, op_intersect, sub_problem_tdd_id, constraint_id)
|
||||||
|
implies_violated = check_emptiness(intersect_sub_with_constraint) == Store.false_node_id()
|
||||||
|
|
||||||
|
# 3. Branch based on the logical outcome.
|
||||||
|
cond do
|
||||||
|
implies_satisfied and implies_violated ->
|
||||||
|
# The sub-problem itself must be empty/impossible under the current assumptions.
|
||||||
|
# This whole path is a contradiction.
|
||||||
|
Store.false_node_id()
|
||||||
|
|
||||||
|
implies_satisfied ->
|
||||||
|
# The constraint is guaranteed by the path. Follow the 'yes' branch.
|
||||||
|
# The assumptions already imply this, so no change to them is needed.
|
||||||
case algo_type do
|
case algo_type do
|
||||||
:simplify -> do_simplify(y, sorted_assumptions, context)
|
:simplify -> do_simplify(y, sorted_assumptions, context)
|
||||||
:check_emptiness -> do_check_emptiness(y, sorted_assumptions, context)
|
:check_emptiness -> do_check_emptiness(y, sorted_assumptions, context)
|
||||||
end
|
end
|
||||||
else
|
|
||||||
# The constraint is violated, so the predicate `var` is false.
|
|
||||||
# We follow the 'no' branch, adding this new information to the assumption set.
|
|
||||||
new_assumptions = Map.put(assumptions, var, false) |> Enum.sort()
|
|
||||||
|
|
||||||
|
implies_violated ->
|
||||||
|
# The constraint is impossible given the path. Follow the 'no' branch.
|
||||||
|
# We can add this new fact `{var, false}` to strengthen the assumptions.
|
||||||
|
new_assumptions = Map.put(assumptions, var, false) |> Enum.sort()
|
||||||
case algo_type do
|
case algo_type do
|
||||||
:simplify -> do_simplify(n, new_assumptions, context)
|
:simplify -> do_simplify(n, new_assumptions, context)
|
||||||
:check_emptiness -> do_check_emptiness(n, new_assumptions, context)
|
:check_emptiness -> do_check_emptiness(n, new_assumptions, context)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Neither outcome is guaranteed. Both are possible.
|
||||||
|
# We must explore both branches and combine the results.
|
||||||
|
new_assumptions_for_no_branch = Map.put(assumptions, var, false) |> Enum.sort()
|
||||||
|
|
||||||
|
case algo_type do
|
||||||
|
:check_emptiness ->
|
||||||
|
# Is there ANY path to true? Explore both and take their union.
|
||||||
|
res_y = do_check_emptiness(y, sorted_assumptions, context)
|
||||||
|
res_n = do_check_emptiness(n, new_assumptions_for_no_branch, context)
|
||||||
|
|
||||||
|
# Define local terminal logic for union
|
||||||
|
op_union = fn
|
||||||
|
:true_terminal, _ -> :true_terminal
|
||||||
|
_, :true_terminal -> :true_terminal
|
||||||
|
t, :false_terminal -> t
|
||||||
|
:false_terminal, t -> t
|
||||||
|
end
|
||||||
|
apply(:sum, op_union, res_y, res_n)
|
||||||
|
|
||||||
|
:simplify ->
|
||||||
|
# Simplify both sub-trees and rebuild the node, as we cannot simplify this node away.
|
||||||
|
res_y = do_simplify(y, sorted_assumptions, context)
|
||||||
|
res_n = do_simplify(n, new_assumptions_for_no_branch, context)
|
||||||
|
res_d = do_simplify(d, sorted_assumptions, context) # 'dc' branch is independent of 'var'
|
||||||
|
Store.find_or_create_node(var, res_y, res_n, res_d)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule Tdd.TypeReconstructor do
|
|
||||||
@moduledoc """
|
|
||||||
Reconstructs a high-level `TypeSpec` from a low-level assumption map.
|
|
||||||
REFAC: This module is now fully implemented as per the architectural plan.
|
|
||||||
It serves as the bridge between raw TDD path assumptions and TypeSpecs,
|
|
||||||
enabling the new `handle_recursive_subproblem` logic in `Tdd.Algo`.
|
|
||||||
"""
|
|
||||||
alias Tdd.TypeSpec
|
|
||||||
alias Tdd.Predicate.Info
|
|
||||||
alias Tdd.Consistency.Engine
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Takes a map of `{variable, boolean}` assumptions and returns a `TypeSpec`.
|
|
||||||
"""
|
|
||||||
@spec spec_from_assumptions(map()) :: TypeSpec.t()
|
|
||||||
def spec_from_assumptions(assumptions) do
|
|
||||||
# 1. Partition assumptions into groups for the top-level entity and its sub-components.
|
|
||||||
partitions =
|
|
||||||
Enum.group_by(assumptions, fn {var, _val} ->
|
|
||||||
(Info.get_traits(var) || %{})[:sub_key]
|
|
||||||
end)
|
|
||||||
|
|
||||||
# 2. Reconstruct the spec for the top-level entity from its flat assumptions.
|
|
||||||
top_level_assumptions = Map.get(partitions, nil, []) |> Map.new()
|
|
||||||
top_level_spec = spec_from_flat_assumptions(top_level_assumptions)
|
|
||||||
|
|
||||||
# 3. Recursively reconstruct specs for all sub-problems (head, tail, elements).
|
|
||||||
sub_problem_specs =
|
|
||||||
partitions
|
|
||||||
|> Map.drop([nil])
|
|
||||||
|> Enum.map(fn {sub_key, sub_assumptions_list} ->
|
|
||||||
# Re-map the nested variables back to their base form for the recursive call.
|
|
||||||
remapped_assumptions = Engine.remap_sub_problem_vars(sub_assumptions_list)
|
|
||||||
|
|
||||||
# Recursively build the spec for the sub-problem
|
|
||||||
sub_spec = spec_from_assumptions(remapped_assumptions)
|
|
||||||
|
|
||||||
# Create a partial spec representing the constraint on the sub-problem
|
|
||||||
case sub_key do
|
|
||||||
:head -> {:cons, sub_spec, :any}
|
|
||||||
:tail -> {:cons, :any, sub_spec}
|
|
||||||
# If we have info on element N, we can only say the type is `tuple`.
|
|
||||||
# A more advanced reconstructor might try to find the size and build a
|
|
||||||
# sparse tuple spec, but intersecting with `:tuple` is correct and sufficient.
|
|
||||||
{:elem, _index} -> :tuple
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
# 4. The final spec is the intersection of the top-level spec and all sub-problem specs.
|
|
||||||
final_spec_list = [top_level_spec | sub_problem_specs]
|
|
||||||
TypeSpec.normalize({:intersect, final_spec_list})
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc "Handles only the 'flat' (non-recursive) assumptions for a single entity."
|
|
||||||
defp spec_from_flat_assumptions(assumptions) do
|
|
||||||
specs =
|
|
||||||
Enum.map(assumptions, fn {var, bool_val} ->
|
|
||||||
spec =
|
|
||||||
case var do
|
|
||||||
{0, :is_atom, _, _} -> :atom
|
|
||||||
{0, :is_integer, _, _} -> :integer
|
|
||||||
{0, :is_list, _, _} -> :list
|
|
||||||
{0, :is_tuple, _, _} -> :tuple
|
|
||||||
{1, :value, val, _} -> {:literal, val}
|
|
||||||
{2, :alt, n, _} -> {:integer_range, :neg_inf, n - 1}
|
|
||||||
{2, :beq, n, _} -> {:literal, n}
|
|
||||||
{2, :cgt, n, _} -> {:integer_range, n + 1, :pos_inf}
|
|
||||||
{4, :a_size, size, _} ->
|
|
||||||
# This assumption alone doesn't give element types.
|
|
||||||
# A full reconstruction of a tuple with a specific size and any elements
|
|
||||||
# is {:tuple, List.duplicate(:any, size)}.
|
|
||||||
# For simplicity, we can just say `:tuple`.
|
|
||||||
:tuple
|
|
||||||
|
|
||||||
{5, :b_is_empty, _, _} -> {:literal, []}
|
|
||||||
# Recursive vars are handled by the caller, so ignore here.
|
|
||||||
_ -> :any
|
|
||||||
end
|
|
||||||
|
|
||||||
if bool_val, do: spec, else: {:negation, spec}
|
|
||||||
end)
|
|
||||||
|
|
||||||
TypeSpec.normalize({:intersect, specs})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defmodule Tdd.Compiler do
|
defmodule Tdd.Compiler do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
@ -1909,6 +1870,8 @@ 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
|
||||||
|
# to ensure a clean slate for caches and node IDs. This makes calls independent.
|
||||||
normalized_spec = TypeSpec.normalize(spec)
|
normalized_spec = TypeSpec.normalize(spec)
|
||||||
compile_normalized_spec(normalized_spec, %{})
|
compile_normalized_spec(normalized_spec, %{})
|
||||||
end
|
end
|
||||||
@ -1939,6 +1902,7 @@ defmodule Tdd.Compiler do
|
|||||||
placeholder_id = Store.create_placeholder(placeholder_node_variable_tag)
|
placeholder_id = Store.create_placeholder(placeholder_node_variable_tag)
|
||||||
new_context = Map.put(context, var_name, placeholder_id)
|
new_context = Map.put(context, var_name, placeholder_id)
|
||||||
compiled_body_id = compile_normalized_spec(body_spec, new_context)
|
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)
|
final_id = Algo.substitute(compiled_body_id, placeholder_id, compiled_body_id)
|
||||||
Algo.simplify(final_id)
|
Algo.simplify(final_id)
|
||||||
|
|
||||||
@ -2133,6 +2097,7 @@ defmodule TddStoreTests do
|
|||||||
|
|
||||||
def run() do
|
def run() do
|
||||||
IO.puts("\n--- Running Tdd.Store Tests ---")
|
IO.puts("\n--- Running Tdd.Store Tests ---")
|
||||||
|
Tdd.Store.init()
|
||||||
Process.put(:test_failures, [])
|
Process.put(:test_failures, [])
|
||||||
|
|
||||||
# --- Test Setup ---
|
# --- Test Setup ---
|
||||||
@ -2255,6 +2220,7 @@ defmodule TypeSpecTests do
|
|||||||
|
|
||||||
def run() do
|
def run() do
|
||||||
IO.puts("\n--- Running Tdd.TypeSpec.normalize/1 Tests ---")
|
IO.puts("\n--- Running Tdd.TypeSpec.normalize/1 Tests ---")
|
||||||
|
Tdd.Store.init()
|
||||||
Process.put(:test_failures, [])
|
Process.put(:test_failures, [])
|
||||||
|
|
||||||
# --- Test Section: Base & Simple Types ---
|
# --- Test Section: Base & Simple Types ---
|
||||||
@ -2424,37 +2390,38 @@ defmodule TddVariableTests do
|
|||||||
Process.put(:test_failures, [])
|
Process.put(:test_failures, [])
|
||||||
# Setup for TDD IDs
|
# Setup for TDD IDs
|
||||||
Tdd.Store.init()
|
Tdd.Store.init()
|
||||||
|
# Use a dummy context for these simple types
|
||||||
id_atom = Tdd.Compiler.spec_to_id(:atom)
|
id_atom = Tdd.Compiler.spec_to_id(:atom)
|
||||||
id_integer = Tdd.Compiler.spec_to_id(:integer)
|
id_integer = Tdd.Compiler.spec_to_id(:integer)
|
||||||
|
|
||||||
# --- Test Section: Variable Structure ---
|
# --- Test Section: Variable Structure ---
|
||||||
IO.puts("\n--- Section: Variable Structure ---")
|
IO.puts("\n--- Section: Variable Structure ---")
|
||||||
test("v_is_atom returns correct tuple", {0, :is_atom, nil, nil}, Variable.v_is_atom())
|
test("v_is_atom returns correct tuple", {0, :is_atom, nil, nil}, Tdd.Variable.v_is_atom())
|
||||||
test("v_atom_eq returns correct tuple", {1, :value, :foo, nil}, Variable.v_atom_eq(:foo))
|
test("v_atom_eq returns correct tuple", {1, :value, :foo, nil}, Tdd.Variable.v_atom_eq(:foo))
|
||||||
test("v_int_lt returns correct tuple", {2, :alt, 10, nil}, Variable.v_int_lt(10))
|
test("v_int_lt returns correct tuple", {2, :alt, 10, nil}, Tdd.Variable.v_int_lt(10))
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"v_tuple_size_eq returns correct tuple",
|
"v_tuple_size_eq returns correct tuple",
|
||||||
{4, :a_size, 2, nil},
|
{4, :a_size, 2, nil},
|
||||||
Variable.v_tuple_size_eq(2)
|
Tdd.Variable.v_tuple_size_eq(2)
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"v_tuple_elem_pred nests a TDD ID correctly",
|
"v_tuple_elem_pred nests a TDD ID correctly",
|
||||||
{4, :b_element, 0, id_integer},
|
{4, :b_element, 0, id_integer},
|
||||||
Variable.v_tuple_elem_pred(0, id_integer)
|
Tdd.Variable.v_tuple_elem_pred(0, id_integer)
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"v_list_is_empty returns correct tuple",
|
"v_list_is_empty returns correct tuple",
|
||||||
{5, :b_is_empty, nil, nil},
|
{5, :b_is_empty, nil, nil},
|
||||||
Variable.v_list_is_empty()
|
Tdd.Variable.v_list_is_empty()
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"v_list_head_pred nests a TDD ID correctly",
|
"v_list_head_pred nests a TDD ID correctly",
|
||||||
{5, :c_head, id_atom, nil},
|
{5, :c_head, id_atom, nil},
|
||||||
Variable.v_list_head_pred(id_atom)
|
Tdd.Variable.v_list_head_pred(id_atom)
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Test Section: Global Ordering ---
|
# --- Test Section: Global Ordering ---
|
||||||
@ -2463,56 +2430,56 @@ defmodule TddVariableTests do
|
|||||||
test(
|
test(
|
||||||
"Primary type var < Atom property var",
|
"Primary type var < Atom property var",
|
||||||
true,
|
true,
|
||||||
Variable.v_is_tuple() < Variable.v_atom_eq(:anything)
|
Tdd.Variable.v_is_tuple() < Tdd.Variable.v_atom_eq(:anything)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Within Category 2: alt < beq < cgt
|
# Within Category 2: alt < beq < cgt
|
||||||
test(
|
test(
|
||||||
"Integer :lt var < Integer :eq var",
|
"Integer :lt var < Integer :eq var",
|
||||||
true,
|
true,
|
||||||
Variable.v_int_lt(10) < Variable.v_int_eq(10)
|
Tdd.Variable.v_int_lt(10) < Tdd.Variable.v_int_eq(10)
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"Integer :eq var < Integer :gt var",
|
"Integer :eq var < Integer :gt var",
|
||||||
true,
|
true,
|
||||||
Variable.v_int_eq(10) < Variable.v_int_gt(10)
|
Tdd.Variable.v_int_eq(10) < Tdd.Variable.v_int_gt(10)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Within Category 2: comparison of value
|
# Within Category 2: comparison of value
|
||||||
test(
|
test(
|
||||||
"Integer :eq(5) var < Integer :eq(10) var",
|
"Integer :eq(5) var < Integer :eq(10) var",
|
||||||
true,
|
true,
|
||||||
Variable.v_int_eq(5) < Variable.v_int_eq(10)
|
Tdd.Variable.v_int_eq(5) < Tdd.Variable.v_int_eq(10)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Within Category 4: comparison of index
|
# Within Category 4: comparison of index
|
||||||
test(
|
test(
|
||||||
"Tuple elem(0) var < Tuple elem(1) var",
|
"Tuple elem(0) var < Tuple elem(1) var",
|
||||||
true,
|
true,
|
||||||
Variable.v_tuple_elem_pred(0, id_atom) <
|
Tdd.Variable.v_tuple_elem_pred(0, id_atom) <
|
||||||
Variable.v_tuple_elem_pred(1, id_atom)
|
Tdd.Variable.v_tuple_elem_pred(1, id_atom)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Within Category 4, same index: comparison of nested ID
|
# Within Category 4, same index: comparison of nested ID
|
||||||
test(
|
test(
|
||||||
"Tuple elem(0, id_atom) var vs Tuple elem(0, id_int) var",
|
"Tuple elem(0, id_atom) var vs Tuple elem(0, id_int) var",
|
||||||
id_atom < id_integer,
|
id_atom < id_integer,
|
||||||
Variable.v_tuple_elem_pred(0, id_atom) <
|
Tdd.Variable.v_tuple_elem_pred(0, id_atom) <
|
||||||
Variable.v_tuple_elem_pred(0, id_integer)
|
Tdd.Variable.v_tuple_elem_pred(0, id_integer)
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"List :b_is_empty var < List :c_head var",
|
"List :b_is_empty var < List :c_head var",
|
||||||
true,
|
true,
|
||||||
Variable.v_list_is_empty() < Variable.v_list_head_pred(id_atom)
|
Tdd.Variable.v_list_is_empty() < Tdd.Variable.v_list_head_pred(id_atom)
|
||||||
)
|
)
|
||||||
|
|
||||||
test(
|
test(
|
||||||
"List :c_head var < List :tail var",
|
"List :c_head var < List :tail var",
|
||||||
true,
|
true,
|
||||||
Variable.v_list_head_pred(id_atom) <
|
Tdd.Variable.v_list_head_pred(id_atom) <
|
||||||
Variable.v_list_tail_pred(id_atom)
|
Tdd.Variable.v_list_tail_pred(id_atom)
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Final Report ---
|
# --- Final Report ---
|
||||||
@ -2778,58 +2745,7 @@ defmodule TddAlgoTests do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule TypeReconstructorTests do
|
# NOTE: Tdd.TypeReconstructor and its tests are removed.
|
||||||
alias Tdd.TypeReconstructor
|
|
||||||
alias Tdd.Variable
|
|
||||||
alias Tdd.TypeSpec
|
|
||||||
|
|
||||||
defp test(name, expected_spec, assumptions) do
|
|
||||||
expected = TypeSpec.normalize(expected_spec)
|
|
||||||
result = TypeSpec.normalize(TypeReconstructor.spec_from_assumptions(assumptions))
|
|
||||||
is_ok = expected == result
|
|
||||||
status = if is_ok, do: "[PASS]", else: "[FAIL]"
|
|
||||||
IO.puts("#{status} #{name}")
|
|
||||||
unless is_ok do
|
|
||||||
IO.puts(" Expected: #{inspect(expected)}")
|
|
||||||
IO.puts(" Got: #{inspect(result)}")
|
|
||||||
Process.put(:test_failures, [name | Process.get(:test_failures, [])])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def run() do
|
|
||||||
IO.puts("\n--- Running Tdd.TypeReconstructor Tests ---")
|
|
||||||
Process.put(:test_failures, [])
|
|
||||||
Tdd.Store.init()
|
|
||||||
id_atom = Tdd.Compiler.spec_to_id(:atom)
|
|
||||||
|
|
||||||
IO.puts("\n--- Section: Basic Flat Reconstructions ---")
|
|
||||||
test("is_atom=true -> atom", :atom, %{Variable.v_is_atom() => true})
|
|
||||||
test("is_atom=false -> ¬atom", {:negation, :atom}, %{Variable.v_is_atom() => false})
|
|
||||||
|
|
||||||
test(
|
|
||||||
"is_atom=true AND value==:foo -> :foo",
|
|
||||||
{:literal, :foo},
|
|
||||||
%{Variable.v_is_atom() => true, Variable.v_atom_eq(:foo) => true}
|
|
||||||
)
|
|
||||||
|
|
||||||
test(
|
|
||||||
"is_list=true AND is_empty=true -> []",
|
|
||||||
{:literal, []},
|
|
||||||
%{Variable.v_is_list() => true, Variable.v_list_is_empty() => true}
|
|
||||||
)
|
|
||||||
|
|
||||||
IO.puts("\n--- Section: Recursive Reconstructions ---")
|
|
||||||
test(
|
|
||||||
"head is an atom",
|
|
||||||
{:intersect, [:list, {:cons, :atom, :any}]},
|
|
||||||
%{Variable.v_list_head_pred(id_atom) => true}
|
|
||||||
)
|
|
||||||
|
|
||||||
failures = Process.get(:test_failures, [])
|
|
||||||
if failures == [], do: IO.puts("\n✅ All TypeReconstructor tests passed!"),
|
|
||||||
else: IO.puts("\n❌ Found #{length(failures)} test failures.")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defmodule CompilerAlgoTests do
|
defmodule CompilerAlgoTests do
|
||||||
alias Tdd.Compiler
|
alias Tdd.Compiler
|
||||||
@ -2854,8 +2770,9 @@ defmodule CompilerAlgoTests do
|
|||||||
|
|
||||||
def run() do
|
def run() do
|
||||||
IO.puts("\n--- Running Compiler & Algo Integration Tests ---")
|
IO.puts("\n--- Running Compiler & Algo Integration Tests ---")
|
||||||
Process.put(:test_failures, [])
|
|
||||||
Tdd.Store.init()
|
Tdd.Store.init()
|
||||||
|
Process.put(:test_failures, [])
|
||||||
|
# No top-level init, Compiler.spec_to_id does it.
|
||||||
|
|
||||||
IO.puts("\n--- Section: Basic Equivalences ---")
|
IO.puts("\n--- Section: Basic Equivalences ---")
|
||||||
test_equiv("atom & any == atom", true, {:intersect, [:atom, :any]}, :atom)
|
test_equiv("atom & any == atom", true, {:intersect, [:atom, :any]}, :atom)
|
||||||
@ -2893,8 +2810,9 @@ defmodule TddCompilerRecursiveTests do
|
|||||||
|
|
||||||
def run() do
|
def run() do
|
||||||
IO.puts("\n--- Running Tdd.Compiler Recursive Type Tests ---")
|
IO.puts("\n--- Running Tdd.Compiler Recursive Type Tests ---")
|
||||||
Process.put(:test_failures, [])
|
|
||||||
Tdd.Store.init()
|
Tdd.Store.init()
|
||||||
|
Process.put(:test_failures, [])
|
||||||
|
# No top-level init
|
||||||
|
|
||||||
IO.puts("\n--- Section: :cons ---")
|
IO.puts("\n--- Section: :cons ---")
|
||||||
test_subtype(":cons is a subtype of :list", true, {:cons, :atom, :list}, :list)
|
test_subtype(":cons is a subtype of :list", true, {:cons, :atom, :list}, :list)
|
||||||
@ -2979,12 +2897,37 @@ defmodule Tdd.TypeSpecAdvancedTests do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp test_raise(name, expected_error_struct, expected_message_regex, input_spec) do
|
defp test_raise(name, expected_error_struct, expected_message_regex, fun) do
|
||||||
# ... (unchanged)
|
try do
|
||||||
|
fun.()
|
||||||
|
# If it gets here, the function did not raise
|
||||||
|
IO.puts("[FAIL] #{name}")
|
||||||
|
IO.puts(" Expected an error to be raised, but nothing was.")
|
||||||
|
Process.put(:test_failures, [name | Process.get(:test_failures, [])])
|
||||||
|
rescue
|
||||||
|
e ->
|
||||||
|
if String.match?(e.message, expected_message_regex) do
|
||||||
|
IO.puts("[PASS] #{name}")
|
||||||
|
else
|
||||||
|
IO.puts("[FAIL] #{name}")
|
||||||
|
IO.puts(" Raised correct error type, but message did not match.")
|
||||||
|
IO.puts(" Expected message to match: #{inspect(expected_message_regex)}")
|
||||||
|
IO.puts(" Got message: #{e.message}")
|
||||||
|
Process.put(:test_failures, [name | Process.get(:test_failures, [])])
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
type, value ->
|
||||||
|
IO.puts("[FAIL] #{name}")
|
||||||
|
IO.puts(" Raised unexpected error type.")
|
||||||
|
IO.puts(" Expected: #{inspect(expected_error_struct)}")
|
||||||
|
IO.puts(" Got: #{type}:#{inspect(value)}")
|
||||||
|
Process.put(:test_failures, [name | Process.get(:test_failures, [])])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def run() do
|
def run() do
|
||||||
IO.puts("\n--- Running Tdd.TypeSpec Advanced Normalization Tests ---")
|
IO.puts("\n--- Running Tdd.TypeSpec Advanced Normalization Tests ---")
|
||||||
|
Tdd.Store.init()
|
||||||
Process.put(:test_failures, [])
|
Process.put(:test_failures, [])
|
||||||
|
|
||||||
IO.puts("\n--- Section: μ-type (Recursive Type) Normalization ---")
|
IO.puts("\n--- Section: μ-type (Recursive Type) Normalization ---")
|
||||||
@ -3041,8 +2984,9 @@ defmodule Tdd.CompilerAdvancedTests do
|
|||||||
|
|
||||||
def run() do
|
def run() do
|
||||||
IO.puts("\n--- Running Tdd.Compiler Advanced Feature Tests (μ, Λ, Apply) ---")
|
IO.puts("\n--- Running Tdd.Compiler Advanced Feature Tests (μ, Λ, Apply) ---")
|
||||||
Process.put(:test_failures, [])
|
|
||||||
Tdd.Store.init()
|
Tdd.Store.init()
|
||||||
|
Process.put(:test_failures, [])
|
||||||
|
# No top-level init
|
||||||
|
|
||||||
IO.puts("\n--- Section: Basic μ-type (list_of) ---")
|
IO.puts("\n--- Section: Basic μ-type (list_of) ---")
|
||||||
int_list = {:list_of, :integer}
|
int_list = {:list_of, :integer}
|
||||||
@ -3098,13 +3042,12 @@ end
|
|||||||
Process.sleep(100)
|
Process.sleep(100)
|
||||||
TypeSpecTests.run()
|
TypeSpecTests.run()
|
||||||
TddStoreTests.run()
|
TddStoreTests.run()
|
||||||
# The variable tests need a compiler, so run after init
|
# The variable tests need a compiler and its dependencies
|
||||||
Tdd.Store.init()
|
|
||||||
TddVariableTests.run()
|
TddVariableTests.run()
|
||||||
TddAlgoTests.run()
|
TddAlgoTests.run()
|
||||||
ConsistencyEngineTests.run()
|
ConsistencyEngineTests.run()
|
||||||
TypeReconstructorTests.run()
|
|
||||||
CompilerAlgoTests.run()
|
CompilerAlgoTests.run()
|
||||||
TddCompilerRecursiveTests.run()
|
TddCompilerRecursiveTests.run()
|
||||||
Tdd.TypeSpecAdvancedTests.run()
|
Tdd.TypeSpecAdvancedTests.run()
|
||||||
Tdd.CompilerAdvancedTests.run()
|
Tdd.CompilerAdvancedTests.run()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user