checkpoint
checkpoint failing test after fixing tests checkpoint checkpoint checkpoint re-work asd checkpoint checkpoint checkpoint mix proj checkpoint mix first parser impl checkpoint fix tests re-org parser checkpoint strings fix multiline strings tuples checkpoint maps checkpoint checkpoint checkpoint checkpoint fix weird eof expression parse error checkpoint before typing checkpoint checpoint checkpoint checkpoint checkpoint ids in primitive types checkpoint checkpoint fix tests initial annotation checkpoint checkpoint checkpoint union subtyping conventions refactor - split typer typing tuples checkpoint test refactor checkpoint test refactor parsing atoms checkpoint atoms wip lists checkpoint typing lists checkopint checkpoint wip fixing correct list typing map discussion checkpoint map basic typing fix tests checkpoint checkpoint checkpoint checkpoint fix condition typing fix literal keys in map types checkpoint union types checkpoint union type checkpoint row types discussion & bidirectional typecheck checkpoint basic lambdas checkpoint lambdas typing application wip function application checkpoint checkpoint checkpoint cduce checkpoint checkpoint checkpoint checkpoint checkpoint checkpoint checkpoint
This commit is contained in:
parent
4f13a98189
commit
748f87636a
56999
.aider.chat.history.md
Normal file
56999
.aider.chat.history.md
Normal file
File diff suppressed because it is too large
Load Diff
4160
.aider.input.history
Normal file
4160
.aider.input.history
Normal file
File diff suppressed because it is too large
Load Diff
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
||||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||||
|
/.fetch
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
pl-*.tar
|
||||||
|
|
||||||
|
# Temporary files, for example, from tests.
|
||||||
|
/tmp/
|
||||||
6
Conventions.md
Normal file
6
Conventions.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
Be concise in Your implementation, do not add more comments than an experienced developer would need.
|
||||||
|
IF You know an some logic needs to be extended at some point - leave a comment.
|
||||||
|
Be smart and use the existing code in the project, where it makes sense.
|
||||||
|
Write clear functional code.
|
||||||
|
If not specified, don't remove existing functionality, but adapt it to the new requirements.
|
||||||
|
|
||||||
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Til
|
||||||
|
|
||||||
|
**TODO: Add description**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||||
|
by adding `pl` to your list of dependencies in `mix.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def deps do
|
||||||
|
[
|
||||||
|
{:pl, "~> 0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||||
|
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||||
|
be found at <https://hexdocs.pm/pl>.
|
||||||
|
|
||||||
6589
cduce_code.md
Normal file
6589
cduce_code.md
Normal file
File diff suppressed because it is too large
Load Diff
18
lib/til.ex
Normal file
18
lib/til.ex
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
defmodule Til do
|
||||||
|
@moduledoc """
|
||||||
|
Documentation for `Til`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Hello world.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Til.hello()
|
||||||
|
:world
|
||||||
|
|
||||||
|
"""
|
||||||
|
def hello do
|
||||||
|
:world
|
||||||
|
end
|
||||||
|
end
|
||||||
246
lib/til/ast_utils.ex
Normal file
246
lib/til/ast_utils.ex
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
defmodule Til.AstUtils do
|
||||||
|
@moduledoc """
|
||||||
|
Utility functions for working with Til AST node maps.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves a node from the nodes_map by its ID.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> nodes = %{1 => %{id: 1, name: "node1"}, 2 => %{id: 2, name: "node2"}}
|
||||||
|
iex> Til.AstUtils.get_node(nodes, 1)
|
||||||
|
%{id: 1, name: "node1"}
|
||||||
|
|
||||||
|
iex> nodes = %{1 => %{id: 1, name: "node1"}}
|
||||||
|
iex> Til.AstUtils.get_node(nodes, 3)
|
||||||
|
nil
|
||||||
|
"""
|
||||||
|
def get_node(nodes_map, node_id) when is_map(nodes_map) and is_integer(node_id) do
|
||||||
|
Map.get(nodes_map, node_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves the child nodes of a given parent node.
|
||||||
|
The parent can be specified by its ID or as a node map.
|
||||||
|
Assumes child IDs are stored in the parent's `:children` field.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> node1 = %{id: 1, children: [2, 3]}
|
||||||
|
iex> node2 = %{id: 2, value: "child1"}
|
||||||
|
iex> node3 = %{id: 3, value: "child2"}
|
||||||
|
iex> nodes = %{1 => node1, 2 => node2, 3 => node3}
|
||||||
|
iex> Til.AstUtils.get_child_nodes(nodes, 1)
|
||||||
|
[%{id: 2, value: "child1"}, %{id: 3, value: "child2"}]
|
||||||
|
|
||||||
|
iex> Til.AstUtils.get_child_nodes(nodes, node1)
|
||||||
|
[%{id: 2, value: "child1"}, %{id: 3, value: "child2"}]
|
||||||
|
|
||||||
|
iex> node4 = %{id: 4} # No children field
|
||||||
|
iex> nodes_no_children = %{4 => node4}
|
||||||
|
iex> Til.AstUtils.get_child_nodes(nodes_no_children, 4)
|
||||||
|
[]
|
||||||
|
"""
|
||||||
|
def get_child_nodes(nodes_map, parent_node_id)
|
||||||
|
when is_map(nodes_map) and is_integer(parent_node_id) do
|
||||||
|
case get_node(nodes_map, parent_node_id) do
|
||||||
|
nil -> []
|
||||||
|
parent_node -> get_child_nodes_from_node(nodes_map, parent_node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_child_nodes(nodes_map, parent_node) when is_map(nodes_map) and is_map(parent_node) do
|
||||||
|
get_child_nodes_from_node(nodes_map, parent_node)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_child_nodes_from_node(nodes_map, parent_node) do
|
||||||
|
parent_node
|
||||||
|
|> Map.get(:children, [])
|
||||||
|
|> Enum.map(&get_node(nodes_map, &1))
|
||||||
|
# Filter out if a child_id doesn't resolve to a node
|
||||||
|
|> Enum.reject(&is_nil(&1))
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves the parent node of a given child node.
|
||||||
|
The child can be specified by its ID or as a node map.
|
||||||
|
Assumes parent ID is stored in the child's `:parent_id` field.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> parent = %{id: 1, name: "parent"}
|
||||||
|
iex> child = %{id: 2, parent_id: 1, name: "child"}
|
||||||
|
iex> nodes = %{1 => parent, 2 => child}
|
||||||
|
iex> Til.AstUtils.get_parent_node(nodes, 2)
|
||||||
|
%{id: 1, name: "parent"}
|
||||||
|
|
||||||
|
iex> Til.AstUtils.get_parent_node(nodes, child)
|
||||||
|
%{id: 1, name: "parent"}
|
||||||
|
|
||||||
|
iex> root_node = %{id: 3, parent_id: nil}
|
||||||
|
iex> nodes_with_root = %{3 => root_node}
|
||||||
|
iex> Til.AstUtils.get_parent_node(nodes_with_root, 3)
|
||||||
|
nil
|
||||||
|
"""
|
||||||
|
def get_parent_node(nodes_map, child_node_id)
|
||||||
|
when is_map(nodes_map) and is_integer(child_node_id) do
|
||||||
|
case get_node(nodes_map, child_node_id) do
|
||||||
|
nil -> nil
|
||||||
|
child_node -> get_parent_node_from_node(nodes_map, child_node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_parent_node(nodes_map, child_node) when is_map(nodes_map) and is_map(child_node) do
|
||||||
|
get_parent_node_from_node(nodes_map, child_node)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_parent_node_from_node(nodes_map, child_node) do
|
||||||
|
case Map.get(child_node, :parent_id) do
|
||||||
|
nil -> nil
|
||||||
|
parent_id -> get_node(nodes_map, parent_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates a string representation of the AST for pretty printing.
|
||||||
|
This is a basic implementation and can be expanded for more detail.
|
||||||
|
"""
|
||||||
|
def pretty_print_ast(nodes_map) when is_map(nodes_map) do
|
||||||
|
all_node_ids = Map.keys(nodes_map)
|
||||||
|
|
||||||
|
root_nodes =
|
||||||
|
nodes_map
|
||||||
|
|> Map.values()
|
||||||
|
|> Enum.filter(fn node ->
|
||||||
|
parent_id = Map.get(node, :parent_id)
|
||||||
|
is_nil(parent_id) or not Enum.member?(all_node_ids, parent_id)
|
||||||
|
end)
|
||||||
|
|> Enum.sort_by(fn node ->
|
||||||
|
case Map.get(node, :location) do
|
||||||
|
[start_offset | _] when is_integer(start_offset) -> start_offset
|
||||||
|
# Fallback to id if location is not as expected or not present
|
||||||
|
_ -> node.id
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
Enum.map_join(root_nodes, "\n", fn root_node ->
|
||||||
|
do_pretty_print_node(nodes_map, root_node, 0)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_pretty_print_node(nodes_map, node, indent_level) do
|
||||||
|
prefix = String.duplicate(" ", indent_level)
|
||||||
|
node_id = node.id
|
||||||
|
ast_type = Map.get(node, :ast_node_type, :unknown)
|
||||||
|
raw_string = Map.get(node, :raw_string, "") |> String.replace("\n", "\\n")
|
||||||
|
|
||||||
|
details =
|
||||||
|
case ast_type do
|
||||||
|
:literal_integer -> "value: #{Map.get(node, :value)}"
|
||||||
|
# Consider truncating
|
||||||
|
:literal_string -> "value: \"#{Map.get(node, :value)}\""
|
||||||
|
:symbol -> "name: #{Map.get(node, :name)}"
|
||||||
|
_ -> ""
|
||||||
|
end
|
||||||
|
|
||||||
|
error_info =
|
||||||
|
if parsing_error = Map.get(node, :parsing_error) do
|
||||||
|
" (ERROR: #{parsing_error})"
|
||||||
|
else
|
||||||
|
""
|
||||||
|
end
|
||||||
|
|
||||||
|
current_node_str =
|
||||||
|
"#{prefix}Node #{node_id} [#{ast_type}] raw: \"#{raw_string}\" #{details}#{error_info}"
|
||||||
|
|
||||||
|
children_str =
|
||||||
|
node
|
||||||
|
|> Map.get(:children, [])
|
||||||
|
|> Enum.map(fn child_id ->
|
||||||
|
case get_node(nodes_map, child_id) do
|
||||||
|
# Should not happen in valid AST
|
||||||
|
nil -> "#{prefix} Child ID #{child_id} (not found)"
|
||||||
|
child_node -> do_pretty_print_node(nodes_map, child_node, indent_level + 1)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.join("\n")
|
||||||
|
|
||||||
|
if String.trim(children_str) == "" do
|
||||||
|
current_node_str
|
||||||
|
else
|
||||||
|
current_node_str <> "\n" <> children_str
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Generates a nested data structure representation of the AST for debugging.
|
||||||
|
"""
|
||||||
|
def build_debug_ast_data(nodes_map) when is_map(nodes_map) do
|
||||||
|
all_node_ids = Map.keys(nodes_map)
|
||||||
|
|
||||||
|
root_nodes =
|
||||||
|
nodes_map
|
||||||
|
|> Map.values()
|
||||||
|
|> Enum.filter(fn node ->
|
||||||
|
parent_id = Map.get(node, :parent_id)
|
||||||
|
is_nil(parent_id) or not Enum.member?(all_node_ids, parent_id)
|
||||||
|
end)
|
||||||
|
|> Enum.sort_by(fn node ->
|
||||||
|
case Map.get(node, :location) do
|
||||||
|
[start_offset | _] when is_integer(start_offset) -> start_offset
|
||||||
|
_ -> node.id
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
Enum.map(root_nodes, fn root_node ->
|
||||||
|
do_build_debug_node_data(nodes_map, root_node)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_build_debug_node_data(nodes_map, node) do
|
||||||
|
node_id = node.id
|
||||||
|
ast_type = Map.get(node, :ast_node_type, :unknown)
|
||||||
|
raw_string = Map.get(node, :raw_string, "")
|
||||||
|
|
||||||
|
details =
|
||||||
|
case ast_type do
|
||||||
|
:literal_integer -> %{value: Map.get(node, :value)}
|
||||||
|
:literal_string -> %{value: Map.get(node, :value)}
|
||||||
|
:symbol -> %{name: Map.get(node, :name)}
|
||||||
|
_ -> %{}
|
||||||
|
end
|
||||||
|
|
||||||
|
error_info = Map.get(node, :parsing_error)
|
||||||
|
|
||||||
|
base_node_data = %{
|
||||||
|
id: node_id,
|
||||||
|
ast_node_type: ast_type,
|
||||||
|
raw_string: raw_string,
|
||||||
|
details: details
|
||||||
|
}
|
||||||
|
|
||||||
|
node_data_with_error =
|
||||||
|
if error_info do
|
||||||
|
Map.put(base_node_data, :parsing_error, error_info)
|
||||||
|
else
|
||||||
|
base_node_data
|
||||||
|
end
|
||||||
|
|
||||||
|
children_data =
|
||||||
|
node
|
||||||
|
|> Map.get(:children, [])
|
||||||
|
|> Enum.map(fn child_id ->
|
||||||
|
case get_node(nodes_map, child_id) do
|
||||||
|
nil -> %{error: "Child ID #{child_id} (not found)"}
|
||||||
|
child_node -> do_build_debug_node_data(nodes_map, child_node)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
if Enum.empty?(children_data) do
|
||||||
|
node_data_with_error
|
||||||
|
else
|
||||||
|
Map.put(node_data_with_error, :children, children_data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
963
lib/til/parser.ex
Normal file
963
lib/til/parser.ex
Normal file
@ -0,0 +1,963 @@
|
|||||||
|
defmodule Til.Parser do
|
||||||
|
@moduledoc """
|
||||||
|
Parser for the Tilly Lisp dialect.
|
||||||
|
It transforms source code into a collection of Node Maps.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Represents the current parsing position
|
||||||
|
defstruct offset: 0, line: 1, col: 1, file_name: "unknown", nodes: %{}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Parses a source string into a map of AST nodes.
|
||||||
|
"""
|
||||||
|
def parse(source_string, file_name \\ "unknown") do
|
||||||
|
file_node_id = System.unique_integer([:monotonic, :positive])
|
||||||
|
|
||||||
|
# Initial location for the file node (starts at the beginning)
|
||||||
|
file_start_offset = 0
|
||||||
|
file_start_line = 1
|
||||||
|
file_start_col = 1
|
||||||
|
|
||||||
|
# End location and raw_string will be finalized after parsing all content
|
||||||
|
prelim_file_node = %{
|
||||||
|
id: file_node_id,
|
||||||
|
type_id: nil,
|
||||||
|
# File node is the root
|
||||||
|
parent_id: nil,
|
||||||
|
file: file_name,
|
||||||
|
# End TBD
|
||||||
|
location: [file_start_offset, file_start_line, file_start_col, 0, 0, 0],
|
||||||
|
# TBD
|
||||||
|
raw_string: "",
|
||||||
|
ast_node_type: :file,
|
||||||
|
# TBD
|
||||||
|
children: [],
|
||||||
|
parsing_error: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
initial_state = %__MODULE__{
|
||||||
|
file_name: file_name,
|
||||||
|
# Add prelim file node
|
||||||
|
nodes: %{file_node_id => prelim_file_node},
|
||||||
|
# Initial state offset should be 0 for the file
|
||||||
|
offset: 0,
|
||||||
|
# Initial state line should be 1
|
||||||
|
line: 1,
|
||||||
|
# Initial state col should be 1
|
||||||
|
col: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pass original_source_string for raw_string extraction, and file_node_id as parent for top-level exprs
|
||||||
|
final_state_after_expressions =
|
||||||
|
parse_all_expressions(source_string, source_string, initial_state, file_node_id)
|
||||||
|
|
||||||
|
# Finalize the file node
|
||||||
|
# Calculate end position of the entire source string
|
||||||
|
{file_end_line, file_end_col} = calculate_new_line_col(source_string, 1, 1)
|
||||||
|
# Offset is 0-indexed, length is the count of characters, so end_offset is length.
|
||||||
|
file_end_offset = String.length(source_string)
|
||||||
|
|
||||||
|
# Collect children of the file node
|
||||||
|
file_children_ids =
|
||||||
|
final_state_after_expressions.nodes
|
||||||
|
|> Map.values()
|
||||||
|
|> Enum.filter(&(&1.parent_id == file_node_id))
|
||||||
|
# Sort by start offset to maintain order of appearance in the source
|
||||||
|
|> Enum.sort_by(fn node -> hd(node.location) end)
|
||||||
|
|> Enum.map(& &1.id)
|
||||||
|
|
||||||
|
updated_file_node =
|
||||||
|
final_state_after_expressions.nodes
|
||||||
|
|> Map.get(file_node_id)
|
||||||
|
|> Map.merge(%{
|
||||||
|
location: [
|
||||||
|
file_start_offset,
|
||||||
|
file_start_line,
|
||||||
|
file_start_col,
|
||||||
|
file_end_offset,
|
||||||
|
file_end_line,
|
||||||
|
file_end_col
|
||||||
|
],
|
||||||
|
# The entire source is the raw string of the file node
|
||||||
|
raw_string: source_string,
|
||||||
|
children: file_children_ids
|
||||||
|
})
|
||||||
|
|
||||||
|
final_nodes =
|
||||||
|
Map.put(final_state_after_expressions.nodes, file_node_id, updated_file_node)
|
||||||
|
|
||||||
|
{:ok, final_nodes}
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Main Parsing Logic ---
|
||||||
|
|
||||||
|
# original_source_string is the complete initial source, source_string is the current remainder
|
||||||
|
# parent_id_for_top_level_expressions is the ID of the node that top-level expressions should be parented to (e.g., the :file node)
|
||||||
|
defp parse_all_expressions(
|
||||||
|
original_source_string,
|
||||||
|
source_string,
|
||||||
|
state,
|
||||||
|
parent_id_for_top_level_expressions
|
||||||
|
) do
|
||||||
|
case skip_whitespace(source_string, state) do
|
||||||
|
{:eos, final_state} ->
|
||||||
|
final_state
|
||||||
|
|
||||||
|
{:ok, remaining_source, current_state} ->
|
||||||
|
if remaining_source == "" do
|
||||||
|
# All content parsed, nothing left after skipping whitespace.
|
||||||
|
# This is a successful termination of parsing for the current branch.
|
||||||
|
current_state
|
||||||
|
else
|
||||||
|
# There's actual content to parse.
|
||||||
|
case parse_datum(
|
||||||
|
original_source_string,
|
||||||
|
remaining_source,
|
||||||
|
current_state,
|
||||||
|
parent_id_for_top_level_expressions
|
||||||
|
) do
|
||||||
|
{:ok, _node_id, next_source, next_state} ->
|
||||||
|
parse_all_expressions(
|
||||||
|
original_source_string,
|
||||||
|
next_source,
|
||||||
|
next_state,
|
||||||
|
parent_id_for_top_level_expressions
|
||||||
|
)
|
||||||
|
|
||||||
|
{:error_node, _node_id, _reason, next_source, next_state} ->
|
||||||
|
# An error node was created by parse_datum.
|
||||||
|
# Input was consumed. Continue parsing from next_source.
|
||||||
|
parse_all_expressions(
|
||||||
|
original_source_string,
|
||||||
|
next_source,
|
||||||
|
next_state,
|
||||||
|
parent_id_for_top_level_expressions
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: This relies on parse_datum and its components (like create_error_node_and_advance)
|
||||||
|
# to always consume input if source_string is not empty. If parse_datum could return
|
||||||
|
# :error_node without consuming input on a non-empty string, an infinite loop is possible.
|
||||||
|
# Current implementation of create_error_node_and_advance consumes 1 char.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Parses a single datum: an atom (integer, symbol) or a list.
|
||||||
|
defp parse_datum(original_source_string, source, state, parent_id) do
|
||||||
|
# Peek for multi-character tokens first
|
||||||
|
cond do
|
||||||
|
String.starts_with?(source, "m{") ->
|
||||||
|
# Returns {:ok | :error_node, ...}
|
||||||
|
parse_map_expression(original_source_string, source, state, parent_id)
|
||||||
|
|
||||||
|
# Fallback to single character dispatch
|
||||||
|
true ->
|
||||||
|
char = String.first(source)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
char == "(" ->
|
||||||
|
# Returns {:ok | :error_node, ...}
|
||||||
|
parse_s_expression(original_source_string, source, state, parent_id)
|
||||||
|
|
||||||
|
char == ")" ->
|
||||||
|
# Unexpected closing parenthesis, consume 1 char for the error token ')'
|
||||||
|
# Returns {:error_node, ...}
|
||||||
|
create_error_node_and_advance(source, state, parent_id, 1, "Unexpected ')'")
|
||||||
|
|
||||||
|
char == "[" ->
|
||||||
|
# Returns {:ok | :error_node, ...}
|
||||||
|
parse_list_expression(original_source_string, source, state, parent_id)
|
||||||
|
|
||||||
|
char == "]" ->
|
||||||
|
# Unexpected closing square bracket, consume 1 char for the error token ']'
|
||||||
|
# Returns {:error_node, ...}
|
||||||
|
create_error_node_and_advance(source, state, parent_id, 1, "Unexpected ']'")
|
||||||
|
|
||||||
|
# For tuples
|
||||||
|
char == "{" ->
|
||||||
|
# Returns {:ok | :error_node, ...}
|
||||||
|
parse_tuple_expression(original_source_string, source, state, parent_id)
|
||||||
|
|
||||||
|
char == "}" ->
|
||||||
|
# Unexpected closing curly brace
|
||||||
|
# Returns {:error_node, ...}
|
||||||
|
create_error_node_and_advance(source, state, parent_id, 1, "Unexpected '}'")
|
||||||
|
|
||||||
|
char == "'" ->
|
||||||
|
# Returns {:ok | :error_node, ...}
|
||||||
|
parse_string_datum(original_source_string, source, state, parent_id)
|
||||||
|
|
||||||
|
char == ":" ->
|
||||||
|
# If the first char is ':', try to parse as an atom like :foo
|
||||||
|
case parse_atom_datum(source, state, parent_id) do
|
||||||
|
{:ok, node_id, rest, new_state} ->
|
||||||
|
{:ok, node_id, rest, new_state}
|
||||||
|
{:error, :not_atom} ->
|
||||||
|
# Failed to parse as a specific atom (e.g. ":foo").
|
||||||
|
# It could be a symbol that starts with ':' (e.g. if we allow ":" as a symbol).
|
||||||
|
# Fallback to general symbol parsing. Integer parsing won't match if it starts with ':'.
|
||||||
|
case parse_symbol_datum(source, state, parent_id) do
|
||||||
|
{:ok, node_id, rest, new_state} ->
|
||||||
|
{:ok, node_id, rest, new_state}
|
||||||
|
{:error, :not_symbol} ->
|
||||||
|
# If it started with ':' but wasn't a valid atom and also not a valid symbol
|
||||||
|
create_error_node_and_advance(source, state, parent_id, 1, "Unknown token starting with ':'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Default case for other characters
|
||||||
|
# Try parsing as an integer first
|
||||||
|
case parse_integer_datum(source, state, parent_id) do
|
||||||
|
{:ok, node_id, rest, new_state} ->
|
||||||
|
{:ok, node_id, rest, new_state}
|
||||||
|
{:error, :not_integer} ->
|
||||||
|
# Not an integer, try parsing as a symbol
|
||||||
|
case parse_symbol_datum(source, state, parent_id) do
|
||||||
|
{:ok, node_id, rest, new_state} ->
|
||||||
|
{:ok, node_id, rest, new_state}
|
||||||
|
{:error, :not_symbol} ->
|
||||||
|
# Not a symbol either. Consume 1 char for the unknown token.
|
||||||
|
create_error_node_and_advance(source, state, parent_id, 1, "Unknown token")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end # end inner cond
|
||||||
|
end # end outer cond
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Datum Parsing Helpers --- (parse_string_datum, process_string_content)
|
||||||
|
|
||||||
|
defp parse_string_datum(_original_source_string, source, state, parent_id) do
|
||||||
|
# state is before consuming "'"
|
||||||
|
initial_state_for_token = state
|
||||||
|
strip_indent = initial_state_for_token.col - 1
|
||||||
|
|
||||||
|
# Consume opening "'"
|
||||||
|
{opening_tick, source_after_opening_tick} = String.split_at(source, 1)
|
||||||
|
|
||||||
|
case :binary.match(source_after_opening_tick, "'") do
|
||||||
|
:nomatch ->
|
||||||
|
# Unclosed string
|
||||||
|
content_segment = source_after_opening_tick
|
||||||
|
raw_token = opening_tick <> content_segment
|
||||||
|
|
||||||
|
state_at_node_end = advance_pos(initial_state_for_token, raw_token)
|
||||||
|
|
||||||
|
location = [
|
||||||
|
initial_state_for_token.offset,
|
||||||
|
initial_state_for_token.line,
|
||||||
|
initial_state_for_token.col,
|
||||||
|
state_at_node_end.offset,
|
||||||
|
state_at_node_end.line,
|
||||||
|
state_at_node_end.col
|
||||||
|
]
|
||||||
|
|
||||||
|
processed_value = process_string_content(content_segment, strip_indent)
|
||||||
|
|
||||||
|
{node_id, state_with_error_node} =
|
||||||
|
add_node(
|
||||||
|
initial_state_for_token,
|
||||||
|
parent_id,
|
||||||
|
location,
|
||||||
|
raw_token,
|
||||||
|
:literal_string,
|
||||||
|
%{value: processed_value, parsing_error: "Unclosed string literal"}
|
||||||
|
)
|
||||||
|
|
||||||
|
final_state = %{
|
||||||
|
state_with_error_node
|
||||||
|
| offset: state_at_node_end.offset,
|
||||||
|
line: state_at_node_end.line,
|
||||||
|
col: state_at_node_end.col
|
||||||
|
}
|
||||||
|
|
||||||
|
{:error_node, node_id, "Unclosed string literal", "", final_state}
|
||||||
|
|
||||||
|
# _tick_length will be 1 for "`"
|
||||||
|
{idx_closing_tick_in_segment, _tick_length} ->
|
||||||
|
content_segment =
|
||||||
|
String.slice(source_after_opening_tick, 0, idx_closing_tick_in_segment)
|
||||||
|
|
||||||
|
closing_tick = "'"
|
||||||
|
raw_token = opening_tick <> content_segment <> closing_tick
|
||||||
|
|
||||||
|
rest_of_source =
|
||||||
|
String.slice(source_after_opening_tick, (idx_closing_tick_in_segment + 1)..-1)
|
||||||
|
|
||||||
|
state_at_node_end = advance_pos(initial_state_for_token, raw_token)
|
||||||
|
|
||||||
|
location = [
|
||||||
|
initial_state_for_token.offset,
|
||||||
|
initial_state_for_token.line,
|
||||||
|
initial_state_for_token.col,
|
||||||
|
state_at_node_end.offset,
|
||||||
|
state_at_node_end.line,
|
||||||
|
state_at_node_end.col
|
||||||
|
]
|
||||||
|
|
||||||
|
processed_value = process_string_content(content_segment, strip_indent)
|
||||||
|
|
||||||
|
{new_node_id, state_with_node} =
|
||||||
|
add_node(
|
||||||
|
initial_state_for_token,
|
||||||
|
parent_id,
|
||||||
|
location,
|
||||||
|
raw_token,
|
||||||
|
:literal_string,
|
||||||
|
%{value: processed_value}
|
||||||
|
)
|
||||||
|
|
||||||
|
final_state = %{
|
||||||
|
state_with_node
|
||||||
|
| offset: state_at_node_end.offset,
|
||||||
|
line: state_at_node_end.line,
|
||||||
|
col: state_at_node_end.col
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, new_node_id, rest_of_source, final_state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_string_content(content_str, strip_indent) when strip_indent >= 0 do
|
||||||
|
lines = String.split(content_str, "\n", trim: false)
|
||||||
|
# Will always exist, even for empty content_str -> ""
|
||||||
|
first_line = List.first(lines)
|
||||||
|
|
||||||
|
rest_lines =
|
||||||
|
if length(lines) > 1 do
|
||||||
|
List.delete_at(lines, 0)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
|
||||||
|
processed_rest_lines =
|
||||||
|
Enum.map(rest_lines, fn line ->
|
||||||
|
current_leading_spaces_count =
|
||||||
|
Regex.run(~r/^(\s*)/, line)
|
||||||
|
|> List.first()
|
||||||
|
|> String.length()
|
||||||
|
|
||||||
|
spaces_to_remove = min(current_leading_spaces_count, strip_indent)
|
||||||
|
String.slice(line, spaces_to_remove..-1)
|
||||||
|
end)
|
||||||
|
|
||||||
|
all_processed_lines = [first_line | processed_rest_lines]
|
||||||
|
Enum.join(all_processed_lines, "\n")
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Datum Parsing Helpers --- (parse_string_datum, process_string_content)
|
||||||
|
|
||||||
|
# (parse_string_datum remains unchanged)
|
||||||
|
|
||||||
|
defp parse_atom_datum(source, state, parent_id) do
|
||||||
|
# Atom is a colon followed by one or more non-delimiter characters.
|
||||||
|
# Delimiters are whitespace, (, ), [, ], {, }.
|
||||||
|
# The colon itself is part of the atom's raw string.
|
||||||
|
# The `atom_name_part` is what comes after the colon.
|
||||||
|
case Regex.run(~r/^:([^\s\(\)\[\]\{\}]+)/, source) do
|
||||||
|
[raw_atom_str, atom_name_part] -> # raw_atom_str is like ":foo", atom_name_part is "foo"
|
||||||
|
# The regex [^...]+ ensures atom_name_part is not empty.
|
||||||
|
rest_after_atom = String.slice(source, String.length(raw_atom_str)..-1)
|
||||||
|
start_offset = state.offset
|
||||||
|
start_line = state.line
|
||||||
|
start_col = state.col
|
||||||
|
state_after_token = advance_pos(state, raw_atom_str)
|
||||||
|
end_offset = state_after_token.offset
|
||||||
|
end_line = state_after_token.line
|
||||||
|
end_col = state_after_token.col
|
||||||
|
location = [start_offset, start_line, start_col, end_offset, end_line, end_col]
|
||||||
|
|
||||||
|
# Convert the name part (e.g., "foo") to an Elixir atom (e.g., :foo)
|
||||||
|
atom_value = String.to_atom(atom_name_part)
|
||||||
|
|
||||||
|
{new_node_id, state_with_node} =
|
||||||
|
add_node(
|
||||||
|
state,
|
||||||
|
parent_id,
|
||||||
|
location,
|
||||||
|
raw_atom_str,
|
||||||
|
:literal_atom,
|
||||||
|
%{value: atom_value}
|
||||||
|
)
|
||||||
|
|
||||||
|
final_state = %{
|
||||||
|
state_with_node
|
||||||
|
| offset: end_offset,
|
||||||
|
line: end_line,
|
||||||
|
col: end_col
|
||||||
|
}
|
||||||
|
{:ok, new_node_id, rest_after_atom, final_state}
|
||||||
|
|
||||||
|
_ -> # No match (nil or list that doesn't conform, e.g., just ":" or ": followed by space/delimiter")
|
||||||
|
{:error, :not_atom}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_integer_datum(source, state, parent_id) do
|
||||||
|
case Integer.parse(source) do
|
||||||
|
{int_val, rest_after_int} ->
|
||||||
|
raw_int =
|
||||||
|
String.slice(source, 0, String.length(source) - String.length(rest_after_int))
|
||||||
|
|
||||||
|
start_offset = state.offset
|
||||||
|
start_line = state.line
|
||||||
|
start_col = state.col
|
||||||
|
state_after_token = advance_pos(state, raw_int)
|
||||||
|
end_offset = state_after_token.offset
|
||||||
|
end_line = state_after_token.line
|
||||||
|
end_col = state_after_token.col
|
||||||
|
location = [start_offset, start_line, start_col, end_offset, end_line, end_col]
|
||||||
|
|
||||||
|
{new_node_id, state_with_node} =
|
||||||
|
add_node(state, parent_id, location, raw_int, :literal_integer, %{value: int_val})
|
||||||
|
|
||||||
|
# Update state to reflect consumed token
|
||||||
|
final_state = %{state_with_node | offset: end_offset, line: end_line, col: end_col}
|
||||||
|
{:ok, new_node_id, rest_after_int, final_state}
|
||||||
|
|
||||||
|
:error ->
|
||||||
|
# Indicates failure, source and state are unchanged by this attempt
|
||||||
|
{:error, :not_integer}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_symbol_datum(source, state, parent_id) do
|
||||||
|
# Regex excludes common delimiters. `m{` is handled before symbol parsing.
|
||||||
|
case Regex.run(~r/^([^\s\(\)\[\]\{\}]+)/, source) do
|
||||||
|
[raw_symbol | _] ->
|
||||||
|
rest_after_symbol = String.slice(source, String.length(raw_symbol)..-1)
|
||||||
|
start_offset = state.offset
|
||||||
|
start_line = state.line
|
||||||
|
start_col = state.col
|
||||||
|
state_after_token = advance_pos(state, raw_symbol)
|
||||||
|
end_offset = state_after_token.offset
|
||||||
|
end_line = state_after_token.line
|
||||||
|
end_col = state_after_token.col
|
||||||
|
location = [start_offset, start_line, start_col, end_offset, end_line, end_col]
|
||||||
|
|
||||||
|
{new_node_id, state_with_node} =
|
||||||
|
add_node(state, parent_id, location, raw_symbol, :symbol, %{name: raw_symbol})
|
||||||
|
|
||||||
|
# Update state to reflect consumed token
|
||||||
|
final_state = %{
|
||||||
|
state_with_node
|
||||||
|
| offset: end_offset,
|
||||||
|
line: end_line,
|
||||||
|
col: end_col
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, new_node_id, rest_after_symbol, final_state}
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
# Indicates failure, source and state are unchanged by this attempt
|
||||||
|
{:error, :not_symbol}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_error_node_and_advance(
|
||||||
|
source_for_token,
|
||||||
|
state_before_token,
|
||||||
|
parent_id,
|
||||||
|
num_chars_for_token,
|
||||||
|
error_message
|
||||||
|
) do
|
||||||
|
{raw_token, rest_of_source} = String.split_at(source_for_token, num_chars_for_token)
|
||||||
|
|
||||||
|
start_offset = state_before_token.offset
|
||||||
|
start_line = state_before_token.line
|
||||||
|
start_col = state_before_token.col
|
||||||
|
|
||||||
|
state_after_token_consumed = advance_pos(state_before_token, raw_token)
|
||||||
|
end_offset = state_after_token_consumed.offset
|
||||||
|
end_line = state_after_token_consumed.line
|
||||||
|
end_col = state_after_token_consumed.col
|
||||||
|
location = [start_offset, start_line, start_col, end_offset, end_line, end_col]
|
||||||
|
|
||||||
|
{error_node_id, state_with_error_node} =
|
||||||
|
add_node(state_before_token, parent_id, location, raw_token, :unknown, %{
|
||||||
|
parsing_error: error_message
|
||||||
|
})
|
||||||
|
|
||||||
|
# The state for further parsing must reflect the consumed token's position and include the new error node
|
||||||
|
final_error_state = %{
|
||||||
|
state_with_error_node
|
||||||
|
| offset: end_offset,
|
||||||
|
line: end_line,
|
||||||
|
col: end_col
|
||||||
|
}
|
||||||
|
|
||||||
|
{:error_node, error_node_id, error_message, rest_of_source, final_error_state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_s_expression(original_source_string, source, state, parent_id) do
|
||||||
|
# Standard S-expression parsing via parse_collection
|
||||||
|
result = parse_collection(
|
||||||
|
original_source_string,
|
||||||
|
source,
|
||||||
|
state,
|
||||||
|
parent_id,
|
||||||
|
"(",
|
||||||
|
")",
|
||||||
|
:s_expression,
|
||||||
|
"Unclosed S-expression",
|
||||||
|
"Error parsing element in S-expression. Content might be incomplete."
|
||||||
|
)
|
||||||
|
|
||||||
|
# After parsing, check if it's an 'fn' expression
|
||||||
|
case result do
|
||||||
|
{:ok, collection_node_id, rest_after_collection, state_after_collection} ->
|
||||||
|
collection_node = Map.get(state_after_collection.nodes, collection_node_id)
|
||||||
|
|
||||||
|
if is_fn_expression?(collection_node, state_after_collection.nodes) do
|
||||||
|
transformed_node =
|
||||||
|
transform_to_lambda_expression(collection_node, state_after_collection.nodes)
|
||||||
|
|
||||||
|
final_state = %{
|
||||||
|
state_after_collection
|
||||||
|
| nodes:
|
||||||
|
Map.put(state_after_collection.nodes, transformed_node.id, transformed_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, transformed_node.id, rest_after_collection, final_state}
|
||||||
|
else
|
||||||
|
# Not an fn expression, return as is
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
_error_or_other ->
|
||||||
|
# Propagate errors or other results from parse_collection
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to check if an S-expression node is an 'fn' expression
|
||||||
|
defp is_fn_expression?(s_expr_node, nodes_map) do
|
||||||
|
if s_expr_node.ast_node_type == :s_expression && !Enum.empty?(s_expr_node.children) do
|
||||||
|
first_child_id = hd(s_expr_node.children)
|
||||||
|
first_child_node = Map.get(nodes_map, first_child_id)
|
||||||
|
|
||||||
|
first_child_node && first_child_node.ast_node_type == :symbol &&
|
||||||
|
first_child_node.name == "fn"
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to transform a generic S-expression node (known to be an 'fn' form)
|
||||||
|
# into a :lambda_expression node.
|
||||||
|
defp transform_to_lambda_expression(s_expr_node, nodes_map) do
|
||||||
|
# s_expr_node.children = [fn_symbol_id, params_s_expr_id, body_form1_id, ...]
|
||||||
|
_fn_symbol_id = Enum.at(s_expr_node.children, 0) # Already checked
|
||||||
|
|
||||||
|
if length(s_expr_node.children) < 2 do
|
||||||
|
%{s_expr_node | parsing_error: "Malformed 'fn' expression: missing parameters list."}
|
||||||
|
else
|
||||||
|
params_s_expr_id = Enum.at(s_expr_node.children, 1)
|
||||||
|
params_s_expr_node = Map.get(nodes_map, params_s_expr_id)
|
||||||
|
|
||||||
|
if !(params_s_expr_node && params_s_expr_node.ast_node_type == :s_expression) do
|
||||||
|
Map.put(s_expr_node, :parsing_error, "Malformed 'fn' expression: parameters list is not an S-expression.")
|
||||||
|
else
|
||||||
|
# Children of the parameters S-expression, e.g. for (fn ((a integer) (b atom) atom) ...),
|
||||||
|
# param_s_expr_children_ids would be IDs of [(a integer), (b atom), atom]
|
||||||
|
all_param_children_ids = Map.get(params_s_expr_node, :children, [])
|
||||||
|
|
||||||
|
{arg_spec_node_ids, return_type_spec_node_id} =
|
||||||
|
if Enum.empty?(all_param_children_ids) do
|
||||||
|
# Case: (fn () body) -> No args, nil (inferred) return type spec
|
||||||
|
{[], nil}
|
||||||
|
else
|
||||||
|
# Case: (fn (arg1 type1 ... ret_type) body)
|
||||||
|
# Last element is return type spec, rest are arg specs.
|
||||||
|
args = Enum.take(all_param_children_ids, length(all_param_children_ids) - 1)
|
||||||
|
ret_type_id = List.last(all_param_children_ids)
|
||||||
|
{args, ret_type_id}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate arg_spec_node_ids: each must be a symbol or an S-expr (param_symbol type_spec)
|
||||||
|
all_arg_specs_valid =
|
||||||
|
Enum.all?(arg_spec_node_ids, fn arg_id ->
|
||||||
|
arg_node = Map.get(nodes_map, arg_id)
|
||||||
|
case arg_node do
|
||||||
|
%{ast_node_type: :symbol} -> true # e.g. x
|
||||||
|
%{ast_node_type: :s_expression, children: s_children} -> # e.g. (x integer)
|
||||||
|
if length(s_children) == 2 do
|
||||||
|
param_sym_node = Map.get(nodes_map, hd(s_children))
|
||||||
|
type_spec_node = Map.get(nodes_map, hd(tl(s_children)))
|
||||||
|
|
||||||
|
param_sym_node && param_sym_node.ast_node_type == :symbol &&
|
||||||
|
type_spec_node && (type_spec_node.ast_node_type == :symbol || type_spec_node.ast_node_type == :s_expression)
|
||||||
|
else
|
||||||
|
false # Not a valid (param_symbol type_spec) structure
|
||||||
|
end
|
||||||
|
_ -> false # Not a symbol or valid S-expression for arg spec
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Validate return_type_spec_node_id: must be nil or a valid type specifier node
|
||||||
|
return_type_spec_valid =
|
||||||
|
if is_nil(return_type_spec_node_id) do
|
||||||
|
true # Inferred return type is valid
|
||||||
|
else
|
||||||
|
ret_type_node = Map.get(nodes_map, return_type_spec_node_id)
|
||||||
|
ret_type_node && (ret_type_node.ast_node_type == :symbol || ret_type_node.ast_node_type == :s_expression)
|
||||||
|
end
|
||||||
|
|
||||||
|
if all_arg_specs_valid && return_type_spec_valid do
|
||||||
|
body_node_ids = Enum.drop(s_expr_node.children, 2) # Body starts after 'fn' and params_s_expr
|
||||||
|
Map.merge(s_expr_node, %{
|
||||||
|
:ast_node_type => :lambda_expression,
|
||||||
|
:params_s_expr_id => params_s_expr_id,
|
||||||
|
:arg_spec_node_ids => arg_spec_node_ids,
|
||||||
|
:return_type_spec_node_id => return_type_spec_node_id,
|
||||||
|
:body_node_ids => body_node_ids
|
||||||
|
})
|
||||||
|
else
|
||||||
|
# Determine more specific error message
|
||||||
|
error_message =
|
||||||
|
cond do
|
||||||
|
!all_arg_specs_valid -> "Malformed 'fn' expression: invalid argument specification(s)."
|
||||||
|
!return_type_spec_valid -> "Malformed 'fn' expression: invalid return type specification."
|
||||||
|
true -> "Malformed 'fn' expression." # Generic fallback
|
||||||
|
end
|
||||||
|
Map.put(s_expr_node, :parsing_error, error_message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_list_expression(original_source_string, source, state, parent_id) do
|
||||||
|
parse_collection(
|
||||||
|
original_source_string,
|
||||||
|
source,
|
||||||
|
state,
|
||||||
|
parent_id,
|
||||||
|
"[",
|
||||||
|
"]",
|
||||||
|
:list_expression,
|
||||||
|
"Unclosed list",
|
||||||
|
"Error parsing element in list. Content might be incomplete."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_map_expression(original_source_string, source, state, parent_id) do
|
||||||
|
parse_collection(
|
||||||
|
original_source_string,
|
||||||
|
source,
|
||||||
|
state,
|
||||||
|
parent_id,
|
||||||
|
# Opening token
|
||||||
|
"m{",
|
||||||
|
# Closing token
|
||||||
|
"}",
|
||||||
|
:map_expression,
|
||||||
|
"Unclosed map",
|
||||||
|
"Error parsing element in map. Content might be incomplete."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_tuple_expression(original_source_string, source, state, parent_id) do
|
||||||
|
parse_collection(
|
||||||
|
original_source_string,
|
||||||
|
source,
|
||||||
|
state,
|
||||||
|
parent_id,
|
||||||
|
"{",
|
||||||
|
"}",
|
||||||
|
:tuple_expression,
|
||||||
|
"Unclosed tuple",
|
||||||
|
"Error parsing element in tuple. Content might be incomplete."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_collection(
|
||||||
|
original_source_string,
|
||||||
|
source,
|
||||||
|
state,
|
||||||
|
parent_id,
|
||||||
|
open_char_str,
|
||||||
|
# Used by parse_collection_elements
|
||||||
|
close_char_str,
|
||||||
|
ast_node_type,
|
||||||
|
# Used by parse_collection_elements
|
||||||
|
unclosed_error_msg,
|
||||||
|
# Used by parse_collection_elements
|
||||||
|
element_error_msg
|
||||||
|
) do
|
||||||
|
# Consume opening token (e.g. '(', '[', 'm{')
|
||||||
|
collection_start_offset = state.offset
|
||||||
|
collection_start_line = state.line
|
||||||
|
collection_start_col = state.col
|
||||||
|
open_char_len = String.length(open_char_str)
|
||||||
|
{_opening_token, rest_after_opening_token} = String.split_at(source, open_char_len)
|
||||||
|
current_state = advance_pos(state, open_char_str)
|
||||||
|
|
||||||
|
collection_node_id = System.unique_integer([:monotonic, :positive])
|
||||||
|
|
||||||
|
prelim_collection_node = %{
|
||||||
|
id: collection_node_id,
|
||||||
|
type_id: nil,
|
||||||
|
parent_id: parent_id,
|
||||||
|
file: current_state.file_name,
|
||||||
|
# End TBD
|
||||||
|
location: [collection_start_offset, collection_start_line, collection_start_col, 0, 0, 0],
|
||||||
|
# TBD
|
||||||
|
raw_string: "",
|
||||||
|
ast_node_type: ast_node_type,
|
||||||
|
children: [],
|
||||||
|
parsing_error: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
current_state_with_prelim_node = %{
|
||||||
|
current_state
|
||||||
|
| nodes: Map.put(current_state.nodes, collection_node_id, prelim_collection_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
collection_start_pos_for_children =
|
||||||
|
{collection_start_offset, collection_start_line, collection_start_col}
|
||||||
|
|
||||||
|
# Pass all necessary params to the generalized element parser
|
||||||
|
result =
|
||||||
|
parse_collection_elements(
|
||||||
|
original_source_string,
|
||||||
|
rest_after_opening_token,
|
||||||
|
current_state_with_prelim_node,
|
||||||
|
collection_node_id,
|
||||||
|
[],
|
||||||
|
collection_start_pos_for_children,
|
||||||
|
# Parameters for generalization, passed from parse_collection's arguments:
|
||||||
|
# Used by parse_collection_elements
|
||||||
|
close_char_str,
|
||||||
|
# Used by parse_collection_elements
|
||||||
|
unclosed_error_msg,
|
||||||
|
# Passed to parse_collection_elements (might be unused there now)
|
||||||
|
element_error_msg
|
||||||
|
)
|
||||||
|
|
||||||
|
# Adapt result to {:ok, node_id, ...} or {:error_node, node_id, ...}
|
||||||
|
case result do
|
||||||
|
{:ok, returned_collection_node_id, rest, state_after_elements} ->
|
||||||
|
{:ok, returned_collection_node_id, rest, state_after_elements}
|
||||||
|
|
||||||
|
{:error, reason, rest, state_after_elements} ->
|
||||||
|
# The collection_node_id is the ID of the node that has the error.
|
||||||
|
# This 'reason' is typically for unclosed collections or fatal element errors.
|
||||||
|
{:error_node, collection_node_id, reason, rest, state_after_elements}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Generalized from parse_s_expression_elements
|
||||||
|
defp parse_collection_elements(
|
||||||
|
original_source_string,
|
||||||
|
source,
|
||||||
|
state,
|
||||||
|
collection_node_id,
|
||||||
|
children_ids_acc,
|
||||||
|
collection_start_pos_tuple,
|
||||||
|
# New parameters for generalization:
|
||||||
|
# e.g., ")" or "]"
|
||||||
|
closing_char_str,
|
||||||
|
# e.g., "Unclosed S-expression"
|
||||||
|
unclosed_error_message,
|
||||||
|
# e.g., "Error parsing element in S-expression..."
|
||||||
|
# Now potentially unused, marked with underscore
|
||||||
|
element_error_message
|
||||||
|
) do
|
||||||
|
case skip_whitespace(source, state) do
|
||||||
|
{:eos, current_state_at_eos} ->
|
||||||
|
# Unclosed collection
|
||||||
|
collection_node = Map.get(current_state_at_eos.nodes, collection_node_id)
|
||||||
|
start_offset = elem(collection_start_pos_tuple, 0)
|
||||||
|
end_offset = current_state_at_eos.offset
|
||||||
|
|
||||||
|
actual_raw_string =
|
||||||
|
String.slice(original_source_string, start_offset, end_offset - start_offset)
|
||||||
|
|
||||||
|
updated_collection_node = %{
|
||||||
|
collection_node
|
||||||
|
| # Use generalized message
|
||||||
|
parsing_error: unclosed_error_message,
|
||||||
|
children: Enum.reverse(children_ids_acc),
|
||||||
|
location: [
|
||||||
|
start_offset,
|
||||||
|
elem(collection_start_pos_tuple, 1),
|
||||||
|
elem(collection_start_pos_tuple, 2),
|
||||||
|
end_offset,
|
||||||
|
current_state_at_eos.line,
|
||||||
|
current_state_at_eos.col
|
||||||
|
],
|
||||||
|
raw_string: actual_raw_string
|
||||||
|
}
|
||||||
|
|
||||||
|
final_state = %{
|
||||||
|
current_state_at_eos
|
||||||
|
| nodes:
|
||||||
|
Map.put(current_state_at_eos.nodes, collection_node_id, updated_collection_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
# This error is for the collection itself being unclosed.
|
||||||
|
# The collection_node_id is implicitly the ID of this error node.
|
||||||
|
{:error, unclosed_error_message, "", final_state}
|
||||||
|
|
||||||
|
{:ok, remaining_source, current_state} ->
|
||||||
|
# Check if the remaining source starts with the closing token string
|
||||||
|
if String.starts_with?(remaining_source, closing_char_str) do
|
||||||
|
# End of collection
|
||||||
|
closing_char_len = String.length(closing_char_str)
|
||||||
|
|
||||||
|
{_closing_token, rest_after_closing_token} =
|
||||||
|
String.split_at(remaining_source, closing_char_len)
|
||||||
|
|
||||||
|
final_collection_state = advance_pos(current_state, closing_char_str)
|
||||||
|
collection_node = Map.get(final_collection_state.nodes, collection_node_id)
|
||||||
|
|
||||||
|
coll_final_start_offset = elem(collection_start_pos_tuple, 0)
|
||||||
|
coll_final_start_line = elem(collection_start_pos_tuple, 1)
|
||||||
|
coll_final_start_col = elem(collection_start_pos_tuple, 2)
|
||||||
|
coll_final_end_offset = final_collection_state.offset
|
||||||
|
coll_final_end_line = final_collection_state.line
|
||||||
|
coll_final_end_col = final_collection_state.col
|
||||||
|
|
||||||
|
actual_raw_string =
|
||||||
|
String.slice(
|
||||||
|
original_source_string,
|
||||||
|
coll_final_start_offset,
|
||||||
|
coll_final_end_offset - coll_final_start_offset
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_collection_node = %{
|
||||||
|
collection_node
|
||||||
|
| children: Enum.reverse(children_ids_acc),
|
||||||
|
location: [
|
||||||
|
coll_final_start_offset,
|
||||||
|
coll_final_start_line,
|
||||||
|
coll_final_start_col,
|
||||||
|
coll_final_end_offset,
|
||||||
|
coll_final_end_line,
|
||||||
|
coll_final_end_col
|
||||||
|
],
|
||||||
|
raw_string: actual_raw_string
|
||||||
|
}
|
||||||
|
|
||||||
|
final_state_with_collection = %{
|
||||||
|
final_collection_state
|
||||||
|
| nodes:
|
||||||
|
Map.put(
|
||||||
|
final_collection_state.nodes,
|
||||||
|
collection_node_id,
|
||||||
|
updated_collection_node
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{:ok, collection_node_id, rest_after_closing_token, final_state_with_collection}
|
||||||
|
else
|
||||||
|
# Parse an element
|
||||||
|
case parse_datum(
|
||||||
|
original_source_string,
|
||||||
|
remaining_source,
|
||||||
|
current_state,
|
||||||
|
# parent_id for the element
|
||||||
|
collection_node_id
|
||||||
|
) do
|
||||||
|
{:ok, child_node_id, next_source_after_elem, next_state_after_elem} ->
|
||||||
|
parse_collection_elements(
|
||||||
|
original_source_string,
|
||||||
|
next_source_after_elem,
|
||||||
|
next_state_after_elem,
|
||||||
|
collection_node_id,
|
||||||
|
# Add successful child's ID
|
||||||
|
[child_node_id | children_ids_acc],
|
||||||
|
collection_start_pos_tuple,
|
||||||
|
closing_char_str,
|
||||||
|
unclosed_error_message,
|
||||||
|
# Pass through, though may be unused
|
||||||
|
element_error_message
|
||||||
|
)
|
||||||
|
|
||||||
|
{:error_node, child_error_node_id, _child_reason, next_source_after_elem,
|
||||||
|
next_state_after_elem} ->
|
||||||
|
# An error node was created for the child element. Add its ID and continue.
|
||||||
|
parse_collection_elements(
|
||||||
|
original_source_string,
|
||||||
|
next_source_after_elem,
|
||||||
|
next_state_after_elem,
|
||||||
|
collection_node_id,
|
||||||
|
# Add error child's ID
|
||||||
|
[child_error_node_id | children_ids_acc],
|
||||||
|
collection_start_pos_tuple,
|
||||||
|
closing_char_str,
|
||||||
|
unclosed_error_message,
|
||||||
|
# Pass through
|
||||||
|
element_error_message
|
||||||
|
)
|
||||||
|
|
||||||
|
# No other return types are expected from parse_datum if it always creates a node on error
|
||||||
|
# or succeeds. If parse_datum could fail without creating a node and without consuming input,
|
||||||
|
# that would be an issue here, potentially leading to infinite loops if not handled.
|
||||||
|
# The current changes aim for parse_datum to always return :ok or :error_node.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Utility Functions ---
|
||||||
|
|
||||||
|
# Note: The `extra_fields` argument was changed from optional to required
|
||||||
|
# as the default value was never used according to compiler warnings.
|
||||||
|
defp add_node(state, parent_id, location, raw_string, ast_node_type, extra_fields) do
|
||||||
|
node_id = System.unique_integer([:monotonic, :positive])
|
||||||
|
|
||||||
|
node =
|
||||||
|
%{
|
||||||
|
id: node_id,
|
||||||
|
type_id: nil,
|
||||||
|
parent_id: parent_id,
|
||||||
|
file: state.file_name,
|
||||||
|
# [start_offset, start_line, start_col, end_offset, end_line, end_col]
|
||||||
|
location: location,
|
||||||
|
raw_string: raw_string,
|
||||||
|
ast_node_type: ast_node_type
|
||||||
|
}
|
||||||
|
|> Map.merge(extra_fields)
|
||||||
|
|
||||||
|
{node_id, %{state | nodes: Map.put(state.nodes, node_id, node)}}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp skip_whitespace(source, state = %__MODULE__{offset: o, line: l, col: c}) do
|
||||||
|
whitespace_match = Regex.run(~r/^\s+/, source)
|
||||||
|
|
||||||
|
if whitespace_match do
|
||||||
|
[ws | _] = whitespace_match
|
||||||
|
new_offset = o + String.length(ws)
|
||||||
|
{new_line, new_col} = calculate_new_line_col(ws, l, c)
|
||||||
|
remaining_source = String.slice(source, String.length(ws)..-1)
|
||||||
|
{:ok, remaining_source, %{state | offset: new_offset, line: new_line, col: new_col}}
|
||||||
|
else
|
||||||
|
if String.length(source) == 0 do
|
||||||
|
{:eos, state}
|
||||||
|
else
|
||||||
|
# No leading whitespace
|
||||||
|
{:ok, source, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp calculate_new_line_col(string_segment, start_line, start_col) do
|
||||||
|
string_segment
|
||||||
|
|> String.codepoints()
|
||||||
|
|> Enum.reduce({start_line, start_col}, fn char, {line, col} ->
|
||||||
|
if char == "\n" do
|
||||||
|
{line + 1, 1}
|
||||||
|
else
|
||||||
|
{line, col + 1}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp advance_pos(state = %__MODULE__{offset: o, line: l, col: c}, consumed_string) do
|
||||||
|
new_offset = o + String.length(consumed_string)
|
||||||
|
{new_line, new_col} = calculate_new_line_col(consumed_string, l, c)
|
||||||
|
%{state | offset: new_offset, line: new_line, col: new_col}
|
||||||
|
end
|
||||||
|
end
|
||||||
504
lib/til/typer.ex
Normal file
504
lib/til/typer.ex
Normal file
@ -0,0 +1,504 @@
|
|||||||
|
defmodule Til.Typer do
|
||||||
|
@moduledoc """
|
||||||
|
Handles type checking and type inference for the Tilly Lisp dialect.
|
||||||
|
It processes the AST (Node Maps) generated by the parser and annotates
|
||||||
|
nodes with their inferred or checked types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# alias Til.AstUtils # Removed as it's not used yet and causes a warning.
|
||||||
|
# alias MapSet, as: Set # No longer directly used here, moved to specialized modules
|
||||||
|
|
||||||
|
alias Til.Typer.Types
|
||||||
|
alias Til.Typer.Interner
|
||||||
|
alias Til.Typer.ExpressionTyper
|
||||||
|
# alias Til.Typer.SubtypeChecker # Not directly used in this module after refactor
|
||||||
|
alias Til.Typer.Environment
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Performs type checking and inference on a map of AST nodes.
|
||||||
|
|
||||||
|
It iterates through the nodes, infers their types, and updates the
|
||||||
|
`:type_id` field in each node map with a reference to its type.
|
||||||
|
|
||||||
|
Returns a new map of nodes with type information.
|
||||||
|
"""
|
||||||
|
def type_check(nodes_map) when is_map(nodes_map) do
|
||||||
|
initial_env = %{}
|
||||||
|
pre_populated_nodes_map = Interner.populate_known_types(nodes_map)
|
||||||
|
|
||||||
|
# Find the main file node to start traversal.
|
||||||
|
# Assumes parser always generates a :file node as the root of top-level expressions.
|
||||||
|
case Enum.find(Map.values(pre_populated_nodes_map), &(&1.ast_node_type == :file)) do
|
||||||
|
nil ->
|
||||||
|
# Should not happen with current parser, but handle defensively.
|
||||||
|
# Or an error: {:error, :no_file_node_found}
|
||||||
|
# Return map with known types at least
|
||||||
|
{:ok, pre_populated_nodes_map}
|
||||||
|
|
||||||
|
file_node ->
|
||||||
|
# Start recursive typing from the file node.
|
||||||
|
# The environment modifications will propagate through the traversal.
|
||||||
|
# The result is {:ok, final_nodes_map, _final_env}. We only need final_nodes_map here.
|
||||||
|
case type_node_recursively(file_node.id, pre_populated_nodes_map, initial_env) do
|
||||||
|
{:ok, final_nodes_map, final_env} ->
|
||||||
|
# IO.inspect(final_env, label: "Final Environment after Typing (should show type keys)")
|
||||||
|
|
||||||
|
# IO.inspect(final_nodes_map, label: "Final Nodes Map (should contain type definitions)")
|
||||||
|
{:ok, final_nodes_map}
|
||||||
|
|
||||||
|
# Propagate other return values (e.g., errors) if they occur,
|
||||||
|
# though current implementation of type_node_recursively always returns {:ok, _, _}.
|
||||||
|
other_result ->
|
||||||
|
other_result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Main recursive function for typing nodes.
|
||||||
|
# Handles node lookup and delegates to do_type_node for actual processing.
|
||||||
|
defp type_node_recursively(node_id, nodes_map, env) do
|
||||||
|
case Map.get(nodes_map, node_id) do
|
||||||
|
nil ->
|
||||||
|
# This case should ideally not be reached if node_ids are always valid.
|
||||||
|
# Consider logging an error here.
|
||||||
|
# IO.inspect("Warning: Node ID #{node_id} not found in nodes_map during typing.", label: "Typer")
|
||||||
|
# No change if node_id is invalid
|
||||||
|
{:ok, nodes_map, env}
|
||||||
|
|
||||||
|
node_data ->
|
||||||
|
# Delegate to the worker function that processes the node.
|
||||||
|
do_type_node(node_data, nodes_map, env)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Worker function to process a single node.
|
||||||
|
# Orchestrates typing children, inferring current node's type, and updating environment.
|
||||||
|
defp do_type_node(node_data, nodes_map, env) do
|
||||||
|
# Determine the environment and children to type based on node type
|
||||||
|
{children_to_process_ids, env_for_children, nodes_map_after_pre_processing} =
|
||||||
|
if node_data.ast_node_type == :lambda_expression do
|
||||||
|
# For lambdas: (fn params_s_expr body...)
|
||||||
|
# The 'fn' symbol (child 0) and 'params_s_expr' (child 1) are typed with the outer env.
|
||||||
|
# The body_node_ids are typed with the inner lambda_body_env.
|
||||||
|
|
||||||
|
# Type 'fn' symbol (first child of the original S-expression)
|
||||||
|
fn_op_child_id = hd(node_data.children)
|
||||||
|
|
||||||
|
{:ok, nmap_after_fn_op, env_after_fn_op} =
|
||||||
|
type_node_recursively(fn_op_child_id, nodes_map, env)
|
||||||
|
|
||||||
|
# Type params_s_expr (second child of the original S-expression)
|
||||||
|
# This node (node_data) has `params_s_expr_id` from the parser.
|
||||||
|
params_s_expr_node_id = node_data.params_s_expr_id
|
||||||
|
|
||||||
|
{:ok, nmap_after_params_s_expr, env_after_params_s_expr} =
|
||||||
|
type_node_recursively(params_s_expr_node_id, nmap_after_fn_op, env_after_fn_op)
|
||||||
|
|
||||||
|
# Create lambda body environment using arg_spec_node_ids.
|
||||||
|
# The lambda_expression node has `arg_spec_node_ids` and `return_type_spec_node_id`.
|
||||||
|
# Argument types need to be resolved and interned here to populate the env.
|
||||||
|
# nodes_map is nmap_after_params_s_expr at this point.
|
||||||
|
{lambda_body_env, nmap_after_arg_type_resolution} =
|
||||||
|
Enum.reduce(
|
||||||
|
node_data.arg_spec_node_ids,
|
||||||
|
{env_after_params_s_expr, nmap_after_params_s_expr},
|
||||||
|
fn arg_spec_id, {acc_env, acc_nodes_map} ->
|
||||||
|
arg_spec_node = Map.get(acc_nodes_map, arg_spec_id)
|
||||||
|
|
||||||
|
case arg_spec_node.ast_node_type do
|
||||||
|
# Unannotated param, e.g., x
|
||||||
|
:symbol ->
|
||||||
|
param_name = arg_spec_node.name
|
||||||
|
param_type_key = Types.primitive_type_key(:any)
|
||||||
|
{Map.put(acc_env, param_name, param_type_key), acc_nodes_map}
|
||||||
|
|
||||||
|
# Annotated param, e.g., (x integer)
|
||||||
|
:s_expression ->
|
||||||
|
param_symbol_node_id = hd(arg_spec_node.children)
|
||||||
|
type_spec_node_id = hd(tl(arg_spec_node.children))
|
||||||
|
|
||||||
|
param_symbol_node = Map.get(acc_nodes_map, param_symbol_node_id)
|
||||||
|
type_spec_node = Map.get(acc_nodes_map, type_spec_node_id)
|
||||||
|
|
||||||
|
param_name = param_symbol_node.name
|
||||||
|
|
||||||
|
# Resolve and intern the type specifier
|
||||||
|
{raw_type_def, nmap_after_resolve} =
|
||||||
|
ExpressionTyper.resolve_type_specifier_node(type_spec_node, acc_nodes_map)
|
||||||
|
|
||||||
|
{param_type_key, nmap_after_intern} =
|
||||||
|
Interner.get_or_intern_type(raw_type_def, nmap_after_resolve)
|
||||||
|
|
||||||
|
{Map.put(acc_env, param_name, param_type_key), nmap_after_intern}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
# Children to process with this new env are the body_node_ids
|
||||||
|
{node_data.body_node_ids, lambda_body_env, nmap_after_arg_type_resolution}
|
||||||
|
else
|
||||||
|
# Default: type all children with the current environment
|
||||||
|
{Map.get(node_data, :children, []), env, nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
# 1. Recursively type the identified children with the determined environment.
|
||||||
|
{nodes_map_after_children, env_after_children} =
|
||||||
|
Enum.reduce(
|
||||||
|
children_to_process_ids,
|
||||||
|
{nodes_map_after_pre_processing, env_for_children},
|
||||||
|
fn child_id, {acc_nodes_map, acc_env} ->
|
||||||
|
{:ok, next_nodes_map, next_env} =
|
||||||
|
type_node_recursively(child_id, acc_nodes_map, acc_env)
|
||||||
|
|
||||||
|
{next_nodes_map, next_env}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retrieve the current node's data from the potentially updated nodes_map.
|
||||||
|
# More importantly, infer_type_for_node_ast needs the nodes_map_after_children
|
||||||
|
# to look up typed children.
|
||||||
|
current_node_from_map = Map.get(nodes_map_after_children, node_data.id)
|
||||||
|
|
||||||
|
# 2. Infer type for the current node.
|
||||||
|
# infer_type_for_node_ast now returns {type_definition_map, possibly_updated_nodes_map}.
|
||||||
|
{type_definition_for_current_node, nodes_map_after_inference_logic} =
|
||||||
|
infer_type_for_node_ast(
|
||||||
|
current_node_from_map,
|
||||||
|
nodes_map_after_children,
|
||||||
|
env_after_children
|
||||||
|
)
|
||||||
|
|
||||||
|
# Intern this type definition to get a key and update nodes_map.
|
||||||
|
{type_key_for_current_node, nodes_map_after_interning} =
|
||||||
|
Interner.get_or_intern_type(
|
||||||
|
type_definition_for_current_node,
|
||||||
|
nodes_map_after_inference_logic
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update current node with the type key.
|
||||||
|
# Ensure we are updating the version of the node from nodes_map_after_interning
|
||||||
|
# (which is based on nodes_map_after_children).
|
||||||
|
re_fetched_current_node_data = Map.get(nodes_map_after_interning, current_node_from_map.id)
|
||||||
|
|
||||||
|
updated_current_node =
|
||||||
|
Map.put(re_fetched_current_node_data, :type_id, type_key_for_current_node)
|
||||||
|
|
||||||
|
nodes_map_with_typed_node =
|
||||||
|
Map.put(nodes_map_after_interning, updated_current_node.id, updated_current_node)
|
||||||
|
|
||||||
|
# 3. Update environment based on the current typed node (e.g., for assignments).
|
||||||
|
# update_env_from_node now returns {updated_env, updated_nodes_map}.
|
||||||
|
{env_after_current_node, nodes_map_after_env_update} =
|
||||||
|
Environment.update_env_from_node(
|
||||||
|
updated_current_node,
|
||||||
|
nodes_map_with_typed_node,
|
||||||
|
env_after_children
|
||||||
|
)
|
||||||
|
|
||||||
|
{:ok, nodes_map_after_env_update, env_after_current_node}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Infers the type for a node based on its AST type and current environment.
|
||||||
|
# `nodes_map` contains potentially typed children (whose :type_id is a key) and canonical type definitions.
|
||||||
|
# `env` is the current typing environment (symbol names to type keys).
|
||||||
|
# Returns {type_definition_map, possibly_updated_nodes_map}.
|
||||||
|
defp infer_type_for_node_ast(node_data, nodes_map, env) do
|
||||||
|
case node_data.ast_node_type do
|
||||||
|
:literal_integer ->
|
||||||
|
{%{type_kind: :literal, value: node_data.value}, nodes_map}
|
||||||
|
|
||||||
|
:literal_string ->
|
||||||
|
{%{type_kind: :literal, value: node_data.value}, nodes_map}
|
||||||
|
|
||||||
|
# Atoms are parsed as :literal_atom with a :value field containing the Elixir atom (as per parser.ex)
|
||||||
|
:literal_atom ->
|
||||||
|
{%{type_kind: :literal, value: node_data.value}, nodes_map}
|
||||||
|
|
||||||
|
:symbol ->
|
||||||
|
case node_data.name do
|
||||||
|
"nil" ->
|
||||||
|
{Types.get_literal_type(:nil_atom), nodes_map}
|
||||||
|
|
||||||
|
"true" ->
|
||||||
|
{Types.get_literal_type(:true_atom), nodes_map}
|
||||||
|
|
||||||
|
"false" ->
|
||||||
|
{Types.get_literal_type(:false_atom), nodes_map}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# Look up symbol in the environment. env stores type keys.
|
||||||
|
case Map.get(env, node_data.name) do
|
||||||
|
nil ->
|
||||||
|
# Symbol not found. Default to :any type definition.
|
||||||
|
# TODO: Handle unresolved symbols more robustly (e.g., specific error type).
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
|
||||||
|
found_type_key ->
|
||||||
|
# Resolve the key to its definition from nodes_map.
|
||||||
|
case Map.get(nodes_map, found_type_key) do
|
||||||
|
nil ->
|
||||||
|
# This indicates an inconsistency if a key from env isn't in nodes_map.
|
||||||
|
# Default to :any or an error type.
|
||||||
|
# IO.warn("Type key #{inspect(found_type_key)} for symbol '#{node_data.name}' not found in nodes_map.")
|
||||||
|
# Or a specific error type definition
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
|
||||||
|
type_definition ->
|
||||||
|
{type_definition, nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
:s_expression ->
|
||||||
|
ExpressionTyper.infer_s_expression_type(node_data, nodes_map, env)
|
||||||
|
|
||||||
|
:list_expression ->
|
||||||
|
children_ids = Map.get(node_data, :children, [])
|
||||||
|
num_children = length(children_ids)
|
||||||
|
|
||||||
|
element_type_definition =
|
||||||
|
cond do
|
||||||
|
num_children == 0 ->
|
||||||
|
Types.get_primitive_type(:nothing)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Children are already typed. Get their type definitions.
|
||||||
|
child_type_defs =
|
||||||
|
Enum.map(children_ids, fn child_id ->
|
||||||
|
# nodes_map is nodes_map_after_children
|
||||||
|
child_node = Map.get(nodes_map, child_id)
|
||||||
|
type_key_for_child = child_node.type_id
|
||||||
|
|
||||||
|
# Resolve the type key to its definition.
|
||||||
|
type_def_for_child = Map.get(nodes_map, type_key_for_child)
|
||||||
|
|
||||||
|
if is_nil(type_def_for_child) do
|
||||||
|
# Fallback, should ideally not happen if children are correctly typed.
|
||||||
|
Types.get_primitive_type(:any)
|
||||||
|
else
|
||||||
|
type_def_for_child
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Determine a common element type.
|
||||||
|
distinct_child_type_defs = Enum.uniq(child_type_defs)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
length(distinct_child_type_defs) == 1 ->
|
||||||
|
# All elements effectively have the same type definition (e.g., [1, 1, 1] -> Literal 1).
|
||||||
|
List.first(distinct_child_type_defs)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Form a union of the distinct child types.
|
||||||
|
# E.g., [1, 2, 3] -> (Union (Literal 1) (Literal 2) (Literal 3))
|
||||||
|
# E.g., [1, "a"] -> (Union (Literal 1) (Literal "a"))
|
||||||
|
# The types in distinct_child_type_defs are already resolved definitions.
|
||||||
|
# The interner will handle canonicalizing this union type.
|
||||||
|
%{type_kind: :union, types: MapSet.new(distinct_child_type_defs)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
list_type_def = %{
|
||||||
|
type_kind: :list,
|
||||||
|
# This is the full def; interner will use its key.
|
||||||
|
element_type: element_type_definition,
|
||||||
|
length: num_children
|
||||||
|
}
|
||||||
|
|
||||||
|
{list_type_def, nodes_map}
|
||||||
|
|
||||||
|
:file ->
|
||||||
|
# The :file node itself doesn't have a typical "type".
|
||||||
|
{Types.get_special_type(:file_marker), nodes_map}
|
||||||
|
|
||||||
|
:map_expression ->
|
||||||
|
children_ids = Map.get(node_data, :children, [])
|
||||||
|
# Children are [key1, value1, key2, value2, ...]
|
||||||
|
|
||||||
|
known_elements_raw =
|
||||||
|
children_ids
|
||||||
|
# [[k1,v1], [k2,v2]]
|
||||||
|
|> Enum.chunk_every(2)
|
||||||
|
|> Enum.reduce_while(%{}, fn [key_node_id, value_node_id], acc_known_elements ->
|
||||||
|
key_node = Map.get(nodes_map, key_node_id)
|
||||||
|
value_node = Map.get(nodes_map, value_node_id)
|
||||||
|
|
||||||
|
# Key's type must be a literal type for it to be used in known_elements.
|
||||||
|
# Child nodes (keys and values) are already typed at this stage.
|
||||||
|
key_type_def =
|
||||||
|
if key_node && key_node.type_id do
|
||||||
|
Map.get(nodes_map, key_node.type_id)
|
||||||
|
else
|
||||||
|
# Key node or its type_id is missing
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
cond do
|
||||||
|
key_type_def && key_type_def.type_kind == :literal && value_node ->
|
||||||
|
literal_key_value = key_type_def.value
|
||||||
|
# Value node should have been typed, its type_id points to its definition
|
||||||
|
value_type_def =
|
||||||
|
Map.get(nodes_map, value_node.type_id, Types.get_primitive_type(:any))
|
||||||
|
|
||||||
|
updated_elements =
|
||||||
|
Map.put(
|
||||||
|
acc_known_elements,
|
||||||
|
literal_key_value,
|
||||||
|
%{value_type: value_type_def, optional: false}
|
||||||
|
)
|
||||||
|
|
||||||
|
{:cont, updated_elements}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# If a key's type is not a literal, or key/value nodes are missing,
|
||||||
|
# this map literal cannot be precisely typed with known_elements.
|
||||||
|
# Halt and return empty known_elements, leading to a less specific type.
|
||||||
|
# IO.warn(
|
||||||
|
# "Map literal key is not a literal type or node data missing. Key node: #{inspect(key_node)}, Key type: #{inspect(key_type_def)}"
|
||||||
|
# )
|
||||||
|
{:halt, %{}}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Default index signature for map literals: any other key maps to any value.
|
||||||
|
default_index_signature = %{
|
||||||
|
key_type: Types.get_primitive_type(:any),
|
||||||
|
value_type: Types.get_primitive_type(:any)
|
||||||
|
}
|
||||||
|
|
||||||
|
map_type_def = %{
|
||||||
|
type_kind: :map,
|
||||||
|
known_elements: known_elements_raw,
|
||||||
|
index_signature: default_index_signature
|
||||||
|
}
|
||||||
|
|
||||||
|
{map_type_def, nodes_map}
|
||||||
|
|
||||||
|
:tuple_expression ->
|
||||||
|
children_ids = Map.get(node_data, :children, [])
|
||||||
|
|
||||||
|
element_type_defs =
|
||||||
|
Enum.map(children_ids, fn child_id ->
|
||||||
|
# nodes_map is nodes_map_after_children
|
||||||
|
child_node = Map.get(nodes_map, child_id)
|
||||||
|
# This should be set from prior typing.
|
||||||
|
type_key_for_child = child_node.type_id
|
||||||
|
|
||||||
|
# Resolve the type key to its definition.
|
||||||
|
type_def_for_child = Map.get(nodes_map, type_key_for_child)
|
||||||
|
|
||||||
|
if is_nil(type_def_for_child) do
|
||||||
|
# This case indicates an internal inconsistency:
|
||||||
|
# a child node has a type_id, but that ID doesn't resolve to a type definition.
|
||||||
|
# This shouldn't happen in a correctly functioning typer.
|
||||||
|
# Fallback to :any for robustness, but log or signal error if possible.
|
||||||
|
# IO.warn("Tuple element #{child_id} (in node #{node_data.id}) has type_id #{type_key_for_child} but no definition in nodes_map.")
|
||||||
|
Types.get_primitive_type(:any)
|
||||||
|
else
|
||||||
|
type_def_for_child
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
tuple_type_def = %{type_kind: :tuple, element_types: element_type_defs}
|
||||||
|
# nodes_map is unchanged here; interning of this new tuple_type_def happens later.
|
||||||
|
{tuple_type_def, nodes_map}
|
||||||
|
|
||||||
|
:lambda_expression ->
|
||||||
|
# node_data is the :lambda_expression node.
|
||||||
|
# Its body_node_ids have been typed using the lambda_body_env.
|
||||||
|
# nodes_map is nodes_map_after_children.
|
||||||
|
|
||||||
|
# Resolve argument types for the function signature
|
||||||
|
{raw_arg_type_defs, nodes_map_after_args} =
|
||||||
|
Enum.map_reduce(
|
||||||
|
node_data.arg_spec_node_ids,
|
||||||
|
# This is nodes_map_after_children from do_type_node
|
||||||
|
nodes_map,
|
||||||
|
fn arg_spec_id, acc_nodes_map ->
|
||||||
|
arg_spec_node = Map.get(acc_nodes_map, arg_spec_id)
|
||||||
|
|
||||||
|
case arg_spec_node.ast_node_type do
|
||||||
|
# Unannotated param
|
||||||
|
:symbol ->
|
||||||
|
{Types.get_primitive_type(:any), acc_nodes_map}
|
||||||
|
|
||||||
|
# Annotated param (param_symbol type_spec)
|
||||||
|
:s_expression ->
|
||||||
|
type_spec_node_id = hd(tl(arg_spec_node.children))
|
||||||
|
type_spec_node = Map.get(acc_nodes_map, type_spec_node_id)
|
||||||
|
ExpressionTyper.resolve_type_specifier_node(type_spec_node, acc_nodes_map)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
# Resolve/Infer return type for the function signature
|
||||||
|
{return_type_def_for_signature, nodes_map_after_return} =
|
||||||
|
if node_data.return_type_spec_node_id do
|
||||||
|
# Explicit return type annotation
|
||||||
|
return_type_spec_node =
|
||||||
|
Map.get(nodes_map_after_args, node_data.return_type_spec_node_id)
|
||||||
|
|
||||||
|
{expected_return_raw_def, nmap_after_ret_resolve} =
|
||||||
|
ExpressionTyper.resolve_type_specifier_node(
|
||||||
|
return_type_spec_node,
|
||||||
|
nodes_map_after_args
|
||||||
|
)
|
||||||
|
|
||||||
|
# Intern the expected return type to get its canonical form for checks
|
||||||
|
{expected_return_key, nmap_after_ret_intern} =
|
||||||
|
Interner.get_or_intern_type(expected_return_raw_def, nmap_after_ret_resolve)
|
||||||
|
|
||||||
|
expected_return_interned_def = Map.get(nmap_after_ret_intern, expected_return_key)
|
||||||
|
|
||||||
|
# Check if actual body return type is subtype of annotated return type
|
||||||
|
_actual_body_return_interned_def =
|
||||||
|
if Enum.empty?(node_data.body_node_ids) do
|
||||||
|
# Raw, but interner handles it
|
||||||
|
Types.get_literal_type(:nil_atom)
|
||||||
|
else
|
||||||
|
last_body_expr_node =
|
||||||
|
Map.get(nmap_after_ret_intern, List.last(node_data.body_node_ids))
|
||||||
|
|
||||||
|
# Already interned
|
||||||
|
Map.get(nmap_after_ret_intern, last_body_expr_node.type_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Perform subtype check if needed (for error reporting, not changing signature type yet)
|
||||||
|
# if !SubtypeChecker.is_subtype?(_actual_body_return_interned_def, expected_return_interned_def, nmap_after_ret_intern) do
|
||||||
|
# IO.warn("Lambda body return type mismatch with annotation.") # Placeholder for error
|
||||||
|
# end
|
||||||
|
|
||||||
|
{expected_return_interned_def, nmap_after_ret_intern}
|
||||||
|
else
|
||||||
|
# Infer return type from body
|
||||||
|
inferred_return_def =
|
||||||
|
if Enum.empty?(node_data.body_node_ids) do
|
||||||
|
Types.get_literal_type(:nil_atom)
|
||||||
|
else
|
||||||
|
last_body_expr_node =
|
||||||
|
Map.get(nodes_map_after_args, List.last(node_data.body_node_ids))
|
||||||
|
|
||||||
|
# Already interned
|
||||||
|
Map.get(nodes_map_after_args, last_body_expr_node.type_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
{inferred_return_def, nodes_map_after_args}
|
||||||
|
end
|
||||||
|
|
||||||
|
function_type_raw_def = %{
|
||||||
|
type_kind: :function,
|
||||||
|
arg_types: raw_arg_type_defs,
|
||||||
|
# This is an interned def or raw primitive/literal
|
||||||
|
return_type: return_type_def_for_signature,
|
||||||
|
type_params: []
|
||||||
|
}
|
||||||
|
|
||||||
|
{function_type_raw_def, nodes_map_after_return}
|
||||||
|
|
||||||
|
# Default for other AST node types
|
||||||
|
_ ->
|
||||||
|
# Placeholder: return :any type definition.
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
59
lib/til/typer/environment.ex
Normal file
59
lib/til/typer/environment.ex
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
defmodule Til.Typer.Environment do
|
||||||
|
@moduledoc """
|
||||||
|
Manages updates to the typing environment (symbol name to type key mappings).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Updates the environment based on the current typed node.
|
||||||
|
# Returns {updated_env, updated_nodes_map}. nodes_map is usually unchanged by this function
|
||||||
|
# unless a construct like (deftype ...) is processed that adds to type definitions.
|
||||||
|
def update_env_from_node(typed_node, nodes_map, env) do
|
||||||
|
case typed_node.ast_node_type do
|
||||||
|
:s_expression ->
|
||||||
|
new_env = update_env_for_s_expression(typed_node, nodes_map, env)
|
||||||
|
# nodes_map typically unchanged by simple assignment.
|
||||||
|
{new_env, nodes_map}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# Most nodes don't modify the environment by default.
|
||||||
|
{env, nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handles environment updates for S-expressions (e.g., assignments).
|
||||||
|
# Returns the updated environment.
|
||||||
|
defp update_env_for_s_expression(s_expr_node, nodes_map, env) do
|
||||||
|
children_ids = Map.get(s_expr_node, :children, [])
|
||||||
|
|
||||||
|
# Basic check for assignment: (= symbol value), three children.
|
||||||
|
if length(children_ids) == 3 do
|
||||||
|
operator_node_id = List.first(children_ids)
|
||||||
|
operator_node = Map.get(nodes_map, operator_node_id)
|
||||||
|
|
||||||
|
if operator_node && operator_node.ast_node_type == :symbol && operator_node.name == "=" do
|
||||||
|
symbol_to_assign_id = Enum.at(children_ids, 1)
|
||||||
|
value_expr_id = Enum.at(children_ids, 2)
|
||||||
|
|
||||||
|
symbol_to_assign_node = Map.get(nodes_map, symbol_to_assign_id)
|
||||||
|
# This node is already typed.
|
||||||
|
value_expr_node = Map.get(nodes_map, value_expr_id)
|
||||||
|
|
||||||
|
if symbol_to_assign_node && symbol_to_assign_node.ast_node_type == :symbol &&
|
||||||
|
value_expr_node && value_expr_node.type_id do
|
||||||
|
# value_expr_node.type_id is the type key of the value.
|
||||||
|
type_key_of_value = value_expr_node.type_id
|
||||||
|
# Update environment with the new symbol binding (symbol name -> type key).
|
||||||
|
Map.put(env, symbol_to_assign_node.name, type_key_of_value)
|
||||||
|
else
|
||||||
|
# Malformed assignment or missing type info, no env change.
|
||||||
|
env
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Not an assignment, env unchanged by this s-expression itself.
|
||||||
|
env
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Not a 3-element s-expression, cannot be our simple assignment.
|
||||||
|
env
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
506
lib/til/typer/expression_typer.ex
Normal file
506
lib/til/typer/expression_typer.ex
Normal file
@ -0,0 +1,506 @@
|
|||||||
|
defmodule Til.Typer.ExpressionTyper do
|
||||||
|
@moduledoc """
|
||||||
|
Handles type inference for specific AST expression types (e.g., S-expressions, if, the).
|
||||||
|
"""
|
||||||
|
alias Til.Typer.Types
|
||||||
|
alias Til.Typer.SubtypeChecker
|
||||||
|
alias Til.Typer.Interner
|
||||||
|
alias MapSet, as: Set
|
||||||
|
|
||||||
|
# Infers the type of an S-expression.
|
||||||
|
# Returns {type_definition_map, possibly_updated_nodes_map}.
|
||||||
|
def infer_s_expression_type(s_expr_node, nodes_map, env) do
|
||||||
|
children_ids = Map.get(s_expr_node, :children, [])
|
||||||
|
|
||||||
|
if Enum.empty?(children_ids) do
|
||||||
|
# Type of empty s-expression '()' is nil.
|
||||||
|
{Types.get_literal_type(:nil_atom), nodes_map}
|
||||||
|
else
|
||||||
|
operator_node_id = List.first(children_ids)
|
||||||
|
operator_node = Map.get(nodes_map, operator_node_id)
|
||||||
|
arg_node_ids = Enum.drop(children_ids, 1)
|
||||||
|
|
||||||
|
if is_nil(operator_node) do
|
||||||
|
# This should ideally not be reached if parser ensures children_ids are valid.
|
||||||
|
# Fallback or error type.
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
else
|
||||||
|
# Check if the operator is a symbol for a known special form first.
|
||||||
|
if operator_node.ast_node_type == :symbol do
|
||||||
|
case operator_node.name do
|
||||||
|
"=" ->
|
||||||
|
# Assignment: (= sym val) - expects 2 arguments (symbol and value)
|
||||||
|
# Total children_ids for s_expr_node should be 3.
|
||||||
|
if length(arg_node_ids) == 2 do
|
||||||
|
# value_expr_node is the second argument to '=', which is the third child of s_expr_node
|
||||||
|
value_expr_node = Map.get(nodes_map, Enum.at(children_ids, 2))
|
||||||
|
|
||||||
|
if value_expr_node && value_expr_node.type_id do
|
||||||
|
value_type_key = value_expr_node.type_id
|
||||||
|
case Map.get(nodes_map, value_type_key) do
|
||||||
|
nil ->
|
||||||
|
{Types.get_error_type_definition(:missing_value_type_in_assignment),
|
||||||
|
nodes_map}
|
||||||
|
value_type_definition ->
|
||||||
|
{value_type_definition, nodes_map}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{Types.get_error_type_definition(:missing_value_type_in_assignment), nodes_map}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Malformed assignment (e.g. (= x) or (= x y z))
|
||||||
|
# TODO: Specific error type for malformed assignment arity
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
"if" ->
|
||||||
|
infer_if_expression_type(s_expr_node, nodes_map, env)
|
||||||
|
|
||||||
|
"the" ->
|
||||||
|
infer_the_expression_type(s_expr_node, nodes_map, env)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# Not a special form symbol, attempt to treat as a regular function call.
|
||||||
|
type_function_call(operator_node, arg_node_ids, nodes_map)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Operator is not a symbol (e.g., a lambda expression itself, or an S-expression like (if ...)).
|
||||||
|
# Attempt to treat as a regular function call.
|
||||||
|
type_function_call(operator_node, arg_node_ids, nodes_map)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper function to type a function call.
|
||||||
|
# operator_node is the node representing the function/operator.
|
||||||
|
# arg_node_ids is a list of IDs for the argument nodes.
|
||||||
|
# nodes_map contains all typed nodes and type definitions.
|
||||||
|
# Returns {type_definition_for_call, nodes_map}.
|
||||||
|
defp type_function_call(operator_node, arg_node_ids, nodes_map) do
|
||||||
|
operator_type_id = operator_node.type_id
|
||||||
|
# The operator_node should have been typed by the main Typer loop before this.
|
||||||
|
operator_type_def = if operator_type_id, do: Map.get(nodes_map, operator_type_id), else: nil
|
||||||
|
|
||||||
|
cond do
|
||||||
|
is_nil(operator_type_def) ->
|
||||||
|
# This means operator_type_id was nil (operator node not typed) or
|
||||||
|
# operator_type_id was not a valid key in nodes_map.
|
||||||
|
# This indicates an issue prior to attempting the call.
|
||||||
|
# For (1 2 3), '1' is typed as literal 1. Its type_id is valid.
|
||||||
|
# This path is more for unexpected states.
|
||||||
|
# Defaulting to :not_a_function with nil actual_operator_type_id.
|
||||||
|
{Types.get_error_type_definition(:not_a_function, nil), nodes_map}
|
||||||
|
|
||||||
|
operator_type_def.type_kind == :function ->
|
||||||
|
expected_arity = length(operator_type_def.arg_types)
|
||||||
|
actual_arity = length(arg_node_ids)
|
||||||
|
|
||||||
|
if expected_arity != actual_arity do
|
||||||
|
{Types.get_error_type_definition(
|
||||||
|
:arity_mismatch,
|
||||||
|
expected_arity,
|
||||||
|
actual_arity,
|
||||||
|
operator_type_id # key of the function type itself
|
||||||
|
), nodes_map}
|
||||||
|
else
|
||||||
|
# TODO: Implement argument type checking loop here when needed.
|
||||||
|
# For Phase 3, if lambdas take 'any' args, this check might not be strictly necessary
|
||||||
|
# for current tests, but the structure should be prepared.
|
||||||
|
# If an argument type mismatch is found:
|
||||||
|
# return {Types.get_error_type_definition(:argument_type_mismatch, ...), nodes_map}
|
||||||
|
|
||||||
|
# If arity matches and (for now) args are assumed compatible,
|
||||||
|
# the type of the call is the function's return type.
|
||||||
|
return_type_key = operator_type_def.return_type
|
||||||
|
return_type_def = Map.get(nodes_map, return_type_key)
|
||||||
|
|
||||||
|
if return_type_def do
|
||||||
|
{return_type_def, nodes_map}
|
||||||
|
else
|
||||||
|
# Return type key from function definition was invalid. Internal error.
|
||||||
|
# Fallback to :any or a specific error.
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Operator is typed, but its type_kind is not :function.
|
||||||
|
{Types.get_error_type_definition(
|
||||||
|
:not_a_function,
|
||||||
|
operator_type_id # key of the operator's actual (non-function) type
|
||||||
|
), nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Infers the type of a (the <type-specifier> <expression>) S-expression.
|
||||||
|
# Returns {type_definition_map, possibly_updated_nodes_map}.
|
||||||
|
def infer_the_expression_type(the_s_expr_node, nodes_map, _env) do
|
||||||
|
children_ids = Map.get(the_s_expr_node, :children, [])
|
||||||
|
# (the type-specifier actual-expression) -> 3 children
|
||||||
|
# children_ids are [the_op_id, type_spec_id, expr_id]
|
||||||
|
|
||||||
|
if length(children_ids) == 3 do
|
||||||
|
type_spec_node_id = Enum.at(children_ids, 1)
|
||||||
|
expr_node_id = Enum.at(children_ids, 2)
|
||||||
|
|
||||||
|
type_spec_node = Map.get(nodes_map, type_spec_node_id)
|
||||||
|
expr_node = Map.get(nodes_map, expr_node_id)
|
||||||
|
|
||||||
|
# Resolve the type specifier node (e.g., symbol 'integer') to a raw type definition
|
||||||
|
# nodes_map is the input to infer_the_expression_type
|
||||||
|
{raw_annotated_def, nodes_map_after_resolve} =
|
||||||
|
resolve_type_specifier_node(type_spec_node, nodes_map)
|
||||||
|
|
||||||
|
# Intern the resolved annotated type definition to get its canonical, interned form
|
||||||
|
{annotated_type_key, current_nodes_map} =
|
||||||
|
Interner.get_or_intern_type(raw_annotated_def, nodes_map_after_resolve)
|
||||||
|
|
||||||
|
# This is the interned definition, e.g. %{... element_type_id: ..., id: ...}
|
||||||
|
annotated_def = Map.get(current_nodes_map, annotated_type_key)
|
||||||
|
|
||||||
|
# The expr_node should have been typed by the recursive call to type_children.
|
||||||
|
# Its type_id points to its actual inferred type definition (which is already interned).
|
||||||
|
actual_expr_type_def =
|
||||||
|
if expr_node && expr_node.type_id do
|
||||||
|
# Fetch using the most up-to-date nodes_map
|
||||||
|
Map.get(current_nodes_map, expr_node.type_id)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if expr_node && expr_node.type_id && actual_expr_type_def do
|
||||||
|
# Both actual_expr_type_def and annotated_def are now interned forms.
|
||||||
|
is_compatible =
|
||||||
|
SubtypeChecker.is_subtype?(
|
||||||
|
actual_expr_type_def,
|
||||||
|
# Use interned form
|
||||||
|
annotated_def,
|
||||||
|
# Use most up-to-date nodes_map
|
||||||
|
current_nodes_map
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_compatible do
|
||||||
|
# The type of the 'the' expression is the raw annotated type.
|
||||||
|
# The caller (Typer.do_type_node) will intern this.
|
||||||
|
{raw_annotated_def, current_nodes_map}
|
||||||
|
else
|
||||||
|
# Type mismatch: actual type is not a subtype of the annotated type.
|
||||||
|
actual_id = actual_expr_type_def.id
|
||||||
|
# This is annotated_def.id
|
||||||
|
expected_id = annotated_type_key
|
||||||
|
|
||||||
|
{Types.get_error_type_definition(:type_annotation_mismatch, actual_id, expected_id),
|
||||||
|
current_nodes_map}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# This 'else' covers cases where the inner expression could not be typed:
|
||||||
|
# 1. expr_node is nil
|
||||||
|
# 2. expr_node.type_id is nil
|
||||||
|
# 3. actual_expr_type_def (resolved from expr_node.type_id) is nil
|
||||||
|
# In these cases, the annotation cannot be validated. Return an error type.
|
||||||
|
# IO.warn("Could not determine actual type of expression in 'the' form: #{inspect(expr_node)}")
|
||||||
|
# actual_type_id is nil as it couldn't be determined. expected_type_id is from annotation.
|
||||||
|
# annotated_type_key might not be set if raw_annotated_def was an error
|
||||||
|
expected_id =
|
||||||
|
if annotated_type_key, do: annotated_type_key, else: nil
|
||||||
|
|
||||||
|
{Types.get_error_type_definition(:type_annotation_mismatch, nil, expected_id),
|
||||||
|
current_nodes_map}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Malformed 'the' expression (e.g., wrong number of children).
|
||||||
|
# IO.warn("Malformed 'the' expression: #{the_s_expr_node.raw_string}")
|
||||||
|
# If a 'the' expression is malformed, both actual and expected types are indeterminate in this context.
|
||||||
|
# nodes_map is original from caller here
|
||||||
|
{Types.get_error_type_definition(:type_annotation_mismatch, nil, nil), nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Infers the type of an IF S-expression.
|
||||||
|
# Returns {type_definition_map, possibly_updated_nodes_map}.
|
||||||
|
def infer_if_expression_type(if_s_expr_node, nodes_map, _env) do
|
||||||
|
children_ids = Map.get(if_s_expr_node, :children, [])
|
||||||
|
num_children_in_sexpr = length(children_ids)
|
||||||
|
|
||||||
|
# Used as fallback
|
||||||
|
canonical_any_type = Map.get(nodes_map, Types.any_type_key())
|
||||||
|
canonical_nil_type = Map.get(nodes_map, Types.nil_literal_type_key())
|
||||||
|
_literal_true_type = Map.get(nodes_map, Types.literal_type_key(:true_atom))
|
||||||
|
_literal_false_type = Map.get(nodes_map, Types.literal_type_key(:false_atom))
|
||||||
|
|
||||||
|
# primitive_boolean_type = Map.get(nodes_map, Types.primitive_type_key(:boolean)) # If we add :boolean
|
||||||
|
|
||||||
|
# Check for malformed 'if' (less than 2 parts: 'if' and condition)
|
||||||
|
if num_children_in_sexpr < 2 do
|
||||||
|
# IO.warn("Malformed 'if' expression (too few parts): #{if_s_expr_node.raw_string}")
|
||||||
|
# Malformed if results in :any for now.
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
else
|
||||||
|
condition_id = Enum.at(children_ids, 1)
|
||||||
|
condition_node = Map.get(nodes_map, condition_id)
|
||||||
|
|
||||||
|
condition_type_def =
|
||||||
|
SubtypeChecker.get_type_definition_from_node(
|
||||||
|
condition_node,
|
||||||
|
nodes_map,
|
||||||
|
canonical_any_type
|
||||||
|
)
|
||||||
|
|
||||||
|
# Condition type is now evaluated based on truthiness/falsiness,
|
||||||
|
# not strict boolean type. The :invalid_if_condition_type error is removed.
|
||||||
|
# Proceed with branch typing based on static truthiness/falsiness.
|
||||||
|
cond do
|
||||||
|
# (if condition then_branch)
|
||||||
|
num_children_in_sexpr == 3 ->
|
||||||
|
then_branch_id = Enum.at(children_ids, 2)
|
||||||
|
then_branch_node = Map.get(nodes_map, then_branch_id)
|
||||||
|
|
||||||
|
then_type_def =
|
||||||
|
SubtypeChecker.get_type_definition_from_node(
|
||||||
|
then_branch_node,
|
||||||
|
nodes_map,
|
||||||
|
canonical_any_type
|
||||||
|
)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
SubtypeChecker.is_statically_truthy?(condition_type_def) ->
|
||||||
|
{then_type_def, nodes_map}
|
||||||
|
|
||||||
|
SubtypeChecker.is_statically_falsy?(condition_type_def) ->
|
||||||
|
# Implicit else is nil
|
||||||
|
{canonical_nil_type, nodes_map}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Condition is ambiguous, form union of then and nil
|
||||||
|
union_members = Set.new([then_type_def, canonical_nil_type])
|
||||||
|
|
||||||
|
if Set.size(union_members) == 1 do
|
||||||
|
{hd(Set.to_list(union_members)), nodes_map}
|
||||||
|
else
|
||||||
|
{%{type_kind: :union, types: union_members}, nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# (if condition then_branch else_branch)
|
||||||
|
num_children_in_sexpr == 4 ->
|
||||||
|
then_branch_id = Enum.at(children_ids, 2)
|
||||||
|
else_branch_id = Enum.at(children_ids, 3)
|
||||||
|
|
||||||
|
then_branch_node = Map.get(nodes_map, then_branch_id)
|
||||||
|
else_branch_node = Map.get(nodes_map, else_branch_id)
|
||||||
|
|
||||||
|
then_type_def =
|
||||||
|
SubtypeChecker.get_type_definition_from_node(
|
||||||
|
then_branch_node,
|
||||||
|
nodes_map,
|
||||||
|
canonical_any_type
|
||||||
|
)
|
||||||
|
|
||||||
|
else_type_def =
|
||||||
|
SubtypeChecker.get_type_definition_from_node(
|
||||||
|
else_branch_node,
|
||||||
|
nodes_map,
|
||||||
|
canonical_any_type
|
||||||
|
)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
SubtypeChecker.is_statically_truthy?(condition_type_def) ->
|
||||||
|
{then_type_def, nodes_map}
|
||||||
|
|
||||||
|
SubtypeChecker.is_statically_falsy?(condition_type_def) ->
|
||||||
|
{else_type_def, nodes_map}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Condition is ambiguous, form union of then and else
|
||||||
|
union_members = Set.new([then_type_def, else_type_def])
|
||||||
|
|
||||||
|
if Set.size(union_members) == 1 do
|
||||||
|
{hd(Set.to_list(union_members)), nodes_map}
|
||||||
|
else
|
||||||
|
{%{type_kind: :union, types: union_members}, nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Malformed 'if' (e.g. (if c t e extra) or already handled (if), (if c))
|
||||||
|
# IO.warn("Malformed 'if' expression (incorrect number of parts): #{if_s_expr_node.raw_string}")
|
||||||
|
# Malformed if results in :any
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
end
|
||||||
|
# Removed: end of 'if not is_valid_condition_type else ...'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves a type specifier AST node (e.g., a symbol like 'integer')
|
||||||
|
# to its corresponding type definition map.
|
||||||
|
# Returns {type_definition_map, nodes_map}. nodes_map can be updated if element type resolution updates it.
|
||||||
|
def resolve_type_specifier_node(type_spec_node, nodes_map) do
|
||||||
|
# Fallback for unknown type specifiers
|
||||||
|
default_type = Types.get_primitive_type(:any)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
type_spec_node && type_spec_node.ast_node_type == :symbol ->
|
||||||
|
type_name_str = type_spec_node.name
|
||||||
|
|
||||||
|
# Map common type names to their definitions.
|
||||||
|
case type_name_str do
|
||||||
|
"integer" ->
|
||||||
|
{Types.get_primitive_type(:integer), nodes_map}
|
||||||
|
|
||||||
|
"string" ->
|
||||||
|
{Types.get_primitive_type(:string), nodes_map}
|
||||||
|
|
||||||
|
"atom" ->
|
||||||
|
{Types.get_primitive_type(:atom), nodes_map}
|
||||||
|
|
||||||
|
"number" ->
|
||||||
|
{Types.get_primitive_type(:number), nodes_map}
|
||||||
|
|
||||||
|
"any" ->
|
||||||
|
{Types.get_primitive_type(:any), nodes_map}
|
||||||
|
|
||||||
|
"nothing" ->
|
||||||
|
{Types.get_primitive_type(:nothing), nodes_map}
|
||||||
|
|
||||||
|
# TODO: Add other built-in types like boolean, map, etc.
|
||||||
|
_ ->
|
||||||
|
# IO.warn("Unknown type specifier symbol: '#{type_name_str}', defaulting to :any.")
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
type_spec_node && type_spec_node.ast_node_type == :s_expression ->
|
||||||
|
# Handle S-expression type specifiers like (list ...), (map ...), (union ...)
|
||||||
|
s_expr_children_ids = Map.get(type_spec_node, :children, [])
|
||||||
|
|
||||||
|
if length(s_expr_children_ids) >= 1 do
|
||||||
|
op_node_id = List.first(s_expr_children_ids)
|
||||||
|
op_node = Map.get(nodes_map, op_node_id)
|
||||||
|
|
||||||
|
if op_node && op_node.ast_node_type == :symbol do
|
||||||
|
case op_node.name do
|
||||||
|
"list" ->
|
||||||
|
if length(s_expr_children_ids) == 2 do
|
||||||
|
element_type_spec_id = Enum.at(s_expr_children_ids, 1)
|
||||||
|
element_type_spec_node = Map.get(nodes_map, element_type_spec_id)
|
||||||
|
|
||||||
|
if element_type_spec_node do
|
||||||
|
# Recursively resolve the element type specifier
|
||||||
|
{resolved_element_type_def, nodes_map_after_element_resolve} =
|
||||||
|
resolve_type_specifier_node(element_type_spec_node, nodes_map)
|
||||||
|
|
||||||
|
list_type_def = %{
|
||||||
|
type_kind: :list,
|
||||||
|
element_type: resolved_element_type_def,
|
||||||
|
length: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
{list_type_def, nodes_map_after_element_resolve}
|
||||||
|
else
|
||||||
|
# Malformed (list ...), missing element type specifier
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Malformed (list ...), wrong arity
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
"map" ->
|
||||||
|
if length(s_expr_children_ids) == 3 do
|
||||||
|
key_type_spec_id = Enum.at(s_expr_children_ids, 1)
|
||||||
|
value_type_spec_id = Enum.at(s_expr_children_ids, 2)
|
||||||
|
|
||||||
|
key_type_spec_node = Map.get(nodes_map, key_type_spec_id)
|
||||||
|
value_type_spec_node = Map.get(nodes_map, value_type_spec_id)
|
||||||
|
|
||||||
|
if key_type_spec_node && value_type_spec_node do
|
||||||
|
{resolved_key_type_def, nodes_map_after_key_resolve} =
|
||||||
|
resolve_type_specifier_node(key_type_spec_node, nodes_map)
|
||||||
|
|
||||||
|
{resolved_value_type_def, nodes_map_after_value_resolve} =
|
||||||
|
resolve_type_specifier_node(
|
||||||
|
value_type_spec_node,
|
||||||
|
nodes_map_after_key_resolve
|
||||||
|
)
|
||||||
|
|
||||||
|
map_type_def = %{
|
||||||
|
type_kind: :map,
|
||||||
|
known_elements: %{},
|
||||||
|
index_signature: %{
|
||||||
|
key_type: resolved_key_type_def,
|
||||||
|
value_type: resolved_value_type_def
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{map_type_def, nodes_map_after_value_resolve}
|
||||||
|
else
|
||||||
|
# Malformed (map ...), missing key/value type specifiers
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Malformed (map ...), wrong arity
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
"union" ->
|
||||||
|
# (union typeA typeB ...)
|
||||||
|
member_type_spec_ids = Enum.drop(s_expr_children_ids, 1)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
length(member_type_spec_ids) == 0 ->
|
||||||
|
# (union) -> nothing
|
||||||
|
{Types.get_primitive_type(:nothing), nodes_map}
|
||||||
|
|
||||||
|
length(member_type_spec_ids) == 1 ->
|
||||||
|
# (union typeA) -> typeA
|
||||||
|
single_member_spec_node_id = List.first(member_type_spec_ids)
|
||||||
|
single_member_spec_node = Map.get(nodes_map, single_member_spec_node_id)
|
||||||
|
resolve_type_specifier_node(single_member_spec_node, nodes_map)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# (union typeA typeB ...) -> resolve each and form union
|
||||||
|
{resolved_member_defs, final_nodes_map} =
|
||||||
|
Enum.map_reduce(
|
||||||
|
member_type_spec_ids,
|
||||||
|
nodes_map,
|
||||||
|
fn member_id, acc_nodes_map ->
|
||||||
|
member_node = Map.get(acc_nodes_map, member_id)
|
||||||
|
|
||||||
|
{resolved_def, updated_nodes_map} =
|
||||||
|
resolve_type_specifier_node(member_node, acc_nodes_map)
|
||||||
|
|
||||||
|
{resolved_def, updated_nodes_map}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
union_type_def = %{
|
||||||
|
type_kind: :union,
|
||||||
|
types: MapSet.new(resolved_member_defs)
|
||||||
|
}
|
||||||
|
|
||||||
|
{union_type_def, final_nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# Unknown S-expression operator for type specifier
|
||||||
|
# IO.warn("Unknown S-expression type specifier operator: #{op_node.name}")
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# First child of S-expression type specifier is not a symbol
|
||||||
|
# IO.warn("S-expression type specifier does not start with a symbol: #{type_spec_node.raw_string}")
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Empty S-expression as type specifier `()` or malformed (e.g. `(list)`)
|
||||||
|
# IO.warn("Empty or malformed S-expression type specifier: #{type_spec_node.raw_string}")
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Type specifier is not a symbol, not a recognized s-expression, or is nil.
|
||||||
|
# IO.warn("Invalid type specifier node: #{inspect(type_spec_node)}, defaulting to :any.")
|
||||||
|
{default_type, nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
499
lib/til/typer/interner.ex
Normal file
499
lib/til/typer/interner.ex
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
defmodule Til.Typer.Interner do
|
||||||
|
@moduledoc """
|
||||||
|
Handles the interning of type definitions into a nodes_map.
|
||||||
|
Ensures that identical type definitions (especially predefined ones)
|
||||||
|
map to canonical keys.
|
||||||
|
"""
|
||||||
|
alias Til.Typer.Types
|
||||||
|
|
||||||
|
def populate_known_types(nodes_map) do
|
||||||
|
initial_map_with_primitives =
|
||||||
|
Enum.reduce(Types.primitive_types(), nodes_map, fn {name, def}, acc_map ->
|
||||||
|
key = Types.primitive_type_key(name)
|
||||||
|
Map.put(acc_map, key, Map.put(def, :id, key))
|
||||||
|
end)
|
||||||
|
|
||||||
|
map_with_literals =
|
||||||
|
Enum.reduce(Types.literal_types(), initial_map_with_primitives, fn {name, def}, acc_map ->
|
||||||
|
key = Types.literal_type_key(name)
|
||||||
|
Map.put(acc_map, key, Map.put(def, :id, key))
|
||||||
|
end)
|
||||||
|
|
||||||
|
initial_map_with_specials =
|
||||||
|
Enum.reduce(Types.special_types(), map_with_literals, fn {name, def}, acc_map ->
|
||||||
|
key = Types.special_type_key(name)
|
||||||
|
Map.put(acc_map, key, Map.put(def, :id, key))
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Filter out :type_annotation_mismatch as it's dynamic and not in error_type_keys_map anymore
|
||||||
|
# Pre-populate other static error types.
|
||||||
|
static_error_defs =
|
||||||
|
Types.error_type_definitions()
|
||||||
|
|> Enum.filter(fn {name, _def} ->
|
||||||
|
# Check if a key exists for this error name (type_annotation_mismatch won't have one)
|
||||||
|
Types.error_type_keys_map()[name] != nil
|
||||||
|
end)
|
||||||
|
|
||||||
|
Enum.reduce(static_error_defs, initial_map_with_specials, fn {name, def}, acc_map ->
|
||||||
|
key = Types.error_type_key(name) # This will only work for names still in error_type_keys_map
|
||||||
|
Map.put(acc_map, key, Map.put(def, :id, key))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to get a canonical key for a type definition, storing it in nodes_map if new.
|
||||||
|
def get_or_intern_type(type_definition_map, nodes_map) do
|
||||||
|
# Normalize incoming type_definition_map by removing :id for matching against base definitions
|
||||||
|
type_def_for_matching = Map.delete(type_definition_map, :id)
|
||||||
|
|
||||||
|
primitive_match =
|
||||||
|
Enum.find(Types.primitive_types(), fn {_name, def} -> def == type_def_for_matching end)
|
||||||
|
|
||||||
|
literal_match =
|
||||||
|
Enum.find(Types.literal_types(), fn {_name, def} -> def == type_def_for_matching end)
|
||||||
|
|
||||||
|
special_match =
|
||||||
|
Enum.find(Types.special_types(), fn {_name, def} -> def == type_def_for_matching end)
|
||||||
|
|
||||||
|
error_match =
|
||||||
|
Enum.find(Types.error_type_definitions(), fn {_name, def} ->
|
||||||
|
def == type_def_for_matching
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
primitive_match ->
|
||||||
|
{name, _def} = primitive_match
|
||||||
|
{Types.primitive_type_key(name), nodes_map}
|
||||||
|
|
||||||
|
literal_match ->
|
||||||
|
{name, _def} = literal_match
|
||||||
|
{Types.literal_type_key(name), nodes_map}
|
||||||
|
|
||||||
|
special_match ->
|
||||||
|
{name, _def} = special_match
|
||||||
|
{Types.special_type_key(name), nodes_map}
|
||||||
|
|
||||||
|
# Handle specific error types first, then general error_match for predefined ones
|
||||||
|
type_definition_map.type_kind == :error and
|
||||||
|
type_definition_map.reason == :type_annotation_mismatch ->
|
||||||
|
# Dynamic error type: %{type_kind: :error, reason: :type_annotation_mismatch, actual_type_id: key1, expected_type_id: key2}
|
||||||
|
# Search for existing based on all relevant fields (excluding :id itself)
|
||||||
|
error_def_for_matching = Map.delete(type_definition_map, :id)
|
||||||
|
|
||||||
|
existing_error_match =
|
||||||
|
Enum.find(nodes_map, fn {_key, existing_def} ->
|
||||||
|
# Ensure it's a type definition (has :type_kind) before checking its properties
|
||||||
|
Map.has_key?(existing_def, :type_kind) &&
|
||||||
|
existing_def.type_kind == :error &&
|
||||||
|
Map.delete(existing_def, :id) == error_def_for_matching
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_error_match ->
|
||||||
|
{_key, existing_def} = existing_error_match
|
||||||
|
{existing_def.id, nodes_map}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# Create a new key for this specific instance of type_annotation_mismatch error
|
||||||
|
actual_id_str = to_string(type_definition_map.actual_type_id || "nil")
|
||||||
|
expected_id_str = to_string(type_definition_map.expected_type_id || "nil")
|
||||||
|
|
||||||
|
new_error_key =
|
||||||
|
:"type_error_tam_#{actual_id_str}_exp_#{expected_id_str}_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
|
||||||
|
final_error_def = Map.put(type_definition_map, :id, new_error_key)
|
||||||
|
{new_error_key, Map.put(nodes_map, new_error_key, final_error_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
type_definition_map.type_kind == :error and
|
||||||
|
type_definition_map.reason == :invalid_if_condition_type ->
|
||||||
|
# Dynamic error type: %{type_kind: :error, reason: :invalid_if_condition_type, actual_condition_type_id: key}
|
||||||
|
error_def_for_matching = Map.delete(type_definition_map, :id)
|
||||||
|
|
||||||
|
existing_error_match =
|
||||||
|
Enum.find(nodes_map, fn {_key, existing_def} ->
|
||||||
|
Map.has_key?(existing_def, :type_kind) &&
|
||||||
|
existing_def.type_kind == :error &&
|
||||||
|
Map.delete(existing_def, :id) == error_def_for_matching
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_error_match ->
|
||||||
|
{_key, existing_def} = existing_error_match
|
||||||
|
{existing_def.id, nodes_map}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
actual_id_str = to_string(type_definition_map.actual_condition_type_id || "nil")
|
||||||
|
|
||||||
|
new_error_key =
|
||||||
|
:"type_error_iict_#{actual_id_str}_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
|
||||||
|
final_error_def = Map.put(type_definition_map, :id, new_error_key)
|
||||||
|
{new_error_key, Map.put(nodes_map, new_error_key, final_error_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
type_definition_map.type_kind == :error and
|
||||||
|
type_definition_map.reason == :not_a_function ->
|
||||||
|
# Dynamic error type: %{type_kind: :error, reason: :not_a_function, actual_operator_type_id: key}
|
||||||
|
error_def_for_matching = Map.delete(type_definition_map, :id)
|
||||||
|
|
||||||
|
existing_error_match =
|
||||||
|
Enum.find(nodes_map, fn {_key, existing_def} ->
|
||||||
|
Map.has_key?(existing_def, :type_kind) &&
|
||||||
|
existing_def.type_kind == :error &&
|
||||||
|
Map.delete(existing_def, :id) == error_def_for_matching
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_error_match ->
|
||||||
|
{_key, existing_def} = existing_error_match
|
||||||
|
{existing_def.id, nodes_map}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
actual_op_id_str = to_string(type_definition_map.actual_operator_type_id || "nil")
|
||||||
|
|
||||||
|
new_error_key =
|
||||||
|
:"type_error_naf_#{actual_op_id_str}_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
|
||||||
|
final_error_def = Map.put(type_definition_map, :id, new_error_key)
|
||||||
|
{new_error_key, Map.put(nodes_map, new_error_key, final_error_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
type_definition_map.type_kind == :error and
|
||||||
|
type_definition_map.reason == :arity_mismatch ->
|
||||||
|
# Dynamic error type: %{type_kind: :error, reason: :arity_mismatch, expected_arity: int, actual_arity: int, function_type_id: key}
|
||||||
|
error_def_for_matching = Map.delete(type_definition_map, :id)
|
||||||
|
|
||||||
|
existing_error_match =
|
||||||
|
Enum.find(nodes_map, fn {_key, existing_def} ->
|
||||||
|
Map.has_key?(existing_def, :type_kind) &&
|
||||||
|
existing_def.type_kind == :error &&
|
||||||
|
Map.delete(existing_def, :id) == error_def_for_matching
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_error_match ->
|
||||||
|
{_key, existing_def} = existing_error_match
|
||||||
|
{existing_def.id, nodes_map}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
exp_arity_str = to_string(type_definition_map.expected_arity)
|
||||||
|
act_arity_str = to_string(type_definition_map.actual_arity)
|
||||||
|
func_id_str = to_string(type_definition_map.function_type_id || "nil")
|
||||||
|
|
||||||
|
new_error_key =
|
||||||
|
:"type_error_am_#{exp_arity_str}_#{act_arity_str}_#{func_id_str}_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
|
||||||
|
final_error_def = Map.put(type_definition_map, :id, new_error_key)
|
||||||
|
{new_error_key, Map.put(nodes_map, new_error_key, final_error_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
type_definition_map.type_kind == :error and
|
||||||
|
type_definition_map.reason == :argument_type_mismatch ->
|
||||||
|
# Dynamic error type: %{type_kind: :error, reason: :argument_type_mismatch, arg_position: int, expected_arg_type_id: key, actual_arg_type_id: key, function_type_id: key}
|
||||||
|
error_def_for_matching = Map.delete(type_definition_map, :id)
|
||||||
|
|
||||||
|
existing_error_match =
|
||||||
|
Enum.find(nodes_map, fn {_key, existing_def} ->
|
||||||
|
Map.has_key?(existing_def, :type_kind) &&
|
||||||
|
existing_def.type_kind == :error &&
|
||||||
|
Map.delete(existing_def, :id) == error_def_for_matching
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_error_match ->
|
||||||
|
{_key, existing_def} = existing_error_match
|
||||||
|
{existing_def.id, nodes_map}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
pos_str = to_string(type_definition_map.arg_position)
|
||||||
|
exp_id_str = to_string(type_definition_map.expected_arg_type_id || "nil")
|
||||||
|
act_id_str = to_string(type_definition_map.actual_arg_type_id || "nil")
|
||||||
|
func_id_str = to_string(type_definition_map.function_type_id || "nil")
|
||||||
|
|
||||||
|
new_error_key =
|
||||||
|
:"type_error_atm_#{pos_str}_#{exp_id_str}_#{act_id_str}_#{func_id_str}_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
|
||||||
|
final_error_def = Map.put(type_definition_map, :id, new_error_key)
|
||||||
|
{new_error_key, Map.put(nodes_map, new_error_key, final_error_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
error_match -> # Handles other predefined errors
|
||||||
|
{name, _def} = error_match
|
||||||
|
{Types.error_type_key(name), nodes_map}
|
||||||
|
|
||||||
|
type_definition_map.type_kind == :list ->
|
||||||
|
# type_definition_map is like %{type_kind: :list, element_type: <full_def_or_key>, length: L}
|
||||||
|
%{element_type: element_full_def_or_key, length: len} = type_definition_map
|
||||||
|
|
||||||
|
# Recursively get/intern the element type to get its key, if it's not already a key
|
||||||
|
{element_type_key, nodes_map_after_element_intern} =
|
||||||
|
if is_map(element_full_def_or_key) do
|
||||||
|
# It's a raw definition, intern it
|
||||||
|
get_or_intern_type(element_full_def_or_key, nodes_map)
|
||||||
|
else
|
||||||
|
# It's already a key
|
||||||
|
{element_full_def_or_key, nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Canonical form for searching/storing uses the element type's key
|
||||||
|
canonical_list_struct = %{
|
||||||
|
type_kind: :list,
|
||||||
|
element_type_id: element_type_key,
|
||||||
|
length: len
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search for an existing identical list type definition
|
||||||
|
existing_list_type_match =
|
||||||
|
Enum.find(nodes_map_after_element_intern, fn {_key, existing_def} ->
|
||||||
|
# Compare structure, excluding the :id field of the existing_def
|
||||||
|
Map.delete(existing_def, :id) == canonical_list_struct
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_list_type_match ->
|
||||||
|
# Found an identical list type, reuse its key
|
||||||
|
{_key, existing_def} = existing_list_type_match
|
||||||
|
{existing_def.id, nodes_map_after_element_intern}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# No existing identical list type, create a new one
|
||||||
|
new_list_key = :"type_list_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
final_list_def = Map.put(canonical_list_struct, :id, new_list_key)
|
||||||
|
{new_list_key, Map.put(nodes_map_after_element_intern, new_list_key, final_list_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
type_definition_map.type_kind == :union ->
|
||||||
|
# type_definition_map is %{type_kind: :union, types: set_of_raw_member_defs}
|
||||||
|
%{types: raw_member_defs_set} = type_definition_map
|
||||||
|
|
||||||
|
# Recursively get/intern each member type to get its interned definition.
|
||||||
|
# Thread nodes_map through these calls.
|
||||||
|
{interned_member_defs_list, nodes_map_after_members_intern} =
|
||||||
|
Enum.map_reduce(
|
||||||
|
MapSet.to_list(raw_member_defs_set),
|
||||||
|
nodes_map,
|
||||||
|
fn raw_member_def_or_key, acc_nodes_map ->
|
||||||
|
if is_map(raw_member_def_or_key) do
|
||||||
|
# It's a map (raw definition or already interned definition).
|
||||||
|
# get_or_intern_type will handle it and return its key.
|
||||||
|
{member_key, updated_nodes_map} =
|
||||||
|
get_or_intern_type(raw_member_def_or_key, acc_nodes_map)
|
||||||
|
# Fetch the interned definition using the key for the canonical set.
|
||||||
|
interned_member_def = Map.get(updated_nodes_map, member_key)
|
||||||
|
{interned_member_def, updated_nodes_map}
|
||||||
|
else
|
||||||
|
# It's an atom (a key). Fetch its definition.
|
||||||
|
interned_member_def = Map.get(acc_nodes_map, raw_member_def_or_key)
|
||||||
|
|
||||||
|
if is_nil(interned_member_def) do
|
||||||
|
# This should not happen if keys are always valid.
|
||||||
|
raise "Interner: Union member key #{inspect(raw_member_def_or_key)} not found in nodes_map."
|
||||||
|
end
|
||||||
|
|
||||||
|
{interned_member_def, acc_nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
interned_member_defs_set = MapSet.new(interned_member_defs_list)
|
||||||
|
|
||||||
|
# Canonical form for searching/storing uses the set of interned member definitions.
|
||||||
|
canonical_union_struct = %{
|
||||||
|
type_kind: :union,
|
||||||
|
types: interned_member_defs_set
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search for an existing identical union type definition.
|
||||||
|
existing_union_type_match =
|
||||||
|
Enum.find(nodes_map_after_members_intern, fn {_key, existing_def} ->
|
||||||
|
Map.delete(existing_def, :id) == canonical_union_struct
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_union_type_match ->
|
||||||
|
{_key, existing_def} = existing_union_type_match
|
||||||
|
{existing_def.id, nodes_map_after_members_intern}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
new_union_key = :"type_union_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
final_union_def = Map.put(canonical_union_struct, :id, new_union_key)
|
||||||
|
|
||||||
|
{new_union_key,
|
||||||
|
Map.put(nodes_map_after_members_intern, new_union_key, final_union_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
type_definition_map.type_kind == :map ->
|
||||||
|
# type_definition_map is %{type_kind: :map, known_elements: KE_raw, index_signature: IS_raw}
|
||||||
|
%{known_elements: ke_raw, index_signature: is_raw} = type_definition_map
|
||||||
|
|
||||||
|
# Intern value types in known_elements
|
||||||
|
{ke_interned_values, nodes_map_after_ke_values} =
|
||||||
|
Enum.map_reduce(
|
||||||
|
ke_raw,
|
||||||
|
nodes_map,
|
||||||
|
fn {_literal_key, %{value_type: raw_value_def_or_key, optional: opt}},
|
||||||
|
acc_nodes_map ->
|
||||||
|
{value_type_key, updated_nodes_map} =
|
||||||
|
if is_map(raw_value_def_or_key) do
|
||||||
|
get_or_intern_type(raw_value_def_or_key, acc_nodes_map)
|
||||||
|
else
|
||||||
|
{raw_value_def_or_key, acc_nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
{%{value_type_id: value_type_key, optional: opt}, updated_nodes_map}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reconstruct known_elements with interned value_type_ids
|
||||||
|
ke_interned =
|
||||||
|
Enum.zip(Map.keys(ke_raw), ke_interned_values)
|
||||||
|
|> Map.new(fn {key, val_map} -> {key, val_map} end)
|
||||||
|
|
||||||
|
# Intern key_type and value_type in index_signature, if they are not already keys
|
||||||
|
{is_key_type_id, nodes_map_after_is_key} =
|
||||||
|
if is_map(is_raw.key_type) do
|
||||||
|
get_or_intern_type(is_raw.key_type, nodes_map_after_ke_values)
|
||||||
|
else
|
||||||
|
{is_raw.key_type, nodes_map_after_ke_values}
|
||||||
|
end
|
||||||
|
|
||||||
|
{is_value_type_id, nodes_map_after_is_value} =
|
||||||
|
if is_map(is_raw.value_type) do
|
||||||
|
get_or_intern_type(is_raw.value_type, nodes_map_after_is_key)
|
||||||
|
else
|
||||||
|
{is_raw.value_type, nodes_map_after_is_key}
|
||||||
|
end
|
||||||
|
|
||||||
|
is_interned = %{
|
||||||
|
key_type_id: is_key_type_id,
|
||||||
|
value_type_id: is_value_type_id
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical_map_struct = %{
|
||||||
|
type_kind: :map,
|
||||||
|
known_elements: ke_interned,
|
||||||
|
index_signature: is_interned
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search for an existing identical map type definition
|
||||||
|
existing_map_type_match =
|
||||||
|
Enum.find(nodes_map_after_is_value, fn {_key, existing_def} ->
|
||||||
|
Map.delete(existing_def, :id) == canonical_map_struct
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_map_type_match ->
|
||||||
|
{_key, existing_def} = existing_map_type_match
|
||||||
|
{existing_def.id, nodes_map_after_is_value}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
new_map_key = :"type_map_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
final_map_def = Map.put(canonical_map_struct, :id, new_map_key)
|
||||||
|
{new_map_key, Map.put(nodes_map_after_is_value, new_map_key, final_map_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
type_definition_map.type_kind == :function ->
|
||||||
|
# type_definition_map is %{type_kind: :function, arg_types: [RawDef1, RawDef2], return_type: RawReturnDef, type_params: [RawParamDef1]}
|
||||||
|
%{
|
||||||
|
arg_types: raw_arg_defs_or_keys,
|
||||||
|
return_type: raw_return_def_or_key,
|
||||||
|
type_params: raw_type_param_defs_or_keys
|
||||||
|
} = type_definition_map
|
||||||
|
|
||||||
|
# Intern argument types
|
||||||
|
{interned_arg_type_keys, nodes_map_after_args} =
|
||||||
|
Enum.map_reduce(
|
||||||
|
raw_arg_defs_or_keys,
|
||||||
|
nodes_map,
|
||||||
|
fn def_or_key, acc_nodes_map ->
|
||||||
|
if is_map(def_or_key) do
|
||||||
|
# It's a raw definition, intern it
|
||||||
|
get_or_intern_type(def_or_key, acc_nodes_map)
|
||||||
|
else
|
||||||
|
# It's already a key
|
||||||
|
{def_or_key, acc_nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
# Intern return type
|
||||||
|
{interned_return_type_key, nodes_map_after_return} =
|
||||||
|
if is_map(raw_return_def_or_key) do
|
||||||
|
# It's a raw definition, intern it
|
||||||
|
get_or_intern_type(raw_return_def_or_key, nodes_map_after_args)
|
||||||
|
else
|
||||||
|
# It's already a key
|
||||||
|
{raw_return_def_or_key, nodes_map_after_args}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Intern type parameters (for polymorphism, initially likely empty)
|
||||||
|
{interned_type_param_keys, nodes_map_after_params} =
|
||||||
|
Enum.map_reduce(
|
||||||
|
raw_type_param_defs_or_keys || [], # Ensure it's a list
|
||||||
|
nodes_map_after_return,
|
||||||
|
fn def_or_key, acc_nodes_map ->
|
||||||
|
if is_map(def_or_key) do
|
||||||
|
get_or_intern_type(def_or_key, acc_nodes_map)
|
||||||
|
else
|
||||||
|
{def_or_key, acc_nodes_map}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
canonical_function_struct = %{
|
||||||
|
type_kind: :function,
|
||||||
|
arg_types: interned_arg_type_keys,
|
||||||
|
return_type: interned_return_type_key,
|
||||||
|
type_params: interned_type_param_keys
|
||||||
|
}
|
||||||
|
|
||||||
|
# Search for an existing identical function type definition
|
||||||
|
existing_function_type_match =
|
||||||
|
Enum.find(nodes_map_after_params, fn {_key, existing_def} ->
|
||||||
|
Map.delete(existing_def, :id) == canonical_function_struct
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_function_type_match ->
|
||||||
|
{_key, existing_def} = existing_function_type_match
|
||||||
|
{existing_def.id, nodes_map_after_params}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
new_function_key = :"type_function_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
final_function_def = Map.put(canonical_function_struct, :id, new_function_key)
|
||||||
|
{new_function_key, Map.put(nodes_map_after_params, new_function_key, final_function_def)}
|
||||||
|
end
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# This is for types not predefined and not list/union/map/function (e.g., literals like 1, "a", or other complex types).
|
||||||
|
# Search for an existing identical type definition.
|
||||||
|
# type_def_for_matching was defined at the start of the function: Map.delete(type_definition_map, :id)
|
||||||
|
existing_match =
|
||||||
|
Enum.find(nodes_map, fn {_key, existing_def} ->
|
||||||
|
# Ensure we are only comparing against actual type definitions
|
||||||
|
Map.has_key?(existing_def, :type_kind) &&
|
||||||
|
Map.delete(existing_def, :id) == type_def_for_matching
|
||||||
|
end)
|
||||||
|
|
||||||
|
cond do
|
||||||
|
existing_match ->
|
||||||
|
# Found an identical type, reuse its key.
|
||||||
|
{_key, existing_def_found} = existing_match
|
||||||
|
{existing_def_found.id, nodes_map} # Return existing key and original nodes_map
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# No existing identical type, create a new one.
|
||||||
|
# Make the key slightly more descriptive by including the type_kind.
|
||||||
|
kind_prefix = Atom.to_string(type_definition_map.type_kind)
|
||||||
|
|
||||||
|
new_key =
|
||||||
|
String.to_atom(
|
||||||
|
"type_#{kind_prefix}_#{System.unique_integer([:monotonic, :positive])}"
|
||||||
|
)
|
||||||
|
|
||||||
|
type_definition_with_id = Map.put(type_definition_map, :id, new_key)
|
||||||
|
{new_key, Map.put(nodes_map, new_key, type_definition_with_id)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
355
lib/til/typer/subtype_checker.ex
Normal file
355
lib/til/typer/subtype_checker.ex
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
defmodule Til.Typer.SubtypeChecker do
|
||||||
|
@moduledoc """
|
||||||
|
Handles subtyping checks and related type utility functions.
|
||||||
|
"""
|
||||||
|
alias Til.Typer.Types
|
||||||
|
# alias MapSet, as: Set # MapSet functions are not directly called with `Set.` prefix here
|
||||||
|
|
||||||
|
# Helper to get a type definition from a node, with a fallback default.
|
||||||
|
# `default_type_definition` should be the actual map, not a key.
|
||||||
|
def get_type_definition_from_node(node, nodes_map, default_type_definition) do
|
||||||
|
if node && node.type_id && Map.has_key?(nodes_map, node.type_id) do
|
||||||
|
Map.get(nodes_map, node.type_id)
|
||||||
|
else
|
||||||
|
# Used if node is nil, node.type_id is nil, or type_id is not in nodes_map.
|
||||||
|
default_type_definition
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to determine if a type definition guarantees a truthy value.
|
||||||
|
# In Tilly Lisp, nil and false are falsy; everything else is truthy.
|
||||||
|
def is_statically_truthy?(type_definition) do
|
||||||
|
case type_definition do
|
||||||
|
%{type_kind: :literal, value: val} ->
|
||||||
|
not (val == nil or val == false)
|
||||||
|
|
||||||
|
# Future: Other types that are guaranteed non-falsy (e.g., non-nullable string)
|
||||||
|
_ ->
|
||||||
|
# Cannot statically determine for :any, :union, etc. by default
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper to determine if a type definition guarantees a falsy value.
|
||||||
|
def is_statically_falsy?(type_definition) do
|
||||||
|
case type_definition do
|
||||||
|
%{type_kind: :literal, value: val} ->
|
||||||
|
val == nil or val == false
|
||||||
|
|
||||||
|
# Future: Other types that are guaranteed falsy (e.g. a union of only nil and false)
|
||||||
|
_ ->
|
||||||
|
# Cannot statically determine for :any, :union, etc. by default
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if subtype_def is a subtype of supertype_def.
|
||||||
|
# nodes_map is needed for resolving further type information if types are complex.
|
||||||
|
def is_subtype?(subtype_def, supertype_def, nodes_map) do
|
||||||
|
# Fetch primitive types from nodes_map to ensure they include their :id field,
|
||||||
|
# consistent with other type definitions retrieved from nodes_map.
|
||||||
|
# Assumes nodes_map is populated with canonical types.
|
||||||
|
any_type = Map.get(nodes_map, Types.primitive_type_key(:any))
|
||||||
|
nothing_type = Map.get(nodes_map, Types.primitive_type_key(:nothing))
|
||||||
|
|
||||||
|
cond do
|
||||||
|
# Ensure any_type and nothing_type were found, otherwise subtyping involving them is problematic.
|
||||||
|
# This check is more of a safeguard; they should always be in a correctly initialized nodes_map.
|
||||||
|
is_nil(any_type) or is_nil(nothing_type) ->
|
||||||
|
# Consider this an internal error or handle as 'not subtype' for safety.
|
||||||
|
# For now, let it proceed; if they are nil, comparisons will likely fail as expected (false).
|
||||||
|
# However, this could mask initialization issues.
|
||||||
|
# A more robust approach might be to raise if they are not found.
|
||||||
|
# For tests and current flow, they are expected to be present.
|
||||||
|
# Placeholder for potential error logging/raising
|
||||||
|
nil
|
||||||
|
|
||||||
|
is_nil(subtype_def) or is_nil(supertype_def) ->
|
||||||
|
# Consider nil to be a subtype of nil, but not of others unless specified.
|
||||||
|
subtype_def == supertype_def
|
||||||
|
|
||||||
|
# Rule 1: Identity (covers identical complex types if canonicalized)
|
||||||
|
subtype_def == supertype_def ->
|
||||||
|
true
|
||||||
|
|
||||||
|
# Rule 2: Anything is a subtype of :any
|
||||||
|
supertype_def == any_type ->
|
||||||
|
true
|
||||||
|
|
||||||
|
# Rule 3: :nothing is a subtype of everything
|
||||||
|
subtype_def == nothing_type ->
|
||||||
|
true
|
||||||
|
|
||||||
|
# Rule 4: Literal to Primitive
|
||||||
|
# e.g., literal 42 is subtype of primitive integer
|
||||||
|
# e.g., literal 42 is subtype of primitive number
|
||||||
|
match?(
|
||||||
|
{%{type_kind: :literal, value: _val}, %{type_kind: :primitive, name: _prim_name}},
|
||||||
|
{subtype_def, supertype_def}
|
||||||
|
) ->
|
||||||
|
# Deconstruct inside the block for clarity if the pattern matches
|
||||||
|
{%{value: val}, %{name: prim_name}} = {subtype_def, supertype_def}
|
||||||
|
|
||||||
|
cond do
|
||||||
|
prim_name == :integer && is_integer(val) -> true
|
||||||
|
prim_name == :string && is_binary(val) -> true
|
||||||
|
prim_name == :atom && is_atom(val) -> true
|
||||||
|
prim_name == :number && (is_integer(val) || is_float(val)) -> true
|
||||||
|
# No specific literal to primitive match
|
||||||
|
true -> false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rule 5: Primitive to Primitive Subtyping
|
||||||
|
# e.g., integer is subtype of number
|
||||||
|
match?(
|
||||||
|
{%{type_kind: :primitive, name: :integer}, %{type_kind: :primitive, name: :number}},
|
||||||
|
{subtype_def, supertype_def}
|
||||||
|
) ->
|
||||||
|
true
|
||||||
|
|
||||||
|
# Rule 6: Handling Union Types
|
||||||
|
# Case 6.1: Subtype is a Union Type. (A | B) <: C iff A <: C and B <: C
|
||||||
|
is_map(subtype_def) and subtype_def.type_kind == :union ->
|
||||||
|
# subtype_def is %{type_kind: :union, types: sub_types_set}
|
||||||
|
sub_types_set = subtype_def.types
|
||||||
|
|
||||||
|
Enum.all?(sub_types_set, fn sub_member_type ->
|
||||||
|
is_subtype?(sub_member_type, supertype_def, nodes_map)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Case 6.2: Supertype is a Union Type. A <: (B | C) iff A <: B or A <: C
|
||||||
|
is_map(supertype_def) and supertype_def.type_kind == :union ->
|
||||||
|
# supertype_def is %{type_kind: :union, types: super_types_set}
|
||||||
|
super_types_set = supertype_def.types
|
||||||
|
|
||||||
|
Enum.any?(super_types_set, fn super_member_type ->
|
||||||
|
is_subtype?(subtype_def, super_member_type, nodes_map)
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Rule 7: Tuple Subtyping
|
||||||
|
# A tuple type T_sub = %{element_types: SubElements} is a subtype of
|
||||||
|
# T_super = %{element_types: SuperElements} iff they have the same arity
|
||||||
|
# and each element type in SubElements is a subtype of the corresponding
|
||||||
|
# element type in SuperElements.
|
||||||
|
match?(
|
||||||
|
{%{type_kind: :tuple, element_types: _sub_elements},
|
||||||
|
%{type_kind: :tuple, element_types: _super_elements}},
|
||||||
|
{subtype_def, supertype_def}
|
||||||
|
) ->
|
||||||
|
sub_elements = subtype_def.element_types
|
||||||
|
super_elements = supertype_def.element_types
|
||||||
|
|
||||||
|
if length(sub_elements) == length(super_elements) do
|
||||||
|
# Check subtyping for each pair of corresponding elements.
|
||||||
|
Enum.zip(sub_elements, super_elements)
|
||||||
|
|> Enum.all?(fn {sub_element_type, super_element_type} ->
|
||||||
|
is_subtype?(sub_element_type, super_element_type, nodes_map)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
# Tuples have different arities, so not a subtype.
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rule 8: List Subtyping
|
||||||
|
# L1 = (List E1 Len1) is subtype of L2 = (List E2 Len2) iff
|
||||||
|
# E1 is subtype of E2 (covariance) AND
|
||||||
|
# (Len2 is nil (any length) OR Len1 == Len2)
|
||||||
|
match?(
|
||||||
|
{%{type_kind: :list, element_type_id: _sub_elem_id, length: _sub_len},
|
||||||
|
%{type_kind: :list, element_type_id: _super_elem_id, length: _super_len}},
|
||||||
|
{subtype_def, supertype_def}
|
||||||
|
) ->
|
||||||
|
# Deconstruct for clarity
|
||||||
|
%{element_type_id: sub_elem_id, length: sub_len} = subtype_def
|
||||||
|
%{element_type_id: super_elem_id, length: super_len} = supertype_def
|
||||||
|
|
||||||
|
sub_elem_type_def = Map.get(nodes_map, sub_elem_id)
|
||||||
|
super_elem_type_def = Map.get(nodes_map, super_elem_id)
|
||||||
|
|
||||||
|
# Ensure element type definitions were found (keys were valid)
|
||||||
|
if sub_elem_type_def && super_elem_type_def do
|
||||||
|
elements_are_subtypes =
|
||||||
|
is_subtype?(sub_elem_type_def, super_elem_type_def, nodes_map)
|
||||||
|
|
||||||
|
# Supertype list can be any length
|
||||||
|
# Or lengths must be identical and known
|
||||||
|
lengths_compatible =
|
||||||
|
is_nil(super_len) or
|
||||||
|
(!is_nil(sub_len) and sub_len == super_len)
|
||||||
|
|
||||||
|
elements_are_subtypes && lengths_compatible
|
||||||
|
else
|
||||||
|
# If element type keys don't resolve, implies an issue. Treat as not subtype.
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Rule 9: Map Subtyping
|
||||||
|
match?(
|
||||||
|
{%{type_kind: :map, known_elements: _sub_ke, index_signature: _sub_is},
|
||||||
|
%{type_kind: :map, known_elements: _super_ke, index_signature: _super_is}},
|
||||||
|
{subtype_def, supertype_def}
|
||||||
|
) ->
|
||||||
|
sub_ke = subtype_def.known_elements
|
||||||
|
# %{key_type_id: Ksub, value_type_id: Vsub}
|
||||||
|
sub_is = subtype_def.index_signature
|
||||||
|
super_ke = supertype_def.known_elements
|
||||||
|
# %{key_type_id: Ksuper, value_type_id: Vsuper}
|
||||||
|
super_is = supertype_def.index_signature
|
||||||
|
|
||||||
|
# 1. Known Elements (Required in Supertype)
|
||||||
|
all_required_super_keys_compatible =
|
||||||
|
Enum.all?(super_ke, fn {super_key, super_key_details} ->
|
||||||
|
# super_key_details is %{value_type_id: super_val_id, optional: opt_val}
|
||||||
|
if super_key_details.optional == false do
|
||||||
|
case Map.get(sub_ke, super_key) do
|
||||||
|
nil ->
|
||||||
|
# Required key in supertype not found in subtype
|
||||||
|
false
|
||||||
|
|
||||||
|
sub_key_details when sub_key_details.optional == false ->
|
||||||
|
# Subtype's value for this key must be a subtype of supertype's value type
|
||||||
|
sub_val_type_def = Map.get(nodes_map, sub_key_details.value_type_id)
|
||||||
|
super_val_type_def = Map.get(nodes_map, super_key_details.value_type_id)
|
||||||
|
is_subtype?(sub_val_type_def, super_val_type_def, nodes_map)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# Key found in subtype but is optional or missing, while supertype requires it non-optional
|
||||||
|
false
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# This specific super_key is optional, handled by the next block
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# 2. Known Elements (Optional in Supertype)
|
||||||
|
all_optional_super_keys_compatible =
|
||||||
|
Enum.all?(super_ke, fn {super_key, super_key_details} ->
|
||||||
|
if super_key_details.optional == true do
|
||||||
|
case Map.get(sub_ke, super_key) do
|
||||||
|
nil ->
|
||||||
|
# Optional key in supertype not present in subtype is fine
|
||||||
|
true
|
||||||
|
|
||||||
|
# Optionality in subtype doesn't matter here for compatibility
|
||||||
|
sub_key_details ->
|
||||||
|
sub_val_type_def = Map.get(nodes_map, sub_key_details.value_type_id)
|
||||||
|
super_val_type_def = Map.get(nodes_map, super_key_details.value_type_id)
|
||||||
|
is_subtype?(sub_val_type_def, super_val_type_def, nodes_map)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# This specific super_key is required, handled by the previous block
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# 3. Index Signature Compatibility
|
||||||
|
# Ksuper <: Ksub (contravariance for key types)
|
||||||
|
# Vsub <: Vsuper (covariance for value types)
|
||||||
|
super_is_key_type_def = Map.get(nodes_map, super_is.key_type_id)
|
||||||
|
sub_is_key_type_def = Map.get(nodes_map, sub_is.key_type_id)
|
||||||
|
|
||||||
|
index_sig_keys_compatible =
|
||||||
|
is_subtype?(super_is_key_type_def, sub_is_key_type_def, nodes_map)
|
||||||
|
|
||||||
|
sub_is_value_type_def = Map.get(nodes_map, sub_is.value_type_id)
|
||||||
|
super_is_value_type_def = Map.get(nodes_map, super_is.value_type_id)
|
||||||
|
|
||||||
|
index_sig_values_compatible =
|
||||||
|
is_subtype?(sub_is_value_type_def, super_is_value_type_def, nodes_map)
|
||||||
|
|
||||||
|
index_signatures_compatible = index_sig_keys_compatible && index_sig_values_compatible
|
||||||
|
|
||||||
|
# 4. Width Subtyping: Keys in sub.known_elements not in super.known_elements
|
||||||
|
# must conform to super.index_signature.
|
||||||
|
extra_sub_keys_compatible =
|
||||||
|
Enum.all?(sub_ke, fn {sub_k_literal, %{value_type_id: sub_k_val_id}} ->
|
||||||
|
if Map.has_key?(super_ke, sub_k_literal) do
|
||||||
|
# Already checked by rules 1 and 2
|
||||||
|
true
|
||||||
|
else
|
||||||
|
# Key sub_k_literal is in sub_ke but not super_ke.
|
||||||
|
# Its type must conform to super_is.key_type_id
|
||||||
|
# Its value's type must conform to super_is.value_type_id
|
||||||
|
|
||||||
|
# Create a literal type for sub_k_literal to check against super_is.key_type_id
|
||||||
|
# This requires interning the literal type of sub_k_literal on the fly.
|
||||||
|
# For simplicity here, we assume sub_k_literal's type can be directly checked.
|
||||||
|
# A full implementation would intern {type_kind: :literal, value: sub_k_literal}
|
||||||
|
# and then use that definition.
|
||||||
|
# Let's assume for now that if sub_k_literal is e.g. :foo, its type is literal :foo.
|
||||||
|
|
||||||
|
# This part is tricky without a helper to get/intern literal type on the fly.
|
||||||
|
# For now, we'll simplify: if super_is.key_type_id is :any, it's fine.
|
||||||
|
# A proper check needs to construct the literal type for sub_k_literal.
|
||||||
|
# For now, let's assume this check is more complex and might need refinement.
|
||||||
|
# A placeholder for the check:
|
||||||
|
# literal_sub_k_type_def = create_and_intern_literal_type(sub_k_literal, nodes_map)
|
||||||
|
# key_conforms = is_subtype?(literal_sub_k_type_def, super_is_key_type_def, nodes_map)
|
||||||
|
|
||||||
|
# Simplified: if super_is key type is :any, it allows any extra keys.
|
||||||
|
# This is not fully correct but a step.
|
||||||
|
# key_conforms = super_is.key_type_id == Types.any_type_key() # Simplified check - REMOVED as unused
|
||||||
|
|
||||||
|
# Value type check
|
||||||
|
sub_k_val_type_def = Map.get(nodes_map, sub_k_val_id)
|
||||||
|
|
||||||
|
value_conforms =
|
||||||
|
is_subtype?(sub_k_val_type_def, super_is_value_type_def, nodes_map)
|
||||||
|
|
||||||
|
# A more accurate key_conforms:
|
||||||
|
# 1. Create raw literal type for sub_k_literal
|
||||||
|
# raw_lit_type = %{type_kind: :literal, value: sub_k_literal} # REMOVED as unused
|
||||||
|
# 2. Intern it (this might modify nodes_map, but is_subtype? shouldn't modify nodes_map)
|
||||||
|
# This is problematic. is_subtype? should be pure.
|
||||||
|
# The types passed to is_subtype? should already be fully resolved/interned.
|
||||||
|
# This implies that the literal keys themselves should perhaps be thought of as types.
|
||||||
|
# For now, we'll stick to the simplified check or assume this needs a helper
|
||||||
|
# that doesn't modify nodes_map or that nodes_map is pre-populated with all possible literal types.
|
||||||
|
|
||||||
|
# Let's refine the key_conforms check slightly.
|
||||||
|
# We need to check if the type of the literal key `sub_k_literal` is a subtype of `super_is_key_type_def`.
|
||||||
|
# We can construct the raw literal type and check it.
|
||||||
|
# This requires `is_subtype?` to handle raw type definitions for its first arg if not careful.
|
||||||
|
# However, `is_subtype?` expects canonical defs.
|
||||||
|
# This is a known challenge. For now, we'll assume a helper or a specific way to handle this.
|
||||||
|
# Let's assume `super_is_key_type_def` is general enough, e.g. :atom if sub_k_literal is an atom.
|
||||||
|
# This part needs the most careful thought for full correctness.
|
||||||
|
# A pragmatic approach: if super_is.key_type is :any, it's true.
|
||||||
|
# If super_is.key_type is :atom and sub_k_literal is an atom, true. etc.
|
||||||
|
key_type_of_sub_k_literal =
|
||||||
|
cond do
|
||||||
|
is_atom(sub_k_literal) ->
|
||||||
|
Map.get(nodes_map, Types.primitive_type_key(:atom))
|
||||||
|
|
||||||
|
is_binary(sub_k_literal) ->
|
||||||
|
Map.get(nodes_map, Types.primitive_type_key(:string))
|
||||||
|
|
||||||
|
is_integer(sub_k_literal) ->
|
||||||
|
Map.get(nodes_map, Types.primitive_type_key(:integer))
|
||||||
|
|
||||||
|
# Add other literal types if necessary
|
||||||
|
# Fallback
|
||||||
|
true ->
|
||||||
|
Map.get(nodes_map, Types.any_type_key())
|
||||||
|
end
|
||||||
|
|
||||||
|
key_conforms_refined =
|
||||||
|
is_subtype?(key_type_of_sub_k_literal, super_is_key_type_def, nodes_map)
|
||||||
|
|
||||||
|
key_conforms_refined && value_conforms
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
all_required_super_keys_compatible &&
|
||||||
|
all_optional_super_keys_compatible &&
|
||||||
|
index_signatures_compatible &&
|
||||||
|
extra_sub_keys_compatible
|
||||||
|
|
||||||
|
# TODO: Add more subtyping rules (e.g., for intersection)
|
||||||
|
true ->
|
||||||
|
# Default: not a subtype
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
192
lib/til/typer/types.ex
Normal file
192
lib/til/typer/types.ex
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
defmodule Til.Typer.Types do
|
||||||
|
@moduledoc """
|
||||||
|
Defines and provides access to predefined type structures and their canonical keys
|
||||||
|
for the Tilly type system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# --- Predefined Type Keys ---
|
||||||
|
@integer_type_key :til_type_integer
|
||||||
|
@string_type_key :til_type_string
|
||||||
|
@atom_type_key :til_type_atom # Added for primitive atom type
|
||||||
|
@number_type_key :til_type_number
|
||||||
|
@any_type_key :til_type_any
|
||||||
|
@nothing_type_key :til_type_nothing
|
||||||
|
@file_marker_type_key :til_type_file_marker
|
||||||
|
@nil_literal_type_key :til_type_literal_nil_atom
|
||||||
|
@true_literal_type_key :til_type_literal_true_atom
|
||||||
|
@false_literal_type_key :til_type_literal_false_atom
|
||||||
|
@error_missing_value_type_in_assignment_key :til_type_error_missing_value_type_in_assignment
|
||||||
|
@error_not_a_function_key :til_type_error_not_a_function
|
||||||
|
@error_arity_mismatch_key :til_type_error_arity_mismatch
|
||||||
|
@error_argument_type_mismatch_key :til_type_error_argument_type_mismatch
|
||||||
|
# @error_invalid_if_condition_type_key :til_type_error_invalid_if_condition_type # Removed as unused
|
||||||
|
# @type_annotation_mismatch_error_key is removed as this error is now dynamic
|
||||||
|
|
||||||
|
@primitive_types %{
|
||||||
|
integer: %{type_kind: :primitive, name: :integer},
|
||||||
|
string: %{type_kind: :primitive, name: :string},
|
||||||
|
atom: %{type_kind: :primitive, name: :atom}, # Added primitive atom
|
||||||
|
number: %{type_kind: :primitive, name: :number},
|
||||||
|
any: %{type_kind: :primitive, name: :any},
|
||||||
|
nothing: %{type_kind: :primitive, name: :nothing}
|
||||||
|
}
|
||||||
|
|
||||||
|
@primitive_type_keys %{
|
||||||
|
integer: @integer_type_key,
|
||||||
|
string: @string_type_key,
|
||||||
|
atom: @atom_type_key, # Added key for primitive atom
|
||||||
|
number: @number_type_key,
|
||||||
|
any: @any_type_key,
|
||||||
|
nothing: @nothing_type_key
|
||||||
|
}
|
||||||
|
|
||||||
|
@literal_types %{
|
||||||
|
nil_atom: %{type_kind: :literal, value: nil},
|
||||||
|
true_atom: %{type_kind: :literal, value: true},
|
||||||
|
false_atom: %{type_kind: :literal, value: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
@literal_type_keys %{
|
||||||
|
nil_atom: @nil_literal_type_key,
|
||||||
|
true_atom: @true_literal_type_key,
|
||||||
|
false_atom: @false_literal_type_key
|
||||||
|
}
|
||||||
|
|
||||||
|
@special_types %{
|
||||||
|
file_marker: %{type_kind: :special, name: :file_marker}
|
||||||
|
}
|
||||||
|
|
||||||
|
@special_type_keys %{
|
||||||
|
file_marker: @file_marker_type_key
|
||||||
|
}
|
||||||
|
|
||||||
|
@error_type_definitions %{
|
||||||
|
missing_value_type_in_assignment: %{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :missing_value_type_in_assignment
|
||||||
|
},
|
||||||
|
not_a_function: %{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :not_a_function
|
||||||
|
# actual_operator_type_id is dynamic
|
||||||
|
},
|
||||||
|
arity_mismatch: %{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :arity_mismatch
|
||||||
|
# expected_arity, actual_arity, function_type_id are dynamic
|
||||||
|
},
|
||||||
|
argument_type_mismatch: %{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :argument_type_mismatch
|
||||||
|
# arg_position, expected_arg_type_id, actual_arg_type_id, function_type_id are dynamic
|
||||||
|
}
|
||||||
|
# :invalid_if_condition_type is dynamically generated with actual_condition_type_id
|
||||||
|
# No static definition here, but we need a key for the reason atom.
|
||||||
|
# The dynamic error interner will use the reason atom.
|
||||||
|
}
|
||||||
|
|
||||||
|
@error_type_keys %{
|
||||||
|
missing_value_type_in_assignment: @error_missing_value_type_in_assignment_key,
|
||||||
|
not_a_function: @error_not_a_function_key,
|
||||||
|
arity_mismatch: @error_arity_mismatch_key,
|
||||||
|
argument_type_mismatch: @error_argument_type_mismatch_key
|
||||||
|
# :type_annotation_mismatch key is removed
|
||||||
|
# :invalid_if_condition_type does not have a static key for a full definition,
|
||||||
|
# but the reason :invalid_if_condition_type is used.
|
||||||
|
}
|
||||||
|
|
||||||
|
def primitive_types, do: @primitive_types
|
||||||
|
def primitive_type_key(name), do: @primitive_type_keys[name]
|
||||||
|
def primitive_type_keys_map, do: @primitive_type_keys
|
||||||
|
|
||||||
|
def literal_types, do: @literal_types
|
||||||
|
def literal_type_key(name), do: @literal_type_keys[name]
|
||||||
|
def literal_type_keys_map, do: @literal_type_keys
|
||||||
|
|
||||||
|
def special_types, do: @special_types
|
||||||
|
def special_type_key(name), do: @special_type_keys[name]
|
||||||
|
def special_type_keys_map, do: @special_type_keys
|
||||||
|
|
||||||
|
def error_type_definitions, do: @error_type_definitions
|
||||||
|
def error_type_key(name), do: @error_type_keys[name]
|
||||||
|
def error_type_keys_map, do: @error_type_keys
|
||||||
|
|
||||||
|
# Accessors for specific type definitions
|
||||||
|
def get_primitive_type(:any), do: @primitive_types[:any]
|
||||||
|
def get_primitive_type(:integer), do: @primitive_types[:integer]
|
||||||
|
def get_primitive_type(:string), do: @primitive_types[:string]
|
||||||
|
def get_primitive_type(:atom), do: @primitive_types[:atom] # Accessor for primitive atom
|
||||||
|
def get_primitive_type(:number), do: @primitive_types[:number]
|
||||||
|
def get_primitive_type(:nothing), do: @primitive_types[:nothing]
|
||||||
|
|
||||||
|
def get_literal_type(:nil_atom), do: @literal_types[:nil_atom]
|
||||||
|
def get_literal_type(:true_atom), do: @literal_types[:true_atom]
|
||||||
|
def get_literal_type(:false_atom), do: @literal_types[:false_atom]
|
||||||
|
|
||||||
|
def get_special_type(:file_marker), do: @special_types[:file_marker]
|
||||||
|
|
||||||
|
def get_error_type_definition(:missing_value_type_in_assignment),
|
||||||
|
do: @error_type_definitions[:missing_value_type_in_assignment]
|
||||||
|
|
||||||
|
# For type_annotation_mismatch, we now expect actual_type_id and expected_type_id
|
||||||
|
def get_error_type_definition(:type_annotation_mismatch, actual_type_id, expected_type_id) do
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :type_annotation_mismatch,
|
||||||
|
actual_type_id: actual_type_id,
|
||||||
|
expected_type_id: expected_type_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_error_type_definition(:invalid_if_condition_type, actual_condition_type_id) do
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :invalid_if_condition_type,
|
||||||
|
actual_condition_type_id: actual_condition_type_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_error_type_definition(:not_a_function, actual_operator_type_id) do
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :not_a_function,
|
||||||
|
actual_operator_type_id: actual_operator_type_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_error_type_definition(
|
||||||
|
:arity_mismatch,
|
||||||
|
expected_arity,
|
||||||
|
actual_arity,
|
||||||
|
function_type_id
|
||||||
|
) do
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :arity_mismatch,
|
||||||
|
expected_arity: expected_arity,
|
||||||
|
actual_arity: actual_arity,
|
||||||
|
function_type_id: function_type_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_error_type_definition(
|
||||||
|
:argument_type_mismatch,
|
||||||
|
arg_position,
|
||||||
|
expected_arg_type_id,
|
||||||
|
actual_arg_type_id,
|
||||||
|
function_type_id
|
||||||
|
) do
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :argument_type_mismatch,
|
||||||
|
arg_position: arg_position,
|
||||||
|
expected_arg_type_id: expected_arg_type_id,
|
||||||
|
actual_arg_type_id: actual_arg_type_id,
|
||||||
|
function_type_id: function_type_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Accessors for specific type keys
|
||||||
|
def any_type_key, do: @any_type_key
|
||||||
|
def nil_literal_type_key, do: @nil_literal_type_key
|
||||||
|
end
|
||||||
146
lib/tilly/bdd.ex
Normal file
146
lib/tilly/bdd.ex
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
defmodule Tilly.BDD do
|
||||||
|
@moduledoc """
|
||||||
|
Manages the BDD store, including hash-consing of BDD nodes.
|
||||||
|
The BDD store is expected to be part of a `typing_ctx` map under the key `:bdd_store`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Tilly.BDD.Node
|
||||||
|
|
||||||
|
@false_node_id 0
|
||||||
|
@true_node_id 1
|
||||||
|
@initial_next_node_id 2
|
||||||
|
@universal_ops_module :universal_ops
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initializes the BDD store within the typing context.
|
||||||
|
Pre-interns canonical `false` and `true` BDD nodes.
|
||||||
|
"""
|
||||||
|
def init_bdd_store(typing_ctx) when is_map(typing_ctx) do
|
||||||
|
false_structure = Node.mk_false()
|
||||||
|
true_structure = Node.mk_true()
|
||||||
|
|
||||||
|
bdd_store = %{
|
||||||
|
nodes_by_structure: %{
|
||||||
|
{false_structure, @universal_ops_module} => @false_node_id,
|
||||||
|
{true_structure, @universal_ops_module} => @true_node_id
|
||||||
|
},
|
||||||
|
structures_by_id: %{
|
||||||
|
@false_node_id => %{structure: false_structure, ops_module: @universal_ops_module},
|
||||||
|
@true_node_id => %{structure: true_structure, ops_module: @universal_ops_module}
|
||||||
|
},
|
||||||
|
next_node_id: @initial_next_node_id,
|
||||||
|
ops_cache: %{} # Cache for BDD operations {op_key, id1, id2} -> result_id
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.put(typing_ctx, :bdd_store, bdd_store)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets an existing BDD node ID or interns a new one if it's not already in the store.
|
||||||
|
|
||||||
|
Returns a tuple `{new_typing_ctx, node_id}`.
|
||||||
|
The `typing_ctx` is updated if a new node is interned.
|
||||||
|
"""
|
||||||
|
def get_or_intern_node(typing_ctx, logical_structure, ops_module_atom) do
|
||||||
|
bdd_store = Map.get(typing_ctx, :bdd_store)
|
||||||
|
|
||||||
|
unless bdd_store do
|
||||||
|
raise ArgumentError, "BDD store not initialized in typing_ctx. Call init_bdd_store first."
|
||||||
|
end
|
||||||
|
|
||||||
|
key = {logical_structure, ops_module_atom}
|
||||||
|
|
||||||
|
case Map.get(bdd_store.nodes_by_structure, key) do
|
||||||
|
nil ->
|
||||||
|
# Node not found, intern it
|
||||||
|
node_id = bdd_store.next_node_id
|
||||||
|
|
||||||
|
new_nodes_by_structure = Map.put(bdd_store.nodes_by_structure, key, node_id)
|
||||||
|
|
||||||
|
node_data = %{structure: logical_structure, ops_module: ops_module_atom}
|
||||||
|
new_structures_by_id = Map.put(bdd_store.structures_by_id, node_id, node_data)
|
||||||
|
|
||||||
|
new_next_node_id = node_id + 1
|
||||||
|
|
||||||
|
new_bdd_store =
|
||||||
|
%{
|
||||||
|
bdd_store
|
||||||
|
| nodes_by_structure: new_nodes_by_structure,
|
||||||
|
structures_by_id: new_structures_by_id,
|
||||||
|
next_node_id: new_next_node_id
|
||||||
|
}
|
||||||
|
|
||||||
|
new_typing_ctx = Map.put(typing_ctx, :bdd_store, new_bdd_store)
|
||||||
|
{new_typing_ctx, node_id}
|
||||||
|
|
||||||
|
existing_node_id ->
|
||||||
|
# Node found
|
||||||
|
{typing_ctx, existing_node_id}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves the node's structure and ops_module from the BDD store.
|
||||||
|
Returns `%{structure: logical_structure_tuple, ops_module: ops_module_atom}` or `nil` if not found.
|
||||||
|
"""
|
||||||
|
def get_node_data(typing_ctx, node_id) do
|
||||||
|
with %{bdd_store: %{structures_by_id: structures_by_id}} <- typing_ctx,
|
||||||
|
data when not is_nil(data) <- Map.get(structures_by_id, node_id) do
|
||||||
|
data
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if the given node ID corresponds to the canonical `false` BDD node.
|
||||||
|
"""
|
||||||
|
def is_false_node?(typing_ctx, node_id) do
|
||||||
|
# Optimized check for the predefined ID
|
||||||
|
if node_id == @false_node_id do
|
||||||
|
true
|
||||||
|
else
|
||||||
|
# Fallback for cases where a node might be structurally false but not have the canonical ID.
|
||||||
|
# This should ideally not happen with proper interning of Node.mk_false() via get_or_intern_node.
|
||||||
|
case get_node_data(typing_ctx, node_id) do
|
||||||
|
%{structure: structure, ops_module: @universal_ops_module} ->
|
||||||
|
structure == Node.mk_false()
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if the given node ID corresponds to the canonical `true` BDD node.
|
||||||
|
"""
|
||||||
|
def is_true_node?(typing_ctx, node_id) do
|
||||||
|
# Optimized check for the predefined ID
|
||||||
|
if node_id == @true_node_id do
|
||||||
|
true
|
||||||
|
else
|
||||||
|
# Fallback for cases where a node might be structurally true but not have the canonical ID.
|
||||||
|
case get_node_data(typing_ctx, node_id) do
|
||||||
|
%{structure: structure, ops_module: @universal_ops_module} ->
|
||||||
|
structure == Node.mk_true()
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the canonical ID for the `false` BDD node.
|
||||||
|
"""
|
||||||
|
def false_node_id(), do: @false_node_id
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the canonical ID for the `true` BDD node.
|
||||||
|
"""
|
||||||
|
def true_node_id(), do: @true_node_id
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the atom used as the `ops_module` for universal nodes like `true` and `false`.
|
||||||
|
"""
|
||||||
|
def universal_ops_module(), do: @universal_ops_module
|
||||||
|
end
|
||||||
89
lib/tilly/bdd/atom_bool_ops.ex
Normal file
89
lib/tilly/bdd/atom_bool_ops.ex
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
defmodule Tilly.BDD.AtomBoolOps do
|
||||||
|
@moduledoc """
|
||||||
|
BDD operations module for sets of atoms.
|
||||||
|
Elements are atoms, and leaf values are booleans.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Compares two atoms.
|
||||||
|
Returns `:lt`, `:eq`, or `:gt`.
|
||||||
|
"""
|
||||||
|
def compare_elements(elem1, elem2) when is_atom(elem1) and is_atom(elem2) do
|
||||||
|
cond do
|
||||||
|
elem1 < elem2 -> :lt
|
||||||
|
elem1 > elem2 -> :gt
|
||||||
|
true -> :eq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if two atoms are equal.
|
||||||
|
"""
|
||||||
|
def equal_element?(elem1, elem2) when is_atom(elem1) and is_atom(elem2) do
|
||||||
|
elem1 == elem2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Hashes an atom.
|
||||||
|
"""
|
||||||
|
def hash_element(elem) when is_atom(elem) do
|
||||||
|
# erlang.phash2 is suitable for term hashing
|
||||||
|
:erlang.phash2(elem)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The leaf value representing an empty set of atoms (false).
|
||||||
|
"""
|
||||||
|
def empty_leaf(), do: false
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The leaf value representing the universal set of atoms (true).
|
||||||
|
This is used if a BDD simplifies to a state where all atoms of this kind are included.
|
||||||
|
"""
|
||||||
|
def any_leaf(), do: true
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if a leaf value represents an empty set.
|
||||||
|
"""
|
||||||
|
def is_empty_leaf?(leaf_val) when is_boolean(leaf_val) do
|
||||||
|
leaf_val == false
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the union of two leaf values.
|
||||||
|
`typing_ctx` is included for interface consistency, but not used for boolean leaves.
|
||||||
|
"""
|
||||||
|
def union_leaves(_typing_ctx, leaf1, leaf2) when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
leaf1 or leaf2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the intersection of two leaf values.
|
||||||
|
`typing_ctx` is included for interface consistency, but not used for boolean leaves.
|
||||||
|
"""
|
||||||
|
def intersection_leaves(_typing_ctx, leaf1, leaf2)
|
||||||
|
when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
leaf1 and leaf2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the negation of a leaf value.
|
||||||
|
`typing_ctx` is included for interface consistency, but not used for boolean leaves.
|
||||||
|
"""
|
||||||
|
def negation_leaf(_typing_ctx, leaf) when is_boolean(leaf) do
|
||||||
|
not leaf
|
||||||
|
end
|
||||||
|
|
||||||
|
# def difference_leaves(_typing_ctx, leaf1, leaf2) when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
# leaf1 and (not leaf2)
|
||||||
|
# end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Tests a leaf value to determine if it represents an empty, full, or other set.
|
||||||
|
Returns `:empty`, `:full`, or `:other`.
|
||||||
|
"""
|
||||||
|
def test_leaf_value(true), do: :full
|
||||||
|
def test_leaf_value(false), do: :empty
|
||||||
|
# Add a clause for other types if atoms could have non-boolean leaf values
|
||||||
|
# def test_leaf_value(_other), do: :other
|
||||||
|
end
|
||||||
87
lib/tilly/bdd/integer_bool_ops.ex
Normal file
87
lib/tilly/bdd/integer_bool_ops.ex
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
defmodule Tilly.BDD.IntegerBoolOps do
|
||||||
|
@moduledoc """
|
||||||
|
BDD Operations module for BDDs where elements are integers and leaves are booleans.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Compares two integer elements.
|
||||||
|
Returns `:lt`, `:eq`, or `:gt`.
|
||||||
|
"""
|
||||||
|
def compare_elements(elem1, elem2) when is_integer(elem1) and is_integer(elem2) do
|
||||||
|
cond do
|
||||||
|
elem1 < elem2 -> :lt
|
||||||
|
elem1 > elem2 -> :gt
|
||||||
|
true -> :eq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if two integer elements are equal.
|
||||||
|
"""
|
||||||
|
def equal_element?(elem1, elem2) when is_integer(elem1) and is_integer(elem2) do
|
||||||
|
elem1 == elem2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Hashes an integer element.
|
||||||
|
"""
|
||||||
|
def hash_element(elem) when is_integer(elem) do
|
||||||
|
elem
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the leaf value representing emptiness (false).
|
||||||
|
"""
|
||||||
|
def empty_leaf(), do: false
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the leaf value representing universality (true).
|
||||||
|
"""
|
||||||
|
def any_leaf(), do: true
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if the leaf value represents emptiness.
|
||||||
|
"""
|
||||||
|
def is_empty_leaf?(leaf_val) when is_boolean(leaf_val) do
|
||||||
|
leaf_val == false
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the union of two boolean leaf values.
|
||||||
|
The `_typing_ctx` is ignored for this simple ops module.
|
||||||
|
"""
|
||||||
|
def union_leaves(_typing_ctx, leaf1, leaf2) when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
leaf1 or leaf2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the intersection of two boolean leaf values.
|
||||||
|
The `_typing_ctx` is ignored for this simple ops module.
|
||||||
|
"""
|
||||||
|
def intersection_leaves(_typing_ctx, leaf1, leaf2)
|
||||||
|
when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
leaf1 and leaf2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the negation of a boolean leaf value.
|
||||||
|
The `_typing_ctx` is ignored for this simple ops module.
|
||||||
|
"""
|
||||||
|
def negation_leaf(_typing_ctx, leaf) when is_boolean(leaf) do
|
||||||
|
not leaf
|
||||||
|
end
|
||||||
|
|
||||||
|
# def difference_leaves(_typing_ctx, leaf1, leaf2) when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
# leaf1 and (not leaf2)
|
||||||
|
# end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Tests a leaf value to determine if it represents an empty, full, or other set.
|
||||||
|
For boolean leaves with integers, this mirrors AtomBoolOps and StringBoolOps.
|
||||||
|
Returns `:empty`, `:full`, or `:other`.
|
||||||
|
"""
|
||||||
|
def test_leaf_value(true), do: :full
|
||||||
|
def test_leaf_value(false), do: :empty
|
||||||
|
# If integer BDDs could have non-boolean leaves that are not empty/full:
|
||||||
|
# def test_leaf_value(_other_leaf_value), do: :other
|
||||||
|
end
|
||||||
124
lib/tilly/bdd/node.ex
Normal file
124
lib/tilly/bdd/node.ex
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
defmodule Tilly.BDD.Node do
|
||||||
|
@moduledoc """
|
||||||
|
Defines the structure of BDD nodes and provides basic helper functions.
|
||||||
|
|
||||||
|
BDD nodes can be one of the following Elixir terms:
|
||||||
|
- `true`: Represents the universal set BDD.
|
||||||
|
- `false`: Represents the empty set BDD.
|
||||||
|
- `{:leaf, leaf_value_id}`: Represents a leaf node.
|
||||||
|
`leaf_value_id`'s interpretation depends on the specific BDD's `ops_module`.
|
||||||
|
- `{:split, element_id, positive_child_id, ignore_child_id, negative_child_id}`:
|
||||||
|
Represents an internal decision node.
|
||||||
|
`element_id` is the value being split upon.
|
||||||
|
`positive_child_id`, `ignore_child_id`, `negative_child_id` are IDs of other BDD nodes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@typedoc "A BDD node representing the universal set."
|
||||||
|
@type true_node :: true
|
||||||
|
|
||||||
|
@typedoc "A BDD node representing the empty set."
|
||||||
|
@type false_node :: false
|
||||||
|
|
||||||
|
@typedoc "A BDD leaf node."
|
||||||
|
@type leaf_node(leaf_value) :: {:leaf, leaf_value}
|
||||||
|
|
||||||
|
@typedoc "A BDD split node."
|
||||||
|
@type split_node(element, node_id) ::
|
||||||
|
{:split, element, node_id, node_id, node_id}
|
||||||
|
|
||||||
|
@typedoc "Any valid BDD node structure."
|
||||||
|
@type t(element, leaf_value, node_id) ::
|
||||||
|
true_node()
|
||||||
|
| false_node()
|
||||||
|
| leaf_node(leaf_value)
|
||||||
|
| split_node(element, node_id)
|
||||||
|
|
||||||
|
# --- Smart Constructors (Low-Level) ---
|
||||||
|
|
||||||
|
@doc "Creates a true BDD node."
|
||||||
|
@spec mk_true() :: true_node()
|
||||||
|
def mk_true, do: true
|
||||||
|
|
||||||
|
@doc "Creates a false BDD node."
|
||||||
|
@spec mk_false() :: false_node()
|
||||||
|
def mk_false, do: false
|
||||||
|
|
||||||
|
@doc "Creates a leaf BDD node."
|
||||||
|
@spec mk_leaf(leaf_value :: any()) :: leaf_node(any())
|
||||||
|
def mk_leaf(leaf_value_id), do: {:leaf, leaf_value_id}
|
||||||
|
|
||||||
|
@doc "Creates a split BDD node."
|
||||||
|
@spec mk_split(
|
||||||
|
element_id :: any(),
|
||||||
|
positive_child_id :: any(),
|
||||||
|
ignore_child_id :: any(),
|
||||||
|
negative_child_id :: any()
|
||||||
|
) :: split_node(any(), any())
|
||||||
|
def mk_split(element_id, positive_child_id, ignore_child_id, negative_child_id) do
|
||||||
|
{:split, element_id, positive_child_id, ignore_child_id, negative_child_id}
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Predicates ---
|
||||||
|
|
||||||
|
@doc "Checks if the node is a true node."
|
||||||
|
@spec is_true?(node :: t(any(), any(), any())) :: boolean()
|
||||||
|
def is_true?(true), do: true
|
||||||
|
def is_true?(_other), do: false
|
||||||
|
|
||||||
|
@doc "Checks if the node is a false node."
|
||||||
|
@spec is_false?(node :: t(any(), any(), any())) :: boolean()
|
||||||
|
def is_false?(false), do: true
|
||||||
|
def is_false?(_other), do: false
|
||||||
|
|
||||||
|
@doc "Checks if the node is a leaf node."
|
||||||
|
@spec is_leaf?(node :: t(any(), any(), any())) :: boolean()
|
||||||
|
def is_leaf?({:leaf, _value}), do: true
|
||||||
|
def is_leaf?(_other), do: false
|
||||||
|
|
||||||
|
@doc "Checks if the node is a split node."
|
||||||
|
@spec is_split?(node :: t(any(), any(), any())) :: boolean()
|
||||||
|
def is_split?({:split, _el, _p, _i, _n}), do: true
|
||||||
|
def is_split?(_other), do: false
|
||||||
|
|
||||||
|
# --- Accessors ---
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the value of a leaf node.
|
||||||
|
Raises an error if the node is not a leaf node.
|
||||||
|
"""
|
||||||
|
@spec value(leaf_node :: leaf_node(any())) :: any()
|
||||||
|
def value({:leaf, value_id}), do: value_id
|
||||||
|
def value(other), do: raise(ArgumentError, "Not a leaf node: #{inspect(other)}")
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the element of a split node.
|
||||||
|
Raises an error if the node is not a split node.
|
||||||
|
"""
|
||||||
|
@spec element(split_node :: split_node(any(), any())) :: any()
|
||||||
|
def element({:split, element_id, _, _, _}), do: element_id
|
||||||
|
def element(other), do: raise(ArgumentError, "Not a split node: #{inspect(other)}")
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the positive child ID of a split node.
|
||||||
|
Raises an error if the node is not a split node.
|
||||||
|
"""
|
||||||
|
@spec positive_child(split_node :: split_node(any(), any())) :: any()
|
||||||
|
def positive_child({:split, _, p_child_id, _, _}), do: p_child_id
|
||||||
|
def positive_child(other), do: raise(ArgumentError, "Not a split node: #{inspect(other)}")
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the ignore child ID of a split node.
|
||||||
|
Raises an error if the node is not a split node.
|
||||||
|
"""
|
||||||
|
@spec ignore_child(split_node :: split_node(any(), any())) :: any()
|
||||||
|
def ignore_child({:split, _, _, i_child_id, _}), do: i_child_id
|
||||||
|
def ignore_child(other), do: raise(ArgumentError, "Not a split node: #{inspect(other)}")
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the negative child ID of a split node.
|
||||||
|
Raises an error if the node is not a split node.
|
||||||
|
"""
|
||||||
|
@spec negative_child(split_node :: split_node(any(), any())) :: any()
|
||||||
|
def negative_child({:split, _, _, _, n_child_id}), do: n_child_id
|
||||||
|
def negative_child(other), do: raise(ArgumentError, "Not a split node: #{inspect(other)}")
|
||||||
|
end
|
||||||
347
lib/tilly/bdd/ops.ex
Normal file
347
lib/tilly/bdd/ops.ex
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
defmodule Tilly.BDD.Ops do
|
||||||
|
@moduledoc """
|
||||||
|
Generic BDD algorithms and smart constructors.
|
||||||
|
These functions operate on BDD node IDs and use an `ops_module`
|
||||||
|
to dispatch to specific element/leaf operations.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Tilly.BDD
|
||||||
|
alias Tilly.BDD.Node
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Smart constructor for leaf nodes.
|
||||||
|
Uses the `ops_module` to test if the `leaf_value` corresponds to
|
||||||
|
an empty or universal set for that module.
|
||||||
|
Returns `{new_typing_ctx, node_id}`.
|
||||||
|
"""
|
||||||
|
def leaf(typing_ctx, leaf_value, ops_module) do
|
||||||
|
case apply(ops_module, :test_leaf_value, [leaf_value]) do
|
||||||
|
:empty ->
|
||||||
|
{typing_ctx, BDD.false_node_id()}
|
||||||
|
|
||||||
|
:full ->
|
||||||
|
{typing_ctx, BDD.true_node_id()}
|
||||||
|
|
||||||
|
:other ->
|
||||||
|
logical_structure = Node.mk_leaf(leaf_value)
|
||||||
|
BDD.get_or_intern_node(typing_ctx, logical_structure, ops_module)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Smart constructor for split nodes. Applies simplification rules.
|
||||||
|
Returns `{new_typing_ctx, node_id}`.
|
||||||
|
"""
|
||||||
|
def split(typing_ctx, element, p_id, i_id, n_id, ops_module) do
|
||||||
|
# Apply simplification rules. Order can be important.
|
||||||
|
cond do
|
||||||
|
# If ignore and negative children are False, result is positive child.
|
||||||
|
BDD.is_false_node?(typing_ctx, i_id) and
|
||||||
|
BDD.is_false_node?(typing_ctx, n_id) ->
|
||||||
|
{typing_ctx, p_id}
|
||||||
|
|
||||||
|
# If ignore child is True, the whole BDD is True.
|
||||||
|
BDD.is_true_node?(typing_ctx, i_id) ->
|
||||||
|
{typing_ctx, BDD.true_node_id()}
|
||||||
|
|
||||||
|
# If positive and negative children are the same.
|
||||||
|
p_id == n_id ->
|
||||||
|
if p_id == i_id do
|
||||||
|
# All three children are identical.
|
||||||
|
{typing_ctx, p_id}
|
||||||
|
else
|
||||||
|
# Result is p_id (or n_id) unioned with i_id.
|
||||||
|
# This creates a potential mutual recursion with union_bdds
|
||||||
|
# which needs to be handled by the apply_op cache.
|
||||||
|
union_bdds(typing_ctx, p_id, i_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: Add more simplification rules from CDuce bdd.ml `split` as needed.
|
||||||
|
# e.g. if p=T, i=F, n=T -> True
|
||||||
|
# e.g. if p=F, i=F, n=T -> not(x) relative to this BDD's element universe (complex)
|
||||||
|
|
||||||
|
true ->
|
||||||
|
# No further simplification rule applied, intern the node.
|
||||||
|
logical_structure = Node.mk_split(element, p_id, i_id, n_id)
|
||||||
|
BDD.get_or_intern_node(typing_ctx, logical_structure, ops_module)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the union of two BDDs.
|
||||||
|
Returns `{new_typing_ctx, result_node_id}`.
|
||||||
|
"""
|
||||||
|
def union_bdds(typing_ctx, bdd1_id, bdd2_id) do
|
||||||
|
apply_op(typing_ctx, :union, bdd1_id, bdd2_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the intersection of two BDDs.
|
||||||
|
Returns `{new_typing_ctx, result_node_id}`.
|
||||||
|
"""
|
||||||
|
def intersection_bdds(typing_ctx, bdd1_id, bdd2_id) do
|
||||||
|
apply_op(typing_ctx, :intersection, bdd1_id, bdd2_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the negation of a BDD.
|
||||||
|
Returns `{new_typing_ctx, result_node_id}`.
|
||||||
|
"""
|
||||||
|
def negation_bdd(typing_ctx, bdd_id) do
|
||||||
|
# The second argument to apply_op is nil for unary operations like negation.
|
||||||
|
apply_op(typing_ctx, :negation, bdd_id, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the difference of two BDDs (bdd1 - bdd2).
|
||||||
|
Returns `{new_typing_ctx, result_node_id}`.
|
||||||
|
Implemented as `bdd1 INTERSECTION (NEGATION bdd2)`.
|
||||||
|
"""
|
||||||
|
def difference_bdd(typing_ctx, bdd1_id, bdd2_id) do
|
||||||
|
{ctx, neg_bdd2_id} = negation_bdd(typing_ctx, bdd2_id)
|
||||||
|
intersection_bdds(ctx, bdd1_id, neg_bdd2_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Internal function to handle actual BDD operations, bypassing cache for direct calls.
|
||||||
|
defp do_union_bdds(typing_ctx, bdd1_id, bdd2_id) do
|
||||||
|
# Ensure canonical order for commutative operations if not handled by apply_op key
|
||||||
|
# For simplicity, apply_op will handle canonical key generation.
|
||||||
|
|
||||||
|
# 1. Handle terminal cases
|
||||||
|
cond do
|
||||||
|
bdd1_id == bdd2_id -> {typing_ctx, bdd1_id}
|
||||||
|
BDD.is_true_node?(typing_ctx, bdd1_id) -> {typing_ctx, BDD.true_node_id()}
|
||||||
|
BDD.is_true_node?(typing_ctx, bdd2_id) -> {typing_ctx, BDD.true_node_id()}
|
||||||
|
BDD.is_false_node?(typing_ctx, bdd1_id) -> {typing_ctx, bdd2_id}
|
||||||
|
BDD.is_false_node?(typing_ctx, bdd2_id) -> {typing_ctx, bdd1_id}
|
||||||
|
true -> perform_union(typing_ctx, bdd1_id, bdd2_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp perform_union(typing_ctx, bdd1_id, bdd2_id) do
|
||||||
|
%{structure: s1, ops_module: ops_m1} = BDD.get_node_data(typing_ctx, bdd1_id)
|
||||||
|
%{structure: s2, ops_module: ops_m2} = BDD.get_node_data(typing_ctx, bdd2_id)
|
||||||
|
|
||||||
|
# For now, assume ops_modules must match for simplicity.
|
||||||
|
# Production systems might need more complex logic or type errors here.
|
||||||
|
if ops_m1 != ops_m2 do
|
||||||
|
raise ArgumentError,
|
||||||
|
"Cannot union BDDs with different ops_modules: #{inspect(ops_m1)} and #{inspect(ops_m2)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
ops_m = ops_m1
|
||||||
|
|
||||||
|
case {s1, s2} do
|
||||||
|
# Both are leaves
|
||||||
|
{{:leaf, v1}, {:leaf, v2}} ->
|
||||||
|
new_leaf_val = apply(ops_m, :union_leaves, [typing_ctx, v1, v2])
|
||||||
|
leaf(typing_ctx, new_leaf_val, ops_m)
|
||||||
|
|
||||||
|
# s1 is split, s2 is leaf
|
||||||
|
{{:split, x1, p1_id, i1_id, n1_id}, {:leaf, _v2}} ->
|
||||||
|
# CDuce: split x1 p1 (i1 ++ b) n1
|
||||||
|
{ctx, new_i1_id} = union_bdds(typing_ctx, i1_id, bdd2_id)
|
||||||
|
split(ctx, x1, p1_id, new_i1_id, n1_id, ops_m)
|
||||||
|
|
||||||
|
# s1 is leaf, s2 is split
|
||||||
|
{{:leaf, _v1}, {:split, x2, p2_id, i2_id, n2_id}} ->
|
||||||
|
# CDuce: split x2 p2 (i2 ++ a) n2 (symmetric to above)
|
||||||
|
{ctx, new_i2_id} = union_bdds(typing_ctx, i2_id, bdd1_id)
|
||||||
|
split(ctx, x2, p2_id, new_i2_id, n2_id, ops_m)
|
||||||
|
|
||||||
|
# Both are splits
|
||||||
|
{{:split, x1, p1_id, i1_id, n1_id}, {:split, x2, p2_id, i2_id, n2_id}} ->
|
||||||
|
# Compare elements using the ops_module
|
||||||
|
comp_result = apply(ops_m, :compare_elements, [x1, x2])
|
||||||
|
|
||||||
|
cond do
|
||||||
|
comp_result == :eq ->
|
||||||
|
# Elements are equal, merge children
|
||||||
|
{ctx0, new_p_id} = union_bdds(typing_ctx, p1_id, p2_id)
|
||||||
|
{ctx1, new_i_id} = union_bdds(ctx0, i1_id, i2_id)
|
||||||
|
{ctx2, new_n_id} = union_bdds(ctx1, n1_id, n2_id)
|
||||||
|
split(ctx2, x1, new_p_id, new_i_id, new_n_id, ops_m)
|
||||||
|
|
||||||
|
comp_result == :lt ->
|
||||||
|
# x1 < x2
|
||||||
|
# CDuce: split x1 p1 (i1 ++ b) n1
|
||||||
|
{ctx, new_i1_id} = union_bdds(typing_ctx, i1_id, bdd2_id)
|
||||||
|
split(ctx, x1, p1_id, new_i1_id, n1_id, ops_m)
|
||||||
|
|
||||||
|
comp_result == :gt ->
|
||||||
|
# x1 > x2
|
||||||
|
# CDuce: split x2 p2 (i2 ++ a) n2
|
||||||
|
{ctx, new_i2_id} = union_bdds(typing_ctx, i2_id, bdd1_id)
|
||||||
|
split(ctx, x2, p2_id, new_i2_id, n2_id, ops_m)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_intersection_bdds(typing_ctx, bdd1_id, bdd2_id) do
|
||||||
|
# Canonical order handled by apply_op key generation.
|
||||||
|
|
||||||
|
# Fast path for disjoint singleton BDDs
|
||||||
|
case {BDD.get_node_data(typing_ctx, bdd1_id), BDD.get_node_data(typing_ctx, bdd2_id)} do
|
||||||
|
{%{structure: {:split, x1, t, f, f}, ops_module: m},
|
||||||
|
%{structure: {:split, x2, t, f, f}, ops_module: m}}
|
||||||
|
when x1 != x2 ->
|
||||||
|
{typing_ctx, BDD.false_node_id()}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
# 1. Handle terminal cases
|
||||||
|
cond do
|
||||||
|
bdd1_id == bdd2_id -> {typing_ctx, bdd1_id}
|
||||||
|
BDD.is_false_node?(typing_ctx, bdd1_id) -> {typing_ctx, BDD.false_node_id()}
|
||||||
|
BDD.is_false_node?(typing_ctx, bdd2_id) -> {typing_ctx, BDD.false_node_id()}
|
||||||
|
BDD.is_true_node?(typing_ctx, bdd1_id) -> {typing_ctx, bdd2_id}
|
||||||
|
BDD.is_true_node?(typing_ctx, bdd2_id) -> {typing_ctx, bdd1_id}
|
||||||
|
true -> perform_intersection(typing_ctx, bdd1_id, bdd2_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp perform_intersection(typing_ctx, bdd1_id, bdd2_id) do
|
||||||
|
%{structure: s1, ops_module: ops_m1} = BDD.get_node_data(typing_ctx, bdd1_id)
|
||||||
|
%{structure: s2, ops_module: ops_m2} = BDD.get_node_data(typing_ctx, bdd2_id)
|
||||||
|
|
||||||
|
if ops_m1 != ops_m2 do
|
||||||
|
raise ArgumentError,
|
||||||
|
"Cannot intersect BDDs with different ops_modules: #{inspect(ops_m1)} and #{inspect(ops_m2)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
ops_m = ops_m1
|
||||||
|
|
||||||
|
case {s1, s2} do
|
||||||
|
# Both are leaves
|
||||||
|
{{:leaf, v1}, {:leaf, v2}} ->
|
||||||
|
new_leaf_val = apply(ops_m, :intersection_leaves, [typing_ctx, v1, v2])
|
||||||
|
leaf(typing_ctx, new_leaf_val, ops_m)
|
||||||
|
|
||||||
|
# s1 is split, s2 is leaf
|
||||||
|
{{:split, x1, p1_id, i1_id, n1_id}, {:leaf, _v2}} ->
|
||||||
|
{ctx0, new_p1_id} = intersection_bdds(typing_ctx, p1_id, bdd2_id)
|
||||||
|
{ctx1, new_i1_id} = intersection_bdds(ctx0, i1_id, bdd2_id)
|
||||||
|
{ctx2, new_n1_id} = intersection_bdds(ctx1, n1_id, bdd2_id)
|
||||||
|
split(ctx2, x1, new_p1_id, new_i1_id, new_n1_id, ops_m)
|
||||||
|
|
||||||
|
# s1 is leaf, s2 is split
|
||||||
|
{{:leaf, _v1}, {:split, x2, p2_id, i2_id, n2_id}} ->
|
||||||
|
{ctx0, new_p2_id} = intersection_bdds(typing_ctx, bdd1_id, p2_id)
|
||||||
|
{ctx1, new_i2_id} = intersection_bdds(ctx0, bdd1_id, i2_id)
|
||||||
|
{ctx2, new_n2_id} = intersection_bdds(ctx1, bdd1_id, n2_id)
|
||||||
|
split(ctx2, x2, new_p2_id, new_i2_id, new_n2_id, ops_m)
|
||||||
|
|
||||||
|
# Both are splits
|
||||||
|
{{:split, x1, p1_id, i1_id, n1_id}, {:split, x2, p2_id, i2_id, n2_id}} ->
|
||||||
|
comp_result = apply(ops_m, :compare_elements, [x1, x2])
|
||||||
|
|
||||||
|
cond do
|
||||||
|
comp_result == :eq ->
|
||||||
|
# CDuce: split x1 ((p1**(p2++i2))++(p2**i1)) (i1**i2) ((n1**(n2++i2))++(n2**i1))
|
||||||
|
{ctx0, p2_u_i2} = union_bdds(typing_ctx, p2_id, i2_id)
|
||||||
|
{ctx1, n2_u_i2} = union_bdds(ctx0, n2_id, i2_id)
|
||||||
|
|
||||||
|
{ctx2, p1_i_p2ui2} = intersection_bdds(ctx1, p1_id, p2_u_i2)
|
||||||
|
{ctx3, p2_i_i1} = intersection_bdds(ctx2, p2_id, i1_id)
|
||||||
|
{ctx4, new_p_id} = union_bdds(ctx3, p1_i_p2ui2, p2_i_i1)
|
||||||
|
|
||||||
|
{ctx5, new_i_id} = intersection_bdds(ctx4, i1_id, i2_id)
|
||||||
|
|
||||||
|
{ctx6, n1_i_n2ui2} = intersection_bdds(ctx5, n1_id, n2_u_i2)
|
||||||
|
{ctx7, n2_i_i1} = intersection_bdds(ctx6, n2_id, i1_id)
|
||||||
|
{ctx8, new_n_id} = union_bdds(ctx7, n1_i_n2ui2, n2_i_i1)
|
||||||
|
|
||||||
|
split(ctx8, x1, new_p_id, new_i_id, new_n_id, ops_m)
|
||||||
|
|
||||||
|
# x1 < x2
|
||||||
|
comp_result == :lt ->
|
||||||
|
# CDuce: split x1 (p1 ** b) (i1 ** b) (n1 ** b) where b is bdd2
|
||||||
|
{ctx0, new_p1_id} = intersection_bdds(typing_ctx, p1_id, bdd2_id)
|
||||||
|
{ctx1, new_i1_id} = intersection_bdds(ctx0, i1_id, bdd2_id)
|
||||||
|
{ctx2, new_n1_id} = intersection_bdds(ctx1, n1_id, bdd2_id)
|
||||||
|
split(ctx2, x1, new_p1_id, new_i1_id, new_n1_id, ops_m)
|
||||||
|
|
||||||
|
# x1 > x2
|
||||||
|
comp_result == :gt ->
|
||||||
|
# CDuce: split x2 (a ** p2) (a ** i2) (a ** n2) where a is bdd1
|
||||||
|
{ctx0, new_p2_id} = intersection_bdds(typing_ctx, bdd1_id, p2_id)
|
||||||
|
{ctx1, new_i2_id} = intersection_bdds(ctx0, bdd1_id, i2_id)
|
||||||
|
{ctx2, new_n2_id} = intersection_bdds(ctx1, bdd1_id, n2_id)
|
||||||
|
split(ctx2, x2, new_p2_id, new_i2_id, new_n2_id, ops_m)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_negation_bdd(typing_ctx, bdd_id) do
|
||||||
|
# 1. Handle terminal cases
|
||||||
|
cond do
|
||||||
|
BDD.is_true_node?(typing_ctx, bdd_id) -> {typing_ctx, BDD.false_node_id()}
|
||||||
|
BDD.is_false_node?(typing_ctx, bdd_id) -> {typing_ctx, BDD.true_node_id()}
|
||||||
|
true -> perform_negation(typing_ctx, bdd_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp perform_negation(typing_ctx, bdd_id) do
|
||||||
|
%{structure: s, ops_module: ops_m} = BDD.get_node_data(typing_ctx, bdd_id)
|
||||||
|
|
||||||
|
case s do
|
||||||
|
# Leaf
|
||||||
|
{:leaf, v} ->
|
||||||
|
neg_leaf_val = apply(ops_m, :negation_leaf, [typing_ctx, v])
|
||||||
|
leaf(typing_ctx, neg_leaf_val, ops_m)
|
||||||
|
|
||||||
|
# Split
|
||||||
|
{:split, x, p_id, i_id, n_id} ->
|
||||||
|
# CDuce: ~~i ** split x (~~p) (~~(p++n)) (~~n)
|
||||||
|
{ctx0, neg_i_id} = negation_bdd(typing_ctx, i_id)
|
||||||
|
{ctx1, neg_p_id} = negation_bdd(ctx0, p_id)
|
||||||
|
{ctx2, p_u_n_id} = union_bdds(ctx1, p_id, n_id)
|
||||||
|
{ctx3, neg_p_u_n_id} = negation_bdd(ctx2, p_u_n_id)
|
||||||
|
{ctx4, neg_n_id} = negation_bdd(ctx3, n_id)
|
||||||
|
{ctx5, split_part_id} = split(ctx4, x, neg_p_id, neg_p_u_n_id, neg_n_id, ops_m)
|
||||||
|
intersection_bdds(ctx5, neg_i_id, split_part_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Caching Wrapper for BDD Operations ---
|
||||||
|
defp apply_op(typing_ctx, op_key, bdd1_id, bdd2_id) do
|
||||||
|
cache_key = make_cache_key(op_key, bdd1_id, bdd2_id)
|
||||||
|
bdd_store = Map.get(typing_ctx, :bdd_store)
|
||||||
|
|
||||||
|
case Map.get(bdd_store.ops_cache, cache_key) do
|
||||||
|
nil ->
|
||||||
|
# Not in cache, compute it
|
||||||
|
{new_typing_ctx, result_id} =
|
||||||
|
case op_key do
|
||||||
|
:union -> do_union_bdds(typing_ctx, bdd1_id, bdd2_id)
|
||||||
|
:intersection -> do_intersection_bdds(typing_ctx, bdd1_id, bdd2_id)
|
||||||
|
# bdd2_id is nil here
|
||||||
|
:negation -> do_negation_bdd(typing_ctx, bdd1_id)
|
||||||
|
_ -> raise "Unsupported op_key: #{op_key}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Store in cache
|
||||||
|
# IMPORTANT: Use new_typing_ctx (from the operation) to get the potentially updated bdd_store
|
||||||
|
current_bdd_store_after_op = Map.get(new_typing_ctx, :bdd_store)
|
||||||
|
new_ops_cache = Map.put(current_bdd_store_after_op.ops_cache, cache_key, result_id)
|
||||||
|
final_bdd_store_with_cache = %{current_bdd_store_after_op | ops_cache: new_ops_cache}
|
||||||
|
# And put this updated bdd_store back into new_typing_ctx
|
||||||
|
final_typing_ctx_with_cache =
|
||||||
|
Map.put(new_typing_ctx, :bdd_store, final_bdd_store_with_cache)
|
||||||
|
|
||||||
|
{final_typing_ctx_with_cache, result_id}
|
||||||
|
|
||||||
|
cached_result_id ->
|
||||||
|
{typing_ctx, cached_result_id}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_cache_key(:negation, bdd_id, nil), do: {:negation, bdd_id}
|
||||||
|
|
||||||
|
defp make_cache_key(op_key, id1, id2) when op_key in [:union, :intersection] do
|
||||||
|
# Canonical order for commutative binary operations
|
||||||
|
if id1 <= id2, do: {op_key, id1, id2}, else: {op_key, id2, id1}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_cache_key(op_key, id1, id2), do: {op_key, id1, id2}
|
||||||
|
end
|
||||||
87
lib/tilly/bdd/string_bool_ops.ex
Normal file
87
lib/tilly/bdd/string_bool_ops.ex
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
defmodule Tilly.BDD.StringBoolOps do
|
||||||
|
@moduledoc """
|
||||||
|
BDD operations module for sets of strings.
|
||||||
|
Elements are strings, and leaf values are booleans.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Compares two strings.
|
||||||
|
Returns `:lt`, `:eq`, or `:gt`.
|
||||||
|
"""
|
||||||
|
def compare_elements(elem1, elem2) when is_binary(elem1) and is_binary(elem2) do
|
||||||
|
cond do
|
||||||
|
elem1 < elem2 -> :lt
|
||||||
|
elem1 > elem2 -> :gt
|
||||||
|
true -> :eq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if two strings are equal.
|
||||||
|
"""
|
||||||
|
def equal_element?(elem1, elem2) when is_binary(elem1) and is_binary(elem2) do
|
||||||
|
elem1 == elem2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Hashes a string.
|
||||||
|
"""
|
||||||
|
def hash_element(elem) when is_binary(elem) do
|
||||||
|
# erlang.phash2 is suitable for term hashing
|
||||||
|
:erlang.phash2(elem)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The leaf value representing an empty set of strings (false).
|
||||||
|
"""
|
||||||
|
def empty_leaf(), do: false
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The leaf value representing the universal set of strings (true).
|
||||||
|
"""
|
||||||
|
def any_leaf(), do: true
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if a leaf value represents an empty set.
|
||||||
|
"""
|
||||||
|
def is_empty_leaf?(leaf_val) when is_boolean(leaf_val) do
|
||||||
|
leaf_val == false
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the union of two leaf values.
|
||||||
|
`typing_ctx` is included for interface consistency, but not used for boolean leaves.
|
||||||
|
"""
|
||||||
|
def union_leaves(_typing_ctx, leaf1, leaf2) when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
leaf1 or leaf2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the intersection of two leaf values.
|
||||||
|
`typing_ctx` is included for interface consistency, but not used for boolean leaves.
|
||||||
|
"""
|
||||||
|
def intersection_leaves(_typing_ctx, leaf1, leaf2)
|
||||||
|
when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
leaf1 and leaf2
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the negation of a leaf value.
|
||||||
|
`typing_ctx` is included for interface consistency, but not used for boolean leaves.
|
||||||
|
"""
|
||||||
|
def negation_leaf(_typing_ctx, leaf) when is_boolean(leaf) do
|
||||||
|
not leaf
|
||||||
|
end
|
||||||
|
|
||||||
|
# def difference_leaves(_typing_ctx, leaf1, leaf2) when is_boolean(leaf1) and is_boolean(leaf2) do
|
||||||
|
# leaf1 and (not leaf2)
|
||||||
|
# end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Tests a leaf value to determine if it represents an empty, full, or other set.
|
||||||
|
Returns `:empty`, `:full`, or `:other`.
|
||||||
|
"""
|
||||||
|
def test_leaf_value(true), do: :full
|
||||||
|
def test_leaf_value(false), do: :empty
|
||||||
|
# def test_leaf_value(_other), do: :other
|
||||||
|
end
|
||||||
57
lib/tilly/type.ex
Normal file
57
lib/tilly/type.ex
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
defmodule Tilly.Type do
|
||||||
|
@moduledoc """
|
||||||
|
Defines the structure of a Type Descriptor (`Descr`) and provides
|
||||||
|
helper functions for creating fundamental type descriptors.
|
||||||
|
|
||||||
|
A Type Descriptor is a map representing a type. Each field in the map
|
||||||
|
corresponds to a basic kind of type component (e.g., atoms, integers, pairs)
|
||||||
|
and holds a BDD node ID. These BDDs represent the set of values
|
||||||
|
allowed for that particular component of the type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Tilly.BDD
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a `Descr` map representing the empty type (Nothing).
|
||||||
|
All BDD IDs in this `Descr` point to the canonical `false` BDD node.
|
||||||
|
The `typing_ctx` is passed for consistency but not modified by this function.
|
||||||
|
"""
|
||||||
|
def empty_descr(_typing_ctx) do
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
|
||||||
|
%{
|
||||||
|
atoms_bdd_id: false_id,
|
||||||
|
integers_bdd_id: false_id,
|
||||||
|
strings_bdd_id: false_id,
|
||||||
|
pairs_bdd_id: false_id,
|
||||||
|
records_bdd_id: false_id,
|
||||||
|
functions_bdd_id: false_id,
|
||||||
|
absent_marker_bdd_id: false_id
|
||||||
|
# Add other kinds as needed, e.g., for abstract types
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a `Descr` map representing the universal type (Any).
|
||||||
|
All BDD IDs in this `Descr` point to the canonical `true` BDD node.
|
||||||
|
The `typing_ctx` is passed for consistency but not modified by this function.
|
||||||
|
"""
|
||||||
|
def any_descr(_typing_ctx) do
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
|
||||||
|
%{
|
||||||
|
atoms_bdd_id: true_id,
|
||||||
|
integers_bdd_id: true_id,
|
||||||
|
strings_bdd_id: true_id,
|
||||||
|
pairs_bdd_id: true_id,
|
||||||
|
records_bdd_id: true_id,
|
||||||
|
functions_bdd_id: true_id,
|
||||||
|
# For 'Any', absence is typically not included unless explicitly modeled.
|
||||||
|
# If 'Any' should include the possibility of absence, this would be true_id.
|
||||||
|
# For now, let's assume 'Any' means any *value*, so absence is false.
|
||||||
|
# This can be refined based on the desired semantics of 'Any'.
|
||||||
|
# CDuce 'Any' does not include 'Absent'.
|
||||||
|
absent_marker_bdd_id: BDD.false_node_id()
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
305
lib/tilly/type/ops.ex
Normal file
305
lib/tilly/type/ops.ex
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
defmodule Tilly.Type.Ops do
|
||||||
|
@moduledoc """
|
||||||
|
Implements set-theoretic operations on Type Descriptors (`Descr` maps)
|
||||||
|
and provides helper functions for constructing specific types.
|
||||||
|
Operations work with interned `Descr` IDs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Tilly.BDD
|
||||||
|
alias Tilly.Type
|
||||||
|
alias Tilly.Type.Store
|
||||||
|
|
||||||
|
# Defines the fields in a Descr map that hold BDD IDs.
|
||||||
|
# Order can be relevant if specific iteration order is ever needed, but for field-wise ops it's not.
|
||||||
|
defp descr_fields do
|
||||||
|
[
|
||||||
|
:atoms_bdd_id,
|
||||||
|
:integers_bdd_id,
|
||||||
|
:strings_bdd_id,
|
||||||
|
:pairs_bdd_id,
|
||||||
|
:records_bdd_id,
|
||||||
|
:functions_bdd_id,
|
||||||
|
:absent_marker_bdd_id
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Core Set Operations ---
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the union of two types represented by their `Descr` IDs.
|
||||||
|
Returns `{new_typing_ctx, result_descr_id}`.
|
||||||
|
"""
|
||||||
|
def union_types(typing_ctx, descr1_id, descr2_id) do
|
||||||
|
apply_type_op(typing_ctx, :union, descr1_id, descr2_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the intersection of two types represented by their `Descr` IDs.
|
||||||
|
Returns `{new_typing_ctx, result_descr_id}`.
|
||||||
|
"""
|
||||||
|
def intersection_types(typing_ctx, descr1_id, descr2_id) do
|
||||||
|
apply_type_op(typing_ctx, :intersection, descr1_id, descr2_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Computes the negation of a type represented by its `Descr` ID.
|
||||||
|
Returns `{new_typing_ctx, result_descr_id}`.
|
||||||
|
"""
|
||||||
|
def negation_type(typing_ctx, descr_id) do
|
||||||
|
apply_type_op(typing_ctx, :negation, descr_id, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_union_types(typing_ctx, descr1_id, descr2_id) do
|
||||||
|
descr1 = Store.get_descr_by_id(typing_ctx, descr1_id)
|
||||||
|
descr2 = Store.get_descr_by_id(typing_ctx, descr2_id)
|
||||||
|
|
||||||
|
{final_ctx, result_fields_map} =
|
||||||
|
Enum.reduce(descr_fields(), {typing_ctx, %{}}, fn field, {current_ctx, acc_fields} ->
|
||||||
|
bdd1_id = Map.get(descr1, field)
|
||||||
|
bdd2_id = Map.get(descr2, field)
|
||||||
|
{new_ctx, result_bdd_id} = BDD.Ops.union_bdds(current_ctx, bdd1_id, bdd2_id)
|
||||||
|
{new_ctx, Map.put(acc_fields, field, result_bdd_id)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Store.get_or_intern_descr(final_ctx, result_fields_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_intersection_types(typing_ctx, descr1_id, descr2_id) do
|
||||||
|
descr1 = Store.get_descr_by_id(typing_ctx, descr1_id)
|
||||||
|
descr2 = Store.get_descr_by_id(typing_ctx, descr2_id)
|
||||||
|
|
||||||
|
{final_ctx, result_fields_map} =
|
||||||
|
Enum.reduce(descr_fields(), {typing_ctx, %{}}, fn field, {current_ctx, acc_fields} ->
|
||||||
|
bdd1_id = Map.get(descr1, field)
|
||||||
|
bdd2_id = Map.get(descr2, field)
|
||||||
|
{new_ctx, result_bdd_id} = BDD.Ops.intersection_bdds(current_ctx, bdd1_id, bdd2_id)
|
||||||
|
{new_ctx, Map.put(acc_fields, field, result_bdd_id)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
Store.get_or_intern_descr(final_ctx, result_fields_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_negation_type(typing_ctx, descr_id) do
|
||||||
|
descr = Store.get_descr_by_id(typing_ctx, descr_id)
|
||||||
|
|
||||||
|
{final_ctx, result_fields_map} =
|
||||||
|
Enum.reduce(descr_fields(), {typing_ctx, %{}}, fn field, {current_ctx, acc_fields} ->
|
||||||
|
bdd_id = Map.get(descr, field)
|
||||||
|
|
||||||
|
{ctx_after_neg, result_bdd_id} =
|
||||||
|
if field == :absent_marker_bdd_id do
|
||||||
|
{current_ctx, BDD.false_node_id()}
|
||||||
|
else
|
||||||
|
BDD.Ops.negation_bdd(current_ctx, bdd_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
{ctx_after_neg, Map.put(acc_fields, field, result_bdd_id)}
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Re-evaluate context threading if BDD ops significantly alter it beyond caching during reduce.
|
||||||
|
# The primary context update happens with Store.get_or_intern_descr.
|
||||||
|
# The reduce passes current_ctx, which accumulates cache updates from BDD ops.
|
||||||
|
Store.get_or_intern_descr(final_ctx, result_fields_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Caching Wrapper for Type Operations ---
|
||||||
|
defp apply_type_op(typing_ctx, op_key, descr1_id, descr2_id) do
|
||||||
|
cache_key = make_type_op_cache_key(op_key, descr1_id, descr2_id)
|
||||||
|
type_store = Map.get(typing_ctx, :type_store)
|
||||||
|
|
||||||
|
case Map.get(type_store.ops_cache, cache_key) do
|
||||||
|
nil ->
|
||||||
|
# Not in cache, compute it
|
||||||
|
{new_typing_ctx, result_id} =
|
||||||
|
case op_key do
|
||||||
|
:union -> do_union_types(typing_ctx, descr1_id, descr2_id)
|
||||||
|
:intersection -> do_intersection_types(typing_ctx, descr1_id, descr2_id)
|
||||||
|
:negation -> do_negation_type(typing_ctx, descr1_id) # descr2_id is nil here
|
||||||
|
_ -> raise "Unsupported type op_key: #{op_key}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Store in cache (important: use new_typing_ctx to get potentially updated type_store)
|
||||||
|
current_type_store_after_op = Map.get(new_typing_ctx, :type_store)
|
||||||
|
new_ops_cache = Map.put(current_type_store_after_op.ops_cache, cache_key, result_id)
|
||||||
|
final_type_store_with_cache = %{current_type_store_after_op | ops_cache: new_ops_cache}
|
||||||
|
# And put this updated type_store back into new_typing_ctx
|
||||||
|
final_typing_ctx_with_cache = Map.put(new_typing_ctx, :type_store, final_type_store_with_cache)
|
||||||
|
{final_typing_ctx_with_cache, result_id}
|
||||||
|
|
||||||
|
cached_result_id ->
|
||||||
|
{typing_ctx, cached_result_id}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_type_op_cache_key(:negation, descr_id, nil), do: {:negation, descr_id}
|
||||||
|
defp make_type_op_cache_key(op_key, id1, id2) when op_key in [:union, :intersection] do
|
||||||
|
if id1 <= id2, do: {op_key, id1, id2}, else: {op_key, id2, id1}
|
||||||
|
end
|
||||||
|
defp make_type_op_cache_key(op_key, id1, id2), do: {op_key, id1, id2}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Utility Functions ---
|
||||||
|
@doc """
|
||||||
|
Checks if a type represented by its `Descr` ID is the empty type (Nothing).
|
||||||
|
Does not modify `typing_ctx`.
|
||||||
|
"""
|
||||||
|
def is_empty_type?(typing_ctx, descr_id) do
|
||||||
|
descr_map = Store.get_descr_by_id(typing_ctx, descr_id)
|
||||||
|
|
||||||
|
Enum.all?(descr_fields(), fn field ->
|
||||||
|
bdd_id = Map.get(descr_map, field)
|
||||||
|
BDD.is_false_node?(typing_ctx, bdd_id)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Construction Helper Functions ---
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets the `Descr` ID for the canonical 'Nothing' type.
|
||||||
|
"""
|
||||||
|
def get_type_nothing(typing_ctx) do
|
||||||
|
empty_descr_map = Type.empty_descr(typing_ctx)
|
||||||
|
Store.get_or_intern_descr(typing_ctx, empty_descr_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets the `Descr` ID for the canonical 'Any' type.
|
||||||
|
"""
|
||||||
|
def get_type_any(typing_ctx) do
|
||||||
|
any_descr_map = Type.any_descr(typing_ctx)
|
||||||
|
Store.get_or_intern_descr(typing_ctx, any_descr_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a type `Descr` ID representing a single atom literal.
|
||||||
|
"""
|
||||||
|
def create_atom_literal_type(typing_ctx, atom_value) when is_atom(atom_value) do
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
|
||||||
|
# Create a BDD for the single atom: Split(atom_value, True, False, False)
|
||||||
|
# The ops_module Tilly.BDD.AtomBoolOps is crucial here.
|
||||||
|
{ctx1, atom_bdd_id} =
|
||||||
|
BDD.Ops.split(typing_ctx, atom_value, true_id, false_id, false_id, Tilly.BDD.AtomBoolOps)
|
||||||
|
|
||||||
|
descr_map = %{
|
||||||
|
atoms_bdd_id: atom_bdd_id,
|
||||||
|
integers_bdd_id: false_id,
|
||||||
|
strings_bdd_id: false_id,
|
||||||
|
pairs_bdd_id: false_id,
|
||||||
|
records_bdd_id: false_id,
|
||||||
|
functions_bdd_id: false_id,
|
||||||
|
absent_marker_bdd_id: false_id
|
||||||
|
}
|
||||||
|
|
||||||
|
Store.get_or_intern_descr(ctx1, descr_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a type `Descr` ID representing a single integer literal.
|
||||||
|
"""
|
||||||
|
def create_integer_literal_type(typing_ctx, integer_value) when is_integer(integer_value) do
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
|
||||||
|
{ctx1, integer_bdd_id} =
|
||||||
|
BDD.Ops.split(typing_ctx, integer_value, true_id, false_id, false_id, Tilly.BDD.IntegerBoolOps)
|
||||||
|
|
||||||
|
descr_map = %{
|
||||||
|
atoms_bdd_id: false_id,
|
||||||
|
integers_bdd_id: integer_bdd_id,
|
||||||
|
strings_bdd_id: false_id,
|
||||||
|
pairs_bdd_id: false_id,
|
||||||
|
records_bdd_id: false_id,
|
||||||
|
functions_bdd_id: false_id,
|
||||||
|
absent_marker_bdd_id: false_id
|
||||||
|
}
|
||||||
|
Store.get_or_intern_descr(ctx1, descr_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a type `Descr` ID representing a single string literal.
|
||||||
|
"""
|
||||||
|
def create_string_literal_type(typing_ctx, string_value) when is_binary(string_value) do
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
|
||||||
|
{ctx1, string_bdd_id} =
|
||||||
|
BDD.Ops.split(typing_ctx, string_value, true_id, false_id, false_id, Tilly.BDD.StringBoolOps)
|
||||||
|
|
||||||
|
descr_map = %{
|
||||||
|
atoms_bdd_id: false_id,
|
||||||
|
integers_bdd_id: false_id,
|
||||||
|
strings_bdd_id: string_bdd_id,
|
||||||
|
pairs_bdd_id: false_id,
|
||||||
|
records_bdd_id: false_id,
|
||||||
|
functions_bdd_id: false_id,
|
||||||
|
absent_marker_bdd_id: false_id
|
||||||
|
}
|
||||||
|
Store.get_or_intern_descr(ctx1, descr_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets the `Descr` ID for the type representing all atoms.
|
||||||
|
"""
|
||||||
|
def get_primitive_type_any_atom(typing_ctx) do
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
true_id = BDD.true_node_id() # This BDD must be interned with :atom_bool_ops if it's not universal
|
||||||
|
|
||||||
|
# For a BDD representing "all atoms", its structure is simply True,
|
||||||
|
# but it must be associated with :atom_bool_ops.
|
||||||
|
# BDD.true_node_id() is universal. If we need a specific "true for atoms",
|
||||||
|
# we'd intern it: BDD.get_or_intern_node(ctx, Node.mk_true(), :atom_bool_ops)
|
||||||
|
# However, BDD.Ops functions fetch ops_module from operands.
|
||||||
|
# Universal true/false should work correctly.
|
||||||
|
|
||||||
|
descr_map = %{
|
||||||
|
atoms_bdd_id: true_id,
|
||||||
|
integers_bdd_id: false_id,
|
||||||
|
strings_bdd_id: false_id,
|
||||||
|
pairs_bdd_id: false_id,
|
||||||
|
records_bdd_id: false_id,
|
||||||
|
functions_bdd_id: false_id,
|
||||||
|
absent_marker_bdd_id: false_id
|
||||||
|
}
|
||||||
|
Store.get_or_intern_descr(typing_ctx, descr_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets the `Descr` ID for the type representing all integers.
|
||||||
|
"""
|
||||||
|
def get_primitive_type_any_integer(typing_ctx) do
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
|
||||||
|
descr_map = %{
|
||||||
|
atoms_bdd_id: false_id,
|
||||||
|
integers_bdd_id: true_id,
|
||||||
|
strings_bdd_id: false_id,
|
||||||
|
pairs_bdd_id: false_id,
|
||||||
|
records_bdd_id: false_id,
|
||||||
|
functions_bdd_id: false_id,
|
||||||
|
absent_marker_bdd_id: false_id
|
||||||
|
}
|
||||||
|
Store.get_or_intern_descr(typing_ctx, descr_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets the `Descr` ID for the type representing all strings.
|
||||||
|
"""
|
||||||
|
def get_primitive_type_any_string(typing_ctx) do
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
|
||||||
|
descr_map = %{
|
||||||
|
atoms_bdd_id: false_id,
|
||||||
|
integers_bdd_id: false_id,
|
||||||
|
strings_bdd_id: true_id,
|
||||||
|
pairs_bdd_id: false_id,
|
||||||
|
records_bdd_id: false_id,
|
||||||
|
functions_bdd_id: false_id,
|
||||||
|
absent_marker_bdd_id: false_id
|
||||||
|
}
|
||||||
|
Store.get_or_intern_descr(typing_ctx, descr_map)
|
||||||
|
end
|
||||||
|
end
|
||||||
79
lib/tilly/type/store.ex
Normal file
79
lib/tilly/type/store.ex
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
defmodule Tilly.Type.Store do
|
||||||
|
@moduledoc """
|
||||||
|
Manages the interning (hash-consing) of Type Descriptor maps (`Descr` maps).
|
||||||
|
Ensures that for any unique `Descr` map, there is one canonical integer ID.
|
||||||
|
The type store is expected to be part of a `typing_ctx` map under the key `:type_store`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@initial_next_descr_id 0
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Initializes the type store within the typing context.
|
||||||
|
"""
|
||||||
|
def init_type_store(typing_ctx) when is_map(typing_ctx) do
|
||||||
|
type_store = %{
|
||||||
|
descrs_by_structure: %{},
|
||||||
|
structures_by_id: %{},
|
||||||
|
next_descr_id: @initial_next_descr_id,
|
||||||
|
ops_cache: %{} # Cache for type operations {op_key, descr_id1, descr_id2} -> result_descr_id
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.put(typing_ctx, :type_store, type_store)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Gets an existing Type Descriptor ID or interns a new one if it's not already in the store.
|
||||||
|
|
||||||
|
All BDD IDs within the `descr_map` must already be canonical integer IDs.
|
||||||
|
|
||||||
|
Returns a tuple `{new_typing_ctx, descr_id}`.
|
||||||
|
The `typing_ctx` is updated if a new `Descr` is interned.
|
||||||
|
"""
|
||||||
|
def get_or_intern_descr(typing_ctx, descr_map) do
|
||||||
|
type_store = Map.get(typing_ctx, :type_store)
|
||||||
|
|
||||||
|
unless type_store do
|
||||||
|
raise ArgumentError, "Type store not initialized in typing_ctx. Call init_type_store first."
|
||||||
|
end
|
||||||
|
|
||||||
|
# The descr_map itself is the key for interning.
|
||||||
|
# Assumes BDD IDs within descr_map are already canonical.
|
||||||
|
case Map.get(type_store.descrs_by_structure, descr_map) do
|
||||||
|
nil ->
|
||||||
|
# Descr not found, intern it
|
||||||
|
descr_id = type_store.next_descr_id
|
||||||
|
|
||||||
|
new_descrs_by_structure = Map.put(type_store.descrs_by_structure, descr_map, descr_id)
|
||||||
|
new_structures_by_id = Map.put(type_store.structures_by_id, descr_id, descr_map)
|
||||||
|
new_next_descr_id = descr_id + 1
|
||||||
|
|
||||||
|
new_type_store =
|
||||||
|
%{
|
||||||
|
type_store
|
||||||
|
| descrs_by_structure: new_descrs_by_structure,
|
||||||
|
structures_by_id: new_structures_by_id,
|
||||||
|
next_descr_id: new_next_descr_id
|
||||||
|
}
|
||||||
|
|
||||||
|
new_typing_ctx = Map.put(typing_ctx, :type_store, new_type_store)
|
||||||
|
{new_typing_ctx, descr_id}
|
||||||
|
|
||||||
|
existing_descr_id ->
|
||||||
|
# Descr found
|
||||||
|
{typing_ctx, existing_descr_id}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieves the `Descr` map from the type store given its ID.
|
||||||
|
Returns the `Descr` map or `nil` if not found.
|
||||||
|
"""
|
||||||
|
def get_descr_by_id(typing_ctx, descr_id) do
|
||||||
|
with %{type_store: %{structures_by_id: structures_by_id}} <- typing_ctx,
|
||||||
|
descr when not is_nil(descr) <- Map.get(structures_by_id, descr_id) do
|
||||||
|
descr
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
67
log.pl
67
log.pl
@ -1,67 +0,0 @@
|
|||||||
:- module(log,
|
|
||||||
[ log/1,
|
|
||||||
debug/1,
|
|
||||||
log/2,
|
|
||||||
explain_error/2,
|
|
||||||
why_failed/2,
|
|
||||||
set_verbosity/1,
|
|
||||||
if_verbose/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
:- dynamic verbose_level/1.
|
|
||||||
verbose_level(0). % Default verbosity: 0 (errors only), 1 (log), 2 (debug)
|
|
||||||
|
|
||||||
set_verbosity(Level) :-
|
|
||||||
retractall(verbose_level(_)),
|
|
||||||
assertz(verbose_level(Level)).
|
|
||||||
|
|
||||||
if_verbose(Goal) :-
|
|
||||||
verbose_level(Level),
|
|
||||||
Level > 0,
|
|
||||||
call(Goal).
|
|
||||||
|
|
||||||
log(Message) :-
|
|
||||||
verbose_level(Level),
|
|
||||||
Level >= 1,
|
|
||||||
format(user_error, '[LOG] ~w~n', [Message]).
|
|
||||||
|
|
||||||
debug(Message) :-
|
|
||||||
verbose_level(Level),
|
|
||||||
Level >= 2,
|
|
||||||
format(user_error, '[DEBUG] ~w~n', [Message]).
|
|
||||||
|
|
||||||
log(Phase, Term) :-
|
|
||||||
verbose_level(Level),
|
|
||||||
Level >= 1,
|
|
||||||
format(user_error, '[LOG][~w] ~w~n', [Phase, Term]).
|
|
||||||
|
|
||||||
explain_error(ErrorTerm, Explanation) :-
|
|
||||||
% Basic error explanation, to be expanded
|
|
||||||
( ErrorTerm = type_mismatch(Expected, Actual, Location) ->
|
|
||||||
format(string(Explanation), "Type mismatch at ~w: Expected ~w, but got ~w.", [Location, Expected, Actual])
|
|
||||||
; ErrorTerm = unification_failure(Term1, Term2, Context) ->
|
|
||||||
format(string(Explanation), "Unification failed between ~w and ~w in context: ~w.", [Term1, Term2, Context])
|
|
||||||
; ErrorTerm = unbound_variable(Var, Location) ->
|
|
||||||
format(string(Explanation), "Unbound variable ~w at ~w.", [Var, Location])
|
|
||||||
; ErrorTerm = unknown_predicate(Pred, Location) ->
|
|
||||||
format(string(Explanation), "Unknown predicate ~w at ~w.", [Pred, Location])
|
|
||||||
; format(string(Explanation), "An error occurred: ~w", [ErrorTerm])
|
|
||||||
),
|
|
||||||
log(error, Explanation).
|
|
||||||
|
|
||||||
why_failed(ErrorTerm, Details) :-
|
|
||||||
% Provide more context or steps leading to failure
|
|
||||||
( ErrorTerm = type_mismatch(Expected, Actual, _Location) ->
|
|
||||||
% Placeholder for more detailed logic, e.g., tracing type inference steps
|
|
||||||
format(string(Details), "Failure due to ~w not being compatible with ~w.", [Actual, Expected])
|
|
||||||
; format(string(Details), "Details for ~w not yet implemented.", [ErrorTerm])
|
|
||||||
),
|
|
||||||
log(error_details, Details).
|
|
||||||
|
|
||||||
% Example usage (can be removed or moved to tests.pl)
|
|
||||||
% :- set_verbosity(2).
|
|
||||||
% :- log('Logging system initialized.').
|
|
||||||
% :- debug('This is a debug message.').
|
|
||||||
% :- log(parser, 'Parsing phase started.').
|
|
||||||
% :- explain_error(type_mismatch(number, string, 'line 5'), Explanation), writeln(Explanation).
|
|
||||||
% :- why_failed(type_mismatch(number, string, 'line 5'), Details), writeln(Details).
|
|
||||||
31
mix.exs
Normal file
31
mix.exs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
defmodule Til.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :pl,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.15",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||||
|
defp elixirc_paths(_), do: ["lib"]
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||||
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
148
parser.pl
148
parser.pl
@ -1,148 +0,0 @@
|
|||||||
:- module(parser,
|
|
||||||
[ parse/2 % parse(-CodeString, -AST)
|
|
||||||
]).
|
|
||||||
|
|
||||||
:- use_module(library(dcg/basics)).
|
|
||||||
:- use_module(log).
|
|
||||||
|
|
||||||
% --- Entry point ---
|
|
||||||
parse(CodeString, AST) :-
|
|
||||||
string_codes(CodeString, Codes),
|
|
||||||
phrase(s_expression(AST), Codes),
|
|
||||||
log(parser, ast(AST)).
|
|
||||||
|
|
||||||
% --- S-expression parsing ---
|
|
||||||
s_expression(Ast) -->
|
|
||||||
ws, "(", ws, s_expression_items(Items), ws, ")", ws,
|
|
||||||
{ build_ast(Items, Ast) }.
|
|
||||||
s_expression(int(N)) --> integer(N), ws.
|
|
||||||
s_expression(string_val(S)) --> string_literal(S), ws.
|
|
||||||
s_expression(bool(true)) --> "true", ws. % Must be before id(Atom)
|
|
||||||
s_expression(bool(false)) --> "false", ws. % Must be before id(Atom)
|
|
||||||
s_expression(id(Atom)) --> identifier(Atom), ws.
|
|
||||||
s_expression(list_nil) --> ws, "(", ws, ")", ws. % Special case for empty list '()'
|
|
||||||
|
|
||||||
s_expression_items([Item | Rest]) -->
|
|
||||||
s_expression(Item), ws,
|
|
||||||
( s_expression_items(Rest)
|
|
||||||
| {Rest = []} ).
|
|
||||||
s_expression_items([]) --> []. % For items inside '(...)'
|
|
||||||
|
|
||||||
% --- AST Construction from S-expression items ---
|
|
||||||
% build_ast(ItemsList, AST)
|
|
||||||
% ItemsList is a Prolog list of ASTs from inside the parens.
|
|
||||||
% E.g., for (if c t e), ItemsList = [id(if), AST_c, AST_t, AST_e]
|
|
||||||
|
|
||||||
% Keywords
|
|
||||||
build_ast([id(if), Cond, Then, Else], if(Cond, Then, Else)) :- !.
|
|
||||||
build_ast([id(let), id(Var), Value, Body], let(Var, Value, Body)) :- !.
|
|
||||||
|
|
||||||
% Lambda: (lambda (params...) body) -> (lambda (p1 p2) body)
|
|
||||||
% ParamsSExpr is the AST for (p1 p2 ...), e.g. generic_list([id(p1), id(p2)])
|
|
||||||
build_ast([id(lambda), ParamsSExpr, Body], lambda(Params, Body)) :-
|
|
||||||
extract_lambda_params(ParamsSExpr, Params), !.
|
|
||||||
|
|
||||||
% Match: (match Expr ((Pat1 Body1) (Pat2 Body2) ...))
|
|
||||||
% ClausesSExpr is the AST for ((Pat1 Body1) (Pat2 Body2) ...),
|
|
||||||
% e.g. generic_list([generic_list([Pat1AST, Body1AST]), ...])
|
|
||||||
build_ast([id(match), Expr, ClausesSExpr], match(Expr, Clauses)) :-
|
|
||||||
extract_match_clauses(ClausesSExpr, Clauses), !.
|
|
||||||
|
|
||||||
% Data constructors
|
|
||||||
build_ast([id(tuple) | Elements], tuple(Elements)) :- !.
|
|
||||||
build_ast([id(list) | Elements], list_val(Elements)) :- !.
|
|
||||||
|
|
||||||
% Function application (must be last among id-starting rules for simple names)
|
|
||||||
build_ast([id(FunctorName) | Args], Application) :-
|
|
||||||
atom(FunctorName), % Ensure FunctorName is an atom, not a complex term
|
|
||||||
Application =.. [FunctorName | Args], !.
|
|
||||||
|
|
||||||
% Higher-order function application: ((lambda (x) x) 10) or (VarHoldingLambda 10)
|
|
||||||
% Head of ItemsList is a complex AST (e.g., lambda(...), id(Var))
|
|
||||||
build_ast([FunctorSExpr | Args], apply(FunctorSExpr, Args)) :- Args \= [], !. % Ensure there are arguments
|
|
||||||
|
|
||||||
% Generic list structure if not a keyword or application, e.g. for parameters or clause pairs
|
|
||||||
% Also handles (X) where X is a complex term, parsing to generic_list([X])
|
|
||||||
build_ast(Items, generic_list(Items)) :- Items \= [], !.
|
|
||||||
build_ast([], list_nil) :- !. % Should have been caught by s_expression(list_nil) if it's top-level ()
|
|
||||||
|
|
||||||
% --- Helpers for AST construction ---
|
|
||||||
|
|
||||||
% extract_lambda_params(SExpr_representing_param_list, PrologListOfParamNames)
|
|
||||||
% SExpr for (p1 p2 ...): generic_list([id(p1), id(p2), ...])
|
|
||||||
extract_lambda_params(generic_list(IdASTs), ParamNames) :-
|
|
||||||
maplist(get_id_name_from_ast, IdASTs, ParamNames), !.
|
|
||||||
extract_lambda_params(list_nil, []) :- !. % (lambda () body)
|
|
||||||
extract_lambda_params(id(ParamAST_single_param), [ParamName]) :- % (lambda x body)
|
|
||||||
get_id_name_from_ast(id(ParamAST_single_param), ParamName), !.
|
|
||||||
|
|
||||||
get_id_name_from_ast(id(Name), Name).
|
|
||||||
|
|
||||||
% extract_match_clauses(SExpr_representing_list_of_clauses, PrologListOfClauseASTs)
|
|
||||||
% SExpr for ((p1 b1) (p2 b2)): generic_list([ ClauseSExpr1, ClauseSExpr2, ... ])
|
|
||||||
% ClauseSExpr1 is generic_list([Pat1AST, Body1AST])
|
|
||||||
extract_match_clauses(generic_list(ClauseSExprs), ClauseASTs) :-
|
|
||||||
maplist(parse_one_match_clause, ClauseSExprs, ClauseASTs), !.
|
|
||||||
extract_match_clauses(list_nil, []) :- !. % (match expr ()) - no clauses
|
|
||||||
|
|
||||||
% parse_one_match_clause(SExpr_for_one_clause, clause(PatternAST, true, BodyAST))
|
|
||||||
% SExpr for (pat body): generic_list([RawPatternAST, BodyAST])
|
|
||||||
parse_one_match_clause(generic_list([RawPatternAST, BodyAST]), clause(Pattern, true, BodyAST)) :-
|
|
||||||
ast_to_pattern(RawPatternAST, Pattern).
|
|
||||||
|
|
||||||
ast_to_pattern(id(Name), pvar(Name)) :- Name \= '_', !.
|
|
||||||
ast_to_pattern(id('_'), pwild) :- !.
|
|
||||||
ast_to_pattern(int(N), pint(N)) :- !.
|
|
||||||
ast_to_pattern(string_val(S), pstring(S)) :- !.
|
|
||||||
ast_to_pattern(bool(B), pbool(B)) :- !. % Pattern for true/false
|
|
||||||
ast_to_pattern(list_nil, plist([])) :- !. % Pattern for ()
|
|
||||||
ast_to_pattern(tuple(ElementASTs), ptuple(PatternElements)) :- % (tuple p1 p2)
|
|
||||||
maplist(ast_to_pattern, ElementASTs, PatternElements), !.
|
|
||||||
ast_to_pattern(list_val(ElementASTs), plist(PatternElements)) :- % (list p1 p2)
|
|
||||||
maplist(ast_to_pattern, ElementASTs, PatternElements), !.
|
|
||||||
% For more complex list patterns like (cons head tail) or (list-star p1 p2 ... rest)
|
|
||||||
% ast_to_pattern(generic_list([id(cons), HAST, TAST]), pcons(HPatt, TPatt)) :- !,
|
|
||||||
% ast_to_pattern(HAST, HPatt), ast_to_pattern(TAST, TPatt).
|
|
||||||
% This requires 'cons' to be parsed into generic_list([id(cons),...]) in pattern context.
|
|
||||||
|
|
||||||
% --- Low-level parsers (mostly unchanged) ---
|
|
||||||
identifier(Atom) -->
|
|
||||||
[C], { code_type(C, alpha) }, % Simplified: starts with alpha
|
|
||||||
string_without(" ()", Codes), % Simplified: no spaces or parens in identifier
|
|
||||||
{ atom_codes(Atom, [C|Codes]) }.
|
|
||||||
|
|
||||||
string_literal(String) -->
|
|
||||||
"\"", string_without("\"", Codes), "\"",
|
|
||||||
{ atom_codes(String, Codes) }.
|
|
||||||
|
|
||||||
ws --> white, ws.
|
|
||||||
ws --> [].
|
|
||||||
|
|
||||||
|
|
||||||
% --- Example Usage (for testing in REPL) ---
|
|
||||||
% ?- parse("(if (is_number x) x 0)", AST).
|
|
||||||
% AST = if(is_number(id(x)), id(x), int(0))
|
|
||||||
%
|
|
||||||
% ?- parse("(let y 10 y)", AST).
|
|
||||||
% AST = let(y, int(10), id(y))
|
|
||||||
%
|
|
||||||
% ?- parse("(match input ((list x y) x))", AST).
|
|
||||||
% AST = match(id(input), [clause(plist([pvar(x), pvar(y)]), true, id(x))])
|
|
||||||
%
|
|
||||||
% ?- parse("(match data ((tuple a _) a))", AST).
|
|
||||||
% AST = match(id(data), [clause(ptuple([pvar(a), pwild]), true, id(a))])
|
|
||||||
%
|
|
||||||
% ?- parse("(lambda (x y) (add x y))", AST).
|
|
||||||
% AST = lambda([x,y], add(id(x),id(y)))
|
|
||||||
%
|
|
||||||
% ?- parse("((lambda (x) x) 10)", AST).
|
|
||||||
% AST = apply(lambda([x],id(x)),[int(10)])
|
|
||||||
%
|
|
||||||
% ?- parse("(list 1 2 3)", AST).
|
|
||||||
% AST = list_val([int(1), int(2), int(3)])
|
|
||||||
%
|
|
||||||
% ?- parse("(tuple \"a\" true)", AST).
|
|
||||||
% AST = tuple([string_val("a"), bool(true)])
|
|
||||||
%
|
|
||||||
% ?- parse("()", AST).
|
|
||||||
% AST = list_nil
|
|
||||||
428
project.md
Normal file
428
project.md
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
# Typed Lisp to Elixir Compiler (codename Tilly)
|
||||||
|
|
||||||
|
## Project Goals
|
||||||
|
|
||||||
|
To build a Lisp dialect with a strong, expressive type system that compiles to readable Elixir code. The type system will support advanced features like type inference, union, intersection, negation, refinement, and elements of dependent typing. The long-term vision includes support for compiling to other target languages.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Core Language:**
|
||||||
|
- Lisp syntax and semantics.
|
||||||
|
- Basic data structures (lists, atoms, numbers, etc.).
|
||||||
|
- numbers
|
||||||
|
1 2 3 4
|
||||||
|
- strings
|
||||||
|
'' 'string' 'other string'
|
||||||
|
- lists
|
||||||
|
[] [1 2 3 4]
|
||||||
|
- tuples
|
||||||
|
{} {1 2 3 4}
|
||||||
|
- maps
|
||||||
|
m{} m{:a 1 :b 2}
|
||||||
|
- Functions:
|
||||||
|
- only fixed arity functions, no variadic arg lists
|
||||||
|
- structure of the function definition
|
||||||
|
```
|
||||||
|
(defn
|
||||||
|
name
|
||||||
|
(list of parameters with the return type at the end)
|
||||||
|
'optional docstring'
|
||||||
|
...forms for the function body
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- function definition where one parameter doesnt have a type -> its to be inferred from usage inside function_body
|
||||||
|
`(defn name (arg1 (arg2 type_2) return_type) function_body)`
|
||||||
|
- function definition where one parameter doesnt have a type -> its to be inferred, but it is pattern-matched in the function head
|
||||||
|
`(defn name (m{:a a_field} (arg2 type_2) return_type) function_body)`
|
||||||
|
- function definition where one parameter doesnt have a type -> its to be inferred, but it is pattern-matched and bound to a name
|
||||||
|
`(defn name ((= arg1 m{:a a_field :b (= b_list [])}) (arg2 type_2) return_type) function_body)`
|
||||||
|
- function definition where one some parameters are generic | universally quantified
|
||||||
|
`(defn map_head ( (coll (list ~a)) (mapper (function ~a ~b)) (union ~b nil)) function_body)`
|
||||||
|
- function definition guards, simillar to elixir (additional `where` s-expr after the return type)
|
||||||
|
`(defn map_head ( (coll (list ~a)) (mapper (function ~a ~b)) ~b (where (some_guard coll))) function_body)`
|
||||||
|
- Lambdas:
|
||||||
|
- lone lambda form
|
||||||
|
`(fn (elem) (+ elem 1)`
|
||||||
|
- lambda used as a parameter
|
||||||
|
`(Enum.map collection (fn (elem) (+ elem 1))`
|
||||||
|
|
||||||
|
- **Type System:** (See "Type Representation" under "Key Implementation Areas" for detailed structures)
|
||||||
|
- **Type Inference:** Automatically deduce types where possible.
|
||||||
|
- **Union Types:** `A | B` (e.g., `%{type_kind: :union, types: Set.new([type_A, type_B])}`).
|
||||||
|
- **Intersection Types:** `A & B` (e.g., `%{type_kind: :intersection, types: Set.new([type_A, type_B])}`).
|
||||||
|
- **Negation Types:** `!A` (e.g., `%{type_kind: :negation, negated_type: type_A}`).
|
||||||
|
- **Refinement Types:** Types refined by predicates (e.g., `%{type_kind: :refinement, base_type: %{type_kind: :primitive, name: :integer}, var_name: :value, predicate_expr_id: <node_id>}`).
|
||||||
|
- **Dependent Types (Elements of):** Types that can depend on values.
|
||||||
|
- **Length-Indexed Data Structures:**
|
||||||
|
- e.g., A list of 3 integers: `%{type_kind: :list, element_type: %{type_kind: :primitive, name: :integer}, length: 3}`.
|
||||||
|
- A tuple of specific types: `%{type_kind: :tuple, element_types: [type_A, type_B, type_C]}`.
|
||||||
|
- Benefits: Enables safer operations like `nth`, `take`, `drop`, and can ensure arity for functions expecting fixed-length lists/tuples.
|
||||||
|
- **Types Dependent on Literal Values:**
|
||||||
|
- Function return types or argument types can be specialized based on a *literal value* argument by using `%{type_kind: :literal, value: <actual_value>}` in type rules.
|
||||||
|
- **Refinement Types (as a key form of dependent type):**
|
||||||
|
- e.g., `%{type_kind: :refinement, base_type: %{type_kind: :primitive, name: :integer}, var_name: :value, predicate_expr_id: <node_id_of_gt_0_expr>}`.
|
||||||
|
- Initial implementation would focus on simple, evaluable predicates.
|
||||||
|
- **Value-Types (Typed Literals):**
|
||||||
|
- e.g., `:some_atom` has type `%{type_kind: :literal, value: :some_atom}`.
|
||||||
|
- `42` has type `%{type_kind: :literal, value: 42}`.
|
||||||
|
- **Heterogeneous List/Tuple Types:**
|
||||||
|
- Handled by `%{type_kind: :tuple, element_types: [type_A, type_B, ...]}`.
|
||||||
|
- **Structural Map Types:**
|
||||||
|
- **Key-Specific Types:** Defined via the `known_elements` field in the map type representation. Example: `%{type_kind: :map, known_elements: %{:name => %{value_type: %{type_kind: :primitive, name: :string}, optional: false}, :age => %{value_type: %{type_kind: :primitive, name: :integer}, optional: false}}, index_signature: nil}`.
|
||||||
|
- **Optional Keys:** Indicated by `optional: true` for an entry in `known_elements`.
|
||||||
|
- **Key/Value Constraints (Open Maps):** Defined by the `index_signature` field. Example: `%{type_kind: :map, known_elements: %{}, index_signature: %{key_type: %{type_kind: :primitive, name: :atom}, value_type: %{type_kind: :primitive, name: :any}}}`.
|
||||||
|
- **Type Transformation/Refinement:** To be handled by type system rules for map operations.
|
||||||
|
- **Compilation:**
|
||||||
|
- **Target: Elixir:** Generate readable and idiomatic Elixir code.
|
||||||
|
- **Future Targets:** Design with extensibility in mind for other languages (e.g., JavaScript).
|
||||||
|
- **Tooling:**
|
||||||
|
- Clear error messages from the type checker and compiler.
|
||||||
|
|
||||||
|
### Key Implementation Areas
|
||||||
|
|
||||||
|
1. **Parser:**
|
||||||
|
- Implement an S-expression parser for the Lisp dialect.
|
||||||
|
2. **Type System Core:**
|
||||||
|
- **Type Representation:** Types are represented as Elixir maps, each with a `:type_kind` atom (in `:snake_case`) and other fields specific to that kind.
|
||||||
|
- **Primitive Types:**
|
||||||
|
- `%{type_kind: :primitive, name: <atom>}`
|
||||||
|
- Examples: `%{type_kind: :primitive, name: :any}`, `%{type_kind: :primitive, name: :nothing}`, `%{type_kind: :primitive, name: :integer}`, `%{type_kind: :primitive, name: :float}`, `%{type_kind: :primitive, name: :number}`, `%{type_kind: :primitive, name: :boolean}`, `%{type_kind: :primitive, name: :string}`, `%{type_kind: :primitive, name: :atom}`.
|
||||||
|
- **Literal Types (Value Types):**
|
||||||
|
- `%{type_kind: :literal, value: <any_elixir_literal>}`
|
||||||
|
- Examples: `%{type_kind: :literal, value: 42}`, `%{type_kind: :literal, value: :my_atom}`, `%{type_kind: :literal, value: "hello"}`.
|
||||||
|
- strings are started and ended with single quote e.g. 'string'
|
||||||
|
- **Union Types:**
|
||||||
|
- `%{type_kind: :union, types: Set.new([<type_map>])}`
|
||||||
|
- `types`: A set of type maps.
|
||||||
|
- **Intersection Types:**
|
||||||
|
- `%{type_kind: :intersection, types: Set.new([<type_map>])}`
|
||||||
|
- `types`: A set of type maps.
|
||||||
|
- **Negation Types:**
|
||||||
|
- `%{type_kind: :negation, negated_type: <type_map>}`
|
||||||
|
- **Function Types:**
|
||||||
|
- `%{type_kind: :function, arg_types: [<type_map>], return_type: <type_map>, rest_arg_type: <type_map> | nil}`
|
||||||
|
- `arg_types`: Ordered list of type maps.
|
||||||
|
- `rest_arg_type`: Type map for variadic arguments, or `nil`.
|
||||||
|
- **List Types:**
|
||||||
|
- `%{type_kind: :list, element_type: <type_map>, length: <non_neg_integer | type_variable_map | nil>}`
|
||||||
|
- `length`: `nil` for any length, an integer for fixed length, or a type variable map for generic/inferred length.
|
||||||
|
- **Tuple Types:**
|
||||||
|
- `%{type_kind: :tuple, element_types: [<type_map>]}`
|
||||||
|
- `element_types`: Ordered list of type maps.
|
||||||
|
- **Map Types (Structural):**
|
||||||
|
Maps in Tilly Lisp are inherently open, meaning they can contain any keys beyond those explicitly known at compile time. The type system aims to provide as much precision as possible for known keys while defining a general pattern for all other keys.
|
||||||
|
|
||||||
|
- **Representation:**
|
||||||
|
- **Raw Form (before interning):**
|
||||||
|
`%{type_kind: :map, known_elements: KE_raw, index_signature: IS_raw}`
|
||||||
|
- `known_elements` (KE_raw): An Elixir map where keys are literal Elixir terms (e.g., `:name`, `"id"`) and values are `%{value_type: <type_map_for_value>, optional: <boolean>}`.
|
||||||
|
- `index_signature` (IS_raw): Always present. A map `%{key_type: <type_map_for_key>, value_type: <type_map_for_value>}` describing the types for keys not in `known_elements`.
|
||||||
|
- **Interned Form (stored in `nodes_map`):**
|
||||||
|
`%{type_kind: :map, id: <unique_type_key>, known_elements: KE_interned, index_signature: IS_interned}`
|
||||||
|
- `id`: A unique atom key identifying this canonical map type definition (e.g., `:type_map_123`).
|
||||||
|
- `known_elements` (KE_interned): An Elixir map where keys are literal Elixir terms and values are `%{value_type_id: <type_key_for_value>, optional: <boolean>}`.
|
||||||
|
- `index_signature` (IS_interned): Always present. A map `%{key_type_id: <type_key_for_general_keys>, value_type_id: <type_key_for_general_values>}`.
|
||||||
|
|
||||||
|
- **Use Case Scenarios & Typing Approach:**
|
||||||
|
1. **Map Literals:**
|
||||||
|
- Example: `m{:a "hello" :b 1}`
|
||||||
|
- Inferred Type:
|
||||||
|
- `known_elements`: `%{ :a => %{value_type: <type_for_"hello">, optional: false}, :b => %{value_type: <type_for_1>, optional: false} }`
|
||||||
|
- `index_signature`: Defaults to `%{key_type: <any_type>, value_type: <any_type>}`. This signifies that any other keys of any type can exist and map to values of any type.
|
||||||
|
- Keys in map literals must be literal values to contribute to `known_elements`.
|
||||||
|
|
||||||
|
2. **Type Annotations:**
|
||||||
|
- Example: `(the (map string integer) my-var)`
|
||||||
|
- The type `(map string integer)` resolves to:
|
||||||
|
- `known_elements`: `{}` (empty, as the annotation describes a general pattern, not specific known keys).
|
||||||
|
- `index_signature`: `%{key_type: <string_type>, value_type: <integer_type>}`.
|
||||||
|
|
||||||
|
3. **Core Map Operations (Language Constructs):**
|
||||||
|
The type system will define rules for the following fundamental runtime operations:
|
||||||
|
- `(map-get map key)`:
|
||||||
|
- If `key` is a literal (e.g., `:a`):
|
||||||
|
- If `:a` is in `map_type.known_elements`: Result is `map_type.known_elements[:a].value_type_id`. If optional, result is union with `nil_type`.
|
||||||
|
- If `:a` is not in `known_elements` but matches `map_type.index_signature.key_type_id`: Result is `map_type.index_signature.value_type_id` unioned with `nil_type` (as the specific key might not exist at runtime).
|
||||||
|
- If `key`'s type is general (e.g., `atom`):
|
||||||
|
- Collect types from all matching `known_elements` (e.g., if `:a` and `:b` are known atom keys).
|
||||||
|
- Include `map_type.index_signature.value_type_id` if `atom` is a subtype of `map_type.index_signature.key_type_id`.
|
||||||
|
- Union all collected types with `nil_type`.
|
||||||
|
- Example: `(map-get m{:a "s" :b 1} some_atom_var)` could result in type `(union string integer nil)`.
|
||||||
|
|
||||||
|
- `(map-put map key value)`:
|
||||||
|
- If `key` is a literal (e.g., `:a`):
|
||||||
|
- Resulting map type updates or adds `:a` to `known_elements` with `value`'s type. `index_signature` is generally preserved.
|
||||||
|
- Example: `(map-put m{:b 1} :a "s")` results in type for `m{:a "s" :b 1}`.
|
||||||
|
- If `key`'s type is general (e.g., `atom`):
|
||||||
|
- `known_elements` of the input `map` type remain unchanged.
|
||||||
|
- The `index_signature` of the resulting map type may become more general. E.g., if `map` is `(map string any)` and we `map-put` with `key_type=atom` and `value_type=integer`, the new `index_signature` might be `%{key_type: (union string atom), value_type: (union any integer)}`. This is complex and requires careful rule definition.
|
||||||
|
|
||||||
|
- `(map-delete map key)`:
|
||||||
|
- If `key` is a literal (e.g., `:a`):
|
||||||
|
- Resulting map type removes `:a` from `known_elements`. `index_signature` is preserved.
|
||||||
|
- Example: `(map-delete m{:a "s" :b 1} :a)` results in type for `m{:b 1}`.
|
||||||
|
- If `key`'s type is general (e.g., `atom`):
|
||||||
|
- This is complex. Deleting by a general key type doesn't easily translate to a precise change in `known_elements`. The `index_signature` might remain, or the operation might be disallowed or result in a very general map type. (Further thought needed for precise semantics).
|
||||||
|
|
||||||
|
- `(map-merge map1 map2)`:
|
||||||
|
- Resulting map type combines `known_elements` from `map1` and `map2`.
|
||||||
|
- For keys only in `map1` or `map2`, they are included as is.
|
||||||
|
- For keys in both: The value type from `map2` takes precedence (last-one-wins semantics for types).
|
||||||
|
- The `index_signature` of the resulting map type will be the most general combination of `map1.index_signature` and `map2.index_signature`. (e.g., union of key types, union of value types).
|
||||||
|
|
||||||
|
- **Known Limitations:**
|
||||||
|
- Typing `map-put` and `map-delete` with non-literal (general type) keys precisely is challenging and may result in less specific types or require advanced type system features not yet planned (e.g., negation types for keys).
|
||||||
|
- Duplicate literal keys in a map literal: The parser/typer will likely adopt a "last one wins" semantic for the value and its type.
|
||||||
|
|
||||||
|
- **Required Future Steps & Prerequisites:**
|
||||||
|
1. **Parser Support for `(map K V)` Annotations:**
|
||||||
|
- Modify `Til.Typer.ExpressionTyper.resolve_type_specifier_node` to parse S-expressions like `(map <key-type-spec> <value-type-spec>)` into a raw map type definition.
|
||||||
|
- Prerequisite: Basic type specifier resolution (for K and V).
|
||||||
|
2. **Typing Map Literals:**
|
||||||
|
- In `Til.Typer.infer_type_for_node_ast` (for `:map_expression`):
|
||||||
|
- Construct `known_elements` from literal keys and inferred value types.
|
||||||
|
- Assign a default `index_signature` (e.g., `key_type: any, value_type: any`).
|
||||||
|
- Prerequisite: Recursive typing of child nodes (values in the map literal).
|
||||||
|
3. **Interning Map Types:**
|
||||||
|
- In `Til.Typer.Interner.get_or_intern_type`:
|
||||||
|
- Add logic for `type_kind: :map`.
|
||||||
|
- Recursively intern types within `known_elements` values and `index_signature` key/value types to create a canonical, interned map type definition.
|
||||||
|
- Store/retrieve these canonical map types.
|
||||||
|
- Prerequisite: Interning for primitive and other relevant types.
|
||||||
|
4. **Subtyping Rules for Maps:**
|
||||||
|
- In `Til.Typer.SubtypeChecker.is_subtype?`:
|
||||||
|
- Implement rules for `map_subtype <?: map_supertype`. This involves checking:
|
||||||
|
- Compatibility of `known_elements` (required keys in super must be present and non-optional in sub, with compatible value types).
|
||||||
|
- Compatibility of `index_signatures` (contravariant key types, covariant value types).
|
||||||
|
- Keys in `sub.known_elements` not in `super.known_elements` must conform to `super.index_signature`.
|
||||||
|
- Prerequisite: Interned map type representation.
|
||||||
|
5. **Typing Map Operations:**
|
||||||
|
- In `Til.Typer.ExpressionTyper` (or a new `MapExpressionTyper` module):
|
||||||
|
- Define S-expression forms for `(map-get map key)`, `(map-put map key value)`, `(map-delete map key)`, `(map-merge map1 map2)`.
|
||||||
|
- Implement type inference rules for each of these operations based on the principles outlined in "Use Case Scenarios".
|
||||||
|
- Prerequisite: Subtyping rules, interned map types, ability to type map literals and resolve map type annotations.
|
||||||
|
- **Advanced Map Typing and Row Polymorphism Considerations:**
|
||||||
|
|
||||||
|
While the current map type system provides flexibility with `known_elements` and an `index_signature`, a future enhancement could be the introduction of **row polymorphism**. This would allow for more precise typing of functions that operate on maps with a common set of known fields while allowing other fields to vary.
|
||||||
|
|
||||||
|
- **Conceptualization:** In Tilly's context, a row variable in a map type (e.g., `m{ :name string | r }`) could represent "the rest of the map fields." Given Tilly's rich map keys (literals of various types, not just simple labels) and the existing `index_signature` concept, the row variable `r` could itself be considered a placeholder for another Tilly map type. This means `r` could have its own `known_elements` and `index_signature`, making it more expressive than traditional record-based row polymorphism.
|
||||||
|
|
||||||
|
- **Requirements for Implementation:**
|
||||||
|
- **Type Representation:** Extend the map type definition to include a row variable (e.g., `%{type_kind: :map, known_elements: KE, row_variable_id: <type_key_for_row_var>}`). The interaction between a row variable and the `index_signature` would need careful definition; they might be mutually exclusive or complementary.
|
||||||
|
- **Unification for Rows:** Develop a unification algorithm capable of solving constraints involving row variables (e.g., `m{a: T1 | r1} = m{a: T1, b: T2 | r2}` implies `r1` must unify with or extend `m{b: T2 | r2}`).
|
||||||
|
- **Subtyping for Rows:** Define subtyping rules (e.g., `m{a: T1, b: T2}` is a subtype of `m{a: T1 | r}` if `r` can be instantiated with `m{b: T2}`).
|
||||||
|
- **Generalization & Instantiation:** Implement mechanisms for generalizing functions over row variables and instantiating them at call sites.
|
||||||
|
- **Syntax:** Design user-facing syntax for map types with row variables (e.g., `(map :key1 type1 ... | row_var_name)`).
|
||||||
|
|
||||||
|
Implementing full row polymorphism is a significant undertaking and would build upon the existing map typing foundations. It is currently not in the immediate plan but represents a valuable direction for enhancing the type system's expressiveness for structural data.
|
||||||
|
- **Refinement Types:**
|
||||||
|
- `%{type_kind: :refinement, base_type: <type_map>, var_name: <atom>, predicate_expr_id: <integer_node_id>}`
|
||||||
|
- `var_name`: Atom used to refer to the value within the predicate.
|
||||||
|
- `predicate_expr_id`: AST node ID of the predicate expression.
|
||||||
|
- **Type Variables:**
|
||||||
|
- `%{type_kind: :type_variable, id: <any_unique_id>, name: <String.t | nil>}`
|
||||||
|
- `id`: Unique identifier.
|
||||||
|
- `name`: Optional human-readable name.
|
||||||
|
- **Alias Types (Named Types):**
|
||||||
|
- `%{type_kind: :alias, name: <atom_alias_name>, parameters: [<atom_param_name>], definition: <type_map>}`
|
||||||
|
- `name`: The atom for the alias (e.g., `:positive_integer`).
|
||||||
|
- `parameters`: List of atoms for generic type parameter names (e.g., `[:T]`).
|
||||||
|
- `definition`: The type map this alias expands to (may contain type variables from `parameters`).
|
||||||
|
- Function types
|
||||||
|
- (fn (arg_1_type arg_2_type) return_type)
|
||||||
|
- **Type Checking Algorithm:** Develop the core logic for verifying type correctness. This will likely involve algorithms for:
|
||||||
|
- Unification.
|
||||||
|
- Subtyping (e.g., `%{type_kind: :primitive, name: :integer}` is a subtype of `%{type_kind: :primitive, name: :number}`).
|
||||||
|
- Constraint solving for inference and refinement types.
|
||||||
|
- **Type Inference Engine:** Implement the mechanism to infer types of expressions and definitions.
|
||||||
|
- **Bidirectional Type Inference:**
|
||||||
|
To enhance type inference capabilities, reduce the need for explicit annotations, and provide more precise error messages, the type system could be evolved to use bidirectional type inference. This approach distinguishes between two modes of operation:
|
||||||
|
- **Synthesis Mode (`=>`):** Infers or synthesizes the type of an expression from its constituent parts. For example, `infer(e)` yields type `T`.
|
||||||
|
- **Checking Mode (`<=`):** Checks if an expression conforms to an *expected type* provided by its context. For example, `check(e, T_expected)` verifies `e` has type `T_expected`.
|
||||||
|
|
||||||
|
- **Benefits:**
|
||||||
|
- More precise type error reporting (e.g., "expected type X, got type Y in context Z").
|
||||||
|
- Reduced annotation burden, as types can flow top-down into expressions.
|
||||||
|
- Better handling of polymorphic functions and complex type constructs.
|
||||||
|
|
||||||
|
- **Implementation Requirements:**
|
||||||
|
- **Explicit Modes:** The core typing algorithm (currently in `Til.Typer`) would need to be refactored to explicitly support and switch between synthesis and checking modes.
|
||||||
|
- **Top-Down Type Flow:** The `expected_type` must be propagated downwards during AST traversal in checking mode.
|
||||||
|
- **Dual Typing Rules:** Each language construct (literals, variables, function calls, conditionals, `lambda`s, etc.) would require distinct typing rules for both synthesis and checking. For instance:
|
||||||
|
- A `lambda` expression, when checked against an expected function type `(TA -> TR)`, can use `TA` for its parameter types and then check its body against `TR`. In synthesis mode, parameter annotations might be required.
|
||||||
|
- A function application `(f arg)` would typically synthesize `f`'s type, then check `arg` against the expected parameter type, and the function's return type becomes the synthesized type of the application.
|
||||||
|
- **Integration with Polymorphism:** Rules for instantiating polymorphic types (when checking) and generalizing types (e.g., for `let`-bound expressions in synthesis) are crucial.
|
||||||
|
|
||||||
|
Adopting bidirectional type inference would be a significant architectural evolution of the `Til.Typer` module, moving beyond the current primarily bottom-up synthesis approach.
|
||||||
|
- **Environment Management:** Handle scopes and bindings of names to types (type maps).
|
||||||
|
|
||||||
|
- **Function Types, `defn`, and `fn` Implementation Plan:**
|
||||||
|
This section outlines the plan for introducing function types, user-defined functions (`defn`), and lambdas (`fn`) into Tilly.
|
||||||
|
|
||||||
|
**1. Type Representation for Functions:**
|
||||||
|
* **Structure:** `%{type_kind: :function, arg_types: [<type_map_key>], return_type: <type_map_key>, type_params: [<type_variable_key>] | nil}`
|
||||||
|
* `arg_types`: An ordered list of type keys for each argument.
|
||||||
|
* `return_type`: A type key for the return value.
|
||||||
|
* `type_params`: (Optional, for polymorphic functions) An ordered list of type keys for universally quantified type variables (e.g., for `~a`, `~b`). Initially `nil` for monomorphic functions.
|
||||||
|
* **Note:** Variadic functions (`rest_arg_type`) are not planned, aligning with Elixir's fixed arity.
|
||||||
|
|
||||||
|
**2. Parser Modifications (`Til.Parser`):**
|
||||||
|
* **`defn` (User-Defined Function):**
|
||||||
|
* **Syntax:** `(defn name (arg_spec1 arg_spec2 ... return_type_spec) 'optional_docstring' body_forms...)`
|
||||||
|
* The `return_type_spec` is the last element in the parameter S-expression.
|
||||||
|
* `'optional_docstring'` is a string literal between the parameter S-expression and the body.
|
||||||
|
* **AST Node (`:defn_expression`):**
|
||||||
|
* `name_node_id`: ID of the function name symbol.
|
||||||
|
* `params_and_return_s_expr_id`: ID of the S-expression node `(arg_spec1 ... return_type_spec)`.
|
||||||
|
* `arg_spec_node_ids`: List of IDs of argument specifier nodes (derived from children of `params_and_return_s_expr_id`, excluding the last).
|
||||||
|
* `return_type_spec_node_id`: ID of the return type specifier node (last child of `params_and_return_s_expr_id`).
|
||||||
|
* `docstring_node_id`: Optional ID of the docstring node.
|
||||||
|
* `body_node_ids`: List of IDs for body expressions.
|
||||||
|
* **`fn` (Lambda):**
|
||||||
|
* **Syntax:** `(fn (arg_spec1 ...) body_forms...)`
|
||||||
|
* **AST Node (`:lambda_expression`):**
|
||||||
|
* `params_s_expr_id`: ID of the S-expression node `(arg_spec1 ...)`.
|
||||||
|
* `arg_spec_node_ids`: List of IDs of argument specifier nodes.
|
||||||
|
* `body_node_ids`: List of IDs for body expressions.
|
||||||
|
* **Argument Specifications (`arg_spec`):**
|
||||||
|
* Initially, `arg_spec` nodes will represent simple symbols (for lambda arguments) or `(symbol type_spec)` (for `defn` arguments).
|
||||||
|
* More complex patterns (`m{:key val}`, `(= symbol pattern)`) and type variables (`~a`) will be introduced in later phases.
|
||||||
|
|
||||||
|
**3. Phased Implementation Plan:**
|
||||||
|
* **Phase 1: Core Function Type Representation & Interning. (Completed)**
|
||||||
|
* Defined the `%{type_kind: :function, ...}` structure.
|
||||||
|
* Implemented interning logic for this type in `Til.Typer.Interner`.
|
||||||
|
* **Phase 2: Basic Lambdas (`fn`). (In Progress)**
|
||||||
|
* Parser: Implement parsing for `(fn (arg_name1 ...) body_forms...)`. Argument specs are simple symbols.
|
||||||
|
* Typer (`infer_type_for_node_ast`): For `:lambda_expression`, argument types default to `any`. Infer return type from the last body expression. Construct raw function type for interning.
|
||||||
|
* **Lambda Argument Typing Strategy:** Defaulting to `any` initially. Later, with bidirectional type inference, argument types will be inferred more precisely from usage within the body (potentially using type intersection for multiple constraints) or from the context in which the lambda is used (checking mode).
|
||||||
|
* **Phase 3: Basic Monomorphic Function Calls.**
|
||||||
|
* Typer (`ExpressionTyper.infer_s_expression_type`): Handle S-expressions where the operator's type is a function type. Perform arity checks and subtype checks for arguments. The S-expression's type is the function's return type.
|
||||||
|
* **Phase 4: Monomorphic `defn`.**
|
||||||
|
* Parser: Implement parsing for `(defn name (arg_spec1 type_spec1 ... return_type_spec) 'optional_docstring' body_forms...)`. Require explicit `(symbol type_spec)` for arguments.
|
||||||
|
* Typer (`infer_type_for_node_ast` and `Environment`): For `:defn_expression`, resolve explicit types, construct/intern the function type, update environment for recursion, type body in new lexical scope, and validate return type. The `:defn_expression` node's type is its interned function type. `Til.Typer.Environment.update_env_from_node` will add the function to the environment.
|
||||||
|
* **Phase 5: Introduce Polymorphism (Type Variables, `~a`).**
|
||||||
|
* Update type representations, parser, interner, and typer for type variables and polymorphic function types. Implement unification for function calls.
|
||||||
|
* **Phase 6: Advanced Argument/Inference Features.**
|
||||||
|
* Allow `defn` arguments as `symbol` (type to be inferred).
|
||||||
|
* More sophisticated type inference for lambda arguments.
|
||||||
|
* (Later) Pattern matching in arguments, typing for `where` guards.
|
||||||
|
|
||||||
|
3. **Compiler Backend (Elixir):**
|
||||||
|
- **AST Transformation:** Transform the Lisp AST (potentially type-annotated) into an Elixir-compatible AST or directly to Elixir code.
|
||||||
|
- **Mapping Lisp Constructs:** Define how Lisp functions, data structures, control flow, and type information translate to Elixir equivalents.
|
||||||
|
- **Code Generation:** Produce Elixir source files.
|
||||||
|
- **Interop:** Consider how the Lisp code will call Elixir code and vice-versa.
|
||||||
|
4. **Standard Library:**
|
||||||
|
- Define and implement a basic set of core functions and their types (e.g., list operations, arithmetic, type predicates).
|
||||||
|
5. **Error Reporting Infrastructure:**
|
||||||
|
- Design a system for collecting and presenting type errors, compiler errors, and runtime errors (if applicable during compilation phases).
|
||||||
|
6. **Testing Framework:**
|
||||||
|
- Develop a comprehensive suite of tests covering:
|
||||||
|
- Parser correctness.
|
||||||
|
- Type checker correctness (valid and invalid programs).
|
||||||
|
- Compiler output (comparing generated Elixir against expected output or behavior).
|
||||||
|
7. **CLI / Build Tool Integration (Future):**
|
||||||
|
- A command-line interface for the compiler.
|
||||||
|
- Potential integration with build tools like Mix.
|
||||||
|
|
||||||
|
## Main Data Structure: Node Maps
|
||||||
|
|
||||||
|
The core data structure for representing code throughout the parsing, type checking, and transpiling phases will be a collection of "Node Maps." Each syntactic element or significant semantic component of the source code will be represented as an Elixir map.
|
||||||
|
|
||||||
|
**Structure of a Node Map:**
|
||||||
|
|
||||||
|
Each node map will contain a set of common fields and a set of fields specific to the kind of AST element it represents.
|
||||||
|
|
||||||
|
* **Common Fields (present in all node maps, based on `lib/til/parser.ex`):**
|
||||||
|
* `id`: A unique integer (generated by `System.unique_integer([:monotonic, :positive])`) for this node.
|
||||||
|
* `type_id`: Initially `nil`. After type checking/inference, this field will store or reference the type map (as defined in "Type Representation") associated with this AST node.
|
||||||
|
* `parent_id`: The `id` of the parent node in the AST, or `nil` if it's a root node or an orphaned element (e.g. an element of an unclosed collection).
|
||||||
|
* `file`: A string indicating the source file name (defaults to "unknown").
|
||||||
|
* `location`: A list: `[start_offset, start_line, start_col, end_offset, end_line, end_col]`.
|
||||||
|
* `raw_string`: The literal string segment from the source code that corresponds to this node.
|
||||||
|
* `ast_node_type`: An atom identifying the kind of AST node.
|
||||||
|
* `parsing_error`: `nil` if parsing was successful for this node, or a string message if an error occurred specific to this node (e.g., "Unclosed string literal"). For collection nodes, this can indicate issues like being unclosed.
|
||||||
|
|
||||||
|
* **AST-Specific Fields & Node Types (current implementation in `lib/til/parser.ex`):**
|
||||||
|
* `ast_node_type: :literal_integer`
|
||||||
|
* `value`: The integer value (e.g., `42`).
|
||||||
|
* `ast_node_type: :symbol`
|
||||||
|
* `name`: The string representation of the symbol (e.g., `"my-symbol"`).
|
||||||
|
* `ast_node_type: :literal_string`
|
||||||
|
* `value`: The processed string content (escape sequences are not yet handled, but leading whitespace on subsequent lines is stripped based on the opening quote's column).
|
||||||
|
* `parsing_error`: Can be `"Unclosed string literal"`.
|
||||||
|
* `ast_node_type: :s_expression`
|
||||||
|
* `children`: A list of `id`s of the child nodes within the S-expression.
|
||||||
|
* `parsing_error`: Can be `"Unclosed S-expression"`.
|
||||||
|
* `ast_node_type: :list_expression` (parsed from `[...]`)
|
||||||
|
* `children`: A list of `id`s of the child nodes within the list.
|
||||||
|
* `parsing_error`: Can be `"Unclosed list"`.
|
||||||
|
* `ast_node_type: :map_expression` (parsed from `m{...}`)
|
||||||
|
* `children`: A list of `id`s of the child nodes (key-value pairs) within the map.
|
||||||
|
* `parsing_error`: Can be `"Unclosed map"`.
|
||||||
|
* `ast_node_type: :tuple_expression` (parsed from `{...}`)
|
||||||
|
* `children`: A list of `id`s of the child nodes within the tuple.
|
||||||
|
* `parsing_error`: Can be `"Unclosed tuple"`.
|
||||||
|
* `ast_node_type: :unknown` (used for tokens that couldn't be parsed into a more specific type, or for unexpected characters)
|
||||||
|
* `parsing_error`: A string describing the error (e.g., "Unexpected ')'", "Unknown token").
|
||||||
|
* `ast_node_type: :file`
|
||||||
|
* `children`: A list of `id`s of the top-level expression nodes in the file, in order of appearance.
|
||||||
|
* `raw_string`: The entire content of the parsed file.
|
||||||
|
* `parsing_error`: Typically `nil` for the file node itself, errors would be on child nodes or during parsing of specific structures.
|
||||||
|
|
||||||
|
* **Note on `children` field:** For collection types (`:s_expression`, `:list_expression`, `:map_expression`, `:tuple_expression`, `:file`), this field holds a list of child node `id`s in the order they appear in the source.
|
||||||
|
|
||||||
|
* **Pseudo-code example of a parsed integer node:**
|
||||||
|
```elixir
|
||||||
|
%{
|
||||||
|
id: 1,
|
||||||
|
type_id: nil,
|
||||||
|
parent_id: nil, # Assuming it's a top-level expression
|
||||||
|
file: "input.til",
|
||||||
|
location: [0, 1, 1, 2, 1, 3], # [offset_start, line_start, col_start, offset_end, line_end, col_end]
|
||||||
|
raw_string: "42",
|
||||||
|
ast_node_type: :literal_integer,
|
||||||
|
value: 42,
|
||||||
|
parsing_error: nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* **Pseudo-code example of a parsed S-expression node:**
|
||||||
|
```elixir
|
||||||
|
%{
|
||||||
|
id: 2,
|
||||||
|
type_id: nil,
|
||||||
|
parent_id: nil,
|
||||||
|
file: "input.til",
|
||||||
|
location: [4, 1, 5, 15, 1, 16], # Location spans the entire "(add 1 2)"
|
||||||
|
raw_string: "(add 1 2)",
|
||||||
|
ast_node_type: :s_expression,
|
||||||
|
children: [3, 4, 5], # IDs of :symbol "add", :literal_integer 1, :literal_integer 2
|
||||||
|
parsing_error: nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* **Pseudo-code example of an unclosed string node:**
|
||||||
|
```elixir
|
||||||
|
%{
|
||||||
|
id: 6,
|
||||||
|
type_id: nil,
|
||||||
|
parent_id: nil,
|
||||||
|
file: "input.til",
|
||||||
|
location: [17, 2, 1, 25, 2, 9], # Spans from opening ' to end of consumed input for the error
|
||||||
|
raw_string: "'unclosed",
|
||||||
|
ast_node_type: :literal_string,
|
||||||
|
value: "unclosed", # The content parsed so far
|
||||||
|
parsing_error: "Unclosed string literal"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
**Intended Use:**
|
||||||
|
|
||||||
|
This collection of interconnected node maps forms a graph (specifically, a tree for the basic AST structure, with additional edges for type references, variable bindings, etc.).
|
||||||
|
|
||||||
|
1. **Parsing:** The parser will transform the source code into this collection of node maps.
|
||||||
|
2. **Type Checking/Inference:** The type system will operate on these node maps. Type information (`type_id`) will be populated or updated. Constraints for type inference can be associated with node `id`s. The immutability of Elixir maps means that updating a node's type information creates a new version of that node map, facilitating the tracking of changes during constraint resolution.
|
||||||
|
3. **Transpiling:** The transpiler will traverse this graph of node maps (potentially enriched with type information) to generate the target Elixir code.
|
||||||
|
|
||||||
|
A central registry or context (e.g., a map of `id => node_map_data`) might be used to store and access all node maps, allowing for efficient lookup and modification (creation of new versions) of individual nodes during various compiler phases.
|
||||||
9
session
Normal file
9
session
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/drop
|
||||||
|
/add lib/til/typer.ex
|
||||||
|
/add lib/til/typer/environment.ex
|
||||||
|
/add lib/til/typer/expression_typer.ex
|
||||||
|
/add lib/til/typer/interner.ex
|
||||||
|
/add lib/til/typer/subtype_checker.ex
|
||||||
|
/add lib/til/typer/types.ex
|
||||||
|
/add project.md
|
||||||
|
/read-only Conventions.md
|
||||||
99
set_types
Normal file
99
set_types
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
|
||||||
|
|
||||||
|
The core idea is to replace the current map-based type representations with a system centered around **canonicalized type descriptors (`Tilly.Type.Descr`)**, where each descriptor internally uses **Binary Decision Diagrams (BDDs)** to represent sets of values for different kinds of types (integers, atoms, pairs, etc.).
|
||||||
|
|
||||||
|
Here's a plan for adaptation:
|
||||||
|
|
||||||
|
** overarching concerns:
|
||||||
|
|
||||||
|
- lets use bare maps instead of structs. Structs are closed, and I want to keep the option of adding new fields later by the user of the compiler
|
||||||
|
|
||||||
|
**I. Foundational BDD and Type Descriptor Infrastructure (New Modules)**
|
||||||
|
|
||||||
|
This phase focuses on building the core components described in `set_types.md`. These will mostly be new modules.
|
||||||
|
|
||||||
|
1. **`Tilly.BDD.Node` (New):**
|
||||||
|
* Define the Elixir tagged tuples for BDD nodes: `false`, `true`, `{:leaf, leaf_value_id}`, `{:split, element_id, p_child_id, i_child_id, n_child_id}`.
|
||||||
|
* include smart constructors
|
||||||
|
2. **`Tilly.BDD.Store` (New):**
|
||||||
|
* Implement the hash-consing mechanism for BDD nodes.
|
||||||
|
* This store will be part of the compiler `ctx` map.
|
||||||
|
* Key function: `get_or_intern_node(typing_ctx, logical_structure_tuple) :: {new_typing_ctx, node_id}`.
|
||||||
|
3. **`Tilly.BDD.ElementProtocol` and `Tilly.BDD.LeafProtocol` (New):**
|
||||||
|
* Define these protocols. Implementations will be provided for basic elements (e.g., atoms, integers for splitting) and basic leaves (e.g., booleans for simple set membership).
|
||||||
|
4. **`Tilly.BDD.Ops` (New):**
|
||||||
|
* Implement core BDD operations: `union_bdds`, `intersection_bdds`, `negation_bdd`, `difference_bdd`.
|
||||||
|
* These functions will take BDD node IDs and the `typing_ctx`, returning a new BDD node ID.
|
||||||
|
* Implement smart constructors for `Split` and `Leaf` nodes that use the `Tilly.BDD.Store` and apply simplification rules.
|
||||||
|
5. **`Tilly.Type.Descr` (New):**
|
||||||
|
* Define the Elixir struct. It will hold IDs of canonical BDDs for each basic kind (e.g., `:atoms_bdd_id`, `:integers_bdd_id`, `:pairs_bdd_id`, etc., mirroring CDuce's `descr` fields).
|
||||||
|
6. **`Tilly.Type.Store` (New or Replaces `Til.Typer.Interner` logic for types):**
|
||||||
|
* Manages the interning of `Tilly.Type.Descr` structs. A `Descr` is canonical if its constituent BDD IDs are canonical and the combination is unique.
|
||||||
|
* This store will also be part of the `typing_ctx`.
|
||||||
|
7. **`Tilly.Type.Ops` (New):**
|
||||||
|
* Implement `union_types(descr1_id, descr2_id, typing_ctx)`, `intersection_types`, `negation_type`. These operate on `Descr` IDs by performing field-wise BDD operations using `Tilly.BDD.Ops`, then interning the resulting `Descr`.
|
||||||
|
|
||||||
|
**II. Adapting Existing Typer Modules**
|
||||||
|
|
||||||
|
This phase involves refactoring existing modules to use the new infrastructure. The `typing_ctx` will need to be threaded through all relevant type-checking functions.
|
||||||
|
|
||||||
|
1. **`Til.Typer.Types` (Major Refactor):**
|
||||||
|
* Functions like `get_primitive_type(:integer)` will now use the new infrastructure to construct/retrieve an interned `Tilly.Type.Descr` ID for the integer type (e.g., a `Descr` where `integers_bdd_id` points to a "true" BDD for integers, and others are "false").
|
||||||
|
* The concept of predefined type *maps* will be replaced by predefined canonical `Descr` IDs.
|
||||||
|
2. **`Til.Typer.Interner` (Major Refactor/Integration with `Tilly.Type.Store` and `Tilly.BDD.Store`):**
|
||||||
|
* Its role in interning type *maps* will be replaced. The new stores will handle BDD node and `Descr` interning.
|
||||||
|
* `populate_known_types` will pre-populate the `typing_ctx` with canonical `Descr` IDs for base types like `any`, `integer`, `atom`, `nothing`.
|
||||||
|
3. **`Til.Typer` and `Til.Typer.ExpressionTyper` (Significant Refactor):**
|
||||||
|
* `infer_type_for_node_ast` (in `ExpressionTyper` or similar):
|
||||||
|
* For literals (e.g., `123`, `:foo`), it will construct the corresponding `Descr` ID (e.g., an integer `Descr` with a BDD representing only `123`).
|
||||||
|
* For operations that combine types (e.g., if an `if` expression's branches have types A and B, the result is `A | B`), it will use `Tilly.Type.Ops.union_types`.
|
||||||
|
* `resolve_type_specifier_node`:
|
||||||
|
* For basic type names (`Integer`, `Atom`), it will fetch their canonical `Descr` IDs.
|
||||||
|
* For type expressions like `(or TypeA TypeB)`, it will recursively resolve `TypeA` and `TypeB` to `Descr` IDs, then use `Tilly.Type.Ops.union_types`. Similarly for `(and ...)` and `(not ...)`.
|
||||||
|
* The `type_id` field in AST nodes will now store the ID of an interned `Tilly.Type.Descr`.
|
||||||
|
4. **`Til.Typer.SubtypeChecker` (Complete Rewrite):**
|
||||||
|
* `is_subtype?(descr_a_id, descr_b_id, typing_ctx)` will be implemented as:
|
||||||
|
1. `descr_not_b_id = Tilly.Type.Ops.negation_type(descr_b_id, typing_ctx)`.
|
||||||
|
2. `intersection_id = Tilly.Type.Ops.intersection_types(descr_a_id, descr_not_b_id, typing_ctx)`.
|
||||||
|
3. `is_empty_type(intersection_id, typing_ctx)`.
|
||||||
|
* `is_empty_type(descr_id, typing_ctx)` checks if all BDDs within the `Descr` are the canonical `False` BDD.
|
||||||
|
|
||||||
|
**III. Handling Specific Type Kinds from `project.md`**
|
||||||
|
|
||||||
|
The existing type kinds in `project.md` need to be mapped to the new `Descr` model:
|
||||||
|
|
||||||
|
* **Primitive Types:** `%{type_kind: :primitive, name: :integer}` becomes a canonical `Descr` ID where `integers_bdd_id` is "all integers" and other BDDs are empty.
|
||||||
|
* **Literal Types:** `%{type_kind: :literal, value: 42}` becomes a `Descr` ID where `integers_bdd_id` represents only `42`.
|
||||||
|
* **Union/Intersection/Negation Types:** These are no longer explicit `type_kind`s. They are results of operations in `Tilly.Type.Ops` combining other `Descr` objects.
|
||||||
|
* **Function, List, Tuple, Map Types:**
|
||||||
|
* These will be represented by `Descr` objects where the corresponding BDD (e.g., `functions_bdd_id`, `pairs_bdd_id` for lists/tuples, `records_bdd_id` for maps) is non-empty.
|
||||||
|
* The structure of these BDDs will be more complex, as outlined in CDuce (e.g., BDDs whose leaves are other BDDs, or BDDs splitting on type variables/labels). This is a more advanced step.
|
||||||
|
* Initially, `list_type(element_descr_id)` might create a `Descr` where `pairs_bdd_id` points to a BDD representing pairs whose first element matches `element_descr_id` and second is recursively a list or nil.
|
||||||
|
* The detailed map type representation from `project.md` (`known_elements`, `index_signature`) will need to be encoded into the structure of the `records_bdd_id`. This will be a complex part, drawing heavily from how CDuce handles record types with BDDs.
|
||||||
|
|
||||||
|
**IV. Parser (`lib/til/parser.ex`)**
|
||||||
|
|
||||||
|
* No immediate changes are required for the AST structure itself.
|
||||||
|
* If new syntax for type operations (e.g., `(or Int Atom)`) is desired directly in source code (as opposed to only being used by `resolve_type_specifier_node`), the parser will need to be updated to produce AST nodes that `resolve_type_specifier_node` can interpret.
|
||||||
|
|
||||||
|
**V. Documentation (`project.md`)**
|
||||||
|
|
||||||
|
* The "Type Representation" sections will need a complete overhaul to describe the new `Tilly.Type.Descr` and BDD-based system.
|
||||||
|
|
||||||
|
**Suggested Phased Approach:**
|
||||||
|
|
||||||
|
1. **Phase 0: Core BDD Infrastructure:** Implement `Tilly.BDD.{Node, Store, Ops, Protocols}`. Focus on BDDs for simple sets first (e.g., sets of atoms, integers).
|
||||||
|
2. **Phase 1: `Tilly.Type.Descr` and Basic Types:** Implement `Tilly.Type.Descr`, `Tilly.Type.Store`, and `Tilly.Type.Ops`. Implement construction of `Descr` IDs for primitives (`any`, `nothing`, `integer`, `atom`) and literals.
|
||||||
|
3. **Phase 2: Typer Integration for Primitives:** Refactor `Til.Typer.Types`, `Til.Typer.Interner`, and parts of `Til.Typer.ExpressionTyper` to use the new system for primitive and literal types. Update `resolve_type_specifier_node` for basic type names and simple `(or)`, `(and)`, `(not)` forms.
|
||||||
|
4. **Phase 3: Subtyping:** Implement the new `Til.Typer.SubtypeChecker`.
|
||||||
|
5. **Phase 4: Constructed Types (Iterative):**
|
||||||
|
* Start with pairs/tuples, then lists. Design their BDD representations within `Tilly.Type.Descr`.
|
||||||
|
* Tackle records/maps. This will be challenging and require careful mapping of `project.md`'s map features to CDuce-style record BDDs.
|
||||||
|
* Address function types.
|
||||||
|
6. **Phase 5: Advanced Features:** Polymorphism (type variables in BDDs), refinement types.
|
||||||
|
|
||||||
|
This is a substantial refactoring. The key is to build the BDD foundation correctly, as everything else will depend on it. The `typing_ctx` will become a critical piece of data passed throughout the type system.
|
||||||
|
|
||||||
|
> Tokens: 77k sent, 2.3k received. Cost: $0.12 message, $0.15 session.
|
||||||
|
|
||||||
|
#### /edit
|
||||||
169
set_types.md
Normal file
169
set_types.md
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# Tilly Set-Theoretic Type System: Design and CDuce Reference
|
||||||
|
|
||||||
|
## Objective:
|
||||||
|
Implement a CDuce-style set-theoretic type system in Elixir for the Tilly compiler. This document outlines the core principles adapted from CDuce and the planned Elixir implementation strategy.
|
||||||
|
|
||||||
|
Okay, let's lay out a revised and extensive implementation plan for adapting Tilly to a set-theoretic type system.
|
||||||
|
|
||||||
|
**Overarching Concern:**
|
||||||
|
|
||||||
|
* **Bare Maps for Extensibility:** We will prioritize using bare Elixir maps for representations like Type Descriptors (`Descr`) instead of structs. This approach maintains flexibility for future extensions, allowing new fields to be added without altering struct definitions. BDD node structures themselves will be represented by simple Elixir terms (atoms or tuples).
|
||||||
|
|
||||||
|
**I. Foundational BDD and Type Descriptor Infrastructure (New Modules)**
|
||||||
|
|
||||||
|
This phase focuses on building the core components for the set-theoretic type system. These will primarily be new modules. The `typing_ctx` map will be a central piece of data, passed through most functions in this layer, containing the necessary stores and configurations.
|
||||||
|
|
||||||
|
1. **`Tilly.BDD.Node` (Conceptual Definition & Basic Helpers):**
|
||||||
|
* **Node Structures:** We define the canonical Elixir terms representing BDD nodes:
|
||||||
|
* `true`: The atom representing the universal set BDD.
|
||||||
|
* `false`: The atom representing the empty set BDD.
|
||||||
|
* `{:leaf, leaf_value_id}`: Represents a leaf node. `leaf_value_id` can be any term, its interpretation depends on the `ops_module` (e.g., a boolean for simple sets, or another BDD node ID for nested structures).
|
||||||
|
* `{:split, element_id, positive_child_id, ignore_child_id, negative_child_id}`: Represents an internal decision node.
|
||||||
|
* `element_id`: The value being split upon.
|
||||||
|
* `positive_child_id`, `ignore_child_id`, `negative_child_id`: IDs of other BDD nodes in the store.
|
||||||
|
* **Smart Constructors (Low-Level):** This module might contain very basic helper functions for creating these structural tuples if needed, but the primary "smart" construction (with simplification and interning) will occur in `Tilly.BDD.Ops` and use `Tilly.BDD.get_or_intern_node`.
|
||||||
|
* **Purpose:** To establish the fundamental building blocks of BDDs.
|
||||||
|
|
||||||
|
2. **`Tilly.BDD` (BDD Store Management):**
|
||||||
|
* **BDD Store (`typing_ctx.bdd_store`):** This will be a map within the `typing_ctx` responsible for hash-consing BDD nodes. It will contain:
|
||||||
|
* `nodes_by_structure`: A map where keys are `{logical_structure_tuple, ops_module_atom}` and values are `unique_node_id` (integer). The `ops_module_atom` is crucial for canonicalization, ensuring structurally identical nodes intended for different operational semantics are distinct.
|
||||||
|
* `structures_by_id`: A map where keys are `unique_node_id` and values are `%{structure: logical_structure_tuple, ops_module: ops_module_atom}`. This allows retrieval of both the node's structure and its associated operations module.
|
||||||
|
* `next_node_id`: An integer for generating new unique node IDs.
|
||||||
|
* **Key Function: `get_or_intern_node(typing_ctx, logical_structure_tuple, ops_module_atom)`:**
|
||||||
|
* Takes the current `typing_ctx`, the node's structural representation (e.g., `{:split, ...}`), and the atom identifying its operations module (e.g., `:integer_bool_bdd_ops`).
|
||||||
|
* Checks `typing_ctx.bdd_store.nodes_by_structure` for an existing node.
|
||||||
|
* If not found, creates a new `unique_node_id`, updates both `nodes_by_structure` and `structures_by_id`, and increments `next_node_id`.
|
||||||
|
* Returns `{new_typing_ctx, node_id}`.
|
||||||
|
* **Helper Functions:**
|
||||||
|
* `get_node_data(typing_ctx, node_id)`: Returns `%{structure: ..., ops_module: ...}`.
|
||||||
|
* `is_false_node?(node_id, typing_ctx)`, `is_true_node?(node_id, typing_ctx)`.
|
||||||
|
* Functions to extract parts of a node structure if needed (e.g., `get_element_from_split_node_id(typing_ctx, node_id)`).
|
||||||
|
|
||||||
|
3. **BDD Operations Modules (e.g., `Tilly.BDD.IntegerBoolOps`, `Tilly.BDD.AtomSetOps`, `Tilly.BDD.RecursivePairOps`):**
|
||||||
|
* **Purpose:** These modules provide the specific logic for how BDD elements are compared/hashed and how leaf values are combined/negated for a particular kind of BDD. They are the "plugins" for the generic BDD algorithms.
|
||||||
|
* **Interface Convention (Informal or via Elixir `behaviour`):** Each module will implement a consistent set of functions:
|
||||||
|
* `compare_elements(elem1, elem2)`: Returns `:lt`, `:eq`, or `:gt`.
|
||||||
|
* `equal_element?(elem1, elem2)`: Returns boolean.
|
||||||
|
* `hash_element(elem)`: Returns an integer.
|
||||||
|
* `empty_leaf()`: Returns the leaf value for emptiness.
|
||||||
|
* `any_leaf()`: Returns the leaf value for universality.
|
||||||
|
* `is_empty_leaf?(leaf_val)`: Returns boolean.
|
||||||
|
* `union_leaves(leaf1, leaf2, typing_ctx)`: Combines leaves for union. May need `typing_ctx` for recursive BDD operations if leaves are BDD IDs.
|
||||||
|
* `intersection_leaves(leaf1, leaf2, typing_ctx)`: Combines for intersection.
|
||||||
|
* `negation_leaf(leaf, typing_ctx)`: Negates a leaf.
|
||||||
|
* **Example (`Tilly.BDD.IntegerBoolOps`):**
|
||||||
|
* `compare_elements/2`: Uses standard integer comparison.
|
||||||
|
* `empty_leaf/0`: Returns `false`.
|
||||||
|
* `union_leaves/3`: `fn l1, l2, _ctx -> l1 or l2 end`.
|
||||||
|
|
||||||
|
4. **`Tilly.BDD.Ops` (Generic BDD Algorithms):**
|
||||||
|
* **Core Operations:** Implements `union_bdds(node1_id, node2_id, typing_ctx)`, `intersection_bdds/3`, `negation_bdd/2`, `difference_bdd/3`.
|
||||||
|
* These functions take BDD node IDs and the `typing_ctx`.
|
||||||
|
* They fetch node data (structure and `ops_module`) using `Tilly.BDD.get_node_data/2`.
|
||||||
|
* They use `apply(ops_module, :function_name, args)` to invoke element/leaf operations from the appropriate `ops_module`.
|
||||||
|
* They return a `node_id` for the resulting BDD.
|
||||||
|
* **Smart Constructors (Algebraic):** Implements functions like `leaf(leaf_value, ops_module, typing_ctx)` and `split(element, p_id, i_id, n_id, ops_module, typing_ctx)`.
|
||||||
|
* These are the CDuce-style smart constructors that apply simplification rules (e.g., if `p_id == n_id` in `split`, simplify to `p_id ++ i_id`).
|
||||||
|
* After simplification, they call `Tilly.BDD.get_or_intern_node/3` to get/create the canonical node ID.
|
||||||
|
* Example `split/6`:
|
||||||
|
```elixir
|
||||||
|
def split(x, p_id, i_id, n_id, ops_module, ctx) do
|
||||||
|
# Apply simplification rules based on CDuce bdd.ml, e.g.:
|
||||||
|
# if i_id represents True for ops_module, return True node_id.
|
||||||
|
# if p_id == n_id, result is union_bdds(p_id, i_id, ctx_with_ops_module).
|
||||||
|
# ... other rules ...
|
||||||
|
# If no simplification, create logical_structure:
|
||||||
|
logical_structure = {:split, x, p_id, i_id, n_id}
|
||||||
|
Tilly.BDD.get_or_intern_node(ctx, logical_structure, ops_module)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **`Tilly.Type` (Type Descriptor Definition):**
|
||||||
|
* **`Descr` Map Structure:** Defines the structure of a Type Descriptor map. This map holds IDs of canonical BDDs for each basic kind of type.
|
||||||
|
Example:
|
||||||
|
```elixir
|
||||||
|
%{
|
||||||
|
atoms_bdd_id: node_id, # BDD for specific atoms (elements: atoms, leaves: booleans)
|
||||||
|
integers_bdd_id: node_id, # BDD for specific integers (elements: integers, leaves: booleans)
|
||||||
|
strings_bdd_id: node_id, # BDD for specific strings
|
||||||
|
tuples_bdd_id: node_id, # BDD for pairs/tuples. Elements might be type vars or first component types; leaves might be BDD IDs for rest of tuple.
|
||||||
|
maps_bdd_id: node_id, # BDD for maps. Elements might be labels or key types; leaves might be BDD IDs for value types or record structures.
|
||||||
|
functions_bdd_id: node_id,# BDD for functions.
|
||||||
|
# ... other kinds as needed, e.g., for abstract types ...
|
||||||
|
absent_marker_bdd_id: node_id # BDD representing if 'absent' or 'nil' is part of the type
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* **Helper Functions:** May include functions like `empty_descr(typing_ctx)` which creates a `Descr` map where all `*_bdd_id` fields point to the `false` BDD node ID, and `any_descr(typing_ctx)` where they point to `true` BDDs (configured with appropriate `ops_module` for each field).
|
||||||
|
|
||||||
|
6. **`Tilly.Type.Store` (Type Descriptor Interning):**
|
||||||
|
* **Type Store (`typing_ctx.type_store`):** A map within `typing_ctx` for interning `Descr` maps.
|
||||||
|
* `descrs_by_structure`: Maps a canonical `Descr` map (where all BDD IDs are already canonical) to a `unique_descr_id` (integer).
|
||||||
|
* `structures_by_id`: Maps `unique_descr_id` back to the canonical `Descr` map.
|
||||||
|
* `next_descr_id`: For generating new unique `Descr` IDs.
|
||||||
|
* **Key Function: `get_or_intern_descr(typing_ctx, descr_map)`:**
|
||||||
|
* Ensures all `*_bdd_id` values in `descr_map` are canonical.
|
||||||
|
* Looks up `descr_map` in `descrs_by_structure`.
|
||||||
|
* If not found, creates a new ID, updates stores, and increments `next_descr_id`.
|
||||||
|
* Returns `{new_typing_ctx, descr_id}`.
|
||||||
|
|
||||||
|
7. **`Tilly.Type.Ops` (Operations on Type Descriptors):**
|
||||||
|
* **Set Operations:** Implements `union_types(typing_ctx, descr1_id, descr2_id)`, `intersection_types/3`, `negation_type/2`.
|
||||||
|
* These operate on `Descr` IDs.
|
||||||
|
* Example `union_types/3`:
|
||||||
|
1. Fetch `descr_map1` and `descr_map2` from `typing_ctx.type_store` using their IDs.
|
||||||
|
2. For each corresponding field (e.g., `atoms_bdd_id`):
|
||||||
|
* Call `Tilly.BDD.Ops.union_bdds(descr_map1.atoms_bdd_id, descr_map2.atoms_bdd_id, typing_ctx)`. The `typing_ctx` is passed along; the `ops_module` for these BDDs is retrieved from the `bdd_store` by `union_bdds`.
|
||||||
|
3. Construct a `result_descr_map` using the new BDD IDs for each field.
|
||||||
|
4. Intern this `result_descr_map` using `Tilly.Type.Store.get_or_intern_descr(typing_ctx, result_descr_map)` to get the final `result_descr_id`.
|
||||||
|
5. Return `{new_typing_ctx, result_descr_id}`.
|
||||||
|
* **Construction Helpers:** Functions to create specific types, e.g., `create_integer_type(typing_ctx, integer_value)` or `create_atom_type(typing_ctx, atom_value)`. These would construct the appropriate `Descr` map (with a BDD representing only that literal) and intern it.
|
||||||
|
|
||||||
|
**II. Adapting Existing Typer Modules**
|
||||||
|
|
||||||
|
This phase involves refactoring existing modules to use the new BDD-based type infrastructure. The `typing_ctx` (initialized with empty stores) will be threaded through all relevant type-checking functions.
|
||||||
|
|
||||||
|
1. **`Til.Typer.Types` (Major Refactor):**
|
||||||
|
* Functions like `get_primitive_type(:integer)` will now use `Tilly.Type.Ops` or dedicated constructors (e.g., `Tilly.Type.integer_singleton(typing_ctx, value)` or `Tilly.Type.integer_any(typing_ctx)`) to get/create an interned `Descr` ID.
|
||||||
|
* For example, `get_primitive_type(:integer)` would return the `Descr` ID for the type "all integers". This `Descr` would have its `integers_bdd_id` pointing to a `true` BDD (interned with an `ops_module` like `:integer_bool_bdd_ops`), and other fields like `atoms_bdd_id` pointing to `false` BDDs.
|
||||||
|
* The concept of predefined type *maps* (as Elixir maps with `:type_kind`) is replaced by predefined canonical `Descr` IDs, likely initialized once and stored or retrieved via helper functions.
|
||||||
|
|
||||||
|
2. **`Til.Typer.Interner` (Major Refactor/Integration):**
|
||||||
|
* Its current role of interning Elixir map-based type representations is entirely replaced by `Tilly.BDD.get_or_intern_node` and `Tilly.Type.Store.get_or_intern_descr`.
|
||||||
|
* `populate_known_types` will be responsible for initializing the `typing_ctx` by creating and interning the `Descr` IDs for fundamental base types (e.g., `any`, `nothing`, the general `integer` type, `atom` type). This involves creating the necessary BDDs (often just `true` or `false` BDDs for the respective fields in the `Descr`) with their appropriate `ops_module`s.
|
||||||
|
|
||||||
|
3. **`Til.Typer` and `Til.Typer.ExpressionTyper` (Significant Refactor):**
|
||||||
|
* **`typing_ctx` Threading:** All type inference and checking functions will now take `typing_ctx` as an argument and return an updated `typing_ctx` along with their results.
|
||||||
|
* `infer_type_for_node_ast` (or similar functions):
|
||||||
|
* For literals (e.g., `123`, `:foo`): Will call functions like `Tilly.Type.Ops.create_integer_literal_type(typing_ctx, 123)` which constructs a `Descr` map where `integers_bdd_id` points to a BDD representing only `123` (and other fields are empty BDDs), interns it, and returns the `{new_ctx, descr_id}`.
|
||||||
|
* For operations that combine types (e.g., an `if` expression's branches have types `descr_id_A` and `descr_id_B`): Will use `Tilly.Type.Ops.union_types(typing_ctx, descr_id_A, descr_id_B)` to get the `{new_ctx, result_descr_id}`.
|
||||||
|
* `resolve_type_specifier_node`:
|
||||||
|
* For basic type names (`Integer`, `Atom`): Will fetch their canonical `Descr` IDs (e.g., from helper functions in `Til.Typer.Types` that retrieve pre-interned IDs).
|
||||||
|
* For type expressions like `(or TypeA TypeB)`: Will recursively call itself to get `descr_id_A` and `descr_id_B`, then use `Tilly.Type.Ops.union_types(typing_ctx, descr_id_A, descr_id_B)`. Similarly for `(and ...)` and `(not ...)`.
|
||||||
|
* The `type_id` field in AST nodes will now store the `Descr` ID (an integer) obtained from `Tilly.Type.Store`.
|
||||||
|
|
||||||
|
4. **`Til.Typer.SubtypeChecker` (Complete Rewrite):**
|
||||||
|
* `is_subtype?(typing_ctx, descr_a_id, descr_b_id)`:
|
||||||
|
1. `{typing_ctx, descr_not_b_id} = Tilly.Type.Ops.negation_type(typing_ctx, descr_b_id)`.
|
||||||
|
2. `{typing_ctx, intersection_id} = Tilly.Type.Ops.intersection_types(typing_ctx, descr_a_id, descr_not_b_id)`.
|
||||||
|
3. `Tilly.Type.Ops.is_empty_type?(typing_ctx, intersection_id)`.
|
||||||
|
* `Tilly.Type.Ops.is_empty_type?(typing_ctx, descr_id)`:
|
||||||
|
1. Fetches `descr_map` from `typing_ctx.type_store`.
|
||||||
|
2. Checks if all `*_bdd_id` fields in `descr_map` point to the canonical `false` BDD node ID (by calling `Tilly.BDD.is_false_node?/2` for each).
|
||||||
|
|
||||||
|
**III. Handling Specific Type Kinds from `project.md`**
|
||||||
|
|
||||||
|
The existing type kinds in `project.md` will be mapped to specific configurations of `Descr` maps and their constituent BDDs.
|
||||||
|
|
||||||
|
* **Primitive Types:** `%{type_kind: :primitive, name: :integer}` becomes a canonical `Descr` ID. For "all integers", its `integers_bdd_id` points to a `true` BDD (interned with `ops_module: :integer_bool_bdd_ops`), and other fields (like `atoms_bdd_id`) point to `false` BDDs.
|
||||||
|
* **Literal Types:** `%{type_kind: :literal, value: 42}` becomes a `Descr` ID. Its `integers_bdd_id` points to a BDD representing *only* the value `42` (e.g., a single `Split` node leading to a `true` leaf, or a more optimized representation if available), and other fields are `false` BDDs.
|
||||||
|
* **Union/Intersection/Negation Types:** These are no longer explicit `type_kind`s in the `Descr` map itself. They are the *result* of applying `Tilly.Type.Ops.union_types`, `intersection_types`, or `negation_type` to other `Descr` IDs. The resulting `Descr` ID represents the union/intersection/negation.
|
||||||
|
* **Function, List, Tuple, Map Types:**
|
||||||
|
* These will be represented by `Descr` maps where the corresponding BDD field is non-empty and structured appropriately. For example, a list of integers would have a non-empty `pairs_bdd_id`.
|
||||||
|
* The BDDs for these types will be more complex:
|
||||||
|
* **Lists/Tuples (`pairs_bdd_id`):** The BDD might split on the type of the first element, with leaves being BDD IDs representing the type of the rest of the list/tuple. The `ops_module` for this BDD would handle element types and BDD ID leaves (e.g., `:recursive_pair_ops`).
|
||||||
|
* **Maps/Records (`records_bdd_id`):** This BDD would split on record labels or key types. Leaves could be BDD IDs for value types or more complex record structures. This is a significant area requiring careful design inspired by CDuce's record BDDs. The `project.md` details on `known_elements` and `index_signature` will need to be encoded into the BDD structure and its `ops_module`.
|
||||||
|
* **Functions (`functions_bdd_id`):** BDDs might split on argument types or arity, with leaves representing return types (possibly as BDD IDs).
|
||||||
|
* This is an advanced stage. Initially, constructing these might involve specific helper functions in `Tilly.Type.Ops` (e.g., `create_list_type(typing_ctx, element_descr_id)`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
195
test/support/test_helper.ex
Normal file
195
test/support/test_helper.ex
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
defmodule Til.TestHelpers do
|
||||||
|
import ExUnit.Assertions
|
||||||
|
|
||||||
|
alias Til.Parser
|
||||||
|
# For type-checking related helpers
|
||||||
|
alias Til.Typer
|
||||||
|
alias Til.AstUtils
|
||||||
|
|
||||||
|
# --- Node Access Helpers ---
|
||||||
|
|
||||||
|
def get_file_node_from_map(nodes_map) do
|
||||||
|
file_node =
|
||||||
|
Enum.find(Map.values(nodes_map), fn node ->
|
||||||
|
# Ensure the map is an AST node before checking its type
|
||||||
|
Map.has_key?(node, :ast_node_type) && node.ast_node_type == :file
|
||||||
|
end)
|
||||||
|
|
||||||
|
refute is_nil(file_node), "File node not found in #{inspect(nodes_map)}"
|
||||||
|
file_node
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_node_by_id(nodes_map, node_id) do
|
||||||
|
node = Map.get(nodes_map, node_id)
|
||||||
|
refute is_nil(node), "Node with ID #{inspect(node_id)} not found in nodes_map."
|
||||||
|
node
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_first_child_node(nodes_map, parent_node_id \\ nil) do
|
||||||
|
parent_node =
|
||||||
|
if is_nil(parent_node_id) do
|
||||||
|
get_file_node_from_map(nodes_map)
|
||||||
|
else
|
||||||
|
get_node_by_id(nodes_map, parent_node_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
children_ids = Map.get(parent_node, :children, [])
|
||||||
|
|
||||||
|
if Enum.empty?(children_ids) do
|
||||||
|
flunk("Parent node #{parent_node.id} has no children, cannot get first child node.")
|
||||||
|
end
|
||||||
|
|
||||||
|
get_node_by_id(nodes_map, hd(children_ids))
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_nth_child_node(nodes_map, index, parent_node_id \\ nil) do
|
||||||
|
parent_node =
|
||||||
|
if is_nil(parent_node_id) do
|
||||||
|
get_file_node_from_map(nodes_map)
|
||||||
|
else
|
||||||
|
get_node_by_id(nodes_map, parent_node_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
children_ids = Map.get(parent_node, :children, [])
|
||||||
|
|
||||||
|
unless index >= 0 && index < length(children_ids) do
|
||||||
|
flunk(
|
||||||
|
"Child node at index #{index} not found for parent #{parent_node.id}. Parent has #{length(children_ids)} children. Children IDs: #{inspect(children_ids)}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
get_node_by_id(nodes_map, Enum.at(children_ids, index))
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Combined Parse/TypeCheck and Get Node Helpers ---
|
||||||
|
# These return {node, nodes_map}
|
||||||
|
|
||||||
|
def parse_and_get_first_node(source_string, file_name \\ "test_source") do
|
||||||
|
{:ok, parsed_nodes_map} = Parser.parse(source_string, file_name)
|
||||||
|
{get_first_child_node(parsed_nodes_map), parsed_nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_and_get_nth_node(source_string, index, file_name \\ "test_source") do
|
||||||
|
{:ok, parsed_nodes_map} = Parser.parse(source_string, file_name)
|
||||||
|
{get_nth_child_node(parsed_nodes_map, index), parsed_nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
def typecheck_and_get_first_node(source_string, file_name \\ "test_source") do
|
||||||
|
{:ok, parsed_nodes_map} = Parser.parse(source_string, file_name)
|
||||||
|
{:ok, typed_nodes_map} = Typer.type_check(parsed_nodes_map)
|
||||||
|
{get_first_child_node(typed_nodes_map), typed_nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
def typecheck_and_get_nth_node(source_string, index, file_name \\ "test_source") do
|
||||||
|
{:ok, parsed_nodes_map} = Parser.parse(source_string, file_name)
|
||||||
|
{:ok, typed_nodes_map} = Typer.type_check(parsed_nodes_map)
|
||||||
|
{get_nth_child_node(typed_nodes_map, index), typed_nodes_map}
|
||||||
|
end
|
||||||
|
|
||||||
|
# --- Type Assertion Helpers ---
|
||||||
|
|
||||||
|
# Strips :id fields recursively and resolves _id suffixed keys
|
||||||
|
# from a type definition structure, using nodes_map for lookups.
|
||||||
|
def deep_strip_id(type_definition, nodes_map) do
|
||||||
|
cond do
|
||||||
|
is_struct(type_definition, MapSet) ->
|
||||||
|
type_definition
|
||||||
|
|> MapSet.to_list()
|
||||||
|
# Recursively call on elements
|
||||||
|
|> Enum.map(&deep_strip_id(&1, nodes_map))
|
||||||
|
|> MapSet.new()
|
||||||
|
|
||||||
|
is_map(type_definition) ->
|
||||||
|
# Remove its own :id.
|
||||||
|
map_without_id = Map.delete(type_definition, :id)
|
||||||
|
|
||||||
|
# Recursively process its values, resolving _id keys.
|
||||||
|
# Special handling for type_annotation_mismatch error details
|
||||||
|
# Check if this map is a type definition itself (has :type_kind) before specific error checks
|
||||||
|
if Map.has_key?(type_definition, :type_kind) &&
|
||||||
|
type_definition.type_kind == :error &&
|
||||||
|
type_definition.reason == :type_annotation_mismatch do
|
||||||
|
actual_type_clean =
|
||||||
|
case Map.get(type_definition, :actual_type_id) do
|
||||||
|
nil -> nil
|
||||||
|
id -> deep_strip_id(Map.get(nodes_map, id), nodes_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
expected_type_clean =
|
||||||
|
case Map.get(type_definition, :expected_type_id) do
|
||||||
|
nil -> nil
|
||||||
|
id -> deep_strip_id(Map.get(nodes_map, id), nodes_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :type_annotation_mismatch,
|
||||||
|
actual_type: actual_type_clean,
|
||||||
|
expected_type: expected_type_clean
|
||||||
|
}
|
||||||
|
else
|
||||||
|
# Standard processing for other maps
|
||||||
|
Enum.reduce(map_without_id, %{}, fn {original_key, original_value}, acc_map ->
|
||||||
|
{final_key, final_value} =
|
||||||
|
cond do
|
||||||
|
# Handle :element_type_id -> :element_type
|
||||||
|
original_key == :element_type_id && is_atom(original_value) ->
|
||||||
|
resolved_def = Map.get(nodes_map, original_value)
|
||||||
|
{:element_type, deep_strip_id(resolved_def, nodes_map)}
|
||||||
|
|
||||||
|
# Handle :key_type_id -> :key_type
|
||||||
|
original_key == :key_type_id && is_atom(original_value) ->
|
||||||
|
resolved_def = Map.get(nodes_map, original_value)
|
||||||
|
{:key_type, deep_strip_id(resolved_def, nodes_map)}
|
||||||
|
|
||||||
|
# Handle :value_type_id -> :value_type (e.g., in index_signature or %{value_type_id: ..., optional: ...})
|
||||||
|
original_key == :value_type_id && is_atom(original_value) ->
|
||||||
|
resolved_def = Map.get(nodes_map, original_value)
|
||||||
|
{:value_type, deep_strip_id(resolved_def, nodes_map)}
|
||||||
|
|
||||||
|
is_map(original_value) ->
|
||||||
|
{original_key, deep_strip_id(original_value, nodes_map)}
|
||||||
|
|
||||||
|
is_list(original_value) ->
|
||||||
|
{original_key, deep_strip_id(original_value, nodes_map)} # Handles lists of type defs
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{original_key, original_value}
|
||||||
|
end
|
||||||
|
|
||||||
|
Map.put(acc_map, final_key, final_value)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
is_list(type_definition) ->
|
||||||
|
# Recursively call on elements for lists of type definitions
|
||||||
|
Enum.map(type_definition, &deep_strip_id(&1, nodes_map))
|
||||||
|
|
||||||
|
true -> # Literals, atoms, numbers, nil, etc. (leaf nodes in the type structure)
|
||||||
|
type_definition
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def assert_node_typed_as(node, typed_nodes_map, expected_type_definition_clean) do
|
||||||
|
type_key = node.type_id
|
||||||
|
|
||||||
|
assert is_atom(type_key),
|
||||||
|
"Type ID for node #{inspect(node.id)} (#{node.raw_string}) should be an atom key, got: #{inspect(type_key)}"
|
||||||
|
|
||||||
|
actual_type_definition_from_map = Map.get(typed_nodes_map, type_key)
|
||||||
|
|
||||||
|
refute is_nil(actual_type_definition_from_map),
|
||||||
|
"Type definition for key #{inspect(type_key)} (from node #{node.id}, raw: '#{node.raw_string}') not found in nodes_map."
|
||||||
|
|
||||||
|
actual_type_definition_clean =
|
||||||
|
deep_strip_id(actual_type_definition_from_map, typed_nodes_map)
|
||||||
|
|
||||||
|
assert actual_type_definition_clean == expected_type_definition_clean,
|
||||||
|
"Type mismatch for node #{node.id} ('#{node.raw_string}').\nExpected (clean):\n#{inspect(expected_type_definition_clean, pretty: true, limit: :infinity)}\nGot (clean):\n#{inspect(actual_type_definition_clean, pretty: true, limit: :infinity)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def inspect_nodes(node_map) do
|
||||||
|
AstUtils.build_debug_ast_data(node_map)
|
||||||
|
|> IO.inspect(pretty: true, limit: :infinity)
|
||||||
|
end
|
||||||
|
end
|
||||||
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
@ -0,0 +1 @@
|
|||||||
|
ExUnit.start()
|
||||||
38
test/til/adhoc_tests.exs
Normal file
38
test/til/adhoc_tests.exs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
defmodule Til.AdhocTest do
|
||||||
|
@moduledoc """
|
||||||
|
Adhoc tests for quick syntax checking and compiler features.
|
||||||
|
These tests are not part of the main test suite and are used for
|
||||||
|
quick manual checks.
|
||||||
|
"""
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Til.TestHelpers
|
||||||
|
|
||||||
|
describe "Adhoc tests for quick syntax checking and compiler features" do
|
||||||
|
# test "pretty_print_ast with nested structures and errors" do
|
||||||
|
# source = """
|
||||||
|
# (defun my_func [a b]
|
||||||
|
# (add a m{key 'val})
|
||||||
|
# (some_list 1 2
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# {:ok, nodes_map} =
|
||||||
|
# Parser.parse(source, "adhoc_error_test.til")
|
||||||
|
#
|
||||||
|
# AstUtils.pretty_print_ast(nodes_map)
|
||||||
|
# |> IO.puts()
|
||||||
|
#
|
||||||
|
# AstUtils.build_debug_ast_data(nodes_map)
|
||||||
|
# |> IO.inspect(label: "AST Nodes", pretty: true, limit: :infinity)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
test "asdasd" do
|
||||||
|
source = """
|
||||||
|
(defun :asd)
|
||||||
|
"""
|
||||||
|
|
||||||
|
TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
# |> IO.inspect(label: "First Node", pretty: true, limit: :infinity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
21
test/til/file_parse_test.exs
Normal file
21
test/til/file_parse_test.exs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
defmodule Til.FileParseTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Til.Parser
|
||||||
|
alias Til.AstUtils
|
||||||
|
alias Til.TestHelpers
|
||||||
|
|
||||||
|
describe "test File parsing features" do
|
||||||
|
test "parse multiple top-level s-exps into 1 `file` ast node" do
|
||||||
|
source = """
|
||||||
|
(defun)
|
||||||
|
(defun)
|
||||||
|
"""
|
||||||
|
|
||||||
|
{:ok, nodes_map} =
|
||||||
|
Parser.parse(source, "top-level-sexps.til")
|
||||||
|
|
||||||
|
assert TestHelpers.get_file_node_from_map(nodes_map).ast_node_type == :file
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
167
test/til/list_parser_test.exs
Normal file
167
test/til/list_parser_test.exs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
defmodule Til.ListParserTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Til.Parser
|
||||||
|
import Til.TestHelpers
|
||||||
|
|
||||||
|
describe "List Parsing" do
|
||||||
|
test "parses an empty list []" do
|
||||||
|
source = "[]"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
file_node = get_file_node_from_map(nodes_map)
|
||||||
|
list_node_id = hd(file_node.children)
|
||||||
|
list_node = Map.get(nodes_map, list_node_id)
|
||||||
|
|
||||||
|
assert list_node.ast_node_type == :list_expression
|
||||||
|
assert list_node.raw_string == "[]"
|
||||||
|
assert list_node.children == []
|
||||||
|
assert list_node.parsing_error == nil
|
||||||
|
|
||||||
|
# Location check
|
||||||
|
# "[]"
|
||||||
|
# ^ offset 0, line 1, col 1
|
||||||
|
# ^ offset 1, line 1, col 2
|
||||||
|
# ^ offset 2, line 1, col 3 (end position, exclusive for offset, inclusive for col)
|
||||||
|
assert list_node.location == [0, 1, 1, 2, 1, 3]
|
||||||
|
|
||||||
|
# file_node is already fetched and used to get list_node
|
||||||
|
assert file_node.children == [list_node.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a list of integers [1 2 3]" do
|
||||||
|
source = "[1 2 3]"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
file_node = get_file_node_from_map(nodes_map)
|
||||||
|
list_node_id = hd(file_node.children)
|
||||||
|
list_node = Map.get(nodes_map, list_node_id)
|
||||||
|
|
||||||
|
assert list_node.ast_node_type == :list_expression
|
||||||
|
assert list_node.raw_string == "[1 2 3]"
|
||||||
|
assert list_node.parsing_error == nil
|
||||||
|
assert length(list_node.children) == 3
|
||||||
|
|
||||||
|
# Location check
|
||||||
|
# "[1 2 3]"
|
||||||
|
# ^ offset 0, line 1, col 1
|
||||||
|
# ^ offset 7, line 1, col 8
|
||||||
|
assert list_node.location == [0, 1, 1, 7, 1, 8]
|
||||||
|
|
||||||
|
# Check children
|
||||||
|
child1_id = Enum.at(list_node.children, 0)
|
||||||
|
child2_id = Enum.at(list_node.children, 1)
|
||||||
|
child3_id = Enum.at(list_node.children, 2)
|
||||||
|
|
||||||
|
child1 = Map.get(nodes_map, child1_id)
|
||||||
|
child2 = Map.get(nodes_map, child2_id)
|
||||||
|
child3 = Map.get(nodes_map, child3_id)
|
||||||
|
|
||||||
|
assert child1.ast_node_type == :literal_integer
|
||||||
|
assert child1.value == 1
|
||||||
|
assert child1.raw_string == "1"
|
||||||
|
assert child1.parent_id == list_node.id
|
||||||
|
# "[1 2 3]"
|
||||||
|
# ^ offset 1, line 1, col 2
|
||||||
|
# ^ offset 2, line 1, col 3
|
||||||
|
assert child1.location == [1, 1, 2, 2, 1, 3]
|
||||||
|
|
||||||
|
assert child2.ast_node_type == :literal_integer
|
||||||
|
assert child2.value == 2
|
||||||
|
assert child2.raw_string == "2"
|
||||||
|
assert child2.parent_id == list_node.id
|
||||||
|
# "[1 2 3]"
|
||||||
|
# ^ offset 3, line 1, col 4
|
||||||
|
# ^ offset 4, line 1, col 5
|
||||||
|
assert child2.location == [3, 1, 4, 4, 1, 5]
|
||||||
|
|
||||||
|
assert child3.ast_node_type == :literal_integer
|
||||||
|
assert child3.value == 3
|
||||||
|
assert child3.raw_string == "3"
|
||||||
|
assert child3.parent_id == list_node.id
|
||||||
|
# "[1 2 3]"
|
||||||
|
# ^ offset 5, line 1, col 6
|
||||||
|
# ^ offset 6, line 1, col 7
|
||||||
|
assert child3.location == [5, 1, 6, 6, 1, 7]
|
||||||
|
|
||||||
|
# file_node is already fetched and used to get list_node
|
||||||
|
assert file_node.children == [list_node.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses an unclosed list [1 2" do
|
||||||
|
source = "[1 2"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
file_node = get_file_node_from_map(nodes_map)
|
||||||
|
list_node_id = hd(file_node.children)
|
||||||
|
list_node = Map.get(nodes_map, list_node_id)
|
||||||
|
|
||||||
|
assert list_node.ast_node_type == :list_expression
|
||||||
|
assert list_node.raw_string == "[1 2" # Raw string is what was consumed for the list
|
||||||
|
assert list_node.parsing_error == "Unclosed list"
|
||||||
|
assert length(list_node.children) == 2 # Children that were successfully parsed
|
||||||
|
|
||||||
|
# Location check for the unclosed list node
|
||||||
|
# "[1 2"
|
||||||
|
# ^ offset 0, line 1, col 1
|
||||||
|
# ^ offset 4, line 1, col 5 (end of consumed input for this node)
|
||||||
|
assert list_node.location == [0, 1, 1, 4, 1, 5]
|
||||||
|
|
||||||
|
child1 = get_node_by_id(nodes_map, Enum.at(list_node.children, 0))
|
||||||
|
child2 = get_node_by_id(nodes_map, Enum.at(list_node.children, 1))
|
||||||
|
|
||||||
|
assert child1.value == 1
|
||||||
|
assert child2.value == 2
|
||||||
|
|
||||||
|
# file_node is already fetched and used to get list_node
|
||||||
|
assert file_node.children == [list_node.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses an unexpected closing bracket ] at top level" do
|
||||||
|
source = "]"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
file_node = get_file_node_from_map(nodes_map)
|
||||||
|
error_node_id = hd(file_node.children)
|
||||||
|
error_node = Map.get(nodes_map, error_node_id)
|
||||||
|
|
||||||
|
assert error_node.ast_node_type == :unknown # Or a more specific error type if desired
|
||||||
|
assert error_node.raw_string == "]"
|
||||||
|
assert error_node.parsing_error == "Unexpected ']'"
|
||||||
|
|
||||||
|
# Location check
|
||||||
|
# "]"
|
||||||
|
# ^ offset 0, line 1, col 1
|
||||||
|
# ^ offset 1, line 1, col 2
|
||||||
|
assert error_node.location == [0, 1, 1, 1, 1, 2]
|
||||||
|
|
||||||
|
# file_node is already fetched and used to get error_node
|
||||||
|
assert file_node.children == [error_node.id]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses an unexpected closing bracket ] inside S-expression (foo ])" do
|
||||||
|
source = "(foo ])"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
file_node = get_file_node_from_map(nodes_map)
|
||||||
|
sexpr_node_id = hd(file_node.children) # S-expression is the top-level
|
||||||
|
sexpr_node = Map.get(nodes_map, sexpr_node_id)
|
||||||
|
|
||||||
|
assert sexpr_node.ast_node_type == :s_expression
|
||||||
|
assert sexpr_node.raw_string == "(foo ])"
|
||||||
|
assert sexpr_node.parsing_error == nil # The S-expression itself is not unclosed
|
||||||
|
assert length(sexpr_node.children) == 2 # 'foo' and the error node for ']'
|
||||||
|
|
||||||
|
# First child 'foo'
|
||||||
|
foo_node = get_node_by_id(nodes_map, Enum.at(sexpr_node.children, 0))
|
||||||
|
assert foo_node.ast_node_type == :symbol
|
||||||
|
assert foo_node.name == "foo"
|
||||||
|
|
||||||
|
# Second child is the error node for ']'
|
||||||
|
error_node = get_node_by_id(nodes_map, Enum.at(sexpr_node.children, 1))
|
||||||
|
assert error_node.ast_node_type == :unknown
|
||||||
|
assert error_node.raw_string == "]"
|
||||||
|
assert error_node.parsing_error == "Unexpected ']'"
|
||||||
|
assert error_node.parent_id == sexpr_node.id
|
||||||
|
# Location check for ']'
|
||||||
|
# "(foo ])"
|
||||||
|
# ^ offset 5, line 1, col 6
|
||||||
|
# ^ offset 6, line 1, col 7
|
||||||
|
assert error_node.location == [5, 1, 6, 6, 1, 7]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
241
test/til/map_test.exs
Normal file
241
test/til/map_test.exs
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
defmodule Til.MapParserTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Til.Parser
|
||||||
|
alias Til.TestHelpers
|
||||||
|
|
||||||
|
describe "parse/2 - Map Expressions" do
|
||||||
|
test "parses an empty map" do
|
||||||
|
source = "m{}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# file_node + map_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
map_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
assert map_node.ast_node_type == :map_expression
|
||||||
|
assert map_node.parent_id == file_node.id
|
||||||
|
assert map_node.children == []
|
||||||
|
# "m{}"
|
||||||
|
assert map_node.location == [0, 1, 1, 3, 1, 4]
|
||||||
|
assert map_node.raw_string == "m{}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a simple map with symbol keys and values" do
|
||||||
|
source = "m{key1 val1 key2 val2}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node + 1 map_expression node + 4 symbol nodes = 6 nodes
|
||||||
|
assert map_size(nodes_map) == 6
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
map_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(map_node)
|
||||||
|
assert map_node.ast_node_type == :map_expression
|
||||||
|
assert map_node.parent_id == file_node.id
|
||||||
|
assert length(map_node.children) == 4
|
||||||
|
|
||||||
|
children_nodes = Enum.map(map_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
assert Enum.map(children_nodes, & &1.name) == ["key1", "val1", "key2", "val2"]
|
||||||
|
|
||||||
|
Enum.each(children_nodes, fn child ->
|
||||||
|
assert child.parent_id == map_node.id
|
||||||
|
assert child.ast_node_type == :symbol
|
||||||
|
end)
|
||||||
|
|
||||||
|
# "m{key1 val1 key2 val2}"
|
||||||
|
assert map_node.location == [0, 1, 1, 22, 1, 23]
|
||||||
|
assert map_node.raw_string == "m{key1 val1 key2 val2}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a map with mixed type values" do
|
||||||
|
source = "m{s 'a string' i 123 sym value}"
|
||||||
|
{map_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
|
||||||
|
# 1 file_node + 1 map, 3 keys (symbols), 1 string val, 1 int val, 1 symbol val = 1 + 1 + 3 + 3 = 8 nodes
|
||||||
|
assert map_size(nodes_map) == 8
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(map_node)
|
||||||
|
assert map_node.ast_node_type == :map_expression
|
||||||
|
assert map_node.parent_id == file_node.id
|
||||||
|
assert length(map_node.children) == 6
|
||||||
|
|
||||||
|
children_nodes = Enum.map(map_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
# s
|
||||||
|
assert Enum.at(children_nodes, 0).ast_node_type == :symbol
|
||||||
|
assert Enum.at(children_nodes, 0).name == "s"
|
||||||
|
# 'a string'
|
||||||
|
assert Enum.at(children_nodes, 1).ast_node_type == :literal_string
|
||||||
|
assert Enum.at(children_nodes, 1).value == "a string"
|
||||||
|
# i
|
||||||
|
assert Enum.at(children_nodes, 2).ast_node_type == :symbol
|
||||||
|
assert Enum.at(children_nodes, 2).name == "i"
|
||||||
|
# 123
|
||||||
|
assert Enum.at(children_nodes, 3).ast_node_type == :literal_integer
|
||||||
|
assert Enum.at(children_nodes, 3).value == 123
|
||||||
|
# sym
|
||||||
|
assert Enum.at(children_nodes, 4).ast_node_type == :symbol
|
||||||
|
assert Enum.at(children_nodes, 4).name == "sym"
|
||||||
|
# value
|
||||||
|
assert Enum.at(children_nodes, 5).ast_node_type == :symbol
|
||||||
|
assert Enum.at(children_nodes, 5).name == "value"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses nested maps" do
|
||||||
|
source = "m{outer_key m{inner_key inner_val}}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# Nodes: 1 file_node, outer_map, outer_key, inner_map, inner_key, inner_val => 6 nodes
|
||||||
|
assert map_size(nodes_map) == 6
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
outer_map = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(outer_map)
|
||||||
|
assert outer_map.ast_node_type == :map_expression
|
||||||
|
assert outer_map.parent_id == file_node.id
|
||||||
|
# outer_key, inner_map
|
||||||
|
assert length(outer_map.children) == 2
|
||||||
|
|
||||||
|
outer_key_node = TestHelpers.get_nth_child_node(nodes_map, 0, outer_map.id)
|
||||||
|
inner_map_node = TestHelpers.get_nth_child_node(nodes_map, 1, outer_map.id)
|
||||||
|
|
||||||
|
assert outer_key_node.ast_node_type == :symbol
|
||||||
|
assert outer_key_node.name == "outer_key"
|
||||||
|
assert inner_map_node.ast_node_type == :map_expression
|
||||||
|
assert inner_map_node.parent_id == outer_map.id
|
||||||
|
# inner_key, inner_val
|
||||||
|
assert length(inner_map_node.children) == 2
|
||||||
|
|
||||||
|
inner_key_node = TestHelpers.get_nth_child_node(nodes_map, 0, inner_map_node.id)
|
||||||
|
inner_val_node = TestHelpers.get_nth_child_node(nodes_map, 1, inner_map_node.id)
|
||||||
|
|
||||||
|
assert inner_key_node.ast_node_type == :symbol
|
||||||
|
assert inner_key_node.name == "inner_key"
|
||||||
|
assert inner_val_node.ast_node_type == :symbol
|
||||||
|
assert inner_val_node.name == "inner_val"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses map with varied spacing" do
|
||||||
|
source = "m{ key1 val1\n key2 val2 }"
|
||||||
|
{map_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(map_node)
|
||||||
|
assert map_node.ast_node_type == :map_expression
|
||||||
|
assert map_node.parent_id == file_node.id
|
||||||
|
assert length(map_node.children) == 4
|
||||||
|
|
||||||
|
children_names_values =
|
||||||
|
Enum.map(map_node.children, fn id ->
|
||||||
|
node = TestHelpers.get_node_by_id(nodes_map, id)
|
||||||
|
if node.ast_node_type == :symbol, do: node.name, else: node.value
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert children_names_values == ["key1", "val1", "key2", "val2"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unclosed map" do
|
||||||
|
source = "m{key1 val1"
|
||||||
|
{map_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# Expect 1 file_node, 1 map_expression node (error), 2 symbol nodes = 4 nodes
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(map_node)
|
||||||
|
assert map_node.ast_node_type == :map_expression
|
||||||
|
assert map_node.parent_id == file_node.id
|
||||||
|
assert map_node.parsing_error == "Unclosed map"
|
||||||
|
# key1, val1
|
||||||
|
assert length(map_node.children) == 2
|
||||||
|
# "m{key1 val1"
|
||||||
|
assert map_node.location == [0, 1, 1, 11, 1, 12]
|
||||||
|
assert map_node.raw_string == "m{key1 val1"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unexpected closing curly brace at top level (not map specific, but related)" do
|
||||||
|
# This } is not part of m{}
|
||||||
|
source = "foo } bar"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node + 3 items
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
top_level_children = Enum.map(file_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
error_node =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :unknown && &1.raw_string == "}"))
|
||||||
|
|
||||||
|
refute is_nil(error_node)
|
||||||
|
assert error_node.parent_id == file_node.id
|
||||||
|
assert error_node.parsing_error == "Unexpected '}'"
|
||||||
|
# location of "}"
|
||||||
|
assert error_node.location == [4, 1, 5, 5, 1, 6]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses map with odd number of elements (parser accepts, semantic check later)" do
|
||||||
|
source = "m{key1 val1 key2}"
|
||||||
|
{map_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(map_node)
|
||||||
|
assert map_node.ast_node_type == :map_expression
|
||||||
|
assert map_node.parent_id == file_node.id
|
||||||
|
# key1, val1, key2
|
||||||
|
assert length(map_node.children) == 3
|
||||||
|
children_nodes = Enum.map(map_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
assert Enum.map(children_nodes, & &1.name) == ["key1", "val1", "key2"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "map within an S-expression" do
|
||||||
|
source = "(do-something m{a 1 b 2})"
|
||||||
|
{s_expr_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# 1 file_node, s-expr, do-something, map, a, 1, b, 2 => 8 nodes
|
||||||
|
assert map_size(nodes_map) == 8
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(s_expr_node)
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.parent_id == file_node.id
|
||||||
|
# do-something, map_node
|
||||||
|
assert length(s_expr_node.children) == 2
|
||||||
|
|
||||||
|
map_node = TestHelpers.get_nth_child_node(nodes_map, 1, s_expr_node.id)
|
||||||
|
assert map_node.ast_node_type == :map_expression
|
||||||
|
assert map_node.parent_id == s_expr_node.id
|
||||||
|
# a, 1, b, 2
|
||||||
|
assert length(map_node.children) == 4
|
||||||
|
end
|
||||||
|
|
||||||
|
test "map as a value in another map" do
|
||||||
|
source = "m{data m{x 10 y 20}}"
|
||||||
|
{outer_map_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
|
||||||
|
# 1 file_node, outer_map, data_symbol, inner_map, x_symbol, 10_int, y_symbol, 20_int => 8 nodes
|
||||||
|
assert map_size(nodes_map) == 8
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(outer_map_node)
|
||||||
|
assert outer_map_node.ast_node_type == :map_expression
|
||||||
|
assert outer_map_node.parent_id == file_node.id
|
||||||
|
# data_symbol, inner_map_node
|
||||||
|
assert length(outer_map_node.children) == 2
|
||||||
|
|
||||||
|
inner_map_node = TestHelpers.get_nth_child_node(nodes_map, 1, outer_map_node.id)
|
||||||
|
assert inner_map_node.ast_node_type == :map_expression
|
||||||
|
assert inner_map_node.parent_id == outer_map_node.id
|
||||||
|
# x, 10, y, 20
|
||||||
|
assert length(inner_map_node.children) == 4
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
149
test/til/parse_atom_test.exs
Normal file
149
test/til/parse_atom_test.exs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
defmodule Til.ParseAtomTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Til.Parser
|
||||||
|
import Til.TestHelpers
|
||||||
|
|
||||||
|
describe "Atom parsing" do
|
||||||
|
test "parses a simple atom" do
|
||||||
|
source = ":hello"
|
||||||
|
{:ok, _nodes_map} = Parser.parse(source)
|
||||||
|
{atom_node, _map} = parse_and_get_first_node(source)
|
||||||
|
|
||||||
|
assert atom_node.ast_node_type == :literal_atom
|
||||||
|
assert atom_node.value == :hello
|
||||||
|
assert atom_node.raw_string == ":hello"
|
||||||
|
assert atom_node.location == [0, 1, 1, 6, 1, 7]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses an atom with numbers and underscores" do
|
||||||
|
source = ":foo_123_bar"
|
||||||
|
{:ok, _nodes_map} = Parser.parse(source)
|
||||||
|
{atom_node, _map} = parse_and_get_first_node(source)
|
||||||
|
|
||||||
|
assert atom_node.ast_node_type == :literal_atom
|
||||||
|
assert atom_node.value == :foo_123_bar
|
||||||
|
assert atom_node.raw_string == ":foo_123_bar"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses an atom within an s-expression" do
|
||||||
|
source = "(:an_atom)"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
s_expr_node = get_first_child_node(nodes_map)
|
||||||
|
atom_node_id = hd(s_expr_node.children)
|
||||||
|
atom_node = get_node_by_id(nodes_map, atom_node_id)
|
||||||
|
|
||||||
|
assert atom_node.ast_node_type == :literal_atom
|
||||||
|
assert atom_node.value == :an_atom
|
||||||
|
assert atom_node.raw_string == ":an_atom"
|
||||||
|
# Location of :an_atom within ()
|
||||||
|
assert atom_node.location == [1, 1, 2, 9, 1, 10]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses multiple atoms in an s-expression" do
|
||||||
|
source = "(:first :second)"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
s_expr_node = get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
first_atom_node = get_node_by_id(nodes_map, Enum.at(s_expr_node.children, 0))
|
||||||
|
assert first_atom_node.ast_node_type == :literal_atom
|
||||||
|
assert first_atom_node.value == :first
|
||||||
|
assert first_atom_node.raw_string == ":first"
|
||||||
|
|
||||||
|
second_atom_node = get_node_by_id(nodes_map, Enum.at(s_expr_node.children, 1))
|
||||||
|
assert second_atom_node.ast_node_type == :literal_atom
|
||||||
|
assert second_atom_node.value == :second
|
||||||
|
assert second_atom_node.raw_string == ":second"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses an atom followed immediately by an opening parenthesis (delimiter)" do
|
||||||
|
source = ":atom_name(foo)"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
|
||||||
|
# First child of the file node should be the atom
|
||||||
|
atom_node = get_nth_child_node(nodes_map, 0)
|
||||||
|
assert atom_node.ast_node_type == :literal_atom
|
||||||
|
assert atom_node.value == :atom_name
|
||||||
|
assert atom_node.raw_string == ":atom_name"
|
||||||
|
assert atom_node.location == [0, 1, 1, 10, 1, 11]
|
||||||
|
|
||||||
|
# Second child should be the s-expression
|
||||||
|
s_expr_node = get_nth_child_node(nodes_map, 1)
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.raw_string == "(foo)"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses an atom at the end of input" do
|
||||||
|
source = " :last_atom "
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# Use trimmed for helper
|
||||||
|
{atom_node, _map} = parse_and_get_first_node(String.trim(source))
|
||||||
|
|
||||||
|
assert atom_node.ast_node_type == :literal_atom
|
||||||
|
assert atom_node.value == :last_atom
|
||||||
|
assert atom_node.raw_string == ":last_atom"
|
||||||
|
# Location needs to be checked against the original source with whitespace
|
||||||
|
file_node = get_file_node_from_map(nodes_map)
|
||||||
|
actual_atom_node_id = hd(file_node.children)
|
||||||
|
actual_atom_node = get_node_by_id(nodes_map, actual_atom_node_id)
|
||||||
|
# " :last_atom "
|
||||||
|
assert actual_atom_node.location == [2, 1, 3, 12, 1, 13]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses atom within a list expression" do
|
||||||
|
source = "[:my_list_atom]"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
list_expr_node = get_first_child_node(nodes_map)
|
||||||
|
atom_node_id = hd(list_expr_node.children)
|
||||||
|
atom_node = get_node_by_id(nodes_map, atom_node_id)
|
||||||
|
|
||||||
|
assert atom_node.ast_node_type == :literal_atom
|
||||||
|
assert atom_node.value == :my_list_atom
|
||||||
|
assert atom_node.raw_string == ":my_list_atom"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses atom within a tuple expression" do
|
||||||
|
source = "{:my_tuple_atom}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
tuple_expr_node = get_first_child_node(nodes_map)
|
||||||
|
atom_node_id = hd(tuple_expr_node.children)
|
||||||
|
atom_node = get_node_by_id(nodes_map, atom_node_id)
|
||||||
|
|
||||||
|
assert atom_node.ast_node_type == :literal_atom
|
||||||
|
assert atom_node.value == :my_tuple_atom
|
||||||
|
assert atom_node.raw_string == ":my_tuple_atom"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses atom as a key in a map expression" do
|
||||||
|
source = "m{:key 1}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
map_expr_node = get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
key_node_id = Enum.at(map_expr_node.children, 0)
|
||||||
|
key_node = get_node_by_id(nodes_map, key_node_id)
|
||||||
|
|
||||||
|
assert key_node.ast_node_type == :literal_atom
|
||||||
|
assert key_node.value == :key
|
||||||
|
assert key_node.raw_string == ":key"
|
||||||
|
|
||||||
|
value_node_id = Enum.at(map_expr_node.children, 1)
|
||||||
|
value_node = get_node_by_id(nodes_map, value_node_id)
|
||||||
|
assert value_node.ast_node_type == :literal_integer
|
||||||
|
assert value_node.value == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses atom as a value in a map expression" do
|
||||||
|
source = "m{'string_key' :atom_value}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
map_expr_node = get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
# string_key_node is child 0
|
||||||
|
value_node_id = Enum.at(map_expr_node.children, 1)
|
||||||
|
value_node = get_node_by_id(nodes_map, value_node_id)
|
||||||
|
|
||||||
|
assert value_node.ast_node_type == :literal_atom
|
||||||
|
assert value_node.value == :atom_value
|
||||||
|
assert value_node.raw_string == ":atom_value"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
868
test/til/simple_parser_tests.exs
Normal file
868
test/til/simple_parser_tests.exs
Normal file
@ -0,0 +1,868 @@
|
|||||||
|
defmodule Til.ParserTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Til.Parser
|
||||||
|
alias Til.TestHelpers
|
||||||
|
|
||||||
|
describe "parse/2 - Basic Atoms" do
|
||||||
|
test "parses a simple integer literal" do
|
||||||
|
source = "42"
|
||||||
|
file_name = "test.tly"
|
||||||
|
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source, file_name)
|
||||||
|
# file_node + integer_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
# Still need file_node for parent_id check
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert is_integer(node.id)
|
||||||
|
assert node.type_id == nil
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.file == file_name
|
||||||
|
# "42"
|
||||||
|
assert node.location == [0, 1, 1, 2, 1, 3]
|
||||||
|
assert node.raw_string == source
|
||||||
|
assert node.ast_node_type == :literal_integer
|
||||||
|
assert node.value == 42
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a negative integer literal" do
|
||||||
|
source = "-123"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# file_node + integer_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
# "-123"
|
||||||
|
assert node.location == [0, 1, 1, 4, 1, 5]
|
||||||
|
assert node.raw_string == source
|
||||||
|
assert node.ast_node_type == :literal_integer
|
||||||
|
assert node.value == -123
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a simple symbol" do
|
||||||
|
source = "foo"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# file_node + symbol_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
# "foo"
|
||||||
|
assert node.location == [0, 1, 1, 3, 1, 4]
|
||||||
|
assert node.raw_string == source
|
||||||
|
assert node.ast_node_type == :symbol
|
||||||
|
assert node.name == "foo"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a symbol with hyphens and numbers" do
|
||||||
|
source = "my-var-123"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.raw_string == source
|
||||||
|
assert node.ast_node_type == :symbol
|
||||||
|
assert node.name == "my-var-123"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses operator-like symbols" do
|
||||||
|
source = "+"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.raw_string == source
|
||||||
|
assert node.ast_node_type == :symbol
|
||||||
|
assert node.name == "+"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a sequence of integers and symbols" do
|
||||||
|
source = "10 foo -20 bar+"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 4 items + 1 file_node
|
||||||
|
assert map_size(nodes_map) == 5
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
# Children are already sorted by the parser
|
||||||
|
[n1_id, n2_id, n3_id, n4_id] = file_node.children
|
||||||
|
n1 = TestHelpers.get_node_by_id(nodes_map, n1_id)
|
||||||
|
n2 = TestHelpers.get_node_by_id(nodes_map, n2_id)
|
||||||
|
n3 = TestHelpers.get_node_by_id(nodes_map, n3_id)
|
||||||
|
n4 = TestHelpers.get_node_by_id(nodes_map, n4_id)
|
||||||
|
|
||||||
|
assert n1.ast_node_type == :literal_integer
|
||||||
|
assert n1.value == 10
|
||||||
|
assert n1.raw_string == "10"
|
||||||
|
assert n1.location == [0, 1, 1, 2, 1, 3]
|
||||||
|
assert n1.parent_id == file_node.id
|
||||||
|
|
||||||
|
assert n2.ast_node_type == :symbol
|
||||||
|
assert n2.name == "foo"
|
||||||
|
assert n2.raw_string == "foo"
|
||||||
|
# after "10 "
|
||||||
|
assert n2.location == [3, 1, 4, 6, 1, 7]
|
||||||
|
assert n2.parent_id == file_node.id
|
||||||
|
|
||||||
|
assert n3.ast_node_type == :literal_integer
|
||||||
|
assert n3.value == -20
|
||||||
|
assert n3.raw_string == "-20"
|
||||||
|
# after "foo "
|
||||||
|
assert n3.location == [7, 1, 8, 10, 1, 11]
|
||||||
|
assert n3.parent_id == file_node.id
|
||||||
|
|
||||||
|
assert n4.ast_node_type == :symbol
|
||||||
|
assert n4.name == "bar+"
|
||||||
|
assert n4.raw_string == "bar+"
|
||||||
|
# after "-20 "
|
||||||
|
assert n4.location == [11, 1, 12, 15, 1, 16]
|
||||||
|
assert n4.parent_id == file_node.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses 'unknown' as default file_name" do
|
||||||
|
source = "7"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.file == "unknown"
|
||||||
|
assert file_node.file == "unknown"
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "parse/2 - S-expressions" do
|
||||||
|
test "parses an empty S-expression" do
|
||||||
|
source = "()"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# file_node + s_expr_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
s_expr_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.parent_id == file_node.id
|
||||||
|
assert s_expr_node.children == []
|
||||||
|
# "()"
|
||||||
|
assert s_expr_node.location == [0, 1, 1, 2, 1, 3]
|
||||||
|
|
||||||
|
# Raw string for S-expressions is tricky, current impl might be placeholder like "()" or "(...)"
|
||||||
|
# For now, let's assert it starts with ( and ends with ) if not placeholder
|
||||||
|
assert String.starts_with?(s_expr_node.raw_string, "(") &&
|
||||||
|
String.ends_with?(s_expr_node.raw_string, ")")
|
||||||
|
|
||||||
|
# if it's "()"
|
||||||
|
assert String.length(s_expr_node.raw_string) == 2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a simple S-expression with integers" do
|
||||||
|
source = "(1 22 -3)"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node + 1 S-expr node + 3 integer nodes = 5 nodes
|
||||||
|
assert map_size(nodes_map) == 5
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
s_expr_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(s_expr_node)
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.parent_id == file_node.id
|
||||||
|
assert length(s_expr_node.children) == 3
|
||||||
|
|
||||||
|
children_nodes = Enum.map(s_expr_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
assert Enum.map(children_nodes, & &1.value) == [1, 22, -3]
|
||||||
|
|
||||||
|
Enum.each(children_nodes, fn child ->
|
||||||
|
assert child.parent_id == s_expr_node.id
|
||||||
|
assert child.ast_node_type == :literal_integer
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Source "(1 22 -3)" has length 9. Start offset 0, end offset 9. Start col 1, end col 10.
|
||||||
|
# "(1 22 -3)"
|
||||||
|
assert s_expr_node.location == [0, 1, 1, 9, 1, 10]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a simple S-expression with symbols" do
|
||||||
|
source = "(foo bar baz)"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node, 1 s-expr, 3 symbols = 5 nodes
|
||||||
|
assert map_size(nodes_map) == 5
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
s_expr_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(s_expr_node)
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.parent_id == file_node.id
|
||||||
|
assert length(s_expr_node.children) == 3
|
||||||
|
children_nodes = Enum.map(s_expr_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
assert Enum.map(children_nodes, & &1.name) == ["foo", "bar", "baz"]
|
||||||
|
|
||||||
|
Enum.each(children_nodes, fn child ->
|
||||||
|
assert child.parent_id == s_expr_node.id
|
||||||
|
assert child.ast_node_type == :symbol
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses nested S-expressions" do
|
||||||
|
# outer: a, (b 1), c | inner: b, 1
|
||||||
|
source = "(a (b 1) c)"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# Nodes: 1 file_node, outer_s_expr, a, inner_s_expr, c, b, 1 => 7 nodes
|
||||||
|
assert map_size(nodes_map) == 7
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
outer_s_expr = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(outer_s_expr)
|
||||||
|
assert outer_s_expr.ast_node_type == :s_expression
|
||||||
|
assert outer_s_expr.parent_id == file_node.id
|
||||||
|
assert length(outer_s_expr.children) == 3
|
||||||
|
|
||||||
|
# 'a'
|
||||||
|
child1 = TestHelpers.get_nth_child_node(nodes_map, 0, outer_s_expr.id)
|
||||||
|
# '(b 1)'
|
||||||
|
inner_s_expr = TestHelpers.get_nth_child_node(nodes_map, 1, outer_s_expr.id)
|
||||||
|
# 'c'
|
||||||
|
child3 = TestHelpers.get_nth_child_node(nodes_map, 2, outer_s_expr.id)
|
||||||
|
|
||||||
|
assert child1.ast_node_type == :symbol && child1.name == "a"
|
||||||
|
assert inner_s_expr.ast_node_type == :s_expression
|
||||||
|
assert child3.ast_node_type == :symbol && child3.name == "c"
|
||||||
|
|
||||||
|
assert inner_s_expr.parent_id == outer_s_expr.id
|
||||||
|
assert length(inner_s_expr.children) == 2
|
||||||
|
# 'b'
|
||||||
|
grandchild1 = TestHelpers.get_nth_child_node(nodes_map, 0, inner_s_expr.id)
|
||||||
|
# '1'
|
||||||
|
grandchild2 = TestHelpers.get_nth_child_node(nodes_map, 1, inner_s_expr.id)
|
||||||
|
|
||||||
|
assert grandchild1.ast_node_type == :symbol && grandchild1.name == "b"
|
||||||
|
assert grandchild1.parent_id == inner_s_expr.id
|
||||||
|
assert grandchild2.ast_node_type == :literal_integer && grandchild2.value == 1
|
||||||
|
assert grandchild2.parent_id == inner_s_expr.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses S-expressions with varied spacing" do
|
||||||
|
source = "( foo 1\nbar )"
|
||||||
|
{s_expr_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(s_expr_node)
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.parent_id == file_node.id
|
||||||
|
assert length(s_expr_node.children) == 3
|
||||||
|
|
||||||
|
children_names_values =
|
||||||
|
Enum.map(s_expr_node.children, fn id ->
|
||||||
|
node = TestHelpers.get_node_by_id(nodes_map, id)
|
||||||
|
if node.ast_node_type == :symbol, do: node.name, else: node.value
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert children_names_values == ["foo", 1, "bar"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "parse/2 - Error Handling" do
|
||||||
|
test "handles unclosed S-expression" do
|
||||||
|
source = "(foo bar"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# Expect 1 file_node, 1 S-expr node (marked with error), 2 symbol nodes = 4 nodes
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
s_expr_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(s_expr_node)
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.parent_id == file_node.id
|
||||||
|
assert s_expr_node.parsing_error == "Unclosed S-expression"
|
||||||
|
# foo, bar
|
||||||
|
assert length(s_expr_node.children) == 2
|
||||||
|
# Location should span till end of source
|
||||||
|
# "(foo bar"
|
||||||
|
assert s_expr_node.location == [0, 1, 1, 8, 1, 9]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unexpected closing parenthesis at top level" do
|
||||||
|
# foo, error_node_for_), bar
|
||||||
|
source = "foo ) bar"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 3 items + 1 file_node
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
top_level_children =
|
||||||
|
Enum.map(file_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
error_node =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :unknown && &1.raw_string == ")"))
|
||||||
|
|
||||||
|
refute is_nil(error_node)
|
||||||
|
assert error_node.parent_id == file_node.id
|
||||||
|
assert error_node.parsing_error == "Unexpected ')'"
|
||||||
|
# location of ")"
|
||||||
|
assert error_node.location == [4, 1, 5, 5, 1, 6]
|
||||||
|
|
||||||
|
symbol_foo =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :symbol && &1.name == "foo"))
|
||||||
|
|
||||||
|
symbol_bar =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :symbol && &1.name == "bar"))
|
||||||
|
|
||||||
|
refute is_nil(symbol_foo)
|
||||||
|
assert symbol_foo.parent_id == file_node.id
|
||||||
|
refute is_nil(symbol_bar)
|
||||||
|
assert symbol_bar.parent_id == file_node.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unknown token inside S-expression (partial, basic)" do
|
||||||
|
source = "(foo 123"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
s_expr_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(s_expr_node)
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.parent_id == file_node.id
|
||||||
|
assert s_expr_node.parsing_error == "Unclosed S-expression"
|
||||||
|
assert length(s_expr_node.children) == 2
|
||||||
|
|
||||||
|
child1 = TestHelpers.get_nth_child_node(nodes_map, 0, s_expr_node.id)
|
||||||
|
child2 = TestHelpers.get_nth_child_node(nodes_map, 1, s_expr_node.id)
|
||||||
|
|
||||||
|
assert child1.name == "foo"
|
||||||
|
assert child2.value == 123
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses multiple top-level S-expressions" do
|
||||||
|
source = "() (1 2) (foo)"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# () -> 1 node
|
||||||
|
# (1 2) -> 1 s-expr, 2 ints = 3 nodes
|
||||||
|
# (foo) -> 1 s-expr, 1 symbol = 2 nodes
|
||||||
|
# Total items = 1 + 3 + 2 = 6 nodes. Plus 1 file_node = 7 nodes.
|
||||||
|
assert map_size(nodes_map) == 7
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
top_level_s_expr_nodes =
|
||||||
|
file_node.children
|
||||||
|
|> Enum.map(&TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|> Enum.filter(&(&1.ast_node_type == :s_expression))
|
||||||
|
|
||||||
|
assert length(top_level_s_expr_nodes) == 3
|
||||||
|
Enum.each(top_level_s_expr_nodes, fn node -> assert node.parent_id == file_node.id end)
|
||||||
|
|
||||||
|
# Check children counts or specific content if necessary
|
||||||
|
# For example, the S-expression for (1 2)
|
||||||
|
s_expr_1_2 =
|
||||||
|
Enum.find(top_level_s_expr_nodes, fn node ->
|
||||||
|
children = Enum.map(node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
Enum.map(children, & &1.value) == [1, 2]
|
||||||
|
end)
|
||||||
|
|
||||||
|
refute is_nil(s_expr_1_2)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Test for raw_string of S-expression (this is a known tricky part in the impl)
|
||||||
|
# The current implementation has a placeholder for S-expression raw_string.
|
||||||
|
# This test will likely fail or need adjustment based on how raw_string is actually captured.
|
||||||
|
# For now, I'll skip a very precise raw_string test for S-expressions until it's robustly implemented.
|
||||||
|
# test "S-expression raw_string is captured correctly" do
|
||||||
|
# source = "( add 1 2 )" # Note spaces
|
||||||
|
# {:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# s_expr_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :s_expression))
|
||||||
|
# assert s_expr_node.raw_string == source
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "parse/2 - String Literals" do
|
||||||
|
test "parses a simple single-line string" do
|
||||||
|
source = "'hello world'"
|
||||||
|
file_name = "test_str.tly"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source, file_name)
|
||||||
|
# file_node + string_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.value == "hello world"
|
||||||
|
assert node.raw_string == "'hello world'"
|
||||||
|
assert node.location == [0, 1, 1, 13, 1, 14]
|
||||||
|
assert node.file == file_name
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.type_id == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses an empty string" do
|
||||||
|
source = "''"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# file_node + string_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.value == ""
|
||||||
|
assert node.raw_string == "''"
|
||||||
|
assert node.location == [0, 1, 1, 2, 1, 3]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a multiline string with no initial indent" do
|
||||||
|
# ' is at col 1, strip 0 spaces
|
||||||
|
source = "'hello\nworld'"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.value == "hello\nworld"
|
||||||
|
assert node.raw_string == "'hello\nworld'"
|
||||||
|
assert node.location == [0, 1, 1, 13, 2, 7]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a multiline string with initial indent" do
|
||||||
|
# ' is at col 3, strip 2 spaces
|
||||||
|
source = " 'hello\n world\n end'"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.value == "hello\nworld\nend"
|
||||||
|
assert node.raw_string == "'hello\n world\n end'"
|
||||||
|
# Location of ' is [2,1,3]. Raw string length is 21.
|
||||||
|
# End location: offset 2+21=23. Line 3, Col 7.
|
||||||
|
assert node.location == [2, 1, 3, 23, 3, 7]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a multiline string with varied subsequent indents" do
|
||||||
|
# ' is at col 3, strip 2 spaces
|
||||||
|
source = " 'hello\n world\n more'"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
# " world" -> "world", " more" -> " more"
|
||||||
|
assert node.value == "hello\nworld\n more"
|
||||||
|
assert node.raw_string == "'hello\n world\n more'"
|
||||||
|
# Location of ' is [2,1,3]. Raw string length is 22.
|
||||||
|
# End location: offset 2+22=24. Line 3, Col 9.
|
||||||
|
assert node.location == [2, 1, 3, 24, 3, 9]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a string containing parentheses and other special chars" do
|
||||||
|
source = "' (foo) [bar] - 123 '"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.value == " (foo) [bar] - 123 "
|
||||||
|
assert node.raw_string == "' (foo) [bar] - 123 '"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a string within an S-expression" do
|
||||||
|
source = "('str' 1)"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node, s-expr, string, integer = 4 nodes
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
s_expr_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(s_expr_node)
|
||||||
|
assert s_expr_node.ast_node_type == :s_expression
|
||||||
|
assert s_expr_node.parent_id == file_node.id
|
||||||
|
children_nodes = Enum.map(s_expr_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
string_node = Enum.find(children_nodes, &(&1.ast_node_type == :literal_string))
|
||||||
|
integer_node = Enum.find(children_nodes, &(&1.ast_node_type == :literal_integer))
|
||||||
|
|
||||||
|
refute is_nil(string_node)
|
||||||
|
refute is_nil(integer_node)
|
||||||
|
|
||||||
|
assert string_node.value == "str"
|
||||||
|
assert string_node.raw_string == "'str'"
|
||||||
|
assert string_node.parent_id == s_expr_node.id
|
||||||
|
|
||||||
|
# Location: `(` is [0,1,1]. ` ` is [1,1,2]. `s` is [2,1,3]. `t` is [3,1,4]. `r` is [4,1,5]. ` ` is [5,1,6].
|
||||||
|
# Token "'str'" starts at offset 1, line 1, col 2. Length 5. Ends offset 6, line 1, col 7.
|
||||||
|
assert string_node.location == [1, 1, 2, 6, 1, 7]
|
||||||
|
|
||||||
|
assert integer_node.value == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unclosed string literal" do
|
||||||
|
source = "'abc"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# file_node + string_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.parsing_error == "Unclosed string literal"
|
||||||
|
# Content up to EOF, processed
|
||||||
|
assert node.value == "abc"
|
||||||
|
assert node.raw_string == "'abc"
|
||||||
|
# Location: ' is [0,1,1]. Raw string "'abc" length 4. Ends offset 4, line 1, col 5.
|
||||||
|
assert node.location == [0, 1, 1, 4, 1, 5]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unclosed string literal with newlines" do
|
||||||
|
# ' at col 3, strip 2
|
||||||
|
source = " 'hello\n world"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.parsing_error == "Unclosed string literal"
|
||||||
|
assert node.value == "hello\nworld"
|
||||||
|
assert node.raw_string == "'hello\n world"
|
||||||
|
# Location of ' is [2,1,3]. Raw string "'hello\n world" length 14.
|
||||||
|
# ' (2,1,3) -> (3,1,4)
|
||||||
|
# hello (3,1,4) -> (8,1,9)
|
||||||
|
# \n (8,1,9) -> (9,2,1)
|
||||||
|
# world (9,2,1) -> (16,2,8)
|
||||||
|
# End location: offset 2+14=16. Line 2, Col 8.
|
||||||
|
assert node.location == [2, 1, 3, 16, 2, 8]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "string with only newlines and spaces, respecting indent" do
|
||||||
|
# ' at col 3, strip 2
|
||||||
|
source = " '\n \n '"
|
||||||
|
# Content: "\n \n "
|
||||||
|
# Lines: ["", " ", " "]
|
||||||
|
# Processed: "", " " (strip 2 from " "), "" (strip 2 from " ")
|
||||||
|
# Value: "\n \n"
|
||||||
|
{node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
assert node.ast_node_type == :literal_string
|
||||||
|
assert node.parent_id == file_node.id
|
||||||
|
assert node.value == "\n \n"
|
||||||
|
assert node.raw_string == "'\n \n '"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "large s-expression parse test" do
|
||||||
|
# ' at col 3, strip 2
|
||||||
|
source = """
|
||||||
|
(defn my-function (x y)
|
||||||
|
(= asd 'first line
|
||||||
|
second line
|
||||||
|
third line
|
||||||
|
asdasd')
|
||||||
|
(+ x y))
|
||||||
|
"""
|
||||||
|
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
assert map_size(nodes_map) > 0
|
||||||
|
# IO.inspect(nodes_map, limit: :infinity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "parse/2 - List Expressions" do
|
||||||
|
test "parses an empty list" do
|
||||||
|
source = "[]"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# file_node + list_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
list_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
assert list_node.ast_node_type == :list_expression
|
||||||
|
assert list_node.parent_id == file_node.id
|
||||||
|
assert list_node.children == []
|
||||||
|
# "[]"
|
||||||
|
assert list_node.location == [0, 1, 1, 2, 1, 3]
|
||||||
|
assert list_node.raw_string == "[]"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a simple list with integers" do
|
||||||
|
source = "[1 22 -3]"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node + 1 list_expression node + 3 integer nodes = 5 nodes
|
||||||
|
assert map_size(nodes_map) == 5
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
list_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(list_node)
|
||||||
|
assert list_node.ast_node_type == :list_expression
|
||||||
|
assert list_node.parent_id == file_node.id
|
||||||
|
assert length(list_node.children) == 3
|
||||||
|
|
||||||
|
children_nodes = Enum.map(list_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
assert Enum.map(children_nodes, & &1.value) == [1, 22, -3]
|
||||||
|
|
||||||
|
Enum.each(children_nodes, fn child ->
|
||||||
|
assert child.parent_id == list_node.id
|
||||||
|
assert child.ast_node_type == :literal_integer
|
||||||
|
end)
|
||||||
|
|
||||||
|
# "[1 22 -3]"
|
||||||
|
assert list_node.location == [0, 1, 1, 9, 1, 10]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a simple list with symbols" do
|
||||||
|
source = "[foo bar baz]"
|
||||||
|
{list_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# 1 file_node, 1 list_expr, 3 symbols = 5 nodes
|
||||||
|
assert map_size(nodes_map) == 5
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(list_node)
|
||||||
|
assert list_node.ast_node_type == :list_expression
|
||||||
|
assert list_node.parent_id == file_node.id
|
||||||
|
assert length(list_node.children) == 3
|
||||||
|
children_nodes = Enum.map(list_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
assert Enum.map(children_nodes, & &1.name) == ["foo", "bar", "baz"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses nested lists" do
|
||||||
|
source = "[a [b 1] c]"
|
||||||
|
{outer_list, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# Nodes: 1 file_node, outer_list, a, inner_list, c, b, 1 => 7 nodes
|
||||||
|
assert map_size(nodes_map) == 7
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(outer_list)
|
||||||
|
assert outer_list.ast_node_type == :list_expression
|
||||||
|
assert outer_list.parent_id == file_node.id
|
||||||
|
assert length(outer_list.children) == 3
|
||||||
|
|
||||||
|
# 'a'
|
||||||
|
child1 = TestHelpers.get_nth_child_node(nodes_map, 0, outer_list.id)
|
||||||
|
# '[b 1]'
|
||||||
|
inner_list = TestHelpers.get_nth_child_node(nodes_map, 1, outer_list.id)
|
||||||
|
# 'c'
|
||||||
|
child3 = TestHelpers.get_nth_child_node(nodes_map, 2, outer_list.id)
|
||||||
|
|
||||||
|
assert child1.ast_node_type == :symbol && child1.name == "a"
|
||||||
|
assert inner_list.ast_node_type == :list_expression
|
||||||
|
assert child3.ast_node_type == :symbol && child3.name == "c"
|
||||||
|
|
||||||
|
assert inner_list.parent_id == outer_list.id
|
||||||
|
assert length(inner_list.children) == 2
|
||||||
|
# 'b'
|
||||||
|
grandchild1 = TestHelpers.get_nth_child_node(nodes_map, 0, inner_list.id)
|
||||||
|
# '1'
|
||||||
|
grandchild2 = TestHelpers.get_nth_child_node(nodes_map, 1, inner_list.id)
|
||||||
|
|
||||||
|
assert grandchild1.ast_node_type == :symbol && grandchild1.name == "b"
|
||||||
|
assert grandchild2.ast_node_type == :literal_integer && grandchild2.value == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses lists with varied spacing" do
|
||||||
|
source = "[ foo 1\nbar ]"
|
||||||
|
{list_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(list_node)
|
||||||
|
assert list_node.ast_node_type == :list_expression
|
||||||
|
assert list_node.parent_id == file_node.id
|
||||||
|
assert length(list_node.children) == 3
|
||||||
|
|
||||||
|
children_names_values =
|
||||||
|
Enum.map(list_node.children, fn id ->
|
||||||
|
node = TestHelpers.get_node_by_id(nodes_map, id)
|
||||||
|
if node.ast_node_type == :symbol, do: node.name, else: node.value
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert children_names_values == ["foo", 1, "bar"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unclosed list" do
|
||||||
|
source = "[foo bar"
|
||||||
|
{list_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# Expect 1 file_node, 1 list_expression node (error), 2 symbol nodes = 4 nodes
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(list_node)
|
||||||
|
assert list_node.ast_node_type == :list_expression
|
||||||
|
assert list_node.parent_id == file_node.id
|
||||||
|
assert list_node.parsing_error == "Unclosed list"
|
||||||
|
# foo, bar
|
||||||
|
assert length(list_node.children) == 2
|
||||||
|
# "[foo bar"
|
||||||
|
assert list_node.location == [0, 1, 1, 8, 1, 9]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unexpected closing square bracket at top level" do
|
||||||
|
source = "foo ] bar"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node, foo, error_node_for_], bar = 4 nodes
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
top_level_children =
|
||||||
|
Enum.map(file_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
error_node =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :unknown && &1.raw_string == "]"))
|
||||||
|
|
||||||
|
refute is_nil(error_node)
|
||||||
|
assert error_node.parent_id == file_node.id
|
||||||
|
assert error_node.parsing_error == "Unexpected ']'"
|
||||||
|
# location of "]"
|
||||||
|
assert error_node.location == [4, 1, 5, 5, 1, 6]
|
||||||
|
|
||||||
|
symbol_foo =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :symbol && &1.name == "foo"))
|
||||||
|
|
||||||
|
refute is_nil(symbol_foo)
|
||||||
|
assert symbol_foo.parent_id == file_node.id
|
||||||
|
|
||||||
|
symbol_bar =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :symbol && &1.name == "bar"))
|
||||||
|
|
||||||
|
refute is_nil(symbol_bar)
|
||||||
|
assert symbol_bar.parent_id == file_node.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a list with mixed elements including strings, S-expressions, and other lists" do
|
||||||
|
source = "[1 'hello' (a b) [x y] 'end']"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
|
||||||
|
# Expected items: 1 outer list, 1 int, 1 str, 1 s-expr (with 2 sym children), 1 inner list (with 2 sym children), 1 str
|
||||||
|
# Node counts: outer_list (1) + int (1) + str (1) + s-expr (1) + sym_a (1) + sym_b (1) + inner_list (1) + sym_x (1) + sym_y (1) + str_end (1) = 10 nodes
|
||||||
|
# Plus 1 file_node = 11 nodes
|
||||||
|
assert map_size(nodes_map) == 11
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
outer_list_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(outer_list_node)
|
||||||
|
assert outer_list_node.ast_node_type == :list_expression
|
||||||
|
assert outer_list_node.parent_id == file_node.id
|
||||||
|
assert length(outer_list_node.children) == 5
|
||||||
|
|
||||||
|
children = Enum.map(outer_list_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
# Child 1: Integer 1
|
||||||
|
assert Enum.at(children, 0).ast_node_type == :literal_integer
|
||||||
|
assert Enum.at(children, 0).value == 1
|
||||||
|
|
||||||
|
# Child 2: String 'hello'
|
||||||
|
assert Enum.at(children, 1).ast_node_type == :literal_string
|
||||||
|
assert Enum.at(children, 1).value == "hello"
|
||||||
|
|
||||||
|
# Child 3: S-expression (a b)
|
||||||
|
s_expr_child = Enum.at(children, 2)
|
||||||
|
assert s_expr_child.ast_node_type == :s_expression
|
||||||
|
assert length(s_expr_child.children) == 2
|
||||||
|
|
||||||
|
s_expr_children =
|
||||||
|
Enum.map(s_expr_child.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
assert Enum.map(s_expr_children, & &1.name) == ["a", "b"]
|
||||||
|
|
||||||
|
# Child 4: List [x y]
|
||||||
|
inner_list_child = Enum.at(children, 3)
|
||||||
|
assert inner_list_child.ast_node_type == :list_expression
|
||||||
|
assert length(inner_list_child.children) == 2
|
||||||
|
|
||||||
|
inner_list_children =
|
||||||
|
Enum.map(inner_list_child.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
assert Enum.map(inner_list_children, & &1.name) == ["x", "y"]
|
||||||
|
|
||||||
|
# Child 5: String 'end'
|
||||||
|
assert Enum.at(children, 4).ast_node_type == :literal_string
|
||||||
|
assert Enum.at(children, 4).value == "end"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "symbol cannot contain brackets" do
|
||||||
|
# This should be parsed as symbol "foo[bar]" with current regex
|
||||||
|
source = "foo[bar]"
|
||||||
|
# After regex change to exclude brackets from symbols:
|
||||||
|
# It should be symbol "foo", then an unclosed list "[", then symbol "bar", then error for "]"
|
||||||
|
# Or, if we want `foo[bar]` to be an error or specific construct, tokenizer needs more rules.
|
||||||
|
# For now, with `[^\s\(\)\[\]]+`, "foo[bar]" is not a single symbol.
|
||||||
|
# It would be: "foo" (symbol), then "[" (start list), then "bar" (symbol), then error (unclosed list).
|
||||||
|
# Let's test "foo["
|
||||||
|
source1 = "foo["
|
||||||
|
{:ok, nodes_map1} = Parser.parse(source1)
|
||||||
|
# 1 file_node, "foo" symbol, "[" (unclosed list_expression) = 3 nodes
|
||||||
|
assert map_size(nodes_map1) == 3
|
||||||
|
file_node1 = Enum.find(Map.values(nodes_map1), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node1)
|
||||||
|
children1 = Enum.map(file_node1.children, &TestHelpers.get_node_by_id(nodes_map1, &1))
|
||||||
|
|
||||||
|
foo_node = Enum.find(children1, &(&1.name == "foo"))
|
||||||
|
list_node = Enum.find(children1, &(&1.ast_node_type == :list_expression))
|
||||||
|
|
||||||
|
refute is_nil(foo_node)
|
||||||
|
assert foo_node.parent_id == file_node1.id
|
||||||
|
refute is_nil(list_node)
|
||||||
|
assert list_node.parent_id == file_node1.id
|
||||||
|
assert list_node.parsing_error == "Unclosed list"
|
||||||
|
assert list_node.children == []
|
||||||
|
|
||||||
|
# Test "foo[bar"
|
||||||
|
source2 = "foo[bar"
|
||||||
|
{:ok, nodes_map2} = Parser.parse(source2)
|
||||||
|
# 1 file_node, "foo" symbol, "[" (list_expression), "bar" symbol inside list = 4 nodes
|
||||||
|
assert map_size(nodes_map2) == 4
|
||||||
|
file_node2 = Enum.find(Map.values(nodes_map2), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node2)
|
||||||
|
children2 = Enum.map(file_node2.children, &TestHelpers.get_node_by_id(nodes_map2, &1))
|
||||||
|
|
||||||
|
foo_node2 = Enum.find(children2, &(&1.name == "foo"))
|
||||||
|
list_node2 = Enum.find(children2, &(&1.ast_node_type == :list_expression))
|
||||||
|
|
||||||
|
refute is_nil(foo_node2)
|
||||||
|
assert foo_node2.parent_id == file_node2.id
|
||||||
|
refute is_nil(list_node2)
|
||||||
|
assert list_node2.parent_id == file_node2.id
|
||||||
|
assert list_node2.parsing_error == "Unclosed list"
|
||||||
|
assert length(list_node2.children) == 1
|
||||||
|
bar_node_id = hd(list_node2.children)
|
||||||
|
bar_node = TestHelpers.get_node_by_id(nodes_map2, bar_node_id)
|
||||||
|
assert bar_node.name == "bar"
|
||||||
|
assert bar_node.parent_id == list_node2.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
267
test/til/tuple_test.exs
Normal file
267
test/til/tuple_test.exs
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
defmodule Til.TupleParserTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
alias Til.Parser
|
||||||
|
alias Til.TestHelpers
|
||||||
|
|
||||||
|
describe "parse/2 - Tuple Expressions" do
|
||||||
|
test "parses an empty tuple" do
|
||||||
|
source = "{}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# file_node + tuple_node
|
||||||
|
assert map_size(nodes_map) == 2
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
tuple_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
assert tuple_node.parent_id == file_node.id
|
||||||
|
assert tuple_node.children == []
|
||||||
|
# "{}"
|
||||||
|
assert tuple_node.location == [0, 1, 1, 2, 1, 3]
|
||||||
|
assert tuple_node.raw_string == "{}"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a simple tuple with integers" do
|
||||||
|
source = "{1 22 -3}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node + 1 tuple_expression node + 3 integer nodes = 5 nodes
|
||||||
|
assert map_size(nodes_map) == 5
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
tuple_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(tuple_node)
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
assert tuple_node.parent_id == file_node.id
|
||||||
|
assert length(tuple_node.children) == 3
|
||||||
|
|
||||||
|
children_nodes = Enum.map(tuple_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
assert Enum.map(children_nodes, & &1.value) == [1, 22, -3]
|
||||||
|
|
||||||
|
Enum.each(children_nodes, fn child ->
|
||||||
|
assert child.parent_id == tuple_node.id
|
||||||
|
assert child.ast_node_type == :literal_integer
|
||||||
|
end)
|
||||||
|
|
||||||
|
# "{1 22 -3}"
|
||||||
|
assert tuple_node.location == [0, 1, 1, 9, 1, 10]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a simple tuple with symbols" do
|
||||||
|
source = "{foo bar baz}"
|
||||||
|
{tuple_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# 1 file_node, 1 tuple_expr, 3 symbols = 5 nodes
|
||||||
|
assert map_size(nodes_map) == 5
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(tuple_node)
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
assert tuple_node.parent_id == file_node.id
|
||||||
|
assert length(tuple_node.children) == 3
|
||||||
|
children_nodes = Enum.map(tuple_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
assert Enum.map(children_nodes, & &1.name) == ["foo", "bar", "baz"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses nested tuples" do
|
||||||
|
source = "{a {b 1} c}"
|
||||||
|
{outer_tuple, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# Nodes: 1 file_node, outer_tuple, a, inner_tuple, c, b, 1 => 7 nodes
|
||||||
|
assert map_size(nodes_map) == 7
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(outer_tuple)
|
||||||
|
assert outer_tuple.ast_node_type == :tuple_expression
|
||||||
|
assert outer_tuple.parent_id == file_node.id
|
||||||
|
assert length(outer_tuple.children) == 3
|
||||||
|
|
||||||
|
child1 = TestHelpers.get_nth_child_node(nodes_map, 0, outer_tuple.id)
|
||||||
|
inner_tuple = TestHelpers.get_nth_child_node(nodes_map, 1, outer_tuple.id)
|
||||||
|
child3 = TestHelpers.get_nth_child_node(nodes_map, 2, outer_tuple.id)
|
||||||
|
|
||||||
|
assert child1.ast_node_type == :symbol && child1.name == "a"
|
||||||
|
assert inner_tuple.ast_node_type == :tuple_expression
|
||||||
|
assert child3.ast_node_type == :symbol && child3.name == "c"
|
||||||
|
|
||||||
|
assert inner_tuple.parent_id == outer_tuple.id
|
||||||
|
assert length(inner_tuple.children) == 2
|
||||||
|
grandchild1 = TestHelpers.get_nth_child_node(nodes_map, 0, inner_tuple.id)
|
||||||
|
grandchild2 = TestHelpers.get_nth_child_node(nodes_map, 1, inner_tuple.id)
|
||||||
|
|
||||||
|
assert grandchild1.ast_node_type == :symbol && grandchild1.name == "b"
|
||||||
|
assert grandchild2.ast_node_type == :literal_integer && grandchild2.value == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses tuples with varied spacing" do
|
||||||
|
source = "{ foo 1\nbar }"
|
||||||
|
{tuple_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(tuple_node)
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
assert tuple_node.parent_id == file_node.id
|
||||||
|
assert length(tuple_node.children) == 3
|
||||||
|
|
||||||
|
children_names_values =
|
||||||
|
Enum.map(tuple_node.children, fn id ->
|
||||||
|
node = TestHelpers.get_node_by_id(nodes_map, id)
|
||||||
|
if node.ast_node_type == :symbol, do: node.name, else: node.value
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert children_names_values == ["foo", 1, "bar"]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unclosed tuple" do
|
||||||
|
source = "{foo bar"
|
||||||
|
{tuple_node, nodes_map} = TestHelpers.parse_and_get_first_node(source)
|
||||||
|
# Expect 1 file_node, 1 tuple_expression node (error), 2 symbol nodes = 4 nodes
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
refute is_nil(tuple_node)
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
assert tuple_node.parent_id == file_node.id
|
||||||
|
assert tuple_node.parsing_error == "Unclosed tuple"
|
||||||
|
# foo, bar
|
||||||
|
assert length(tuple_node.children) == 2
|
||||||
|
# "{foo bar"
|
||||||
|
assert tuple_node.location == [0, 1, 1, 8, 1, 9]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "handles unexpected closing curly brace at top level" do
|
||||||
|
source = "foo } bar"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# 1 file_node, foo, error_node_for_}, bar = 4 nodes
|
||||||
|
assert map_size(nodes_map) == 4
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
|
||||||
|
top_level_children =
|
||||||
|
Enum.map(file_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
error_node =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :unknown && &1.raw_string == "}"))
|
||||||
|
|
||||||
|
refute is_nil(error_node)
|
||||||
|
assert error_node.parent_id == file_node.id
|
||||||
|
assert error_node.parsing_error == "Unexpected '}'"
|
||||||
|
# location of "}"
|
||||||
|
assert error_node.location == [4, 1, 5, 5, 1, 6]
|
||||||
|
|
||||||
|
symbol_foo =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :symbol && &1.name == "foo"))
|
||||||
|
|
||||||
|
refute is_nil(symbol_foo)
|
||||||
|
assert symbol_foo.parent_id == file_node.id
|
||||||
|
|
||||||
|
symbol_bar =
|
||||||
|
Enum.find(top_level_children, &(&1.ast_node_type == :symbol && &1.name == "bar"))
|
||||||
|
|
||||||
|
refute is_nil(symbol_bar)
|
||||||
|
assert symbol_bar.parent_id == file_node.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses a tuple with mixed elements including strings, S-expressions, lists, and other tuples" do
|
||||||
|
source = "{1 'hello' (a b) [x y] {z} 'end'}"
|
||||||
|
{:ok, nodes_map} = Parser.parse(source)
|
||||||
|
|
||||||
|
# Expected items: 1 outer tuple, 1 int, 1 str, 1 s-expr (2 children), 1 list (2 children), 1 inner tuple (1 child), 1 str_end
|
||||||
|
# Node counts: outer_tuple (1) + int (1) + str (1) + s-expr (1) + sym_a (1) + sym_b (1) + list (1) + sym_x (1) + sym_y (1) + inner_tuple (1) + sym_z (1) + str_end (1) = 12 nodes
|
||||||
|
# Plus 1 file_node = 13 nodes
|
||||||
|
assert map_size(nodes_map) == 13
|
||||||
|
|
||||||
|
file_node = Enum.find(Map.values(nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node)
|
||||||
|
outer_tuple_node = TestHelpers.get_first_child_node(nodes_map)
|
||||||
|
|
||||||
|
refute is_nil(outer_tuple_node)
|
||||||
|
assert outer_tuple_node.ast_node_type == :tuple_expression
|
||||||
|
assert outer_tuple_node.parent_id == file_node.id
|
||||||
|
assert length(outer_tuple_node.children) == 6
|
||||||
|
|
||||||
|
children = Enum.map(outer_tuple_node.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
assert Enum.at(children, 0).ast_node_type == :literal_integer
|
||||||
|
assert Enum.at(children, 0).value == 1
|
||||||
|
|
||||||
|
assert Enum.at(children, 1).ast_node_type == :literal_string
|
||||||
|
assert Enum.at(children, 1).value == "hello"
|
||||||
|
|
||||||
|
s_expr_child = Enum.at(children, 2)
|
||||||
|
assert s_expr_child.ast_node_type == :s_expression
|
||||||
|
assert length(s_expr_child.children) == 2
|
||||||
|
|
||||||
|
s_expr_children =
|
||||||
|
Enum.map(s_expr_child.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
assert Enum.map(s_expr_children, & &1.name) == ["a", "b"]
|
||||||
|
|
||||||
|
list_child = Enum.at(children, 3)
|
||||||
|
assert list_child.ast_node_type == :list_expression
|
||||||
|
assert length(list_child.children) == 2
|
||||||
|
list_children = Enum.map(list_child.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
assert Enum.map(list_children, & &1.name) == ["x", "y"]
|
||||||
|
|
||||||
|
inner_tuple_child = Enum.at(children, 4)
|
||||||
|
assert inner_tuple_child.ast_node_type == :tuple_expression
|
||||||
|
assert length(inner_tuple_child.children) == 1
|
||||||
|
|
||||||
|
inner_tuple_children =
|
||||||
|
Enum.map(inner_tuple_child.children, &TestHelpers.get_node_by_id(nodes_map, &1))
|
||||||
|
|
||||||
|
assert Enum.map(inner_tuple_children, & &1.name) == ["z"]
|
||||||
|
|
||||||
|
assert Enum.at(children, 5).ast_node_type == :literal_string
|
||||||
|
assert Enum.at(children, 5).value == "end"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "symbol cannot contain curly braces" do
|
||||||
|
# Test "foo{"
|
||||||
|
source1 = "foo{"
|
||||||
|
{:ok, nodes_map1} = Parser.parse(source1)
|
||||||
|
# 1 file_node, "foo" symbol, "{" (unclosed tuple_expression) = 3 nodes
|
||||||
|
assert map_size(nodes_map1) == 3
|
||||||
|
file_node1 = Enum.find(Map.values(nodes_map1), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node1)
|
||||||
|
children1 = Enum.map(file_node1.children, &TestHelpers.get_node_by_id(nodes_map1, &1))
|
||||||
|
|
||||||
|
foo_node = Enum.find(children1, &(&1.name == "foo"))
|
||||||
|
tuple_node = Enum.find(children1, &(&1.ast_node_type == :tuple_expression))
|
||||||
|
|
||||||
|
refute is_nil(foo_node)
|
||||||
|
assert foo_node.parent_id == file_node1.id
|
||||||
|
refute is_nil(tuple_node)
|
||||||
|
assert tuple_node.parent_id == file_node1.id
|
||||||
|
assert tuple_node.parsing_error == "Unclosed tuple"
|
||||||
|
assert tuple_node.children == []
|
||||||
|
|
||||||
|
# Test "foo{bar"
|
||||||
|
source2 = "foo{bar"
|
||||||
|
{:ok, nodes_map2} = Parser.parse(source2)
|
||||||
|
# 1 file_node, "foo" symbol, "{" (tuple_expression), "bar" symbol inside tuple = 4 nodes
|
||||||
|
assert map_size(nodes_map2) == 4
|
||||||
|
file_node2 = Enum.find(Map.values(nodes_map2), &(&1.ast_node_type == :file))
|
||||||
|
refute is_nil(file_node2)
|
||||||
|
children2 = Enum.map(file_node2.children, &TestHelpers.get_node_by_id(nodes_map2, &1))
|
||||||
|
|
||||||
|
foo_node2 = Enum.find(children2, &(&1.name == "foo"))
|
||||||
|
tuple_node2 = Enum.find(children2, &(&1.ast_node_type == :tuple_expression))
|
||||||
|
|
||||||
|
refute is_nil(foo_node2)
|
||||||
|
assert foo_node2.parent_id == file_node2.id
|
||||||
|
refute is_nil(tuple_node2)
|
||||||
|
assert tuple_node2.parent_id == file_node2.id
|
||||||
|
assert tuple_node2.parsing_error == "Unclosed tuple"
|
||||||
|
assert length(tuple_node2.children) == 1
|
||||||
|
bar_node_id = hd(tuple_node2.children)
|
||||||
|
bar_node = TestHelpers.get_node_by_id(nodes_map2, bar_node_id)
|
||||||
|
assert bar_node.name == "bar"
|
||||||
|
assert bar_node.parent_id == tuple_node2.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
291
test/til/type_annotation_test.exs
Normal file
291
test/til/type_annotation_test.exs
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
defmodule Til.TypeAnnotationTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Til.AstUtils
|
||||||
|
# alias Til.Parser # Unused
|
||||||
|
# alias Til.Typer # Unused
|
||||||
|
# Added alias
|
||||||
|
alias Til.TestHelpers
|
||||||
|
|
||||||
|
# --- Predefined Type Definitions for Assertions (without :id) ---
|
||||||
|
# These must match the definitions in Typer, excluding the :id field
|
||||||
|
# that Typer might add when interning.
|
||||||
|
@type_integer %{type_kind: :primitive, name: :integer}
|
||||||
|
@type_string %{type_kind: :primitive, name: :string}
|
||||||
|
@type_number %{type_kind: :primitive, name: :number}
|
||||||
|
@type_any %{type_kind: :primitive, name: :any}
|
||||||
|
# @type_nothing %{type_kind: :primitive, name: :nothing} # Unused
|
||||||
|
# @type_annotation_mismatch_error is replaced by a helper function
|
||||||
|
|
||||||
|
# Helper to create the expected *cleaned* type_annotation_mismatch error structure
|
||||||
|
defp type_error_type_annotation_mismatch(actual_type_clean, expected_type_clean) do
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :type_annotation_mismatch,
|
||||||
|
actual_type: actual_type_clean,
|
||||||
|
expected_type: expected_type_clean
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Union type for testing
|
||||||
|
# @type_integer_or_string %{ # Unused
|
||||||
|
# type_kind: :union,
|
||||||
|
# types: MapSet.new([@type_integer, @type_string])
|
||||||
|
# }
|
||||||
|
|
||||||
|
defp type_literal_int(val), do: %{type_kind: :literal, value: val}
|
||||||
|
defp type_literal_string(val), do: %{type_kind: :literal, value: val}
|
||||||
|
|
||||||
|
defp type_union(type_defs_list) do
|
||||||
|
%{type_kind: :union, types: MapSet.new(type_defs_list)}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "(the <type> <expr>) annotation" do
|
||||||
|
test "annotates a literal integer with its correct type" do
|
||||||
|
source = "(the integer 42)"
|
||||||
|
{the_expr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
assert the_expr_node.ast_node_type == :s_expression
|
||||||
|
|
||||||
|
# The 'the' expression itself should have the annotated type
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node, typed_nodes_map, @type_integer)
|
||||||
|
|
||||||
|
# The inner literal should still have its literal type
|
||||||
|
literal_node = TestHelpers.get_nth_child_node(typed_nodes_map, 2, the_expr_node.id)
|
||||||
|
TestHelpers.assert_node_typed_as(literal_node, typed_nodes_map, type_literal_int(42))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "annotates a literal integer with a supertype (number)" do
|
||||||
|
source = "(the number 42)"
|
||||||
|
{the_expr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node, typed_nodes_map, @type_number)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "annotates a literal string with its correct type" do
|
||||||
|
source = "(the string 'hello')"
|
||||||
|
{the_expr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node, typed_nodes_map, @type_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "annotation mismatch: annotating integer literal as string results in error type" do
|
||||||
|
source = "(the string 42)"
|
||||||
|
{the_expr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
# The 'the' expression should now be typed with an error type
|
||||||
|
# because 42 is not a subtype of string.
|
||||||
|
expected_error_def =
|
||||||
|
type_error_type_annotation_mismatch(type_literal_int(42), @type_string)
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(
|
||||||
|
the_expr_node,
|
||||||
|
typed_nodes_map,
|
||||||
|
expected_error_def
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "assignment with annotated value: (= x (the integer 42))" do
|
||||||
|
source = "(= x (the integer 42))"
|
||||||
|
{assignment_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
assert assignment_node.ast_node_type == :s_expression
|
||||||
|
|
||||||
|
# The assignment expression's type is the type of its RHS (the annotated value)
|
||||||
|
TestHelpers.assert_node_typed_as(assignment_node, typed_nodes_map, @type_integer)
|
||||||
|
|
||||||
|
# The 'the' sub-expression
|
||||||
|
the_expr_node = TestHelpers.get_nth_child_node(typed_nodes_map, 2, assignment_node.id)
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node, typed_nodes_map, @type_integer)
|
||||||
|
|
||||||
|
# Check symbol 'x' in the environment (indirectly by typing another expression)
|
||||||
|
source_with_x = """
|
||||||
|
(= x (the integer 42))
|
||||||
|
x
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 0 is assignment, 1 is 'x'
|
||||||
|
{x_usage_node, typed_nodes_map_vx} =
|
||||||
|
TestHelpers.typecheck_and_get_nth_node(source_with_x, 1)
|
||||||
|
|
||||||
|
assert x_usage_node.ast_node_type == :symbol
|
||||||
|
TestHelpers.assert_node_typed_as(x_usage_node, typed_nodes_map_vx, @type_integer)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "annotating with 'any' type" do
|
||||||
|
source = "(the any 42)"
|
||||||
|
{the_expr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node, typed_nodes_map, @type_any)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "annotating with 'nothing' type (implies contradiction if expr is not nothing)" do
|
||||||
|
# 42 is not 'nothing'
|
||||||
|
source = "(the nothing 42)"
|
||||||
|
{the_expr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
AstUtils.build_debug_ast_data(typed_nodes_map)
|
||||||
|
|
||||||
|
# The expression results in a type annotation mismatch error
|
||||||
|
# because 'literal 42' is not a subtype of 'nothing'.
|
||||||
|
type_nothing_clean = %{type_kind: :primitive, name: :nothing}
|
||||||
|
|
||||||
|
expected_error_def =
|
||||||
|
type_error_type_annotation_mismatch(type_literal_int(42), type_nothing_clean)
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(
|
||||||
|
the_expr_node,
|
||||||
|
typed_nodes_map,
|
||||||
|
expected_error_def
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "unknown type symbol in annotation defaults to 'any'" do
|
||||||
|
# 'foobar' is not a known type
|
||||||
|
source = "(the foobar 42)"
|
||||||
|
{the_expr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
# Defaulting to 'any' for unknown type specifiers for now
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node, typed_nodes_map, @type_any)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "annotating an (if true 1 's') expression with (integer | string) union type" do
|
||||||
|
_source = "(the (union integer string) (if true 1 's'))"
|
||||||
|
# For the Typer to resolve (union integer string) correctly, we need to implement
|
||||||
|
# parsing/handling of such type specifiers first.
|
||||||
|
# For now, this test assumes `resolve_type_specifier_node` can handle it or we mock it.
|
||||||
|
# Let's adjust the test to use a known type that an `if` can produce,
|
||||||
|
# and then check subtyping against a broader union.
|
||||||
|
|
||||||
|
# Test: integer is subtype of (integer | string)
|
||||||
|
_source_int_subtype_of_union = "(the (union integer string) 42)"
|
||||||
|
# This requires `resolve_type_specifier_node` to handle `(union ...)` forms.
|
||||||
|
# For now, let's test the subtyping logic more directly by constructing types.
|
||||||
|
# The tests below will focus on `if` expressions producing unions and checking them.
|
||||||
|
end
|
||||||
|
|
||||||
|
test "if expression producing (literal 1 | literal 's') is subtype of (integer | string)" do
|
||||||
|
_source = """
|
||||||
|
(the (union integer string)
|
||||||
|
(if some_condition 1 's'))
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This test requires:
|
||||||
|
# 1. `(union integer string)` to be resolvable by `resolve_type_specifier_node`.
|
||||||
|
# For now, we'll assume `Typer` needs to be taught to parse this.
|
||||||
|
# Let's simplify the annotation to a known primitive that acts as a supertype for the union.
|
||||||
|
# e.g. (the any (if ...))
|
||||||
|
#
|
||||||
|
# Let's test the type inference of `if` and then `(the ...)` with it.
|
||||||
|
# `(if x 1 "s")` should infer to `(union (literal 1) (literal "s"))` if x is unknown.
|
||||||
|
# Then `(the (union integer string) (if x 1 "s"))` should work.
|
||||||
|
|
||||||
|
# To make this testable *now* without changing `resolve_type_specifier_node` for `(union ...)`:
|
||||||
|
# We need a way to introduce a union type into the system that `is_subtype?` can then check.
|
||||||
|
# The `if` expression itself is the source of the union type.
|
||||||
|
|
||||||
|
# `(if some_condition 1 "foo")` will have type `Union(Literal(1), Literal("foo"))`
|
||||||
|
# We want to check if `Union(Literal(1), Literal("foo"))` is a subtype of `Union(Integer, String)`
|
||||||
|
# And also if `Union(Literal(1), Literal("foo"))` is a subtype of `Any`
|
||||||
|
|
||||||
|
# Setup:
|
||||||
|
# (= cond_val true) ; or some unknown symbol to make the `if` branch ambiguous
|
||||||
|
# (the some_union_type (if cond_val 1 "hello"))
|
||||||
|
# where `some_union_type` is a type that `Union(Literal(1), Literal("hello"))` is a subtype of.
|
||||||
|
|
||||||
|
# Test 1: Union is subtype of Any
|
||||||
|
source_if_any = """
|
||||||
|
(= cond some_unknown_symbol_for_ambiguity)
|
||||||
|
(the any (if cond 1 "hello"))
|
||||||
|
"""
|
||||||
|
|
||||||
|
{the_expr_node_any, typed_nodes_map_any} =
|
||||||
|
TestHelpers.typecheck_and_get_nth_node(source_if_any, 1)
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node_any, typed_nodes_map_any, @type_any)
|
||||||
|
|
||||||
|
# Test 2: Literal integer is subtype of (integer | string)
|
||||||
|
# This requires `(union integer string)` to be a recognized type specifier.
|
||||||
|
# We will add this to `resolve_type_specifier_node` in Typer later.
|
||||||
|
# For now, let's assume we have a way to define such a type.
|
||||||
|
# The following tests are more conceptual for the subtyping logic itself,
|
||||||
|
# rather than the full `(the (union ...) ...)` syntax.
|
||||||
|
|
||||||
|
# Let's test the `if` expression's inferred type first.
|
||||||
|
source_if_expr = "(= cond some_ambiguous_val) (if cond 1 'one')"
|
||||||
|
{if_node, typed_nodes_map_if} = TestHelpers.typecheck_and_get_nth_node(source_if_expr, 1)
|
||||||
|
|
||||||
|
expected_if_type =
|
||||||
|
type_union([type_literal_int(1), type_literal_string("one")])
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(if_node, typed_nodes_map_if, expected_if_type)
|
||||||
|
|
||||||
|
# Now, let's assume `(the (union integer string) ...)` works by enhancing `resolve_type_specifier_node`
|
||||||
|
# This part of the test will *fail* until `resolve_type_specifier_node` is updated.
|
||||||
|
# We'll mark it as a future improvement or adjust `Typer` in a subsequent step.
|
||||||
|
# For now, we are testing the subtyping rules given the types are correctly resolved.
|
||||||
|
|
||||||
|
# To test subtyping with (the X Y) where Y is a union:
|
||||||
|
# (the super_type (if cond val1 val2))
|
||||||
|
# Example: (the any (if cond 1 "s")) -- already covered, works.
|
||||||
|
|
||||||
|
# Example: (the number (if cond 1 2.0))
|
||||||
|
# (if cond 1 2.0) -> Union(Literal(1), Literal(2.0))
|
||||||
|
# Union(Literal(1), Literal(2.0)) <: Number ?
|
||||||
|
# Literal(1) <: Number (true) AND Literal(2.0) <: Number (true) -> true
|
||||||
|
source_if_number = """
|
||||||
|
(= cond some_ambiguous_val)
|
||||||
|
(the number (if cond 1 2))
|
||||||
|
"""
|
||||||
|
|
||||||
|
{the_expr_node_num, typed_nodes_map_num} =
|
||||||
|
TestHelpers.typecheck_and_get_nth_node(source_if_number, 1)
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node_num, typed_nodes_map_num, @type_number)
|
||||||
|
|
||||||
|
# Example: (the integer (if cond 1 2))
|
||||||
|
# (if cond 1 2) -> Union(Literal(1), Literal(2))
|
||||||
|
# Union(Literal(1), Literal(2)) <: Integer ?
|
||||||
|
# Literal(1) <: Integer (true) AND Literal(2) <: Integer (true) -> true
|
||||||
|
source_if_integer = """
|
||||||
|
(= cond some_ambiguous_val)
|
||||||
|
(the integer (if cond 1 2))
|
||||||
|
"""
|
||||||
|
|
||||||
|
{the_expr_node_int, typed_nodes_map_int} =
|
||||||
|
TestHelpers.typecheck_and_get_nth_node(source_if_integer, 1)
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node_int, typed_nodes_map_int, @type_integer)
|
||||||
|
|
||||||
|
# Example: (the string (if cond 1 "s")) -> should be error
|
||||||
|
# (if cond 1 "s") -> Union(Literal(1), Literal("s"))
|
||||||
|
# Union(Literal(1), Literal("s")) <: String ?
|
||||||
|
# Literal(1) <: String (false) -> false
|
||||||
|
source_if_string_error = """
|
||||||
|
(= cond some_ambiguous_val)
|
||||||
|
(the string (if cond 1 's'))
|
||||||
|
"""
|
||||||
|
|
||||||
|
{the_expr_node_str_err, typed_nodes_map_str_err} =
|
||||||
|
TestHelpers.typecheck_and_get_nth_node(source_if_string_error, 1)
|
||||||
|
|
||||||
|
# actual type of (if cond 1 "s") is Union(Literal(1), Literal("s"))
|
||||||
|
# expected type is String
|
||||||
|
actual_type_clean = type_union([type_literal_int(1), type_literal_string("s")])
|
||||||
|
expected_annotated_clean = @type_string
|
||||||
|
|
||||||
|
expected_error_def =
|
||||||
|
type_error_type_annotation_mismatch(actual_type_clean, expected_annotated_clean)
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(
|
||||||
|
the_expr_node_str_err,
|
||||||
|
typed_nodes_map_str_err,
|
||||||
|
expected_error_def
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Test for: A <: (A | B)
|
||||||
|
test "integer is subtype of (integer | string) - requires (union ...) type specifier" do
|
||||||
|
source = "(the (union integer string) 42)"
|
||||||
|
{the_expr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
expected_union_type = type_union([@type_integer, @type_string])
|
||||||
|
TestHelpers.assert_node_typed_as(the_expr_node, typed_nodes_map, expected_union_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
257
test/til/type_atom_test.exs
Normal file
257
test/til/type_atom_test.exs
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
defmodule Til.TypeAtomTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
# alias Til.Parser # Unused
|
||||||
|
# alias Til.Typer # Unused
|
||||||
|
alias Til.Typer.Types
|
||||||
|
alias Til.Typer.SubtypeChecker
|
||||||
|
alias Til.Typer.Interner
|
||||||
|
|
||||||
|
import Til.TestHelpers
|
||||||
|
|
||||||
|
# Helper for literal atom type definition (cleaned, without :id)
|
||||||
|
defp type_literal_atom(val) when is_atom(val) do
|
||||||
|
%{type_kind: :literal, value: val}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper for primitive atom type definition (cleaned, without :id)
|
||||||
|
defp type_primitive_atom do
|
||||||
|
%{type_kind: :primitive, name: :atom}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper for primitive integer type definition (cleaned, without :id)
|
||||||
|
defp type_primitive_integer do
|
||||||
|
%{type_kind: :primitive, name: :integer}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Helper for type annotation mismatch error (cleaned structure)
|
||||||
|
defp type_error_type_annotation_mismatch(actual_type_clean, expected_type_clean) do
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :type_annotation_mismatch,
|
||||||
|
actual_type: actual_type_clean,
|
||||||
|
expected_type: expected_type_clean
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Atom Literal Typing" do
|
||||||
|
test "a literal atom is typed as a literal atom" do
|
||||||
|
source = ":my_atom"
|
||||||
|
{node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
assert_node_typed_as(node, typed_nodes_map, type_literal_atom(:my_atom))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "a different literal atom is typed correctly" do
|
||||||
|
source = ":another"
|
||||||
|
{node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
assert_node_typed_as(node, typed_nodes_map, type_literal_atom(:another))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "'the' expression with 'atom' type specifier" do
|
||||||
|
test "(the atom :some_atom) is typed as primitive atom" do
|
||||||
|
source = "(the atom :some_atom)"
|
||||||
|
{the_expr_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
# The 'the' expression itself should have the annotated type
|
||||||
|
assert_node_typed_as(the_expr_node, typed_nodes_map, type_primitive_atom())
|
||||||
|
|
||||||
|
# Verify the inner atom is typed as literal
|
||||||
|
# Children of s_expr: 'the' symbol, 'atom' symbol, ':some_atom' atom_literal
|
||||||
|
inner_atom_node_id = Enum.at(the_expr_node.children, 2)
|
||||||
|
inner_atom_node = get_node_by_id(typed_nodes_map, inner_atom_node_id)
|
||||||
|
assert_node_typed_as(inner_atom_node, typed_nodes_map, type_literal_atom(:some_atom))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "(the atom 123) results in a type annotation mismatch error" do
|
||||||
|
source = "(the atom 123)"
|
||||||
|
{the_expr_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
# Literal integer
|
||||||
|
actual_clean = %{type_kind: :literal, value: 123}
|
||||||
|
# Primitive atom
|
||||||
|
expected_clean = type_primitive_atom()
|
||||||
|
|
||||||
|
expected_error_def = type_error_type_annotation_mismatch(actual_clean, expected_clean)
|
||||||
|
assert_node_typed_as(the_expr_node, typed_nodes_map, expected_error_def)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "(the atom \"a string\") results in a type annotation mismatch error" do
|
||||||
|
source = "(the atom 'a string')"
|
||||||
|
{the_expr_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
# Literal string
|
||||||
|
actual_clean = %{type_kind: :literal, value: "a string"}
|
||||||
|
# Primitive atom
|
||||||
|
expected_clean = type_primitive_atom()
|
||||||
|
|
||||||
|
expected_error_def = type_error_type_annotation_mismatch(actual_clean, expected_clean)
|
||||||
|
assert_node_typed_as(the_expr_node, typed_nodes_map, expected_error_def)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Atom Subtyping" do
|
||||||
|
# Setup a nodes_map with predefined types for subtyping checks
|
||||||
|
defp setup_nodes_map_for_subtyping do
|
||||||
|
Interner.populate_known_types(%{})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "literal atom is a subtype of primitive atom" do
|
||||||
|
nodes_map_initial = setup_nodes_map_for_subtyping()
|
||||||
|
# This is %{type_kind: :literal, value: :foo}
|
||||||
|
literal_foo_raw = type_literal_atom(:foo)
|
||||||
|
|
||||||
|
# Intern the literal type
|
||||||
|
{foo_key, nodes_map_after_foo} =
|
||||||
|
Interner.get_or_intern_type(literal_foo_raw, nodes_map_initial)
|
||||||
|
|
||||||
|
literal_foo_interned = Map.get(nodes_map_after_foo, foo_key)
|
||||||
|
refute is_nil(literal_foo_interned), "Interned literal :foo type should exist"
|
||||||
|
|
||||||
|
# Get the interned primitive type from the map
|
||||||
|
primitive_atom_interned = Map.get(nodes_map_after_foo, Types.primitive_type_key(:atom))
|
||||||
|
refute is_nil(primitive_atom_interned), "Primitive atom type should be in nodes_map"
|
||||||
|
|
||||||
|
assert SubtypeChecker.is_subtype?(
|
||||||
|
literal_foo_interned,
|
||||||
|
primitive_atom_interned,
|
||||||
|
nodes_map_after_foo
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "literal atom is a subtype of itself" do
|
||||||
|
nodes_map_initial = setup_nodes_map_for_subtyping()
|
||||||
|
literal_foo_raw = type_literal_atom(:foo)
|
||||||
|
|
||||||
|
{foo_key, nodes_map_after_foo} =
|
||||||
|
Interner.get_or_intern_type(literal_foo_raw, nodes_map_initial)
|
||||||
|
|
||||||
|
literal_foo_interned = Map.get(nodes_map_after_foo, foo_key)
|
||||||
|
refute is_nil(literal_foo_interned), "Interned literal :foo type should exist"
|
||||||
|
|
||||||
|
assert SubtypeChecker.is_subtype?(
|
||||||
|
literal_foo_interned,
|
||||||
|
literal_foo_interned,
|
||||||
|
nodes_map_after_foo
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "literal atom :foo is not a subtype of literal atom :bar" do
|
||||||
|
nodes_map_initial = setup_nodes_map_for_subtyping()
|
||||||
|
literal_foo_raw = type_literal_atom(:foo)
|
||||||
|
literal_bar_raw = type_literal_atom(:bar)
|
||||||
|
|
||||||
|
{foo_key, nodes_map_temp} = Interner.get_or_intern_type(literal_foo_raw, nodes_map_initial)
|
||||||
|
literal_foo_interned = Map.get(nodes_map_temp, foo_key)
|
||||||
|
refute is_nil(literal_foo_interned)
|
||||||
|
|
||||||
|
{bar_key, nodes_map_final} = Interner.get_or_intern_type(literal_bar_raw, nodes_map_temp)
|
||||||
|
literal_bar_interned = Map.get(nodes_map_final, bar_key)
|
||||||
|
refute is_nil(literal_bar_interned)
|
||||||
|
|
||||||
|
refute SubtypeChecker.is_subtype?(
|
||||||
|
literal_foo_interned,
|
||||||
|
literal_bar_interned,
|
||||||
|
nodes_map_final
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "primitive integer is not a subtype of primitive atom" do
|
||||||
|
nodes_map = setup_nodes_map_for_subtyping()
|
||||||
|
|
||||||
|
primitive_int_interned = Map.get(nodes_map, Types.primitive_type_key(:integer))
|
||||||
|
refute is_nil(primitive_int_interned), "Primitive integer type should be in nodes_map"
|
||||||
|
|
||||||
|
primitive_atom_interned = Map.get(nodes_map, Types.primitive_type_key(:atom))
|
||||||
|
refute is_nil(primitive_atom_interned), "Primitive atom type should be in nodes_map"
|
||||||
|
|
||||||
|
refute SubtypeChecker.is_subtype?(
|
||||||
|
primitive_int_interned,
|
||||||
|
primitive_atom_interned,
|
||||||
|
nodes_map
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "primitive atom is not a subtype of primitive integer" do
|
||||||
|
nodes_map = setup_nodes_map_for_subtyping()
|
||||||
|
|
||||||
|
primitive_atom_interned = Map.get(nodes_map, Types.primitive_type_key(:atom))
|
||||||
|
refute is_nil(primitive_atom_interned), "Primitive atom type should be in nodes_map"
|
||||||
|
|
||||||
|
primitive_int_interned = Map.get(nodes_map, Types.primitive_type_key(:integer))
|
||||||
|
refute is_nil(primitive_int_interned), "Primitive integer type should be in nodes_map"
|
||||||
|
|
||||||
|
refute SubtypeChecker.is_subtype?(
|
||||||
|
primitive_atom_interned,
|
||||||
|
primitive_int_interned,
|
||||||
|
nodes_map
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "literal atom is a subtype of :any" do
|
||||||
|
nodes_map_initial = setup_nodes_map_for_subtyping()
|
||||||
|
literal_foo_raw = type_literal_atom(:foo)
|
||||||
|
|
||||||
|
{foo_key, nodes_map_after_foo} =
|
||||||
|
Interner.get_or_intern_type(literal_foo_raw, nodes_map_initial)
|
||||||
|
|
||||||
|
literal_foo_interned = Map.get(nodes_map_after_foo, foo_key)
|
||||||
|
refute is_nil(literal_foo_interned), "Interned literal :foo type should exist"
|
||||||
|
|
||||||
|
any_type_interned = Map.get(nodes_map_after_foo, Types.any_type_key())
|
||||||
|
refute is_nil(any_type_interned), ":any type should be in nodes_map"
|
||||||
|
|
||||||
|
assert SubtypeChecker.is_subtype?(
|
||||||
|
literal_foo_interned,
|
||||||
|
any_type_interned,
|
||||||
|
nodes_map_after_foo
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "primitive atom is a subtype of :any" do
|
||||||
|
nodes_map = setup_nodes_map_for_subtyping()
|
||||||
|
|
||||||
|
primitive_atom_interned = Map.get(nodes_map, Types.primitive_type_key(:atom))
|
||||||
|
refute is_nil(primitive_atom_interned), "Primitive atom type should be in nodes_map"
|
||||||
|
|
||||||
|
any_type_interned = Map.get(nodes_map, Types.any_type_key())
|
||||||
|
refute is_nil(any_type_interned), ":any type should be in nodes_map"
|
||||||
|
|
||||||
|
assert SubtypeChecker.is_subtype?(primitive_atom_interned, any_type_interned, nodes_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
test ":nothing is a subtype of primitive atom" do
|
||||||
|
nodes_map = setup_nodes_map_for_subtyping()
|
||||||
|
|
||||||
|
nothing_type_interned = Map.get(nodes_map, Types.primitive_type_key(:nothing))
|
||||||
|
refute is_nil(nothing_type_interned), ":nothing type should be in nodes_map"
|
||||||
|
|
||||||
|
primitive_atom_interned = Map.get(nodes_map, Types.primitive_type_key(:atom))
|
||||||
|
refute is_nil(primitive_atom_interned), "Primitive atom type should be in nodes_map"
|
||||||
|
|
||||||
|
assert SubtypeChecker.is_subtype?(nothing_type_interned, primitive_atom_interned, nodes_map)
|
||||||
|
end
|
||||||
|
|
||||||
|
test ":nothing is a subtype of literal atom :foo (this is debatable, but current rule is :nothing <: all)" do
|
||||||
|
nodes_map_initial = setup_nodes_map_for_subtyping()
|
||||||
|
|
||||||
|
nothing_type_interned = Map.get(nodes_map_initial, Types.primitive_type_key(:nothing))
|
||||||
|
refute is_nil(nothing_type_interned), ":nothing type should be in nodes_map"
|
||||||
|
|
||||||
|
literal_foo_raw = type_literal_atom(:foo)
|
||||||
|
|
||||||
|
{foo_key, nodes_map_after_foo} =
|
||||||
|
Interner.get_or_intern_type(literal_foo_raw, nodes_map_initial)
|
||||||
|
|
||||||
|
literal_foo_interned = Map.get(nodes_map_after_foo, foo_key)
|
||||||
|
refute is_nil(literal_foo_interned), "Interned literal :foo type should exist"
|
||||||
|
|
||||||
|
# According to Rule 3: :nothing is a subtype of everything
|
||||||
|
assert SubtypeChecker.is_subtype?(
|
||||||
|
nothing_type_interned,
|
||||||
|
literal_foo_interned,
|
||||||
|
nodes_map_after_foo
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
135
test/til/type_function_allocation_test.exs
Normal file
135
test/til/type_function_allocation_test.exs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# defmodule Til.TypeFunctionAllocationTest do
|
||||||
|
# use ExUnit.Case, async: true
|
||||||
|
#
|
||||||
|
# alias Til.Typer.Interner
|
||||||
|
# alias Til.Parser
|
||||||
|
# alias Til.Typer
|
||||||
|
# # alias Til.Typer.SubtypeChecker # Not directly used in all tests yet
|
||||||
|
# import Til.TestHelpers
|
||||||
|
#
|
||||||
|
# # Helper functions for expected error types
|
||||||
|
# defp type_error_not_a_function(actual_operator_type_id_clean) do
|
||||||
|
# %{
|
||||||
|
# type_kind: :error,
|
||||||
|
# reason: :not_a_function,
|
||||||
|
# actual_operator_type_id: actual_operator_type_id_clean
|
||||||
|
# }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# defp type_error_arity_mismatch(expected_arity, actual_arity, function_type_id_clean) do
|
||||||
|
# %{
|
||||||
|
# type_kind: :error,
|
||||||
|
# reason: :arity_mismatch,
|
||||||
|
# expected_arity: expected_arity,
|
||||||
|
# actual_arity: actual_arity,
|
||||||
|
# function_type_id: function_type_id_clean
|
||||||
|
# }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# describe "Phase 3: Basic Monomorphic Function Calls" do
|
||||||
|
# test "types a call to a literal lambda with no args: ((fn () 1))" do
|
||||||
|
# {call_node, typed_nodes_map} = typecheck_and_get_first_node("((fn () 1))")
|
||||||
|
# expected_return_type_raw = %{type_kind: :literal, value: 1}
|
||||||
|
# assert_node_typed_as(call_node, typed_nodes_map, expected_return_type_raw)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a call to a literal lambda with one arg: ((fn ((x string) string) x) 'hello')" do
|
||||||
|
# source = "((fn ((x string) string) x) 'hello')"
|
||||||
|
# {call_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
# expected_return_type_raw = %{type_kind: :literal, value: "hello"}
|
||||||
|
# assert_node_typed_as(call_node, typed_nodes_map, expected_return_type_raw)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a call to a literal lambda with multiple args: ((fn ((a integer) (b atom) atom) b) 10 :foo)" do
|
||||||
|
# source = "((fn ((a integer) (b atom) atom) b) 10 :foo)"
|
||||||
|
# {call_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
# expected_return_type_raw = %{type_kind: :literal, value: :foo}
|
||||||
|
# assert_node_typed_as(call_node, typed_nodes_map, expected_return_type_raw)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "error: calling a non-function (integer): (1 2 3)" do
|
||||||
|
# {call_node, typed_nodes_map} = typecheck_and_get_first_node("(1 2 3)")
|
||||||
|
# raw_literal_int_1_type = %{type_kind: :literal, value: 1}
|
||||||
|
# # Interner.populate_known_types is called by typecheck_and_get_first_node
|
||||||
|
# # so typed_nodes_map should already have primitives.
|
||||||
|
# # We need to intern the specific literal type to get its key.
|
||||||
|
# {literal_int_1_key, _final_map_after_intern} =
|
||||||
|
# Interner.get_or_intern_type(raw_literal_int_1_type, typed_nodes_map)
|
||||||
|
#
|
||||||
|
# expected_error_raw = type_error_not_a_function(literal_int_1_key)
|
||||||
|
# assert_node_typed_as(call_node, typed_nodes_map, expected_error_raw)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "error: arity mismatch - too many arguments: ((fn () 1) 123)" do
|
||||||
|
# {call_node, typed_nodes_map} = typecheck_and_get_first_node("((fn () 1) 123)")
|
||||||
|
# # The S-expression is the call_node. Its first child is the lambda S-expression.
|
||||||
|
# # The lambda S-expression node itself is what gets typed as a function.
|
||||||
|
# # The parser creates a :lambda_expression node from the (fn () 1) S-expression.
|
||||||
|
# # So, the first child of call_node is the :lambda_expression node.
|
||||||
|
# lambda_node = get_first_child_node(typed_nodes_map, call_node.id)
|
||||||
|
# function_type_id = lambda_node.type_id
|
||||||
|
#
|
||||||
|
# expected_error_raw = type_error_arity_mismatch(0, 1, function_type_id)
|
||||||
|
# assert_node_typed_as(call_node, typed_nodes_map, expected_error_raw)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "error: arity mismatch - too few arguments: ((fn (x y) x) 1)" do
|
||||||
|
# {call_node, typed_nodes_map} = typecheck_and_get_first_node("((fn (x y integer) x) 1)")
|
||||||
|
# lambda_node = get_first_child_node(typed_nodes_map, call_node.id)
|
||||||
|
# function_type_id = lambda_node.type_id
|
||||||
|
#
|
||||||
|
# expected_error_raw = type_error_arity_mismatch(2, 1, function_type_id)
|
||||||
|
# assert_node_typed_as(call_node, typed_nodes_map, expected_error_raw)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a call where operator is an S-expression evaluating to a function: ((if true (fn () :ok) (fn () :err)))" do
|
||||||
|
# source = "((if true (fn () :ok) (fn () :err)))"
|
||||||
|
# {call_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
# expected_return_type_raw = %{type_kind: :literal, value: :ok}
|
||||||
|
# assert_node_typed_as(call_node, typed_nodes_map, expected_return_type_raw)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a call where operator is a symbol bound to a function: (= id (fn ((z integer) integer) z)) (id 42)" do
|
||||||
|
# source = """
|
||||||
|
# (= id (fn ((z integer) integer) z))
|
||||||
|
# (id 42)
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# {:ok, parsed_map} = Parser.parse(source)
|
||||||
|
# {:ok, typed_map} = Typer.type_check(parsed_map)
|
||||||
|
#
|
||||||
|
# file_node = get_file_node_from_map(typed_map)
|
||||||
|
# call_node_id = Enum.at(file_node.children, 1)
|
||||||
|
# call_node = get_node_by_id(typed_map, call_node_id)
|
||||||
|
#
|
||||||
|
# expected_return_type_raw = %{type_kind: :literal, value: 42}
|
||||||
|
# assert_node_typed_as(call_node, typed_map, expected_return_type_raw)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # Optional: Argument type mismatch test (deferred as lambdas currently take `any`)
|
||||||
|
# # test "error: argument type mismatch: ((the (function integer string) (fn (x) (the string x))) 'bad_arg')" do
|
||||||
|
# # source = "((the (function integer string) (fn (x) (the string x))) 'bad_arg')"
|
||||||
|
# # {call_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
# #
|
||||||
|
# # operator_node = get_first_child_node(typed_nodes_map, call_node.id) # This is the 'the' expression
|
||||||
|
# # function_type_id = operator_node.type_id # Type of the 'the' expression is the function type
|
||||||
|
# #
|
||||||
|
# # # Get interned key for literal string 'bad_arg'
|
||||||
|
# # raw_actual_arg_type = %{type_kind: :literal, value: "bad_arg"}
|
||||||
|
# # {actual_arg_type_key, map_with_actual_arg} = Interner.get_or_intern_type(raw_actual_arg_type, typed_nodes_map)
|
||||||
|
# #
|
||||||
|
# # # Expected arg type is integer (from the (function integer string) annotation)
|
||||||
|
# # raw_expected_arg_type = Types.get_primitive_type(:integer)
|
||||||
|
# # {expected_arg_type_key, _final_map} = Interner.get_or_intern_type(raw_expected_arg_type, map_with_actual_arg)
|
||||||
|
# #
|
||||||
|
# # expected_error_raw =
|
||||||
|
# # type_error_argument_type_mismatch(
|
||||||
|
# # 0, # First argument
|
||||||
|
# # expected_arg_type_key,
|
||||||
|
# # actual_arg_type_key,
|
||||||
|
# # function_type_id
|
||||||
|
# # )
|
||||||
|
# # assert_node_typed_as(call_node, typed_nodes_map, expected_error_raw)
|
||||||
|
# # end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
332
test/til/type_function_test.exs
Normal file
332
test/til/type_function_test.exs
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
# defmodule Til.TypeFunctionTest do
|
||||||
|
# use ExUnit.Case, async: true
|
||||||
|
#
|
||||||
|
# alias Til.Typer.Types
|
||||||
|
# alias Til.Typer.Interner
|
||||||
|
#
|
||||||
|
# # Helper to create a raw function type definition
|
||||||
|
# defp type_function_raw(arg_type_defs, return_type_def, type_param_defs \\ []) do
|
||||||
|
# %{
|
||||||
|
# type_kind: :function,
|
||||||
|
# arg_types: arg_type_defs,
|
||||||
|
# return_type: return_type_def,
|
||||||
|
# # Initially empty for monomorphic
|
||||||
|
# type_params: type_param_defs
|
||||||
|
# }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # Helper to create an interned function type definition
|
||||||
|
# # (what we expect after interning the raw definition's components)
|
||||||
|
# defp type_function_interned(id_key, arg_type_ids, return_type_id, type_param_ids \\ []) do
|
||||||
|
# %{
|
||||||
|
# type_kind: :function,
|
||||||
|
# id: id_key,
|
||||||
|
# arg_types: arg_type_ids,
|
||||||
|
# return_type: return_type_id,
|
||||||
|
# type_params: type_param_ids
|
||||||
|
# }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# describe "Phase 1: Function Type Representation & Interning" do
|
||||||
|
# test "interns a basic monomorphic function type (integer -> string)" do
|
||||||
|
# nodes_map = Interner.populate_known_types(%{})
|
||||||
|
#
|
||||||
|
# raw_integer_type = Types.get_primitive_type(:integer)
|
||||||
|
# raw_string_type = Types.get_primitive_type(:string)
|
||||||
|
#
|
||||||
|
# # Intern primitive types first to get their keys
|
||||||
|
# {integer_type_key, nodes_map_after_int} =
|
||||||
|
# Interner.get_or_intern_type(raw_integer_type, nodes_map)
|
||||||
|
#
|
||||||
|
# {string_type_key, nodes_map_after_str} =
|
||||||
|
# Interner.get_or_intern_type(raw_string_type, nodes_map_after_int)
|
||||||
|
#
|
||||||
|
# # Raw function type using the *definitions* of its components
|
||||||
|
# raw_func_type = type_function_raw([raw_integer_type], raw_string_type)
|
||||||
|
#
|
||||||
|
# # Intern the function type
|
||||||
|
# {func_type_key, final_nodes_map} =
|
||||||
|
# Interner.get_or_intern_type(raw_func_type, nodes_map_after_str)
|
||||||
|
#
|
||||||
|
# refute is_nil(func_type_key)
|
||||||
|
# assert func_type_key != integer_type_key
|
||||||
|
# assert func_type_key != string_type_key
|
||||||
|
#
|
||||||
|
# interned_func_def = Map.get(final_nodes_map, func_type_key)
|
||||||
|
#
|
||||||
|
# expected_interned_func_def =
|
||||||
|
# type_function_interned(
|
||||||
|
# func_type_key,
|
||||||
|
# # Expects keys of interned arg types
|
||||||
|
# [integer_type_key],
|
||||||
|
# # Expects key of interned return type
|
||||||
|
# string_type_key
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# assert interned_func_def == expected_interned_func_def
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "interning identical function type definitions yields the same key" do
|
||||||
|
# nodes_map = Interner.populate_known_types(%{})
|
||||||
|
#
|
||||||
|
# raw_integer_type = Types.get_primitive_type(:integer)
|
||||||
|
# raw_string_type = Types.get_primitive_type(:string)
|
||||||
|
# raw_atom_type = Types.get_primitive_type(:atom)
|
||||||
|
#
|
||||||
|
# # Intern components
|
||||||
|
# {_int_key, nodes_map} = Interner.get_or_intern_type(raw_integer_type, nodes_map)
|
||||||
|
# {_str_key, nodes_map} = Interner.get_or_intern_type(raw_string_type, nodes_map)
|
||||||
|
# {_atom_key, nodes_map} = Interner.get_or_intern_type(raw_atom_type, nodes_map)
|
||||||
|
#
|
||||||
|
# # Define two structurally identical raw function types
|
||||||
|
# raw_func_type_1 = type_function_raw([raw_integer_type, raw_atom_type], raw_string_type)
|
||||||
|
# raw_func_type_2 = type_function_raw([raw_integer_type, raw_atom_type], raw_string_type)
|
||||||
|
#
|
||||||
|
# {key1, nodes_map_after_1} = Interner.get_or_intern_type(raw_func_type_1, nodes_map)
|
||||||
|
# {key2, _final_nodes_map} = Interner.get_or_intern_type(raw_func_type_2, nodes_map_after_1)
|
||||||
|
#
|
||||||
|
# assert key1 == key2
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "interns a function type with no arguments (void -> atom)" do
|
||||||
|
# nodes_map = Interner.populate_known_types(%{})
|
||||||
|
# raw_atom_type = Types.get_primitive_type(:atom)
|
||||||
|
#
|
||||||
|
# {atom_type_key, nodes_map_after_atom} =
|
||||||
|
# Interner.get_or_intern_type(raw_atom_type, nodes_map)
|
||||||
|
#
|
||||||
|
# raw_func_type = type_function_raw([], raw_atom_type)
|
||||||
|
#
|
||||||
|
# {func_type_key, final_nodes_map} =
|
||||||
|
# Interner.get_or_intern_type(raw_func_type, nodes_map_after_atom)
|
||||||
|
#
|
||||||
|
# refute is_nil(func_type_key)
|
||||||
|
# interned_func_def = Map.get(final_nodes_map, func_type_key)
|
||||||
|
#
|
||||||
|
# expected_interned_func_def =
|
||||||
|
# type_function_interned(func_type_key, [], atom_type_key)
|
||||||
|
#
|
||||||
|
# assert interned_func_def == expected_interned_func_def
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "interns a function type whose argument is a complex (list) type" do
|
||||||
|
# nodes_map = Interner.populate_known_types(%{})
|
||||||
|
#
|
||||||
|
# raw_integer_type = Types.get_primitive_type(:integer)
|
||||||
|
# raw_string_type = Types.get_primitive_type(:string)
|
||||||
|
#
|
||||||
|
# # Intern primitive types
|
||||||
|
# # Prefixed integer_type_key as it's not used directly later
|
||||||
|
# {_integer_type_key, nodes_map_after_primitives} =
|
||||||
|
# Interner.get_or_intern_type(raw_integer_type, nodes_map)
|
||||||
|
# |> then(fn {key_param, map} ->
|
||||||
|
# {key_param, Interner.get_or_intern_type(raw_string_type, map) |> elem(1)}
|
||||||
|
# end)
|
||||||
|
#
|
||||||
|
# # Define a raw list type: (list integer)
|
||||||
|
# raw_list_of_int_type = %{
|
||||||
|
# type_kind: :list,
|
||||||
|
# # Use raw def here
|
||||||
|
# element_type: raw_integer_type,
|
||||||
|
# length: nil
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# # Intern the list type to get its key and canonical definition
|
||||||
|
# {list_of_int_key, nodes_map_after_list} =
|
||||||
|
# Interner.get_or_intern_type(raw_list_of_int_type, nodes_map_after_primitives)
|
||||||
|
#
|
||||||
|
# # Raw function type: ((list integer)) -> string
|
||||||
|
# # Its argument type is the *raw* list type definition
|
||||||
|
# raw_func_type = type_function_raw([raw_list_of_int_type], raw_string_type)
|
||||||
|
#
|
||||||
|
# {func_type_key, final_nodes_map} =
|
||||||
|
# Interner.get_or_intern_type(raw_func_type, nodes_map_after_list)
|
||||||
|
#
|
||||||
|
# refute is_nil(func_type_key)
|
||||||
|
# interned_func_def = Map.get(final_nodes_map, func_type_key)
|
||||||
|
#
|
||||||
|
# # The interned function type should refer to the *key* of the interned list type
|
||||||
|
# # and the *key* of the interned string type.
|
||||||
|
# string_type_key = Map.get(final_nodes_map, Types.primitive_type_key(:string)).id
|
||||||
|
#
|
||||||
|
# expected_interned_func_def =
|
||||||
|
# type_function_interned(func_type_key, [list_of_int_key], string_type_key)
|
||||||
|
#
|
||||||
|
# assert interned_func_def == expected_interned_func_def
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# describe "Phase 2: Basic Lambdas (fn)" do
|
||||||
|
# alias Til.Parser
|
||||||
|
# # alias Til.Typer # Typer is used via typecheck_and_get_first_node from TestHelpers
|
||||||
|
# # Added for subtype checking
|
||||||
|
# alias Til.Typer.SubtypeChecker
|
||||||
|
# import Til.TestHelpers
|
||||||
|
#
|
||||||
|
# # --- Parsing Tests for fn ---
|
||||||
|
# test "parses a lambda with no arguments and one body expression: (fn () 1)" do
|
||||||
|
# source = "(fn () 1)"
|
||||||
|
# {lambda_node, nodes_map} = parse_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# assert lambda_node.ast_node_type == :lambda_expression
|
||||||
|
# # Check params_s_expr_id and its structure
|
||||||
|
# params_s_expr_node = get_node_by_id(nodes_map, lambda_node.params_s_expr_id)
|
||||||
|
# assert params_s_expr_node.ast_node_type == :s_expression
|
||||||
|
# # The () for parameters
|
||||||
|
# assert params_s_expr_node.children == []
|
||||||
|
#
|
||||||
|
# # Check arg_spec_node_ids
|
||||||
|
# assert lambda_node.arg_spec_node_ids == []
|
||||||
|
#
|
||||||
|
# # Check body_node_ids
|
||||||
|
# assert length(lambda_node.body_node_ids) == 1
|
||||||
|
# body_expr_node = get_node_by_id(nodes_map, hd(lambda_node.body_node_ids))
|
||||||
|
# assert body_expr_node.ast_node_type == :literal_integer
|
||||||
|
# assert body_expr_node.value == 1
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "parses a lambda with one argument: (fn (x) x)" do
|
||||||
|
# source = "(fn (x) x)"
|
||||||
|
# {lambda_node, nodes_map} = parse_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# assert lambda_node.ast_node_type == :lambda_expression
|
||||||
|
#
|
||||||
|
# params_s_expr_node = get_node_by_id(nodes_map, lambda_node.params_s_expr_id)
|
||||||
|
# assert params_s_expr_node.ast_node_type == :s_expression
|
||||||
|
# assert length(params_s_expr_node.children) == 1
|
||||||
|
#
|
||||||
|
# assert length(lambda_node.arg_spec_node_ids) == 1
|
||||||
|
# arg_spec_node = get_node_by_id(nodes_map, hd(lambda_node.arg_spec_node_ids))
|
||||||
|
# # Initially, arg_specs for lambdas are just symbols
|
||||||
|
# assert arg_spec_node.ast_node_type == :symbol
|
||||||
|
# assert arg_spec_node.name == "x"
|
||||||
|
#
|
||||||
|
# assert length(lambda_node.body_node_ids) == 1
|
||||||
|
# body_expr_node = get_node_by_id(nodes_map, hd(lambda_node.body_node_ids))
|
||||||
|
# assert body_expr_node.ast_node_type == :symbol
|
||||||
|
# assert body_expr_node.name == "x"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "parses a lambda with multiple arguments and multiple body forms: (fn (a b) \"doc\" a)" do
|
||||||
|
# source = "(fn (a b) 'doc' a)"
|
||||||
|
# {lambda_node, nodes_map} = parse_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# assert lambda_node.ast_node_type == :lambda_expression
|
||||||
|
#
|
||||||
|
# params_s_expr_node = get_node_by_id(nodes_map, lambda_node.params_s_expr_id)
|
||||||
|
# # a, b
|
||||||
|
# assert length(params_s_expr_node.children) == 2
|
||||||
|
#
|
||||||
|
# assert length(lambda_node.arg_spec_node_ids) == 2
|
||||||
|
# arg1_node = get_node_by_id(nodes_map, Enum.at(lambda_node.arg_spec_node_ids, 0))
|
||||||
|
# arg2_node = get_node_by_id(nodes_map, Enum.at(lambda_node.arg_spec_node_ids, 1))
|
||||||
|
# assert arg1_node.name == "a"
|
||||||
|
# assert arg2_node.name == "b"
|
||||||
|
#
|
||||||
|
# assert length(lambda_node.body_node_ids) == 2
|
||||||
|
# body1_node = get_node_by_id(nodes_map, Enum.at(lambda_node.body_node_ids, 0))
|
||||||
|
# body2_node = get_node_by_id(nodes_map, Enum.at(lambda_node.body_node_ids, 1))
|
||||||
|
# assert body1_node.ast_node_type == :literal_string
|
||||||
|
# assert body1_node.value == "doc"
|
||||||
|
# assert body2_node.ast_node_type == :symbol
|
||||||
|
# assert body2_node.name == "a"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # --- Typing Tests for fn ---
|
||||||
|
#
|
||||||
|
# test "types a lambda with no arguments: (fn () 1) as (-> integer)" do
|
||||||
|
# {lambda_node, typed_nodes_map} = typecheck_and_get_first_node("(fn () 1)")
|
||||||
|
# func_type_def = Map.get(typed_nodes_map, lambda_node.type_id)
|
||||||
|
# assert func_type_def.type_kind == :function
|
||||||
|
# assert func_type_def.arg_types == []
|
||||||
|
#
|
||||||
|
# # Check that the return type is a subtype of integer
|
||||||
|
# actual_return_type_def = Map.get(typed_nodes_map, func_type_def.return_type)
|
||||||
|
# expected_super_type_def = Map.get(typed_nodes_map, Types.primitive_type_key(:integer))
|
||||||
|
#
|
||||||
|
# assert SubtypeChecker.is_subtype?(
|
||||||
|
# actual_return_type_def,
|
||||||
|
# expected_super_type_def,
|
||||||
|
# typed_nodes_map
|
||||||
|
# ),
|
||||||
|
# "Expected return type #{inspect(actual_return_type_def)} to be a subtype of #{inspect(expected_super_type_def)}"
|
||||||
|
#
|
||||||
|
# assert func_type_def.type_params == []
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a lambda with one argument: (fn (x) x) as (any -> any)" do
|
||||||
|
# {lambda_node, typed_nodes_map} = typecheck_and_get_first_node("(fn (x) x)")
|
||||||
|
# func_type_def = Map.get(typed_nodes_map, lambda_node.type_id)
|
||||||
|
#
|
||||||
|
# assert func_type_def.type_kind == :function
|
||||||
|
# assert func_type_def.arg_types == [Types.primitive_type_key(:any)]
|
||||||
|
# assert func_type_def.return_type == Types.primitive_type_key(:any)
|
||||||
|
# assert func_type_def.type_params == []
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a lambda with multiple arguments: (fn (x y) y) as (any any -> any)" do
|
||||||
|
# {lambda_node, typed_nodes_map} = typecheck_and_get_first_node("(fn (x y) y)")
|
||||||
|
# func_type_def = Map.get(typed_nodes_map, lambda_node.type_id)
|
||||||
|
#
|
||||||
|
# any_key = Types.primitive_type_key(:any)
|
||||||
|
# assert func_type_def.type_kind == :function
|
||||||
|
# assert func_type_def.arg_types == [any_key, any_key]
|
||||||
|
# assert func_type_def.return_type == any_key
|
||||||
|
# assert func_type_def.type_params == []
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a lambda with an empty body: (fn ()) as (-> nil)" do
|
||||||
|
# {lambda_node, typed_nodes_map} = typecheck_and_get_first_node("(fn ())")
|
||||||
|
# func_type_def = Map.get(typed_nodes_map, lambda_node.type_id)
|
||||||
|
#
|
||||||
|
# assert func_type_def.type_kind == :function
|
||||||
|
# assert func_type_def.arg_types == []
|
||||||
|
# assert func_type_def.return_type == Types.literal_type_key(:nil_atom)
|
||||||
|
# assert func_type_def.type_params == []
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a lambda with multiple body forms: (fn () \"doc\" :foo) as (-> atom)" do
|
||||||
|
# {lambda_node, typed_nodes_map} = typecheck_and_get_first_node("(fn () 'doc' :foo)")
|
||||||
|
# func_type_def = Map.get(typed_nodes_map, lambda_node.type_id)
|
||||||
|
#
|
||||||
|
# assert func_type_def.type_kind == :function
|
||||||
|
# assert func_type_def.arg_types == []
|
||||||
|
# # The type of :foo is literal :foo, which is a subtype of primitive atom.
|
||||||
|
# # The inferred return type should be the specific literal type.
|
||||||
|
# # We need to get the key for the interned literal type :foo.
|
||||||
|
# # For simplicity in this test, we'll check against primitive atom,
|
||||||
|
# # assuming the typer might generalize or that literal atom is subtype of primitive atom.
|
||||||
|
# # A more precise test would check for the literal type of :foo.
|
||||||
|
# # Let's assume the typer returns the most specific type, the literal :foo.
|
||||||
|
# # To get its key, we'd intern it.
|
||||||
|
# raw_foo_literal_type = %{type_kind: :literal, value: :foo}
|
||||||
|
# {foo_literal_key, _} = Interner.get_or_intern_type(raw_foo_literal_type, typed_nodes_map)
|
||||||
|
#
|
||||||
|
# assert func_type_def.return_type == foo_literal_key
|
||||||
|
# assert func_type_def.type_params == []
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a lambda where an argument is shadowed by a local binding (behavior check)" do
|
||||||
|
# # Source: (fn (x) (let ((x 1)) x))
|
||||||
|
# # Expected type: (any -> integer)
|
||||||
|
# # The inner x (integer) determines the return type.
|
||||||
|
# # The outer x (any) is the parameter type.
|
||||||
|
# # This test requires `let` to be implemented and typed correctly.
|
||||||
|
# # For now, we'll use an assignment which is simpler: (= x 1)
|
||||||
|
# source = "(fn (x) (= x 1) x)"
|
||||||
|
# {lambda_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
# func_type_def = Map.get(typed_nodes_map, lambda_node.type_id)
|
||||||
|
#
|
||||||
|
# assert func_type_def.type_kind == :function
|
||||||
|
# # Param x is any
|
||||||
|
# assert func_type_def.arg_types == [Types.primitive_type_key(:any)]
|
||||||
|
#
|
||||||
|
# # The return type is the type of the final 'x', which is bound to 1 (integer)
|
||||||
|
# # Intern the literal 1 type to get its key
|
||||||
|
# raw_int_1_type = %{type_kind: :literal, value: 1}
|
||||||
|
# {int_1_key, _} = Interner.get_or_intern_type(raw_int_1_type, typed_nodes_map)
|
||||||
|
# assert func_type_def.return_type == int_1_key
|
||||||
|
# assert func_type_def.type_params == []
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
253
test/til/type_list_test.exs
Normal file
253
test/til/type_list_test.exs
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
# defmodule Til.TypeListTest do
|
||||||
|
# use ExUnit.Case, async: true
|
||||||
|
#
|
||||||
|
# import Til.TestHelpers
|
||||||
|
# alias Til.Typer.Types
|
||||||
|
# alias Til.Typer.Interner
|
||||||
|
# alias Til.Typer.SubtypeChecker
|
||||||
|
#
|
||||||
|
# # Helper to create a primitive type definition (without :id)
|
||||||
|
# defp type_primitive_integer, do: Types.get_primitive_type(:integer)
|
||||||
|
# defp type_primitive_string, do: Types.get_primitive_type(:string)
|
||||||
|
# defp type_primitive_atom, do: Types.get_primitive_type(:atom)
|
||||||
|
# defp type_primitive_any, do: Types.get_primitive_type(:any)
|
||||||
|
# defp type_primitive_nothing, do: Types.get_primitive_type(:nothing)
|
||||||
|
#
|
||||||
|
# # Helper to create a list type definition (without :id, element_type is full def)
|
||||||
|
# # This is what the typer's infer_type_for_node_ast or resolve_type_specifier_node would produce
|
||||||
|
# # before interning resolves element_type to element_type_id.
|
||||||
|
# # For direct assertion against interned types, we'll need a different helper or to use keys.
|
||||||
|
# defp type_list_raw(element_type_def, length) do
|
||||||
|
# %{type_kind: :list, element_type: element_type_def, length: length}
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # Helper to create the expected *interned* list type definition (without :id)
|
||||||
|
# # `element_type_id` is the key of the interned element type.
|
||||||
|
# defp type_list_interned_form(element_type_id, length) do
|
||||||
|
# %{type_kind: :list, element_type_id: element_type_id, length: length}
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # Helper to create the expected *cleaned* type_annotation_mismatch error structure
|
||||||
|
# defp type_error_type_annotation_mismatch(actual_type_clean, expected_type_clean) do
|
||||||
|
# %{
|
||||||
|
# type_kind: :error,
|
||||||
|
# reason: :type_annotation_mismatch,
|
||||||
|
# actual_type: actual_type_clean,
|
||||||
|
# expected_type: expected_type_clean
|
||||||
|
# }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# describe "List Literal Type Inference" do
|
||||||
|
# test "empty list [] is (List Nothing) with length 0" do
|
||||||
|
# source = "[]"
|
||||||
|
# {node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # This is the "clean" structure that deep_strip_id produces
|
||||||
|
# expected_type_def = type_list_raw(type_primitive_nothing(), 0)
|
||||||
|
# assert_node_typed_as(node, typed_nodes_map, expected_type_def)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "list of distinct integer literals [1, 2, 3] is (List (Union (Literal 1) (Literal 2) (Literal 3))) with length 3" do
|
||||||
|
# source = "[1 2 3]"
|
||||||
|
# {node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# lit_1 = %{type_kind: :literal, value: 1}
|
||||||
|
# lit_2 = %{type_kind: :literal, value: 2}
|
||||||
|
# lit_3 = %{type_kind: :literal, value: 3}
|
||||||
|
# union_type = %{type_kind: :union, types: MapSet.new([lit_1, lit_2, lit_3])}
|
||||||
|
# expected_type_def = type_list_raw(union_type, 3)
|
||||||
|
# assert_node_typed_as(node, typed_nodes_map, expected_type_def)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "list of distinct string literals ['a' 'b'] is (List (Union (Literal \"a\") (Literal \"b\"))) with length 2" do
|
||||||
|
# # Using single quotes for strings as per parser
|
||||||
|
# source = "['a' 'b']"
|
||||||
|
# {node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# lit_a = %{type_kind: :literal, value: "a"}
|
||||||
|
# lit_b = %{type_kind: :literal, value: "b"}
|
||||||
|
# union_type = %{type_kind: :union, types: MapSet.new([lit_a, lit_b])}
|
||||||
|
# expected_type_def = type_list_raw(union_type, 2)
|
||||||
|
# assert_node_typed_as(node, typed_nodes_map, expected_type_def)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "list of distinct atom literals [:foo :bar] is (List (Union (Literal :foo) (Literal :bar))) with length 2" do
|
||||||
|
# source = "[:foo :bar]"
|
||||||
|
# {node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# lit_foo = %{type_kind: :literal, value: :foo}
|
||||||
|
# lit_bar = %{type_kind: :literal, value: :bar}
|
||||||
|
# union_type = %{type_kind: :union, types: MapSet.new([lit_foo, lit_bar])}
|
||||||
|
# expected_type_def = type_list_raw(union_type, 2)
|
||||||
|
# assert_node_typed_as(node, typed_nodes_map, expected_type_def)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "list of mixed literals [1 'a'] has type (List (Union (Literal 1) (Literal \"a\"))) length 2" do
|
||||||
|
# source = "[1 'a']"
|
||||||
|
# {node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # Define the clean union type for the elements.
|
||||||
|
# clean_literal_1_def = %{type_kind: :literal, value: 1}
|
||||||
|
# # Parser turns 'a' into "a"
|
||||||
|
# clean_literal_a_def = %{type_kind: :literal, value: "a"}
|
||||||
|
#
|
||||||
|
# clean_element_union_type_def = %{
|
||||||
|
# type_kind: :union,
|
||||||
|
# types: MapSet.new([clean_literal_1_def, clean_literal_a_def])
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# expected_list_type_clean = type_list_raw(clean_element_union_type_def, 2)
|
||||||
|
# assert_node_typed_as(node, typed_nodes_map, expected_list_type_clean)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# describe "List Type Annotation `(the (list <type>) ...)`" do
|
||||||
|
# test "(the (list integer) []) has type (List Integer) with length nil" do
|
||||||
|
# source = "(the (list integer) [])"
|
||||||
|
# # This gets the 'the' node
|
||||||
|
# {node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# expected_type_def = type_list_raw(type_primitive_integer(), nil)
|
||||||
|
# assert_node_typed_as(node, typed_nodes_map, expected_type_def)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (list string) ['a' 'b']) has type (List String) with length nil" do
|
||||||
|
# source = "(the (list string) ['a' 'b'])"
|
||||||
|
# {node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
# # inspect_nodes(typed_nodes_map)
|
||||||
|
# expected_type_def = type_list_raw(type_primitive_string(), nil)
|
||||||
|
# assert_node_typed_as(node, typed_nodes_map, expected_type_def)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (list integer) [1 'a']) results in type_annotation_mismatch" do
|
||||||
|
# source = "(the (list integer) [1 'a'])"
|
||||||
|
# {node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # Expected actual type: (List (Union (Literal 1) (Literal "a"))) length 2
|
||||||
|
# lit_1_clean = %{type_kind: :literal, value: 1}
|
||||||
|
# lit_a_str_clean = %{type_kind: :literal, value: "a"}
|
||||||
|
# union_elements_clean = %{
|
||||||
|
# type_kind: :union,
|
||||||
|
# types: MapSet.new([lit_1_clean, lit_a_str_clean])
|
||||||
|
# }
|
||||||
|
# expected_actual_clean_type = type_list_raw(union_elements_clean, 2)
|
||||||
|
#
|
||||||
|
# # Expected annotated type: (List Integer) length nil
|
||||||
|
# expected_annotated_clean_type = type_list_raw(type_primitive_integer(), nil)
|
||||||
|
#
|
||||||
|
# expected_error_def =
|
||||||
|
# type_error_type_annotation_mismatch(
|
||||||
|
# expected_actual_clean_type,
|
||||||
|
# expected_annotated_clean_type
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# assert_node_typed_as(node, typed_nodes_map, expected_error_def)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (list (list integer)) [[] [1]]) has type (List (List Integer)) with length nil" do
|
||||||
|
# source = "(the (list (list integer)) [[] [1]])"
|
||||||
|
# {the_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # Expected inner list type: (List Integer), length nil (from annotation (list integer))
|
||||||
|
# # This needs to be interned to get its key.
|
||||||
|
# # Manually construct the expected structure for assertion.
|
||||||
|
# # 1. Define the raw inner list type: (List Integer, nil)
|
||||||
|
# # `type_primitive_integer()` gets the base definition of Integer.
|
||||||
|
# # `typed_nodes_map` (from typechecking the source) will contain the interned primitive types.
|
||||||
|
# clean_inner_list_type = type_list_raw(type_primitive_integer(), nil)
|
||||||
|
#
|
||||||
|
# # Expected outer list type: (List <clean_inner_list_type>), length nil
|
||||||
|
# expected_outer_list_type_def = type_list_raw(clean_inner_list_type, nil)
|
||||||
|
#
|
||||||
|
# assert_node_typed_as(the_node, typed_nodes_map, expected_outer_list_type_def)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# describe "List Subtyping" do
|
||||||
|
# # Helper to check subtyping
|
||||||
|
# defp check_subtype(subtype_raw, supertype_raw, expected_result) do
|
||||||
|
# # Populate a base nodes_map with primitive types
|
||||||
|
# nodes_map = Interner.populate_known_types(%{})
|
||||||
|
#
|
||||||
|
# # Intern subtype and supertype to get their canonical forms and keys
|
||||||
|
# {subtype_key, nodes_map_after_sub} = Interner.get_or_intern_type(subtype_raw, nodes_map)
|
||||||
|
#
|
||||||
|
# {supertype_key, final_nodes_map} =
|
||||||
|
# Interner.get_or_intern_type(supertype_raw, nodes_map_after_sub)
|
||||||
|
#
|
||||||
|
# subtype_def = Map.get(final_nodes_map, subtype_key)
|
||||||
|
# supertype_def = Map.get(final_nodes_map, supertype_key)
|
||||||
|
#
|
||||||
|
# assert SubtypeChecker.is_subtype?(subtype_def, supertype_def, final_nodes_map) ==
|
||||||
|
# expected_result
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Integer, 3) is subtype of (List Integer, 3)" do
|
||||||
|
# list_int_3 = type_list_raw(type_primitive_integer(), 3)
|
||||||
|
# check_subtype(list_int_3, list_int_3, true)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Integer, 3) is subtype of (List Integer, nil)" do
|
||||||
|
# list_int_3 = type_list_raw(type_primitive_integer(), 3)
|
||||||
|
# list_int_nil = type_list_raw(type_primitive_integer(), nil)
|
||||||
|
# check_subtype(list_int_3, list_int_nil, true)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Integer, 3) is subtype of (List Number, 3)" do
|
||||||
|
# list_int_3 = type_list_raw(type_primitive_integer(), 3)
|
||||||
|
# list_num_3 = type_list_raw(Types.get_primitive_type(:number), 3)
|
||||||
|
# check_subtype(list_int_3, list_num_3, true)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Integer, 3) is subtype of (List Number, nil)" do
|
||||||
|
# list_int_3 = type_list_raw(type_primitive_integer(), 3)
|
||||||
|
# list_num_nil = type_list_raw(Types.get_primitive_type(:number), nil)
|
||||||
|
# check_subtype(list_int_3, list_num_nil, true)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Integer, 3) is NOT subtype of (List Integer, 2)" do
|
||||||
|
# list_int_3 = type_list_raw(type_primitive_integer(), 3)
|
||||||
|
# list_int_2 = type_list_raw(type_primitive_integer(), 2)
|
||||||
|
# check_subtype(list_int_3, list_int_2, false)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Integer, 3) is NOT subtype of (List String, 3)" do
|
||||||
|
# list_int_3 = type_list_raw(type_primitive_integer(), 3)
|
||||||
|
# list_str_3 = type_list_raw(type_primitive_string(), 3)
|
||||||
|
# check_subtype(list_int_3, list_str_3, false)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Integer, nil) is NOT subtype of (List Integer, 3)" do
|
||||||
|
# list_int_nil = type_list_raw(type_primitive_integer(), nil)
|
||||||
|
# list_int_3 = type_list_raw(type_primitive_integer(), 3)
|
||||||
|
# check_subtype(list_int_nil, list_int_3, false)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Nothing, 0) is subtype of (List Integer, nil) (empty list compatibility)" do
|
||||||
|
# # Type of [] is (List Nothing, 0)
|
||||||
|
# empty_list_type = type_list_raw(type_primitive_nothing(), 0)
|
||||||
|
# # Target type (e.g. from an annotation (list integer))
|
||||||
|
# list_int_nil = type_list_raw(type_primitive_integer(), nil)
|
||||||
|
# check_subtype(empty_list_type, list_int_nil, true)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Nothing, 0) is subtype of (List Any, nil)" do
|
||||||
|
# empty_list_type = type_list_raw(type_primitive_nothing(), 0)
|
||||||
|
# list_any_nil = type_list_raw(type_primitive_any(), nil)
|
||||||
|
# check_subtype(empty_list_type, list_any_nil, true)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Nothing, 0) is subtype of (List Nothing, nil)" do
|
||||||
|
# empty_list_type = type_list_raw(type_primitive_nothing(), 0)
|
||||||
|
# list_nothing_nil = type_list_raw(type_primitive_nothing(), nil)
|
||||||
|
# check_subtype(empty_list_type, list_nothing_nil, true)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(List Nothing, 0) is NOT subtype of (List Integer, 0) unless Nothing is subtype of Integer" do
|
||||||
|
# # This depends on whether Nothing is a subtype of Integer. It is.
|
||||||
|
# empty_list_type = type_list_raw(type_primitive_nothing(), 0)
|
||||||
|
# list_int_0 = type_list_raw(type_primitive_integer(), 0)
|
||||||
|
# check_subtype(empty_list_type, list_int_0, true)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
533
test/til/type_map_test.exs
Normal file
533
test/til/type_map_test.exs
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
defmodule Til.TypeMapTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Til.Parser
|
||||||
|
alias Til.Typer
|
||||||
|
alias Til.Typer.Types
|
||||||
|
alias Til.Typer.Interner
|
||||||
|
alias Til.Typer.SubtypeChecker
|
||||||
|
import Til.TestHelpers
|
||||||
|
|
||||||
|
# Helper to create a raw map type definition for assertions/setup
|
||||||
|
defp type_map_raw(known_elements_raw, index_signature_raw) do
|
||||||
|
%{
|
||||||
|
type_kind: :map,
|
||||||
|
known_elements: known_elements_raw,
|
||||||
|
index_signature: index_signature_raw
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unused helper removed.
|
||||||
|
# defp type_map_interned_form(known_elements_interned, index_signature_interned) do
|
||||||
|
# %{
|
||||||
|
# type_kind: :map,
|
||||||
|
# # :id field is omitted as it's dynamic
|
||||||
|
# known_elements: known_elements_interned,
|
||||||
|
# index_signature: index_signature_interned
|
||||||
|
# }
|
||||||
|
# end
|
||||||
|
|
||||||
|
defp type_primitive_any, do: Types.get_primitive_type(:any)
|
||||||
|
defp type_primitive_integer, do: Types.get_primitive_type(:integer)
|
||||||
|
defp type_primitive_string, do: Types.get_primitive_type(:string)
|
||||||
|
defp type_primitive_atom, do: Types.get_primitive_type(:atom)
|
||||||
|
defp type_primitive_number, do: Types.get_primitive_type(:number)
|
||||||
|
defp type_literal_int(val), do: %{type_kind: :literal, value: val}
|
||||||
|
defp type_literal_string(val), do: %{type_kind: :literal, value: val}
|
||||||
|
defp type_literal_atom(val), do: %{type_kind: :literal, value: val}
|
||||||
|
|
||||||
|
# Helper to create the expected *cleaned* type_annotation_mismatch error structure
|
||||||
|
defp type_error_type_annotation_mismatch(actual_type_clean, expected_type_clean) do
|
||||||
|
%{
|
||||||
|
type_kind: :error,
|
||||||
|
reason: :type_annotation_mismatch,
|
||||||
|
actual_type: actual_type_clean,
|
||||||
|
expected_type: expected_type_clean
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Map Literal Typing" do
|
||||||
|
test "empty map m{} is typed correctly" do
|
||||||
|
source = "m{}"
|
||||||
|
{map_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
expected_raw_type =
|
||||||
|
type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_primitive_any()})
|
||||||
|
|
||||||
|
assert_node_typed_as(map_node, typed_nodes_map, expected_raw_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "map with literal keys and values m{:a 1 :b 's'}" do
|
||||||
|
source = "m{:a 1 :b 's'}"
|
||||||
|
{map_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
expected_raw_type =
|
||||||
|
type_map_raw(
|
||||||
|
%{
|
||||||
|
a: %{value_type: type_literal_int(1), optional: false},
|
||||||
|
b: %{value_type: type_literal_string("s"), optional: false}
|
||||||
|
},
|
||||||
|
%{key_type: type_primitive_any(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_node_typed_as(map_node, typed_nodes_map, expected_raw_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "map with duplicate literal key (last one wins)" do
|
||||||
|
source = "m{:a 1 :a 2}"
|
||||||
|
{map_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
expected_raw_type =
|
||||||
|
type_map_raw(
|
||||||
|
%{a: %{value_type: type_literal_int(2), optional: false}},
|
||||||
|
%{key_type: type_primitive_any(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_node_typed_as(map_node, typed_nodes_map, expected_raw_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "map with a symbol key that resolves to a literal atom" do
|
||||||
|
source = """
|
||||||
|
(= my-key :c)
|
||||||
|
m{:a 1 my-key 2}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We are interested in the type of the map expression, which is the second node.
|
||||||
|
{:ok, parsed_map} = Parser.parse(source)
|
||||||
|
{:ok, typed_nodes_map} = Typer.type_check(parsed_map)
|
||||||
|
# 0 is assignment node, 1 is the map node
|
||||||
|
map_node = get_nth_child_node(typed_nodes_map, 1)
|
||||||
|
|
||||||
|
expected_raw_type =
|
||||||
|
type_map_raw(
|
||||||
|
# my-key is typed as (literal :c), so :c is used in known_elements.
|
||||||
|
%{
|
||||||
|
a: %{value_type: type_literal_int(1), optional: false},
|
||||||
|
c: %{value_type: type_literal_int(2), optional: false}
|
||||||
|
},
|
||||||
|
# Index signature remains default for map literals.
|
||||||
|
%{key_type: type_primitive_any(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_node_typed_as(map_node, typed_nodes_map, expected_raw_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "map with various literal keys (true, false, nil, integer, string, atom)" do
|
||||||
|
source = "m{true 1 false 2 nil 3 4 'four' :five 5}"
|
||||||
|
{map_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
expected_raw_type =
|
||||||
|
type_map_raw(
|
||||||
|
%{
|
||||||
|
true => %{value_type: type_literal_int(1), optional: false},
|
||||||
|
false => %{value_type: type_literal_int(2), optional: false},
|
||||||
|
nil => %{value_type: type_literal_int(3), optional: false},
|
||||||
|
4 => %{value_type: type_literal_string("four"), optional: false},
|
||||||
|
# Note: parser turns :five into a literal atom node,
|
||||||
|
# typer infers its type as %{type_kind: :literal, value: :five}
|
||||||
|
# The key in known_elements should be the atom :five itself.
|
||||||
|
five: %{value_type: type_literal_int(5), optional: false}
|
||||||
|
},
|
||||||
|
%{key_type: type_primitive_any(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_node_typed_as(map_node, typed_nodes_map, expected_raw_type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Map Type Annotation Resolution" do
|
||||||
|
test "(map atom integer) annotation" do
|
||||||
|
source = "(the (map atom integer) m{})"
|
||||||
|
{the_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
# m{}'s default index signature (any -> any) is not a subtype of (atom -> integer)
|
||||||
|
# because 'any' (value) is not a subtype of 'integer'.
|
||||||
|
# Thus, the 'the' expression should result in a type annotation mismatch error.
|
||||||
|
|
||||||
|
# Actual type of m{} (cleaned)
|
||||||
|
actual_m_empty_clean =
|
||||||
|
type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_primitive_any()})
|
||||||
|
|
||||||
|
# Expected annotated type (cleaned)
|
||||||
|
expected_annotated_clean =
|
||||||
|
type_map_raw(
|
||||||
|
%{},
|
||||||
|
%{key_type: type_primitive_atom(), value_type: type_primitive_integer()}
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_error_def =
|
||||||
|
type_error_type_annotation_mismatch(actual_m_empty_clean, expected_annotated_clean)
|
||||||
|
|
||||||
|
assert_node_typed_as(the_node, typed_nodes_map, expected_error_def)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "(map (list string) (map string any)) annotation" do
|
||||||
|
source = "(the (map (list string) (map string any)) m{})"
|
||||||
|
{the_node, typed_nodes_map} = typecheck_and_get_first_node(source)
|
||||||
|
|
||||||
|
# Similar to the above, m{}'s default index signature (any -> any)
|
||||||
|
# will likely fail the subtyping check against the complex map value type in the annotation.
|
||||||
|
|
||||||
|
# Actual type of m{} (cleaned)
|
||||||
|
actual_m_empty_clean =
|
||||||
|
type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_primitive_any()})
|
||||||
|
|
||||||
|
# Expected annotated type (cleaned)
|
||||||
|
# Key type: (List String)
|
||||||
|
key_type_annotated_clean = %{
|
||||||
|
type_kind: :list,
|
||||||
|
element_type: type_primitive_string(),
|
||||||
|
length: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
# Value type: (Map String Any)
|
||||||
|
value_type_annotated_clean =
|
||||||
|
type_map_raw(
|
||||||
|
%{},
|
||||||
|
%{key_type: type_primitive_string(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_annotated_clean =
|
||||||
|
type_map_raw(
|
||||||
|
%{},
|
||||||
|
%{key_type: key_type_annotated_clean, value_type: value_type_annotated_clean}
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_error_def =
|
||||||
|
type_error_type_annotation_mismatch(actual_m_empty_clean, expected_annotated_clean)
|
||||||
|
|
||||||
|
assert_node_typed_as(the_node, typed_nodes_map, expected_error_def)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Map Type Interning" do
|
||||||
|
test "identical map type definitions intern to the same key" do
|
||||||
|
nodes_map = Interner.populate_known_types(%{})
|
||||||
|
|
||||||
|
raw_map_def1 =
|
||||||
|
type_map_raw(
|
||||||
|
%{a: %{value_type: type_literal_int(1), optional: false}},
|
||||||
|
%{key_type: type_primitive_atom(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_map_def2 =
|
||||||
|
type_map_raw(
|
||||||
|
%{a: %{value_type: type_literal_int(1), optional: false}},
|
||||||
|
%{key_type: type_primitive_atom(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
{key1, nodes_map_after_1} = Interner.get_or_intern_type(raw_map_def1, nodes_map)
|
||||||
|
{key2, _nodes_map_after_2} = Interner.get_or_intern_type(raw_map_def2, nodes_map_after_1)
|
||||||
|
|
||||||
|
assert key1 == key2
|
||||||
|
end
|
||||||
|
|
||||||
|
test "structurally different map type definitions intern to different keys" do
|
||||||
|
nodes_map = Interner.populate_known_types(%{})
|
||||||
|
|
||||||
|
raw_map_def1 =
|
||||||
|
type_map_raw(
|
||||||
|
# key :a
|
||||||
|
%{a: %{value_type: type_literal_int(1), optional: false}},
|
||||||
|
%{key_type: type_primitive_atom(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
raw_map_def2 =
|
||||||
|
type_map_raw(
|
||||||
|
# key :b
|
||||||
|
%{b: %{value_type: type_literal_int(1), optional: false}},
|
||||||
|
%{key_type: type_primitive_atom(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
{key1, nodes_map_after_1} = Interner.get_or_intern_type(raw_map_def1, nodes_map)
|
||||||
|
{key2, _nodes_map_after_2} = Interner.get_or_intern_type(raw_map_def2, nodes_map_after_1)
|
||||||
|
|
||||||
|
refute key1 == key2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Map Subtyping" do
|
||||||
|
# Helper to check subtyping for maps
|
||||||
|
defp check_map_subtype(subtype_raw, supertype_raw, expected_result) do
|
||||||
|
nodes_map = Interner.populate_known_types(%{})
|
||||||
|
{subtype_key, nodes_map_after_sub} = Interner.get_or_intern_type(subtype_raw, nodes_map)
|
||||||
|
|
||||||
|
{supertype_key, final_nodes_map} =
|
||||||
|
Interner.get_or_intern_type(supertype_raw, nodes_map_after_sub)
|
||||||
|
|
||||||
|
subtype_def = Map.get(final_nodes_map, subtype_key)
|
||||||
|
supertype_def = Map.get(final_nodes_map, supertype_key)
|
||||||
|
|
||||||
|
assert SubtypeChecker.is_subtype?(subtype_def, supertype_def, final_nodes_map) ==
|
||||||
|
expected_result
|
||||||
|
end
|
||||||
|
|
||||||
|
# Test cases based on todo.md logic for map subtyping
|
||||||
|
# 1. Known Elements (Required in Supertype)
|
||||||
|
test "subtype has required key with correct type" do
|
||||||
|
sub =
|
||||||
|
type_map_raw(%{a: %{value_type: type_literal_int(1), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{a: %{value_type: type_primitive_integer(), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "subtype missing required key" do
|
||||||
|
sub = type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_primitive_any()})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{a: %{value_type: type_primitive_integer(), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "subtype has required key but wrong type" do
|
||||||
|
sub =
|
||||||
|
type_map_raw(%{a: %{value_type: type_literal_string("s"), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{a: %{value_type: type_primitive_integer(), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "subtype has required key as optional" do
|
||||||
|
sub =
|
||||||
|
type_map_raw(%{a: %{value_type: type_literal_int(1), optional: true}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{a: %{value_type: type_primitive_integer(), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 2. Known Elements (Optional in Supertype)
|
||||||
|
test "subtype has optional key with correct type" do
|
||||||
|
sub =
|
||||||
|
type_map_raw(%{a: %{value_type: type_literal_int(1), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{a: %{value_type: type_primitive_integer(), optional: true}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "subtype missing optional key" do
|
||||||
|
sub = type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_primitive_any()})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{a: %{value_type: type_primitive_integer(), optional: true}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 3. Index Signature Compatibility
|
||||||
|
test "compatible index signatures" do
|
||||||
|
# Ksuper <: Ksub (contravariant), Vsub <: Vsuper (covariant)
|
||||||
|
# Ksuper=atom, Ksub=any. Vsub=int, Vsuper=number
|
||||||
|
sub = type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_literal_int(1)})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{}, %{key_type: type_primitive_atom(), value_type: type_primitive_number()})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "incompatible index signature (key type wrong variance)" do
|
||||||
|
# Ksuper=any, Ksub=atom (any is not subtype of atom)
|
||||||
|
sub = type_map_raw(%{}, %{key_type: type_primitive_atom(), value_type: type_literal_int(1)})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_primitive_number()})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "incompatible index signature (value type wrong variance)" do
|
||||||
|
# Vsub=number, Vsuper=int (number is not subtype of int)
|
||||||
|
sub =
|
||||||
|
type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_primitive_number()})
|
||||||
|
|
||||||
|
sup = type_map_raw(%{}, %{key_type: type_primitive_atom(), value_type: type_literal_int(1)})
|
||||||
|
check_map_subtype(sub, sup, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 4. Width Subtyping (Extra keys in subtype conform to supertype's index signature)
|
||||||
|
test "extra key in subtype conforms to supertype index signature" do
|
||||||
|
# Subtype has :b (atom -> int(10)).
|
||||||
|
# Supertype index signature is atom -> number.
|
||||||
|
# Subtype index signature is any -> int.
|
||||||
|
# Check:
|
||||||
|
# 1. Required/Optional super keys: Super has no known keys, so true.
|
||||||
|
# 2. Index Signature Compatibility:
|
||||||
|
# K_super(atom) <: K_sub(any) -> true (contravariance)
|
||||||
|
# V_sub(int) <: V_super(number) -> true (covariance)
|
||||||
|
# So, index signatures are compatible.
|
||||||
|
# 3. Width: Extra key :b (atom) with value int(10) in subtype.
|
||||||
|
# Must conform to super_is (atom -> number).
|
||||||
|
# Key :b (atom) <: super_is.key_type (atom) -> true.
|
||||||
|
# Value int(10) <: super_is.value_type (number) -> true.
|
||||||
|
# All conditions should pass.
|
||||||
|
sub =
|
||||||
|
type_map_raw(
|
||||||
|
# Extra key
|
||||||
|
%{b: %{value_type: type_literal_int(10), optional: false}},
|
||||||
|
# Sub IS
|
||||||
|
%{key_type: type_primitive_any(), value_type: type_primitive_integer()}
|
||||||
|
)
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(
|
||||||
|
# No known keys
|
||||||
|
%{},
|
||||||
|
# Super IS
|
||||||
|
%{key_type: type_primitive_atom(), value_type: type_primitive_number()}
|
||||||
|
)
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "extra key in subtype, key type does not conform to supertype index signature" do
|
||||||
|
# Subtype has "b" (string key), supertype index expects atom keys
|
||||||
|
sub =
|
||||||
|
type_map_raw(%{"b" => %{value_type: type_literal_int(10), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{}, %{key_type: type_primitive_atom(), value_type: type_primitive_integer()})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "extra key in subtype, value type does not conform to supertype index signature" do
|
||||||
|
# Subtype has :b -> "s" (string value), supertype index expects integer values
|
||||||
|
sub =
|
||||||
|
type_map_raw(%{b: %{value_type: type_literal_string("s"), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
sup =
|
||||||
|
type_map_raw(%{}, %{key_type: type_primitive_atom(), value_type: type_primitive_integer()})
|
||||||
|
|
||||||
|
check_map_subtype(sub, sup, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "map is subtype of any" do
|
||||||
|
sub =
|
||||||
|
type_map_raw(%{a: %{value_type: type_literal_int(1), optional: false}}, %{
|
||||||
|
key_type: type_primitive_any(),
|
||||||
|
value_type: type_primitive_any()
|
||||||
|
})
|
||||||
|
|
||||||
|
sup = type_primitive_any()
|
||||||
|
check_map_subtype(sub, sup, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "nothing is subtype of map" do
|
||||||
|
# Raw form
|
||||||
|
sub = Types.get_primitive_type(:nothing)
|
||||||
|
sup = type_map_raw(%{}, %{key_type: type_primitive_any(), value_type: type_primitive_any()})
|
||||||
|
check_map_subtype(sub, sup, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "complex map subtyping: required, optional, index, and width" do
|
||||||
|
sub_map =
|
||||||
|
type_map_raw(
|
||||||
|
%{
|
||||||
|
# Matches required
|
||||||
|
req_present: %{value_type: type_literal_int(1), optional: false},
|
||||||
|
# Matches optional
|
||||||
|
opt_present: %{value_type: type_literal_string("sub"), optional: false},
|
||||||
|
# Width, must match super index
|
||||||
|
extra_key: %{value_type: type_literal_atom(:sub_atom), optional: false}
|
||||||
|
},
|
||||||
|
# Sub index (Ksuper <: Ksub)
|
||||||
|
%{key_type: type_primitive_any(), value_type: type_primitive_any()}
|
||||||
|
)
|
||||||
|
|
||||||
|
super_map =
|
||||||
|
type_map_raw(
|
||||||
|
%{
|
||||||
|
req_present: %{value_type: type_primitive_integer(), optional: false},
|
||||||
|
opt_present: %{value_type: type_primitive_string(), optional: true},
|
||||||
|
opt_absent: %{value_type: type_primitive_atom(), optional: true}
|
||||||
|
},
|
||||||
|
# Super index (Vsub <: Vsuper)
|
||||||
|
%{key_type: type_primitive_atom(), value_type: type_primitive_atom()}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sub index key: any, value: any
|
||||||
|
# Super index key: atom, value: atom
|
||||||
|
# Ksuper (atom) <: Ksub (any) -> true
|
||||||
|
# Vsub (any) <: Vsuper (atom) -> false. This should make it false.
|
||||||
|
# Let's adjust sub index to make it pass:
|
||||||
|
# Vsub (literal :sub_atom) <: Vsuper (atom) -> true
|
||||||
|
# extra_key (:extra_key, type literal :sub_atom) must conform to super_is (atom -> atom)
|
||||||
|
# :extra_key (atom) <: super_is.key (atom) -> true
|
||||||
|
# literal :sub_atom <: super_is.value (atom) -> true
|
||||||
|
|
||||||
|
# Corrected sub_map for a true case:
|
||||||
|
sub_map_corrected =
|
||||||
|
type_map_raw(
|
||||||
|
%{
|
||||||
|
# val_type: int(1) <: integer (super) -> true
|
||||||
|
req_present: %{value_type: type_literal_int(1), optional: false},
|
||||||
|
# val_type: string("sub") <: string (super) -> true
|
||||||
|
opt_present: %{value_type: type_literal_string("sub"), optional: false},
|
||||||
|
# extra key, val_type: atom(:sub_atom)
|
||||||
|
extra_key: %{value_type: type_literal_atom(:sub_atom), optional: false}
|
||||||
|
},
|
||||||
|
# Sub index signature:
|
||||||
|
%{key_type: type_primitive_any(), value_type: type_literal_atom(:another_sub_atom)}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Super index signature: %{key_type: type_primitive_atom(), value_type: type_primitive_atom()}
|
||||||
|
|
||||||
|
# Index Sig Check: Ksuper(atom) <: Ksub(any) -> true
|
||||||
|
# Vsub(literal :another_sub_atom) <: Vsuper(atom) -> true
|
||||||
|
|
||||||
|
# Width Check for :extra_key (atom) with value (literal :sub_atom):
|
||||||
|
# key :extra_key (atom) <: super_is.key_type (atom) -> true
|
||||||
|
# value (literal :sub_atom) <: super_is.value_type (atom) -> true
|
||||||
|
check_map_subtype(sub_map_corrected, super_map, true)
|
||||||
|
|
||||||
|
# Original sub_map that should fail due to index signature value type Vsub(any) not <: Vsuper(atom)
|
||||||
|
check_map_subtype(sub_map, super_map, false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
92
test/til/type_tuple_test.exs
Normal file
92
test/til/type_tuple_test.exs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
defmodule Til.TypeTupleTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Til.Parser
|
||||||
|
alias Til.Typer
|
||||||
|
alias Til.Typer.Types
|
||||||
|
alias Til.TestHelpers # Added alias
|
||||||
|
|
||||||
|
test "empty tuple {}" do
|
||||||
|
{tuple_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node("{}")
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
expected_type = %{type_kind: :tuple, element_types: []}
|
||||||
|
TestHelpers.assert_node_typed_as(tuple_node, typed_nodes_map, expected_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tuple with one integer {1}" do
|
||||||
|
{tuple_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node("{1}")
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
|
||||||
|
expected_type = %{
|
||||||
|
type_kind: :tuple,
|
||||||
|
element_types: [%{type_kind: :literal, value: 1}]
|
||||||
|
}
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(tuple_node, typed_nodes_map, expected_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tuple with integer and string {42 \"hi\"}" do
|
||||||
|
{tuple_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node("{42 'hi'}")
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
|
||||||
|
expected_type = %{
|
||||||
|
type_kind: :tuple,
|
||||||
|
element_types: [
|
||||||
|
%{type_kind: :literal, value: 42},
|
||||||
|
%{type_kind: :literal, value: "hi"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(tuple_node, typed_nodes_map, expected_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tuple with nil and boolean {nil true}" do
|
||||||
|
{tuple_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node("{nil true}")
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
|
||||||
|
expected_type = %{
|
||||||
|
type_kind: :tuple,
|
||||||
|
element_types: [
|
||||||
|
Types.get_literal_type(:nil_atom), # This specific type structure comes from Types module
|
||||||
|
Types.get_literal_type(:true_atom) # This specific type structure comes from Types module
|
||||||
|
]
|
||||||
|
}
|
||||||
|
# Note: deep_strip_id in TestHelpers will handle the :id field if present in Types.get_literal_type results
|
||||||
|
TestHelpers.assert_node_typed_as(tuple_node, typed_nodes_map, expected_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "nested tuple {{1} 'a'}" do
|
||||||
|
{tuple_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node("{{1} 'a'}")
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
|
||||||
|
expected_type = %{
|
||||||
|
type_kind: :tuple,
|
||||||
|
element_types: [
|
||||||
|
%{type_kind: :tuple, element_types: [%{type_kind: :literal, value: 1}]},
|
||||||
|
%{type_kind: :literal, value: "a"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(tuple_node, typed_nodes_map, expected_type)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "tuple with a typed symbol after assignment" do
|
||||||
|
# Two top-level expressions
|
||||||
|
source = "(= x 10) {x 'str'}"
|
||||||
|
# Get the second top-level expression (the tuple)
|
||||||
|
{tuple_node, typed_nodes_map} = TestHelpers.typecheck_and_get_nth_node(source, 1)
|
||||||
|
|
||||||
|
assert tuple_node.ast_node_type == :tuple_expression
|
||||||
|
|
||||||
|
expected_type_clean = %{
|
||||||
|
type_kind: :tuple,
|
||||||
|
element_types: [
|
||||||
|
# Type of x is literal 10
|
||||||
|
%{type_kind: :literal, value: 10},
|
||||||
|
%{type_kind: :literal, value: "str"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
TestHelpers.assert_node_typed_as(tuple_node, typed_nodes_map, expected_type_clean)
|
||||||
|
end
|
||||||
|
end
|
||||||
264
test/til/type_union_test.exs
Normal file
264
test/til/type_union_test.exs
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
# defmodule Til.TypeUnionTest do
|
||||||
|
# use ExUnit.Case, async: true
|
||||||
|
#
|
||||||
|
# alias Til.Typer.Types
|
||||||
|
# alias Til.TestHelpers
|
||||||
|
# alias MapSet
|
||||||
|
#
|
||||||
|
# # --- Predefined Type Definitions for Assertions (Raw Forms) ---
|
||||||
|
# defp type_primitive_integer, do: Types.get_primitive_type(:integer)
|
||||||
|
# defp type_primitive_string, do: Types.get_primitive_type(:string)
|
||||||
|
# defp type_primitive_atom, do: Types.get_primitive_type(:atom)
|
||||||
|
# defp type_primitive_any, do: Types.get_primitive_type(:any)
|
||||||
|
# defp type_primitive_nothing, do: Types.get_primitive_type(:nothing)
|
||||||
|
#
|
||||||
|
# defp type_literal_int(val), do: %{type_kind: :literal, value: val}
|
||||||
|
# defp type_literal_string(val), do: %{type_kind: :literal, value: val}
|
||||||
|
# defp type_literal_atom(val), do: %{type_kind: :literal, value: val}
|
||||||
|
#
|
||||||
|
# defp type_list_raw(element_type_def, length \\ nil) do
|
||||||
|
# %{type_kind: :list, element_type: element_type_def, length: length}
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# defp type_map_raw(known_elements_raw, index_signature_raw) do
|
||||||
|
# %{
|
||||||
|
# type_kind: :map,
|
||||||
|
# known_elements: known_elements_raw,
|
||||||
|
# index_signature: index_signature_raw
|
||||||
|
# }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# defp type_union_raw(type_defs_list) do
|
||||||
|
# %{type_kind: :union, types: MapSet.new(type_defs_list)}
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # Helper to create the expected *cleaned* type_annotation_mismatch error structure
|
||||||
|
# defp type_error_type_annotation_mismatch(actual_type_clean, expected_type_clean) do
|
||||||
|
# %{
|
||||||
|
# type_kind: :error,
|
||||||
|
# reason: :type_annotation_mismatch,
|
||||||
|
# actual_type: actual_type_clean,
|
||||||
|
# expected_type: expected_type_clean
|
||||||
|
# }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# describe "(union ...) type specifier resolution and usage in (the ...)" do
|
||||||
|
# test "(the (union integer string) 42) - integer matches" do
|
||||||
|
# source = "(the (union integer string) 42)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# expected_annotation_type =
|
||||||
|
# type_union_raw([type_primitive_integer(), type_primitive_string()])
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_annotation_type)
|
||||||
|
#
|
||||||
|
# # Inner literal
|
||||||
|
# literal_node = TestHelpers.get_nth_child_node(typed_nodes_map, 2, the_node.id)
|
||||||
|
# TestHelpers.assert_node_typed_as(literal_node, typed_nodes_map, type_literal_int(42))
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (union integer string) \"hello\") - string matches" do
|
||||||
|
# source = "(the (union integer string) 'hello')"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# expected_annotation_type =
|
||||||
|
# type_union_raw([type_primitive_integer(), type_primitive_string()])
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_annotation_type)
|
||||||
|
#
|
||||||
|
# # Inner literal
|
||||||
|
# literal_node = TestHelpers.get_nth_child_node(typed_nodes_map, 2, the_node.id)
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(
|
||||||
|
# literal_node,
|
||||||
|
# typed_nodes_map,
|
||||||
|
# type_literal_string("hello")
|
||||||
|
# )
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (union integer string) :some_atom) - type mismatch" do
|
||||||
|
# source = "(the (union integer string) :some_atom)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# actual_type = type_literal_atom(:some_atom)
|
||||||
|
#
|
||||||
|
# expected_annotated_type =
|
||||||
|
# type_union_raw([type_primitive_integer(), type_primitive_string()])
|
||||||
|
#
|
||||||
|
# expected_error =
|
||||||
|
# type_error_type_annotation_mismatch(actual_type, expected_annotated_type)
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_error)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (union integer) 42) - single member union resolves to member type" do
|
||||||
|
# source = "(the (union integer) 42)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # The annotation (union integer) should resolve to just integer.
|
||||||
|
# # So, the 'the' expression's type is integer.
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, type_primitive_integer())
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (union integer) \"s\") - single member union type mismatch" do
|
||||||
|
# source = "(the (union integer) 's')"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# actual_type = type_literal_string("s")
|
||||||
|
# # (union integer) resolves to integer
|
||||||
|
# expected_annotated_type = type_primitive_integer()
|
||||||
|
#
|
||||||
|
# expected_error =
|
||||||
|
# type_error_type_annotation_mismatch(actual_type, expected_annotated_type)
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_error)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (union) 42) - empty union resolves to nothing, causes mismatch" do
|
||||||
|
# source = "(the (union) 42)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# actual_type = type_literal_int(42)
|
||||||
|
# # (union) resolves to nothing
|
||||||
|
# expected_annotated_type = type_primitive_nothing()
|
||||||
|
#
|
||||||
|
# expected_error =
|
||||||
|
# type_error_type_annotation_mismatch(actual_type, expected_annotated_type)
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_error)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "(the (union) nil) - empty union resolves to nothing, nil is not nothing" do
|
||||||
|
# # In Tilly, nil is a literal atom, not the type :nothing.
|
||||||
|
# # :nothing is the bottom type, no values inhabit it.
|
||||||
|
# source = "(the (union) nil)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # type of 'nil' symbol
|
||||||
|
# actual_type = %{type_kind: :literal, value: nil}
|
||||||
|
# expected_annotated_type = type_primitive_nothing()
|
||||||
|
#
|
||||||
|
# expected_error =
|
||||||
|
# type_error_type_annotation_mismatch(actual_type, expected_annotated_type)
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_error)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "nested unions: (the (union atom (union integer string)) :foo)" do
|
||||||
|
# source = "(the (union atom (union integer string)) :foo)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # (union integer string)
|
||||||
|
# inner_union = type_union_raw([type_primitive_integer(), type_primitive_string()])
|
||||||
|
# # (union atom inner_union)
|
||||||
|
# expected_annotation_type = type_union_raw([type_primitive_atom(), inner_union])
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_annotation_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "nested unions: (the (union (union integer string) atom) 1)" do
|
||||||
|
# source = "(the (union (union integer string) atom) 1)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# inner_union = type_union_raw([type_primitive_integer(), type_primitive_string()])
|
||||||
|
# expected_annotation_type = type_union_raw([inner_union, type_primitive_atom()])
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_annotation_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "unions with complex types: (the (union (list integer) (map atom string)) [1 2])" do
|
||||||
|
# source = "(the (union (list integer) (map atom string)) [1 2])"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # Length is dynamic for annotation
|
||||||
|
# list_int_type = type_list_raw(type_primitive_integer())
|
||||||
|
#
|
||||||
|
# map_atom_string_type =
|
||||||
|
# type_map_raw(
|
||||||
|
# %{},
|
||||||
|
# %{key_type: type_primitive_atom(), value_type: type_primitive_string()}
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# expected_annotation_type = type_union_raw([list_int_type, map_atom_string_type])
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_annotation_type)
|
||||||
|
#
|
||||||
|
# # Check inner list type
|
||||||
|
# # [1,2] -> type is (list (union (literal 1) (literal 2)) 2)
|
||||||
|
# # This is a subtype of (list integer)
|
||||||
|
# inner_list_node = TestHelpers.get_nth_child_node(typed_nodes_map, 2, the_node.id)
|
||||||
|
#
|
||||||
|
# # The type of [1,2] is (list (union (literal 1) (literal 2)) length: 2)
|
||||||
|
# # which simplifies to (list (literal 1 | literal 2) length: 2)
|
||||||
|
# # This is a subtype of (list integer).
|
||||||
|
# # The assertion on `the_node` already confirms the subtyping worked.
|
||||||
|
# # We can assert the specific type of the inner list if needed for clarity.
|
||||||
|
# expected_inner_list_element_type =
|
||||||
|
# type_union_raw([type_literal_int(1), type_literal_int(2)])
|
||||||
|
#
|
||||||
|
# expected_inner_list_type = type_list_raw(expected_inner_list_element_type, 2)
|
||||||
|
# TestHelpers.assert_node_typed_as(inner_list_node, typed_nodes_map, expected_inner_list_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "unions with complex types: (the (union (list integer) (map atom string)) m{:a \"b\"})" do
|
||||||
|
# source = "(the (union (list integer) (map atom string)) m{:a 'b'})"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# list_int_type = type_list_raw(type_primitive_integer())
|
||||||
|
#
|
||||||
|
# map_atom_string_type =
|
||||||
|
# type_map_raw(
|
||||||
|
# %{},
|
||||||
|
# %{key_type: type_primitive_atom(), value_type: type_primitive_string()}
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# expected_annotation_type = type_union_raw([list_int_type, map_atom_string_type])
|
||||||
|
#
|
||||||
|
# # The actual type of m{:a 'b'}
|
||||||
|
# actual_map_type =
|
||||||
|
# type_map_raw(
|
||||||
|
# %{a: %{value_type: type_literal_string("b"), optional: false}},
|
||||||
|
# %{key_type: type_primitive_any(), value_type: type_primitive_any()}
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# expected_error =
|
||||||
|
# type_error_type_annotation_mismatch(actual_map_type, expected_annotation_type)
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_error)
|
||||||
|
#
|
||||||
|
# # We can still check the inner map node's type independently if desired,
|
||||||
|
# # as its typing is correct, even if the 'the' expression results in an error.
|
||||||
|
# inner_map_node = TestHelpers.get_nth_child_node(typed_nodes_map, 2, the_node.id)
|
||||||
|
# TestHelpers.assert_node_typed_as(inner_map_node, typed_nodes_map, actual_map_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "union type specifier with unknown member type defaults to any for that member" do
|
||||||
|
# source = "(the (union integer foobar) 42)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # 'foobar' resolves to 'any'. So (union integer any) is effectively 'any'.
|
||||||
|
# # However, the structure of the union should be (union integer any) before simplification.
|
||||||
|
# # The subtyping check `literal 42 <: (union integer any)` will be true.
|
||||||
|
# # The type of the 'the' expression will be `(union integer any)`.
|
||||||
|
# expected_annotation_type = type_union_raw([type_primitive_integer(), type_primitive_any()])
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_annotation_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "union type specifier with all unknown member types" do
|
||||||
|
# source = "(the (union foo bar) 42)"
|
||||||
|
# {the_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# # (union foo bar) -> (union any any) -> any (after potential simplification by interner/subtype)
|
||||||
|
# # For now, let's expect the raw structure (union any any)
|
||||||
|
# expected_annotation_type = type_union_raw([type_primitive_any(), type_primitive_any()])
|
||||||
|
# TestHelpers.assert_node_typed_as(the_node, typed_nodes_map, expected_annotation_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "malformed union: (union integer string) - not in 'the'" do
|
||||||
|
# # This is not a (the ...) expression, so it's just an s-expression.
|
||||||
|
# # Its type will be 'any' by default for unknown s-expression operators.
|
||||||
|
# source = "(union integer string)"
|
||||||
|
# {sexpr_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
# TestHelpers.assert_node_typed_as(sexpr_node, typed_nodes_map, type_primitive_any())
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
246
test/til/typing_simple_test.exs
Normal file
246
test/til/typing_simple_test.exs
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# defmodule Til.TypingSimpleTest do
|
||||||
|
# use ExUnit.Case, async: true
|
||||||
|
# # lets always alias AstUtils, Typer and Parser for convenience
|
||||||
|
# alias Til.AstUtils
|
||||||
|
# alias Til.Parser
|
||||||
|
# alias Til.Typer
|
||||||
|
# # Added alias
|
||||||
|
# alias Til.TestHelpers
|
||||||
|
#
|
||||||
|
# alias MapSet, as: Set
|
||||||
|
#
|
||||||
|
# describe "simple type inference tests" do
|
||||||
|
# test "types a literal integer" do
|
||||||
|
# source = "42"
|
||||||
|
# {integer_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# assert integer_node.ast_node_type == :literal_integer
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(integer_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :literal,
|
||||||
|
# value: 42
|
||||||
|
# })
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types a literal string" do
|
||||||
|
# source = "'hello'"
|
||||||
|
# {string_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
#
|
||||||
|
# assert string_node.ast_node_type == :literal_string
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(string_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :literal,
|
||||||
|
# value: "hello"
|
||||||
|
# })
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# # test "types a simple s-expression (e.g. function call with literals)" do
|
||||||
|
# # source = "(add 1 2)"
|
||||||
|
# # {:ok, nodes_map} = Parser.parse(source)
|
||||||
|
# #
|
||||||
|
# # flunk("Test not implemented: #{inspect(nodes_map)}")
|
||||||
|
# # end
|
||||||
|
#
|
||||||
|
# test "types an s-expression with a symbol lookup in an environment" do
|
||||||
|
# source = """
|
||||||
|
# (= x 5)
|
||||||
|
# (= y x)
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# # Parse once
|
||||||
|
# {:ok, parsed_nodes_map} = Parser.parse(source)
|
||||||
|
# # Typecheck once
|
||||||
|
# {:ok, typed_nodes_map} = Typer.type_check(parsed_nodes_map)
|
||||||
|
#
|
||||||
|
# file_node = Enum.find(Map.values(typed_nodes_map), &(&1.ast_node_type == :file))
|
||||||
|
# refute is_nil(file_node)
|
||||||
|
# assert length(file_node.children) == 2, "Expected two top-level s-expressions"
|
||||||
|
#
|
||||||
|
# # --- First assignment: (= x 5) ---
|
||||||
|
# s_expr_1_node = TestHelpers.get_nth_child_node(typed_nodes_map, 0, file_node.id)
|
||||||
|
# assert s_expr_1_node.ast_node_type == :s_expression
|
||||||
|
# assert length(s_expr_1_node.children) == 3
|
||||||
|
#
|
||||||
|
# symbol_x_lhs_node = TestHelpers.get_nth_child_node(typed_nodes_map, 1, s_expr_1_node.id)
|
||||||
|
# assert symbol_x_lhs_node.ast_node_type == :symbol
|
||||||
|
# assert symbol_x_lhs_node.name == "x"
|
||||||
|
#
|
||||||
|
# literal_5_node = TestHelpers.get_nth_child_node(typed_nodes_map, 2, s_expr_1_node.id)
|
||||||
|
# assert literal_5_node.ast_node_type == :literal_integer
|
||||||
|
# assert literal_5_node.value == 5
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(literal_5_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :literal,
|
||||||
|
# value: 5
|
||||||
|
# })
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(s_expr_1_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :literal,
|
||||||
|
# value: 5
|
||||||
|
# })
|
||||||
|
#
|
||||||
|
# # --- Second assignment: (= y x) ---
|
||||||
|
# s_expr_2_node = TestHelpers.get_nth_child_node(typed_nodes_map, 1, file_node.id)
|
||||||
|
# assert s_expr_2_node.ast_node_type == :s_expression
|
||||||
|
# assert length(s_expr_2_node.children) == 3
|
||||||
|
#
|
||||||
|
# symbol_y_lhs_node = TestHelpers.get_nth_child_node(typed_nodes_map, 1, s_expr_2_node.id)
|
||||||
|
# assert symbol_y_lhs_node.ast_node_type == :symbol
|
||||||
|
# assert symbol_y_lhs_node.name == "y"
|
||||||
|
#
|
||||||
|
# symbol_x_rhs_node = TestHelpers.get_nth_child_node(typed_nodes_map, 2, s_expr_2_node.id)
|
||||||
|
# assert symbol_x_rhs_node.ast_node_type == :symbol
|
||||||
|
# assert symbol_x_rhs_node.name == "x"
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(symbol_x_rhs_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :literal,
|
||||||
|
# value: 5
|
||||||
|
# })
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(s_expr_2_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :literal,
|
||||||
|
# value: 5
|
||||||
|
# })
|
||||||
|
#
|
||||||
|
# # Assert that 'y' in the environment (and thus its node if we were to look it up again) would be integer.
|
||||||
|
# # The symbol_y_lhs_node itself might be typed as :any before the assignment's effect is fully "realized" on it.
|
||||||
|
# # The critical part is that the environment passed forward contains y: :til_type_integer.
|
||||||
|
# # The final environment is inspected in Typer.type_check, which can be manually verified for now.
|
||||||
|
# # For this test, checking the type of the assignment expression (s_expr_2_node) and the RHS (symbol_x_rhs_node) is sufficient.
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types an if expression with same type in both branches, ambiguous condition" do
|
||||||
|
# source = """
|
||||||
|
# (= cond_var some_ambiguous_symbol)
|
||||||
|
# (if cond_var 1 2)
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_nth_node(source, 1)
|
||||||
|
#
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
#
|
||||||
|
# expected_type_1 = %{type_kind: :literal, value: 1}
|
||||||
|
# expected_type_2 = %{type_kind: :literal, value: 2}
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :union,
|
||||||
|
# types: Set.new([expected_type_1, expected_type_2])
|
||||||
|
# })
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types an if expression with different types, ambiguous condition, resulting in a union" do
|
||||||
|
# source = """
|
||||||
|
# (= cond_var some_ambiguous_symbol)
|
||||||
|
# (if cond_var 1 'hello')
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_nth_node(source, 1)
|
||||||
|
#
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
#
|
||||||
|
# expected_int_type = %{type_kind: :literal, value: 1}
|
||||||
|
# expected_str_type = %{type_kind: :literal, value: "hello"}
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :union,
|
||||||
|
# types: Set.new([expected_int_type, expected_str_type])
|
||||||
|
# })
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types an if expression with a missing else branch, ambiguous condition (union with nil type)" do
|
||||||
|
# source = """
|
||||||
|
# (= cond_var some_ambiguous_symbol)
|
||||||
|
# (if cond_var 1)
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_nth_node(source, 1)
|
||||||
|
#
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
#
|
||||||
|
# expected_int_type = %{type_kind: :literal, value: 1}
|
||||||
|
# expected_nil_type = %{type_kind: :literal, value: nil}
|
||||||
|
#
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, %{
|
||||||
|
# type_kind: :union,
|
||||||
|
# types: Set.new([expected_int_type, expected_nil_type])
|
||||||
|
# })
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types an if expression where then branch is nil, missing else, condition true (results in nil type)" do
|
||||||
|
# source = """
|
||||||
|
# (= x nil)
|
||||||
|
# (if true x)
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_nth_node(source, 1)
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
# expected_nil_type = %{type_kind: :literal, value: nil}
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, expected_nil_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types an if expression where the condition is true, then branch is 1, missing else (results in `1` type)" do
|
||||||
|
# source = """
|
||||||
|
# (= x 1)
|
||||||
|
# (if true x)
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_nth_node(source, 1)
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
# expected_type_of_1 = %{type_kind: :literal, value: 1}
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, expected_type_of_1)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "types an if expression where then and else are nil, condition true (results in nil type)" do
|
||||||
|
# source = """
|
||||||
|
# (= x nil)
|
||||||
|
# (if true x x)
|
||||||
|
# """
|
||||||
|
#
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_nth_node(source, 1)
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
# expected_nil_type = %{type_kind: :literal, value: nil}
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, expected_nil_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "if expression with statically false condition, missing else (results in nil type)" do
|
||||||
|
# source = "(if false 123)"
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
# expected_nil_type = %{type_kind: :literal, value: nil}
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, expected_nil_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "if expression with statically false condition, with else branch (results in else branch type)" do
|
||||||
|
# source = "(if false 123 'else_val')"
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
# expected_else_type = %{type_kind: :literal, value: "else_val"}
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, expected_else_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "if expression with truthy non-boolean literal condition type (integer)" do
|
||||||
|
# source = """
|
||||||
|
# (if 123 'then' 'else')
|
||||||
|
# """
|
||||||
|
# # Since 123 is truthy, the type of the if expression should be the type of 'then'.
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_first_node(source)
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
#
|
||||||
|
# expected_then_type = %{type_kind: :literal, value: "then"}
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, expected_then_type)
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# test "if expression with truthy non-boolean symbol condition type (typed as integer)" do
|
||||||
|
# source = """
|
||||||
|
# (= my_int_cond 123)
|
||||||
|
# (if my_int_cond 'then' 'else')
|
||||||
|
# """
|
||||||
|
# # my_int_cond is 123 (truthy), so the type of the if expression should be the type of 'then'.
|
||||||
|
# {if_node, typed_nodes_map} = TestHelpers.typecheck_and_get_nth_node(source, 1)
|
||||||
|
# assert if_node.ast_node_type == :s_expression
|
||||||
|
#
|
||||||
|
# expected_then_type = %{type_kind: :literal, value: "then"}
|
||||||
|
# TestHelpers.assert_node_typed_as(if_node, typed_nodes_map, expected_then_type)
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
8
test/til_test.exs
Normal file
8
test/til_test.exs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
defmodule TilTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest Til
|
||||||
|
|
||||||
|
test "greets the world" do
|
||||||
|
assert Til.hello() == :world
|
||||||
|
end
|
||||||
|
end
|
||||||
77
test/tilly/bdd/atom_bool_ops_test.exs
Normal file
77
test/tilly/bdd/atom_bool_ops_test.exs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
defmodule Tilly.BDD.AtomBoolOpsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD.AtomBoolOps
|
||||||
|
|
||||||
|
describe "compare_elements/2" do
|
||||||
|
test "correctly compares atoms" do
|
||||||
|
assert AtomBoolOps.compare_elements(:apple, :banana) == :lt
|
||||||
|
assert AtomBoolOps.compare_elements(:banana, :apple) == :gt
|
||||||
|
assert AtomBoolOps.compare_elements(:cherry, :cherry) == :eq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "equal_element?/2" do
|
||||||
|
test "correctly checks atom equality" do
|
||||||
|
assert AtomBoolOps.equal_element?(:apple, :apple) == true
|
||||||
|
assert AtomBoolOps.equal_element?(:apple, :banana) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "hash_element/1" do
|
||||||
|
test "hashes atoms consistently" do
|
||||||
|
assert is_integer(AtomBoolOps.hash_element(:foo))
|
||||||
|
assert AtomBoolOps.hash_element(:foo) == AtomBoolOps.hash_element(:foo)
|
||||||
|
assert AtomBoolOps.hash_element(:foo) != AtomBoolOps.hash_element(:bar)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "leaf operations" do
|
||||||
|
test "empty_leaf/0 returns false" do
|
||||||
|
assert AtomBoolOps.empty_leaf() == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "any_leaf/0 returns true" do
|
||||||
|
assert AtomBoolOps.any_leaf() == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_empty_leaf?/1" do
|
||||||
|
assert AtomBoolOps.is_empty_leaf?(false) == true
|
||||||
|
assert AtomBoolOps.is_empty_leaf?(true) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "union_leaves/3" do
|
||||||
|
assert AtomBoolOps.union_leaves(%{}, false, false) == false
|
||||||
|
assert AtomBoolOps.union_leaves(%{}, true, false) == true
|
||||||
|
assert AtomBoolOps.union_leaves(%{}, false, true) == true
|
||||||
|
assert AtomBoolOps.union_leaves(%{}, true, true) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "intersection_leaves/3" do
|
||||||
|
assert AtomBoolOps.intersection_leaves(%{}, false, false) == false
|
||||||
|
assert AtomBoolOps.intersection_leaves(%{}, true, false) == false
|
||||||
|
assert AtomBoolOps.intersection_leaves(%{}, false, true) == false
|
||||||
|
assert AtomBoolOps.intersection_leaves(%{}, true, true) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "negation_leaf/2" do
|
||||||
|
assert AtomBoolOps.negation_leaf(%{}, false) == true
|
||||||
|
assert AtomBoolOps.negation_leaf(%{}, true) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "test_leaf_value/1" do
|
||||||
|
test "returns :empty for false" do
|
||||||
|
assert AtomBoolOps.test_leaf_value(false) == :empty
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns :full for true" do
|
||||||
|
assert AtomBoolOps.test_leaf_value(true) == :full
|
||||||
|
end
|
||||||
|
|
||||||
|
# Conceptual test if atoms had other leaf values
|
||||||
|
# test "returns :other for other values" do
|
||||||
|
# assert AtomBoolOps.test_leaf_value(:some_other_leaf_marker) == :other
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
end
|
||||||
67
test/tilly/bdd/integer_bool_ops_test.exs
Normal file
67
test/tilly/bdd/integer_bool_ops_test.exs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
defmodule Tilly.BDD.IntegerBoolOpsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD.IntegerBoolOps
|
||||||
|
|
||||||
|
describe "compare_elements/2" do
|
||||||
|
test "correctly compares integers" do
|
||||||
|
assert IntegerBoolOps.compare_elements(1, 2) == :lt
|
||||||
|
assert IntegerBoolOps.compare_elements(2, 1) == :gt
|
||||||
|
assert IntegerBoolOps.compare_elements(1, 1) == :eq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "equal_element?/2" do
|
||||||
|
test "correctly checks equality of integers" do
|
||||||
|
assert IntegerBoolOps.equal_element?(1, 1) == true
|
||||||
|
assert IntegerBoolOps.equal_element?(1, 2) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "hash_element/1" do
|
||||||
|
test "returns the integer itself as hash" do
|
||||||
|
assert IntegerBoolOps.hash_element(123) == 123
|
||||||
|
assert IntegerBoolOps.hash_element(-5) == -5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "leaf operations" do
|
||||||
|
test "empty_leaf/0 returns false" do
|
||||||
|
assert IntegerBoolOps.empty_leaf() == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "any_leaf/0 returns true" do
|
||||||
|
assert IntegerBoolOps.any_leaf() == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_empty_leaf?/1" do
|
||||||
|
assert IntegerBoolOps.is_empty_leaf?(false) == true
|
||||||
|
assert IntegerBoolOps.is_empty_leaf?(true) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "union_leaves/3" do
|
||||||
|
test "computes boolean OR" do
|
||||||
|
assert IntegerBoolOps.union_leaves(%{}, true, true) == true
|
||||||
|
assert IntegerBoolOps.union_leaves(%{}, true, false) == true
|
||||||
|
assert IntegerBoolOps.union_leaves(%{}, false, true) == true
|
||||||
|
assert IntegerBoolOps.union_leaves(%{}, false, false) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "intersection_leaves/3" do
|
||||||
|
test "computes boolean AND" do
|
||||||
|
assert IntegerBoolOps.intersection_leaves(%{}, true, true) == true
|
||||||
|
assert IntegerBoolOps.intersection_leaves(%{}, true, false) == false
|
||||||
|
assert IntegerBoolOps.intersection_leaves(%{}, false, true) == false
|
||||||
|
assert IntegerBoolOps.intersection_leaves(%{}, false, false) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "negation_leaf/2" do
|
||||||
|
test "computes boolean NOT" do
|
||||||
|
assert IntegerBoolOps.negation_leaf(%{}, true) == false
|
||||||
|
assert IntegerBoolOps.negation_leaf(%{}, false) == true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
123
test/tilly/bdd/node_test.exs
Normal file
123
test/tilly/bdd/node_test.exs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
defmodule Tilly.BDD.NodeTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD.Node
|
||||||
|
|
||||||
|
describe "Smart Constructors" do
|
||||||
|
test "mk_true/0 returns true" do
|
||||||
|
assert Node.mk_true() == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "mk_false/0 returns false" do
|
||||||
|
assert Node.mk_false() == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "mk_leaf/1 creates a leaf node" do
|
||||||
|
assert Node.mk_leaf(:some_value) == {:leaf, :some_value}
|
||||||
|
assert Node.mk_leaf(123) == {:leaf, 123}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "mk_split/4 creates a split node" do
|
||||||
|
assert Node.mk_split(:el, :p_id, :i_id, :n_id) == {:split, :el, :p_id, :i_id, :n_id}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Predicates" do
|
||||||
|
setup do
|
||||||
|
%{
|
||||||
|
true_node: Node.mk_true(),
|
||||||
|
false_node: Node.mk_false(),
|
||||||
|
leaf_node: Node.mk_leaf("data"),
|
||||||
|
split_node: Node.mk_split(1, 2, 3, 4)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_true?/1", %{true_node: t, false_node: f, leaf_node: l, split_node: s} do
|
||||||
|
assert Node.is_true?(t) == true
|
||||||
|
assert Node.is_true?(f) == false
|
||||||
|
assert Node.is_true?(l) == false
|
||||||
|
assert Node.is_true?(s) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_false?/1", %{true_node: t, false_node: f, leaf_node: l, split_node: s} do
|
||||||
|
assert Node.is_false?(f) == true
|
||||||
|
assert Node.is_false?(t) == false
|
||||||
|
assert Node.is_false?(l) == false
|
||||||
|
assert Node.is_false?(s) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_leaf?/1", %{true_node: t, false_node: f, leaf_node: l, split_node: s} do
|
||||||
|
assert Node.is_leaf?(l) == true
|
||||||
|
assert Node.is_leaf?(t) == false
|
||||||
|
assert Node.is_leaf?(f) == false
|
||||||
|
assert Node.is_leaf?(s) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_split?/1", %{true_node: t, false_node: f, leaf_node: l, split_node: s} do
|
||||||
|
assert Node.is_split?(s) == true
|
||||||
|
assert Node.is_split?(t) == false
|
||||||
|
assert Node.is_split?(f) == false
|
||||||
|
assert Node.is_split?(l) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Accessors" do
|
||||||
|
setup do
|
||||||
|
%{
|
||||||
|
leaf_node: Node.mk_leaf("leaf_data"),
|
||||||
|
split_node: Node.mk_split(:elem_id, :pos_child, :ign_child, :neg_child)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "value/1 for leaf node", %{leaf_node: l} do
|
||||||
|
assert Node.value(l) == "leaf_data"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "value/1 raises for non-leaf node" do
|
||||||
|
assert_raise ArgumentError, ~r/Not a leaf node/, fn -> Node.value(Node.mk_true()) end
|
||||||
|
|
||||||
|
assert_raise ArgumentError, ~r/Not a leaf node/, fn ->
|
||||||
|
Node.value(Node.mk_split(1, 2, 3, 4))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "element/1 for split node", %{split_node: s} do
|
||||||
|
assert Node.element(s) == :elem_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "element/1 raises for non-split node" do
|
||||||
|
assert_raise ArgumentError, ~r/Not a split node/, fn -> Node.element(Node.mk_true()) end
|
||||||
|
assert_raise ArgumentError, ~r/Not a split node/, fn -> Node.element(Node.mk_leaf(1)) end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "positive_child/1 for split node", %{split_node: s} do
|
||||||
|
assert Node.positive_child(s) == :pos_child
|
||||||
|
end
|
||||||
|
|
||||||
|
test "positive_child/1 raises for non-split node" do
|
||||||
|
assert_raise ArgumentError, ~r/Not a split node/, fn ->
|
||||||
|
Node.positive_child(Node.mk_leaf(1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ignore_child/1 for split node", %{split_node: s} do
|
||||||
|
assert Node.ignore_child(s) == :ign_child
|
||||||
|
end
|
||||||
|
|
||||||
|
test "ignore_child/1 raises for non-split node" do
|
||||||
|
assert_raise ArgumentError, ~r/Not a split node/, fn ->
|
||||||
|
Node.ignore_child(Node.mk_leaf(1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "negative_child/1 for split node", %{split_node: s} do
|
||||||
|
assert Node.negative_child(s) == :neg_child
|
||||||
|
end
|
||||||
|
|
||||||
|
test "negative_child/1 raises for non-split node" do
|
||||||
|
assert_raise ArgumentError, ~r/Not a split node/, fn ->
|
||||||
|
Node.negative_child(Node.mk_leaf(1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
191
test/tilly/bdd/ops_test.exs
Normal file
191
test/tilly/bdd/ops_test.exs
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
defmodule Tilly.BDD.OpsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD
|
||||||
|
alias Tilly.BDD.Node
|
||||||
|
alias Tilly.BDD.Ops
|
||||||
|
alias Tilly.BDD.IntegerBoolOps # Using a concrete ops_module for testing
|
||||||
|
|
||||||
|
setup do
|
||||||
|
typing_ctx = BDD.init_bdd_store(%{})
|
||||||
|
# Pre-intern some common elements for tests if needed, e.g., integers
|
||||||
|
# For now, rely on ops to intern elements as they are used.
|
||||||
|
%{initial_ctx: typing_ctx}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "leaf/3" do
|
||||||
|
test "interning an empty leaf value returns predefined false_id", %{initial_ctx: ctx} do
|
||||||
|
{new_ctx, node_id} = Ops.leaf(ctx, false, IntegerBoolOps)
|
||||||
|
assert node_id == BDD.false_node_id()
|
||||||
|
assert new_ctx.bdd_store.ops_cache == ctx.bdd_store.ops_cache # Cache not used for this path
|
||||||
|
end
|
||||||
|
|
||||||
|
test "interning a full leaf value returns predefined true_id", %{initial_ctx: ctx} do
|
||||||
|
{new_ctx, node_id} = Ops.leaf(ctx, true, IntegerBoolOps)
|
||||||
|
assert node_id == BDD.true_node_id()
|
||||||
|
assert new_ctx.bdd_store.ops_cache == ctx.bdd_store.ops_cache
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag :skip
|
||||||
|
test "interning a new 'other' leaf value returns a new ID", %{initial_ctx: _ctx} do
|
||||||
|
# Assuming IntegerBoolOps.test_leaf_value/1 would return :other for non-booleans
|
||||||
|
# For this test, we'd need an ops_module where e.g. an integer is an :other leaf.
|
||||||
|
# Let's simulate with a mock or by extending IntegerBoolOps if it were not read-only.
|
||||||
|
# For now, this test is conceptual for boolean leaves.
|
||||||
|
# If IntegerBoolOps was extended:
|
||||||
|
# defmodule MockIntegerOps do
|
||||||
|
# defdelegate compare_elements(e1, e2), to: IntegerBoolOps
|
||||||
|
# defdelegate equal_element?(e1, e2), to: IntegerBoolOps
|
||||||
|
# # ... other delegates
|
||||||
|
# def test_leaf_value(10), do: :other # Treat 10 as a specific leaf
|
||||||
|
# def test_leaf_value(true), do: :full
|
||||||
|
# def test_leaf_value(false), do: :empty
|
||||||
|
# end
|
||||||
|
# {ctx_after_intern, node_id} = Ops.leaf(ctx, 10, MockIntegerOps)
|
||||||
|
# assert node_id != BDD.true_node_id() and node_id != BDD.false_node_id()
|
||||||
|
# assert BDD.get_node_data(ctx_after_intern, node_id).structure == Node.mk_leaf(10)
|
||||||
|
# Placeholder for more complex leaf types. Test is skipped.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "split/6 basic simplifications" do
|
||||||
|
test "if i_id is true, returns true_id", %{initial_ctx: ctx} do
|
||||||
|
{_p_ctx, p_id} = Ops.leaf(ctx, false, IntegerBoolOps) # dummy
|
||||||
|
{_n_ctx, n_id} = Ops.leaf(ctx, false, IntegerBoolOps) # dummy
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
|
||||||
|
{new_ctx, result_id} = Ops.split(ctx, 10, p_id, true_id, n_id, IntegerBoolOps)
|
||||||
|
assert result_id == true_id
|
||||||
|
assert new_ctx == ctx # No new nodes or cache entries expected for this rule
|
||||||
|
end
|
||||||
|
|
||||||
|
test "if p_id == n_id and p_id == i_id, returns p_id", %{initial_ctx: ctx} do
|
||||||
|
{ctx, p_id} = BDD.get_or_intern_node(ctx, Node.mk_leaf(false), IntegerBoolOps) # some leaf
|
||||||
|
i_id = p_id
|
||||||
|
n_id = p_id
|
||||||
|
|
||||||
|
{_new_ctx, result_id} = Ops.split(ctx, 10, p_id, i_id, n_id, IntegerBoolOps)
|
||||||
|
assert result_id == p_id
|
||||||
|
# Cache might be touched if union_bdds was called, but this rule is direct.
|
||||||
|
# For p_id == i_id, it's direct.
|
||||||
|
end
|
||||||
|
|
||||||
|
test "if p_id == n_id and p_id != i_id, returns union(p_id, i_id)", %{initial_ctx: ctx} do
|
||||||
|
{ctx, p_id} = BDD.get_or_intern_node(ctx, Node.mk_leaf(false), IntegerBoolOps)
|
||||||
|
{ctx, i_id} = BDD.get_or_intern_node(ctx, Node.mk_leaf(true), IntegerBoolOps) # different leaf
|
||||||
|
n_id = p_id
|
||||||
|
|
||||||
|
# Expected union of p_id (false_leaf) and i_id (true_leaf) is true_id
|
||||||
|
# This relies on union_bdds working.
|
||||||
|
{_new_ctx, result_id} = Ops.split(ctx, 10, p_id, i_id, n_id, IntegerBoolOps)
|
||||||
|
expected_union_id = BDD.true_node_id() # Union of false_leaf and true_leaf
|
||||||
|
assert result_id == expected_union_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "interns a new split node if no simplification rule applies", %{initial_ctx: ctx} do
|
||||||
|
{ctx, p_id} = Ops.leaf(ctx, false, IntegerBoolOps) # false_node_id
|
||||||
|
{ctx, i_id} = Ops.leaf(ctx, false, IntegerBoolOps) # false_node_id
|
||||||
|
{ctx, n_id} = Ops.leaf(ctx, true, IntegerBoolOps) # true_node_id (different from p_id)
|
||||||
|
|
||||||
|
element = 20
|
||||||
|
{new_ctx, split_node_id} = Ops.split(ctx, element, p_id, i_id, n_id, IntegerBoolOps)
|
||||||
|
|
||||||
|
assert split_node_id != p_id and split_node_id != i_id and split_node_id != n_id
|
||||||
|
assert split_node_id != BDD.true_node_id() and split_node_id != BDD.false_node_id()
|
||||||
|
|
||||||
|
node_data = BDD.get_node_data(new_ctx, split_node_id)
|
||||||
|
assert node_data.structure == Node.mk_split(element, p_id, i_id, n_id)
|
||||||
|
assert node_data.ops_module == IntegerBoolOps
|
||||||
|
assert new_ctx.bdd_store.next_node_id > ctx.bdd_store.next_node_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "union_bdds/3" do
|
||||||
|
test "A U A = A", %{initial_ctx: ctx} do
|
||||||
|
{ctx, a_id} = Ops.leaf(ctx, false, IntegerBoolOps) # false_node_id
|
||||||
|
{new_ctx, result_id} = Ops.union_bdds(ctx, a_id, a_id)
|
||||||
|
assert result_id == a_id
|
||||||
|
assert Map.has_key?(new_ctx.bdd_store.ops_cache, {:union, a_id, a_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "A U True = True", %{initial_ctx: ctx} do
|
||||||
|
{ctx, a_id} = Ops.leaf(ctx, false, IntegerBoolOps)
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
{_new_ctx, result_id} = Ops.union_bdds(ctx, a_id, true_id)
|
||||||
|
assert result_id == true_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "A U False = A", %{initial_ctx: ctx} do
|
||||||
|
{ctx, a_id} = Ops.leaf(ctx, true, IntegerBoolOps) # true_node_id
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
{_new_ctx, result_id} = Ops.union_bdds(ctx, a_id, false_id)
|
||||||
|
assert result_id == a_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "union of two distinct leaves", %{initial_ctx: ctx} do
|
||||||
|
# leaf(false) U leaf(true) = leaf(true OR false) = leaf(true) -> true_node_id
|
||||||
|
{ctx, leaf_false_id} = Ops.leaf(ctx, false, IntegerBoolOps)
|
||||||
|
{ctx, leaf_true_id} = Ops.leaf(ctx, true, IntegerBoolOps) # This is BDD.true_node_id()
|
||||||
|
|
||||||
|
{_new_ctx, result_id} = Ops.union_bdds(ctx, leaf_false_id, leaf_true_id)
|
||||||
|
assert result_id == BDD.true_node_id()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "union of two simple split nodes with same element", %{initial_ctx: ctx} do
|
||||||
|
# BDD1: split(10, True, False, False)
|
||||||
|
# BDD2: split(10, False, True, False)
|
||||||
|
# Union: split(10, True U False, False U True, False U False)
|
||||||
|
# = split(10, True, True, False)
|
||||||
|
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
|
||||||
|
{ctx, bdd1_id} = Ops.split(ctx, 10, true_id, false_id, false_id, IntegerBoolOps)
|
||||||
|
{ctx, bdd2_id} = Ops.split(ctx, 10, false_id, true_id, false_id, IntegerBoolOps)
|
||||||
|
|
||||||
|
{final_ctx, union_id} = Ops.union_bdds(ctx, bdd1_id, bdd2_id)
|
||||||
|
|
||||||
|
# Expected structure
|
||||||
|
{_final_ctx, expected_bdd_id} = Ops.split(final_ctx, 10, true_id, true_id, false_id, IntegerBoolOps)
|
||||||
|
assert union_id == expected_bdd_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "union of two simple split nodes with different elements (x1 < x2)", %{initial_ctx: ctx} do
|
||||||
|
# BDD1: split(10, True, False, False)
|
||||||
|
# BDD2: split(20, False, True, False)
|
||||||
|
# Union (x1 < x2): split(10, p1, i1 U BDD2, n1)
|
||||||
|
# = split(10, True, False U BDD2, False)
|
||||||
|
# = split(10, True, BDD2, False)
|
||||||
|
|
||||||
|
{ctx, bdd1_p1_id} = Ops.leaf(ctx, true, IntegerBoolOps)
|
||||||
|
{ctx, bdd1_i1_id} = Ops.leaf(ctx, false, IntegerBoolOps)
|
||||||
|
{ctx, bdd1_n1_id} = Ops.leaf(ctx, false, IntegerBoolOps)
|
||||||
|
{ctx, bdd1_id} = Ops.split(ctx, 10, bdd1_p1_id, bdd1_i1_id, bdd1_n1_id, IntegerBoolOps)
|
||||||
|
|
||||||
|
{ctx, bdd2_p2_id} = Ops.leaf(ctx, false, IntegerBoolOps)
|
||||||
|
{ctx, bdd2_i2_id} = Ops.leaf(ctx, true, IntegerBoolOps)
|
||||||
|
{ctx, bdd2_n2_id} = Ops.leaf(ctx, false, IntegerBoolOps)
|
||||||
|
{ctx, bdd2_id} = Ops.split(ctx, 20, bdd2_p2_id, bdd2_i2_id, bdd2_n2_id, IntegerBoolOps)
|
||||||
|
|
||||||
|
{final_ctx, union_id} = Ops.union_bdds(ctx, bdd1_id, bdd2_id)
|
||||||
|
|
||||||
|
# Expected structure: split(10, True, BDD2, False)
|
||||||
|
{_final_ctx, expected_bdd_id} = Ops.split(final_ctx, 10, bdd1_p1_id, bdd2_id, bdd1_n1_id, IntegerBoolOps)
|
||||||
|
assert union_id == expected_bdd_id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses cache for repeated union operations", %{initial_ctx: ctx} do
|
||||||
|
{ctx, a_id} = Ops.leaf(ctx, false, IntegerBoolOps)
|
||||||
|
{ctx, b_id} = Ops.leaf(ctx, true, IntegerBoolOps)
|
||||||
|
|
||||||
|
{ctx_after_first_union, _result1_id} = Ops.union_bdds(ctx, a_id, b_id)
|
||||||
|
cache_after_first = ctx_after_first_union.bdd_store.ops_cache
|
||||||
|
|
||||||
|
{ctx_after_second_union, _result2_id} = Ops.union_bdds(ctx_after_first_union, a_id, b_id)
|
||||||
|
# The BDD store itself (nodes, next_id) should not change on a cache hit.
|
||||||
|
# The ops_cache map reference will be the same if the result was cached.
|
||||||
|
assert ctx_after_second_union.bdd_store.ops_cache == cache_after_first
|
||||||
|
assert ctx_after_second_union.bdd_store.next_node_id == ctx_after_first_union.bdd_store.next_node_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
72
test/tilly/bdd/string_bool_ops_test.exs
Normal file
72
test/tilly/bdd/string_bool_ops_test.exs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
defmodule Tilly.BDD.StringBoolOpsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD.StringBoolOps
|
||||||
|
|
||||||
|
describe "compare_elements/2" do
|
||||||
|
test "correctly compares strings" do
|
||||||
|
assert StringBoolOps.compare_elements("apple", "banana") == :lt
|
||||||
|
assert StringBoolOps.compare_elements("banana", "apple") == :gt
|
||||||
|
assert StringBoolOps.compare_elements("cherry", "cherry") == :eq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "equal_element?/2" do
|
||||||
|
test "correctly checks string equality" do
|
||||||
|
assert StringBoolOps.equal_element?("apple", "apple") == true
|
||||||
|
assert StringBoolOps.equal_element?("apple", "banana") == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "hash_element/1" do
|
||||||
|
test "hashes strings consistently" do
|
||||||
|
assert is_integer(StringBoolOps.hash_element("foo"))
|
||||||
|
assert StringBoolOps.hash_element("foo") == StringBoolOps.hash_element("foo")
|
||||||
|
assert StringBoolOps.hash_element("foo") != StringBoolOps.hash_element("bar")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "leaf operations" do
|
||||||
|
test "empty_leaf/0 returns false" do
|
||||||
|
assert StringBoolOps.empty_leaf() == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "any_leaf/0 returns true" do
|
||||||
|
assert StringBoolOps.any_leaf() == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_empty_leaf?/1" do
|
||||||
|
assert StringBoolOps.is_empty_leaf?(false) == true
|
||||||
|
assert StringBoolOps.is_empty_leaf?(true) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "union_leaves/3" do
|
||||||
|
assert StringBoolOps.union_leaves(%{}, false, false) == false
|
||||||
|
assert StringBoolOps.union_leaves(%{}, true, false) == true
|
||||||
|
assert StringBoolOps.union_leaves(%{}, false, true) == true
|
||||||
|
assert StringBoolOps.union_leaves(%{}, true, true) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "intersection_leaves/3" do
|
||||||
|
assert StringBoolOps.intersection_leaves(%{}, false, false) == false
|
||||||
|
assert StringBoolOps.intersection_leaves(%{}, true, false) == false
|
||||||
|
assert StringBoolOps.intersection_leaves(%{}, false, true) == false
|
||||||
|
assert StringBoolOps.intersection_leaves(%{}, true, true) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
test "negation_leaf/2" do
|
||||||
|
assert StringBoolOps.negation_leaf(%{}, false) == true
|
||||||
|
assert StringBoolOps.negation_leaf(%{}, true) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "test_leaf_value/1" do
|
||||||
|
test "returns :empty for false" do
|
||||||
|
assert StringBoolOps.test_leaf_value(false) == :empty
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns :full for true" do
|
||||||
|
assert StringBoolOps.test_leaf_value(true) == :full
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
163
test/tilly/bdd_test.exs
Normal file
163
test/tilly/bdd_test.exs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
defmodule Tilly.BDDTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD.Node
|
||||||
|
|
||||||
|
describe "init_bdd_store/1" do
|
||||||
|
test "initializes bdd_store in typing_ctx with predefined false and true nodes" do
|
||||||
|
typing_ctx = %{}
|
||||||
|
new_ctx = Tilly.BDD.init_bdd_store(typing_ctx)
|
||||||
|
|
||||||
|
assert %{bdd_store: bdd_store} = new_ctx
|
||||||
|
assert is_map(bdd_store.nodes_by_structure)
|
||||||
|
assert is_map(bdd_store.structures_by_id)
|
||||||
|
assert bdd_store.next_node_id == 2 # 0 for false, 1 for true
|
||||||
|
assert bdd_store.ops_cache == %{}
|
||||||
|
|
||||||
|
# Check false node
|
||||||
|
false_id = Tilly.BDD.false_node_id()
|
||||||
|
false_ops_module = Tilly.BDD.universal_ops_module()
|
||||||
|
assert bdd_store.nodes_by_structure[{Node.mk_false(), false_ops_module}] == false_id
|
||||||
|
assert bdd_store.structures_by_id[false_id] == %{structure: Node.mk_false(), ops_module: false_ops_module}
|
||||||
|
|
||||||
|
# Check true node
|
||||||
|
true_id = Tilly.BDD.true_node_id()
|
||||||
|
true_ops_module = Tilly.BDD.universal_ops_module()
|
||||||
|
assert bdd_store.nodes_by_structure[{Node.mk_true(), true_ops_module}] == true_id
|
||||||
|
assert bdd_store.structures_by_id[true_id] == %{structure: Node.mk_true(), ops_module: true_ops_module}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_or_intern_node/3" do
|
||||||
|
setup do
|
||||||
|
typing_ctx = Tilly.BDD.init_bdd_store(%{})
|
||||||
|
%{initial_ctx: typing_ctx}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "interning Node.mk_false() returns predefined false_id and doesn't change store", %{initial_ctx: ctx} do
|
||||||
|
false_ops_module = Tilly.BDD.universal_ops_module()
|
||||||
|
{new_ctx, node_id} = Tilly.BDD.get_or_intern_node(ctx, Node.mk_false(), false_ops_module)
|
||||||
|
assert node_id == Tilly.BDD.false_node_id()
|
||||||
|
assert new_ctx.bdd_store == ctx.bdd_store
|
||||||
|
end
|
||||||
|
|
||||||
|
test "interning Node.mk_true() returns predefined true_id and doesn't change store", %{initial_ctx: ctx} do
|
||||||
|
true_ops_module = Tilly.BDD.universal_ops_module()
|
||||||
|
{new_ctx, node_id} = Tilly.BDD.get_or_intern_node(ctx, Node.mk_true(), true_ops_module)
|
||||||
|
assert node_id == Tilly.BDD.true_node_id()
|
||||||
|
assert new_ctx.bdd_store == ctx.bdd_store
|
||||||
|
end
|
||||||
|
|
||||||
|
test "interning a new leaf node returns a new ID and updates the store", %{initial_ctx: ctx} do
|
||||||
|
leaf_structure = Node.mk_leaf("test_leaf")
|
||||||
|
ops_mod = :my_ops
|
||||||
|
|
||||||
|
{ctx_after_intern, node_id} = Tilly.BDD.get_or_intern_node(ctx, leaf_structure, ops_mod)
|
||||||
|
|
||||||
|
assert node_id == 2 # Initial next_node_id
|
||||||
|
assert ctx_after_intern.bdd_store.next_node_id == 3
|
||||||
|
assert ctx_after_intern.bdd_store.nodes_by_structure[{leaf_structure, ops_mod}] == node_id
|
||||||
|
assert ctx_after_intern.bdd_store.structures_by_id[node_id] == %{structure: leaf_structure, ops_module: ops_mod}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "interning the same leaf node again returns the same ID and doesn't change store", %{initial_ctx: ctx} do
|
||||||
|
leaf_structure = Node.mk_leaf("test_leaf")
|
||||||
|
ops_mod = :my_ops
|
||||||
|
|
||||||
|
{ctx_after_first_intern, first_node_id} = Tilly.BDD.get_or_intern_node(ctx, leaf_structure, ops_mod)
|
||||||
|
{ctx_after_second_intern, second_node_id} = Tilly.BDD.get_or_intern_node(ctx_after_first_intern, leaf_structure, ops_mod)
|
||||||
|
|
||||||
|
assert first_node_id == second_node_id
|
||||||
|
assert ctx_after_first_intern.bdd_store == ctx_after_second_intern.bdd_store
|
||||||
|
end
|
||||||
|
|
||||||
|
test "interning a new split node returns a new ID and updates the store", %{initial_ctx: ctx} do
|
||||||
|
split_structure = Node.mk_split(:el, Tilly.BDD.true_node_id(), Tilly.BDD.false_node_id(), Tilly.BDD.true_node_id())
|
||||||
|
ops_mod = :split_ops
|
||||||
|
|
||||||
|
{ctx_after_intern, node_id} = Tilly.BDD.get_or_intern_node(ctx, split_structure, ops_mod)
|
||||||
|
|
||||||
|
assert node_id == 2 # Initial next_node_id
|
||||||
|
assert ctx_after_intern.bdd_store.next_node_id == 3
|
||||||
|
assert ctx_after_intern.bdd_store.nodes_by_structure[{split_structure, ops_mod}] == node_id
|
||||||
|
assert ctx_after_intern.bdd_store.structures_by_id[node_id] == %{structure: split_structure, ops_module: ops_mod}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "interning structurally identical nodes with different ops_modules results in different IDs", %{initial_ctx: ctx} do
|
||||||
|
leaf_structure = Node.mk_leaf("shared_leaf")
|
||||||
|
ops_mod1 = :ops1
|
||||||
|
ops_mod2 = :ops2
|
||||||
|
|
||||||
|
{ctx1, id1} = Tilly.BDD.get_or_intern_node(ctx, leaf_structure, ops_mod1)
|
||||||
|
{_ctx2, id2} = Tilly.BDD.get_or_intern_node(ctx1, leaf_structure, ops_mod2)
|
||||||
|
|
||||||
|
assert id1 != id2
|
||||||
|
assert id1 == 2
|
||||||
|
assert id2 == 3
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raises ArgumentError if bdd_store is not initialized" do
|
||||||
|
assert_raise ArgumentError, ~r/BDD store not initialized/, fn ->
|
||||||
|
Tilly.BDD.get_or_intern_node(%{}, Node.mk_leaf("foo"), :ops)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_node_data/2" do
|
||||||
|
setup do
|
||||||
|
ctx = Tilly.BDD.init_bdd_store(%{})
|
||||||
|
leaf_structure = Node.mk_leaf("data")
|
||||||
|
ops_mod = :leaf_ops
|
||||||
|
{new_ctx, leaf_id_val} = Tilly.BDD.get_or_intern_node(ctx, leaf_structure, ops_mod)
|
||||||
|
%{ctx: new_ctx, leaf_structure: leaf_structure, ops_mod: ops_mod, leaf_id: leaf_id_val}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns correct data for false node", %{ctx: ctx} do
|
||||||
|
false_id = Tilly.BDD.false_node_id()
|
||||||
|
false_ops_module = Tilly.BDD.universal_ops_module()
|
||||||
|
assert Tilly.BDD.get_node_data(ctx, false_id) == %{structure: Node.mk_false(), ops_module: false_ops_module}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns correct data for true node", %{ctx: ctx} do
|
||||||
|
true_id = Tilly.BDD.true_node_id()
|
||||||
|
true_ops_module = Tilly.BDD.universal_ops_module()
|
||||||
|
assert Tilly.BDD.get_node_data(ctx, true_id) == %{structure: Node.mk_true(), ops_module: true_ops_module}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns correct data for a custom interned leaf node", %{ctx: ctx, leaf_structure: ls, ops_mod: om, leaf_id: id} do
|
||||||
|
assert Tilly.BDD.get_node_data(ctx, id) == %{structure: ls, ops_module: om}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns nil for an unknown node ID", %{ctx: ctx} do
|
||||||
|
assert Tilly.BDD.get_node_data(ctx, 999) == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns nil if bdd_store not in ctx" do
|
||||||
|
assert Tilly.BDD.get_node_data(%{}, 0) == nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "is_false_node?/2 and is_true_node?/2" do
|
||||||
|
setup do
|
||||||
|
ctx = Tilly.BDD.init_bdd_store(%{})
|
||||||
|
leaf_structure = Node.mk_leaf("data")
|
||||||
|
ops_mod = :leaf_ops
|
||||||
|
{new_ctx, leaf_id_val} = Tilly.BDD.get_or_intern_node(ctx, leaf_structure, ops_mod)
|
||||||
|
%{ctx: new_ctx, leaf_id: leaf_id_val}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_false_node?/2", %{ctx: ctx, leaf_id: id} do
|
||||||
|
assert Tilly.BDD.is_false_node?(ctx, Tilly.BDD.false_node_id()) == true
|
||||||
|
assert Tilly.BDD.is_false_node?(ctx, Tilly.BDD.true_node_id()) == false
|
||||||
|
assert Tilly.BDD.is_false_node?(ctx, id) == false
|
||||||
|
assert Tilly.BDD.is_false_node?(ctx, 999) == false # Unknown ID
|
||||||
|
end
|
||||||
|
|
||||||
|
test "is_true_node?/2", %{ctx: ctx, leaf_id: id} do
|
||||||
|
assert Tilly.BDD.is_true_node?(ctx, Tilly.BDD.true_node_id()) == true
|
||||||
|
assert Tilly.BDD.is_true_node?(ctx, Tilly.BDD.false_node_id()) == false
|
||||||
|
assert Tilly.BDD.is_true_node?(ctx, id) == false
|
||||||
|
assert Tilly.BDD.is_true_node?(ctx, 999) == false # Unknown ID
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
162
test/tilly/type/ops_test.exs
Normal file
162
test/tilly/type/ops_test.exs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
defmodule Tilly.Type.OpsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD
|
||||||
|
alias Tilly.Type.Store
|
||||||
|
alias Tilly.Type.Ops
|
||||||
|
|
||||||
|
defp init_context do
|
||||||
|
%{}
|
||||||
|
|> BDD.init_bdd_store()
|
||||||
|
|> Store.init_type_store()
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_type_nothing/1 and get_type_any/1" do
|
||||||
|
test "get_type_nothing returns an interned Descr ID for the empty type" do
|
||||||
|
ctx = init_context()
|
||||||
|
{ctx_after_nothing, nothing_id} = Ops.get_type_nothing(ctx)
|
||||||
|
assert Ops.is_empty_type?(ctx_after_nothing, nothing_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get_type_any returns an interned Descr ID for the universal type" do
|
||||||
|
ctx = init_context()
|
||||||
|
{ctx_after_any, any_id} = Ops.get_type_any(ctx)
|
||||||
|
refute Ops.is_empty_type?(ctx_after_any, any_id)
|
||||||
|
|
||||||
|
# Further check: any type negated should be nothing type
|
||||||
|
{ctx1, neg_any_id} = Ops.negation_type(ctx_after_any, any_id)
|
||||||
|
{ctx2, nothing_id} = Ops.get_type_nothing(ctx1)
|
||||||
|
assert neg_any_id == nothing_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "literal type constructors" do
|
||||||
|
test "create_atom_literal_type/2" do
|
||||||
|
ctx = init_context()
|
||||||
|
{ctx1, atom_foo_id} = Ops.create_atom_literal_type(ctx, :foo)
|
||||||
|
{ctx2, atom_bar_id} = Ops.create_atom_literal_type(ctx1, :bar)
|
||||||
|
{ctx3, atom_foo_again_id} = Ops.create_atom_literal_type(ctx2, :foo)
|
||||||
|
|
||||||
|
refute Ops.is_empty_type?(ctx3, atom_foo_id)
|
||||||
|
refute Ops.is_empty_type?(ctx3, atom_bar_id)
|
||||||
|
assert atom_foo_id != atom_bar_id
|
||||||
|
assert atom_foo_id == atom_foo_again_id
|
||||||
|
|
||||||
|
# Test intersection: (:foo & :bar) should be Nothing
|
||||||
|
{ctx4, intersection_id} = Ops.intersection_types(ctx3, atom_foo_id, atom_bar_id)
|
||||||
|
assert Ops.is_empty_type?(ctx4, intersection_id)
|
||||||
|
|
||||||
|
# Test union: (:foo | :bar) should not be empty
|
||||||
|
{ctx5, union_id} = Ops.union_types(ctx4, atom_foo_id, atom_bar_id)
|
||||||
|
refute Ops.is_empty_type?(ctx5, union_id)
|
||||||
|
|
||||||
|
# Test negation: (not :foo) should not be empty and not be :foo
|
||||||
|
{ctx6, not_foo_id} = Ops.negation_type(ctx5, atom_foo_id)
|
||||||
|
refute Ops.is_empty_type?(ctx6, not_foo_id)
|
||||||
|
{ctx7, intersection_not_foo_and_foo} = Ops.intersection_types(ctx6, atom_foo_id, not_foo_id)
|
||||||
|
assert Ops.is_empty_type?(ctx7, intersection_not_foo_and_foo)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create_integer_literal_type/2" do
|
||||||
|
ctx = init_context()
|
||||||
|
{ctx1, int_1_id} = Ops.create_integer_literal_type(ctx, 1)
|
||||||
|
{ctx2, int_2_id} = Ops.create_integer_literal_type(ctx1, 2)
|
||||||
|
|
||||||
|
refute Ops.is_empty_type?(ctx2, int_1_id) # Use ctx2
|
||||||
|
{ctx3, intersection_id} = Ops.intersection_types(ctx2, int_1_id, int_2_id)
|
||||||
|
assert Ops.is_empty_type?(ctx3, intersection_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "create_string_literal_type/2" do
|
||||||
|
ctx = init_context()
|
||||||
|
{ctx1, str_a_id} = Ops.create_string_literal_type(ctx, "a")
|
||||||
|
{ctx2, str_b_id} = Ops.create_string_literal_type(ctx1, "b")
|
||||||
|
|
||||||
|
refute Ops.is_empty_type?(ctx2, str_a_id) # Use ctx2
|
||||||
|
{ctx3, intersection_id} = Ops.intersection_types(ctx2, str_a_id, str_b_id)
|
||||||
|
assert Ops.is_empty_type?(ctx3, intersection_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "primitive type constructors (any_of_kind)" do
|
||||||
|
test "get_primitive_type_any_atom/1" do
|
||||||
|
ctx = init_context()
|
||||||
|
{ctx1, any_atom_id} = Ops.get_primitive_type_any_atom(ctx)
|
||||||
|
{ctx2, atom_foo_id} = Ops.create_atom_literal_type(ctx1, :foo)
|
||||||
|
|
||||||
|
refute Ops.is_empty_type?(ctx2, any_atom_id)
|
||||||
|
# :foo should be a subtype of AnyAtom (i.e., :foo INTERSECTION (NEGATION AnyAtom) == Empty)
|
||||||
|
# Or, :foo UNION AnyAtom == AnyAtom
|
||||||
|
# Or, :foo INTERSECTION AnyAtom == :foo
|
||||||
|
{ctx3, intersection_foo_any_atom_id} = Ops.intersection_types(ctx2, atom_foo_id, any_atom_id)
|
||||||
|
assert intersection_foo_any_atom_id == atom_foo_id # Check it simplifies to :foo
|
||||||
|
|
||||||
|
# Test original subtype logic: (:foo & (not AnyAtom)) == Empty
|
||||||
|
{ctx4, not_any_atom_id} = Ops.negation_type(ctx3, any_atom_id) # Use ctx3
|
||||||
|
{ctx5, intersection_subtype_check_id} = Ops.intersection_types(ctx4, atom_foo_id, not_any_atom_id)
|
||||||
|
assert Ops.is_empty_type?(ctx5, intersection_subtype_check_id)
|
||||||
|
|
||||||
|
# AnyAtom & AnyInteger should be Empty
|
||||||
|
{ctx6, any_integer_id} = Ops.get_primitive_type_any_integer(ctx5) # Use ctx5
|
||||||
|
{ctx7, atom_int_intersect_id} = Ops.intersection_types(ctx6, any_atom_id, any_integer_id)
|
||||||
|
assert Ops.is_empty_type?(ctx7, atom_int_intersect_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "union_types, intersection_types, negation_type" do
|
||||||
|
test "basic set properties" do
|
||||||
|
ctx0 = init_context()
|
||||||
|
{ctx1, type_a_id} = Ops.create_atom_literal_type(ctx0, :a)
|
||||||
|
{ctx2, type_b_id} = Ops.create_atom_literal_type(ctx1, :b)
|
||||||
|
{ctx3, type_c_id} = Ops.create_atom_literal_type(ctx2, :c)
|
||||||
|
{ctx4, nothing_id} = Ops.get_type_nothing(ctx3)
|
||||||
|
|
||||||
|
# A | Nothing = A
|
||||||
|
{ctx5, union_a_nothing_id} = Ops.union_types(ctx4, type_a_id, nothing_id)
|
||||||
|
assert union_a_nothing_id == type_a_id
|
||||||
|
|
||||||
|
# A & Nothing = Nothing
|
||||||
|
{ctx6, intersect_a_nothing_id} = Ops.intersection_types(ctx5, type_a_id, nothing_id)
|
||||||
|
assert intersect_a_nothing_id == nothing_id
|
||||||
|
|
||||||
|
# not (not A) = A
|
||||||
|
{ctx7, not_a_id} = Ops.negation_type(ctx6, type_a_id)
|
||||||
|
{ctx8, not_not_a_id} = Ops.negation_type(ctx7, not_a_id)
|
||||||
|
assert not_not_a_id == type_a_id
|
||||||
|
|
||||||
|
# A | B
|
||||||
|
{ctx9, union_ab_id} = Ops.union_types(ctx8, type_a_id, type_b_id)
|
||||||
|
# (A | B) & A = A
|
||||||
|
{ctx10, intersect_union_a_id} = Ops.intersection_types(ctx9, union_ab_id, type_a_id)
|
||||||
|
assert intersect_union_a_id == type_a_id
|
||||||
|
|
||||||
|
# (A | B) & C = Nothing (if A, B, C are distinct atom literals)
|
||||||
|
{ctx11, intersect_union_c_id} = Ops.intersection_types(ctx10, union_ab_id, type_c_id)
|
||||||
|
assert Ops.is_empty_type?(ctx11, intersect_union_c_id)
|
||||||
|
|
||||||
|
# Commutativity and idempotence of union/intersection are implicitly tested by caching
|
||||||
|
# and canonical key generation in apply_type_op.
|
||||||
|
end
|
||||||
|
|
||||||
|
test "type operations are cached" do
|
||||||
|
ctx0 = init_context()
|
||||||
|
{ctx1, type_a_id} = Ops.create_atom_literal_type(ctx0, :a)
|
||||||
|
{ctx2, type_b_id} = Ops.create_atom_literal_type(ctx1, :b)
|
||||||
|
|
||||||
|
# Perform an operation
|
||||||
|
{ctx3, union1_id} = Ops.union_types(ctx2, type_a_id, type_b_id)
|
||||||
|
initial_cache_size = map_size(ctx3.type_store.ops_cache)
|
||||||
|
assert initial_cache_size > 0 # Ensure something was cached
|
||||||
|
|
||||||
|
# Perform the same operation again
|
||||||
|
{ctx4, union2_id} = Ops.union_types(ctx3, type_a_id, type_b_id)
|
||||||
|
assert union1_id == union2_id
|
||||||
|
assert map_size(ctx4.type_store.ops_cache) == initial_cache_size # Cache size should not change
|
||||||
|
|
||||||
|
# Perform with swapped arguments (commutative)
|
||||||
|
{ctx5, union3_id} = Ops.union_types(ctx4, type_b_id, type_a_id)
|
||||||
|
assert union1_id == union3_id
|
||||||
|
assert map_size(ctx5.type_store.ops_cache) == initial_cache_size # Cache size should not change
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
67
test/tilly/type/store_test.exs
Normal file
67
test/tilly/type/store_test.exs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
defmodule Tilly.Type.StoreTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD
|
||||||
|
alias Tilly.Type
|
||||||
|
alias Tilly.Type.Store
|
||||||
|
|
||||||
|
defp init_context do
|
||||||
|
%{}
|
||||||
|
|> BDD.init_bdd_store()
|
||||||
|
|> Store.init_type_store()
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "init_type_store/1" do
|
||||||
|
test "initializes an empty type store in the typing_ctx" do
|
||||||
|
typing_ctx = %{}
|
||||||
|
new_ctx = Store.init_type_store(typing_ctx)
|
||||||
|
type_store = Map.get(new_ctx, :type_store)
|
||||||
|
|
||||||
|
assert type_store.descrs_by_structure == %{}
|
||||||
|
assert type_store.structures_by_id == %{}
|
||||||
|
assert type_store.next_descr_id == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "get_or_intern_descr/2 and get_descr_by_id/2" do
|
||||||
|
test "interns a new Descr map and retrieves it" do
|
||||||
|
typing_ctx = init_context()
|
||||||
|
descr_map1 = Type.empty_descr(typing_ctx) # Uses canonical BDD.false_node_id()
|
||||||
|
|
||||||
|
# Intern first time
|
||||||
|
{ctx1, id1} = Store.get_or_intern_descr(typing_ctx, descr_map1)
|
||||||
|
assert id1 == 0
|
||||||
|
assert Store.get_descr_by_id(ctx1, id1) == descr_map1
|
||||||
|
assert ctx1.type_store.next_descr_id == 1
|
||||||
|
|
||||||
|
# Retrieve existing
|
||||||
|
{ctx2, id1_retrieved} = Store.get_or_intern_descr(ctx1, descr_map1)
|
||||||
|
assert id1_retrieved == id1
|
||||||
|
assert ctx2 == ctx1 # Context should not change if already interned
|
||||||
|
|
||||||
|
# Intern a different Descr map
|
||||||
|
descr_map2 = Type.any_descr(typing_ctx) # Uses canonical BDD.true_node_id()
|
||||||
|
{ctx3, id2} = Store.get_or_intern_descr(ctx2, descr_map2)
|
||||||
|
assert id2 == 1
|
||||||
|
assert Store.get_descr_by_id(ctx3, id2) == descr_map2
|
||||||
|
assert ctx3.type_store.next_descr_id == 2
|
||||||
|
|
||||||
|
# Ensure original is still retrievable
|
||||||
|
assert Store.get_descr_by_id(ctx3, id1) == descr_map1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "get_descr_by_id returns nil for non-existent ID" do
|
||||||
|
typing_ctx = init_context()
|
||||||
|
assert Store.get_descr_by_id(typing_ctx, 999) == nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "raises an error if type store is not initialized" do
|
||||||
|
uninitialized_ctx = %{}
|
||||||
|
descr_map = Type.empty_descr(uninitialized_ctx)
|
||||||
|
|
||||||
|
assert_raise ArgumentError,
|
||||||
|
"Type store not initialized in typing_ctx. Call init_type_store first.",
|
||||||
|
fn -> Store.get_or_intern_descr(uninitialized_ctx, descr_map) end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
39
test/tilly/type_test.exs
Normal file
39
test/tilly/type_test.exs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
defmodule Tilly.TypeTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Tilly.BDD
|
||||||
|
alias Tilly.Type
|
||||||
|
|
||||||
|
describe "empty_descr/1" do
|
||||||
|
test "returns a Descr map with all BDD IDs pointing to false" do
|
||||||
|
typing_ctx = BDD.init_bdd_store(%{})
|
||||||
|
descr = Type.empty_descr(typing_ctx)
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
|
||||||
|
assert descr.atoms_bdd_id == false_id
|
||||||
|
assert descr.integers_bdd_id == false_id
|
||||||
|
assert descr.strings_bdd_id == false_id
|
||||||
|
assert descr.pairs_bdd_id == false_id
|
||||||
|
assert descr.records_bdd_id == false_id
|
||||||
|
assert descr.functions_bdd_id == false_id
|
||||||
|
assert descr.absent_marker_bdd_id == false_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "any_descr/1" do
|
||||||
|
test "returns a Descr map with BDD IDs pointing to true (and absent_marker to false)" do
|
||||||
|
typing_ctx = BDD.init_bdd_store(%{})
|
||||||
|
descr = Type.any_descr(typing_ctx)
|
||||||
|
true_id = BDD.true_node_id()
|
||||||
|
false_id = BDD.false_node_id()
|
||||||
|
|
||||||
|
assert descr.atoms_bdd_id == true_id
|
||||||
|
assert descr.integers_bdd_id == true_id
|
||||||
|
assert descr.strings_bdd_id == true_id
|
||||||
|
assert descr.pairs_bdd_id == true_id
|
||||||
|
assert descr.records_bdd_id == true_id
|
||||||
|
assert descr.functions_bdd_id == true_id
|
||||||
|
assert descr.absent_marker_bdd_id == false_id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
139
tests.pl
139
tests.pl
@ -1,139 +0,0 @@
|
|||||||
:- module(tests, [run_tests/0]).
|
|
||||||
|
|
||||||
:- use_module(parser).
|
|
||||||
:- use_module(types).
|
|
||||||
:- use_module(log).
|
|
||||||
|
|
||||||
run_test(TestName, Env, Code, ExpectedTypeResult) :-
|
|
||||||
format('--- Test: ~w ---~n', [TestName]),
|
|
||||||
( parse(Code, AST) ->
|
|
||||||
format('Parsed AST: ~w~n', [AST]),
|
|
||||||
log(test_setup, env(Env)),
|
|
||||||
( catch(infer_type(AST, Env, ActualType), Error, (
|
|
||||||
log(error, caught_error(Error)),
|
|
||||||
explain_error(Error, Explanation),
|
|
||||||
format('Type Error: ~w~n', [Explanation]),
|
|
||||||
ActualType = error(Error) % Represent error for comparison
|
|
||||||
))
|
|
||||||
-> true
|
|
||||||
; ActualType = 'inference_failed_silently' % Should not happen if catch works
|
|
||||||
),
|
|
||||||
format('Inferred Type: ~w~n', [ActualType]),
|
|
||||||
( (ActualType == never, ExpectedTypeResult \== never) -> % Explicitly fail if 'never' is inferred unexpectedly
|
|
||||||
Pass = false, SubMatch = 'unexpected_never'
|
|
||||||
; (ExpectedTypeResult = error(_), ActualType = error(_)) -> % Both are errors
|
|
||||||
Pass = true, SubMatch = 'error_expected_and_received'
|
|
||||||
; ExpectedTypeResult == ActualType ->
|
|
||||||
Pass = true, SubMatch = 'exact_match'
|
|
||||||
; unify_types(ExpectedTypeResult, ActualType, ExpectedTypeResult) -> % Actual is subtype of Expected
|
|
||||||
Pass = true, SubMatch = 'subtype_match'
|
|
||||||
; unify_types(ExpectedTypeResult, ActualType, ActualType) -> % Expected is subtype of Actual (and Actual is not 'never' unless Expected is also 'never')
|
|
||||||
Pass = true, SubMatch = 'supertype_match'
|
|
||||||
; Pass = false, SubMatch = 'mismatch'
|
|
||||||
),
|
|
||||||
( Pass == true ->
|
|
||||||
format('Status: PASS (~w)~n~n', [SubMatch])
|
|
||||||
; format('Status: FAIL (~w) - Expected: ~w~n~n', [SubMatch, ExpectedTypeResult])
|
|
||||||
)
|
|
||||||
; format('Parse FAILED for code: ~s~n~n', [Code])
|
|
||||||
).
|
|
||||||
|
|
||||||
run_tests :-
|
|
||||||
set_verbosity(1), % Set verbosity: 0 (errors), 1 (log), 2 (debug)
|
|
||||||
log(tests, 'Starting test suite...'),
|
|
||||||
initial_env(EmptyEnv),
|
|
||||||
|
|
||||||
run_test('Conditional with is_number/1 (x is number)',
|
|
||||||
[x:union(number,string)],
|
|
||||||
"(if (is_number x) x 0)", % x is union(number,string), then branch x is number. else branch x is string.
|
|
||||||
% 0 is number. So then branch is number, else branch is number.
|
|
||||||
% Result should be number.
|
|
||||||
number), % If x is number, then x (number). Else 0 (number). Unified: number.
|
|
||||||
|
|
||||||
run_test('Conditional with is_number/1 (x is string)',
|
|
||||||
[x:union(number,string)],
|
|
||||||
"(if (is_number x) 1 \"not num\")", % x:union(number,string).
|
|
||||||
% Cond: (is_number x)
|
|
||||||
% Then: x refined to number. Body `1` is number.
|
|
||||||
% Else: x refined to string. Body `"not num"` is string.
|
|
||||||
% Result: union(number, string)
|
|
||||||
union(number, string)),
|
|
||||||
|
|
||||||
run_test('Pattern match list',
|
|
||||||
[my_list:list(number)],
|
|
||||||
"(match my_list (((list a b) a)))", % my_list:list(number). a,b become number. returns a (number).
|
|
||||||
% Pattern (list a b), body a
|
|
||||||
number),
|
|
||||||
|
|
||||||
run_test('Pattern match tuple',
|
|
||||||
[my_tuple:tuple([number, string])],
|
|
||||||
"(match my_tuple (((tuple x y) y)))", % my_tuple:tuple([number, string]). x is number, y is string. returns y (string).
|
|
||||||
% Pattern (tuple x y), body y
|
|
||||||
string),
|
|
||||||
|
|
||||||
run_test('Let binding', % z:number, trying to assign string. Current 'let' rebinds.
|
|
||||||
[z:number],
|
|
||||||
"(let z \"text\" z)",
|
|
||||||
string), % With current 'let' semantics (rebinding), z will be string. Env [z:number] is shadowed.
|
|
||||||
|
|
||||||
run_test('Unification in conditional branches',
|
|
||||||
EmptyEnv,
|
|
||||||
"(if true 10 \"text\")", % ThenType=number, ElseType=string. unify_types(number,string) -> union(number,string)
|
|
||||||
union(number,string)
|
|
||||||
),
|
|
||||||
|
|
||||||
run_test('Successful refinement (simulated validate_user)',
|
|
||||||
[user_data:any],
|
|
||||||
% Using is_number to simulate a predicate that refines type.
|
|
||||||
% Env: [user_data:any]
|
|
||||||
% (if (is_number user_data) user_data "not a number")
|
|
||||||
% Cond: (is_number user_data) -> boolean
|
|
||||||
% Then branch: user_data refined to number. Body: user_data -> number
|
|
||||||
% Else branch: user_data refined to not(number). Body: "not a number" -> string
|
|
||||||
% Result: union(number, string)
|
|
||||||
"(if (is_number user_data) user_data \"not a number\")",
|
|
||||||
union(number, string)
|
|
||||||
),
|
|
||||||
|
|
||||||
run_test('Lambda expression (syntax check, type not deeply inferred yet)',
|
|
||||||
EmptyEnv,
|
|
||||||
"(lambda (x y) (add x y))", % `add` is not a defined function, so type of body is an issue.
|
|
||||||
% For now, this tests parsing of lambda.
|
|
||||||
% Expected type depends on how `add` and lambdas are typed.
|
|
||||||
% Let's expect 'any' or a placeholder function type if types.pl is not updated for lambdas.
|
|
||||||
% For now, let's assume it's 'any' as `add` is unknown.
|
|
||||||
any), % Placeholder: Actual type depends on full function type inference.
|
|
||||||
|
|
||||||
run_test('Function application of lambda (syntax check)',
|
|
||||||
EmptyEnv,
|
|
||||||
"((lambda (x) x) 10)",
|
|
||||||
any), % Placeholder: Actual type depends on lambda type inference and application rules.
|
|
||||||
% If lambda is (T->T) and arg is T, result is T. Here, (any->any) and number -> any.
|
|
||||||
% If (lambda (x) x) is typed as fun_type([any],any), then apply to int(10) (number) -> any.
|
|
||||||
|
|
||||||
run_test('Empty list literal',
|
|
||||||
EmptyEnv,
|
|
||||||
"()",
|
|
||||||
list(never) % Or some polymorphic list type list(T) if supported. list(never) is common for empty.
|
|
||||||
),
|
|
||||||
|
|
||||||
run_test('Boolean true literal',
|
|
||||||
EmptyEnv,
|
|
||||||
"true",
|
|
||||||
boolean
|
|
||||||
),
|
|
||||||
|
|
||||||
run_test('Boolean false literal',
|
|
||||||
EmptyEnv,
|
|
||||||
"false",
|
|
||||||
boolean
|
|
||||||
),
|
|
||||||
|
|
||||||
log(tests, 'Test suite finished.').
|
|
||||||
|
|
||||||
% To run:
|
|
||||||
% ?- consult('log.pl').
|
|
||||||
% ?- consult('parser.pl').
|
|
||||||
% ?- consult('types.pl').
|
|
||||||
% ?- consult('tests.pl').
|
|
||||||
% ?- run_tests.
|
|
||||||
55
todo.md
Normal file
55
todo.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
1. **Implement Parsing for `(union <type1> <type2> ...)` Type Specifiers:**
|
||||||
|
* Modify `Til.Typer.ExpressionTyper.resolve_type_specifier_node` to recognize and parse S-expressions like `(union integer string)`.
|
||||||
|
* This will involve recursively resolving the inner type specifiers and constructing a raw union type definition. The existing interning and subtyping logic for unions can then be leveraged.
|
||||||
|
* Add tests for type checking expressions annotated with these explicit union types, e.g., `(the (union integer string) some-expression)`.
|
||||||
|
|
||||||
|
3. **Implement Parsing for Basic Function Type Specifiers:**
|
||||||
|
* Modify `Til.Typer.ExpressionTyper.resolve_type_specifier_node` to parse function type specifiers, e.g., `(function (Arg1Type Arg2Type ...) ReturnType)`.
|
||||||
|
* Add interning support for function types in `Til.Typer.Interner`.
|
||||||
|
* Implement basic subtyping rules for function types in `Til.Typer.SubtypeChecker` (initially, arity checking; then contravariant arguments, covariant return).
|
||||||
|
|
||||||
|
4. **Implement Basic Function Definition (e.g., `def` or `lambda`):**
|
||||||
|
* Define syntax (e.g., `(def my-fn (param1 param2) body-expr)`).
|
||||||
|
* Add parser support for this syntax.
|
||||||
|
* Typer:
|
||||||
|
* For this initial step, infer a basic function type (e.g., based on arity, with `any` for parameter and return types if not annotated).
|
||||||
|
* Add the function name and its inferred type to the environment.
|
||||||
|
|
||||||
|
5. **Implement Basic Function Calls:**
|
||||||
|
* Extend `Til.Typer.ExpressionTyper.infer_s_expression_type` for function calls:
|
||||||
|
* When the operator of an S-expression is a symbol, look up its type in the environment.
|
||||||
|
* If it's a function type (from step 4), perform an arity check against the provided arguments.
|
||||||
|
* The inferred type of the call would be the function's (currently basic) return type.
|
||||||
|
|
||||||
|
6. **Enhance Function Definitions with Type Annotations:**
|
||||||
|
* Extend the function definition syntax to support type annotations for parameters and return types (e.g., `(def my-fn ((p1 P1Type) (p2 P2Type)) :: ReturnType body-expr)`).
|
||||||
|
* Update the parser for this extended syntax.
|
||||||
|
* Typer:
|
||||||
|
* Use these annotations to construct a more precise function type.
|
||||||
|
* When typing the function body, use the annotated parameter types in the local environment.
|
||||||
|
* Verify that the inferred type of the function body is a subtype of the annotated return type.
|
||||||
|
* Update function call typing (from step 5) to use these precise function types for argument type checking and to determine the call's return type.
|
||||||
|
|
||||||
|
7. **Implement Type Inference for Core Map Operation: `(map-get map key)`:**
|
||||||
|
* Define the S-expression syntax `(map-get map-expr key-expr)`.
|
||||||
|
* In `Til.Typer.ExpressionTyper`, implement type inference rules for `map-get` based on the logic outlined in `todo.md`. This includes:
|
||||||
|
* Typing `map-expr` and `key-expr`.
|
||||||
|
* Handling cases where `key-expr`'s type is a literal (allowing lookup in `known_elements`).
|
||||||
|
* Handling cases where `key-expr`'s type is a general type (using `index_signature` and potentially unioning types from `known_elements`).
|
||||||
|
|
||||||
|
8. **Improve User-Facing Type Error Messages:**
|
||||||
|
* For common errors like `type_annotation_mismatch` or function call argument mismatches, enhance the error reporting.
|
||||||
|
* Develop a utility to pretty-print type definitions (from their internal map representation or ID) for inclusion in error messages, making them more readable than raw type IDs or structures.
|
||||||
|
* Ensure source locations (file, line, column) are clearly associated with type errors.
|
||||||
|
|
||||||
|
9. **Implement Parsing for `(intersection <type1> <type2> ...)` Type Specifiers:**
|
||||||
|
* Similar to union types, update `Til.Typer.ExpressionTyper.resolve_type_specifier_node` for intersection type S-expressions.
|
||||||
|
* Add interning and subtyping rules for intersection types in `Til.Typer.Interner` and `Til.Typer.SubtypeChecker`.
|
||||||
|
|
||||||
|
10. **Implement Simple Type Aliases (e.g., `deftype`):**
|
||||||
|
* Define syntax for non-generic type aliases (e.g., `(deftype PositiveInteger (refinement integer ...))` or `(deftype UserMap (map atom any))`).
|
||||||
|
* Add parser support.
|
||||||
|
* Typer:
|
||||||
|
* Store these alias definitions.
|
||||||
|
* Modify `Til.Typer.ExpressionTyper.resolve_type_specifier_node` to recognize and expand these aliases when they are used in type annotations.
|
||||||
|
|
||||||
254
types.pl
254
types.pl
@ -1,254 +0,0 @@
|
|||||||
:- module(types,
|
|
||||||
[ infer_type/3, % infer_type(+AST, +Env, -Type)
|
|
||||||
unify_types/3, % unify_types(+Type1, +Type2, -UnifiedType)
|
|
||||||
refine_env/4, % refine_env(+Var, +Type, +EnvIn, -EnvOut)
|
|
||||||
get_type/3, % get_type(+Var, +Env, -Type)
|
|
||||||
% Type representations (examples)
|
|
||||||
type_number/0, type_string/0, type_boolean/0, type_list_nil/0,
|
|
||||||
type_list/1, type_tuple/1, type_union/2, type_intersection/2, type_negation/1,
|
|
||||||
type_any/0, type_never/0,
|
|
||||||
initial_env/1
|
|
||||||
]).
|
|
||||||
|
|
||||||
:- use_module(log).
|
|
||||||
:- discontiguous unify_types/3.
|
|
||||||
:- discontiguous infer_type/3. % Added to handle infer_type_arg/3 in between
|
|
||||||
|
|
||||||
% --- Type Representations (as atoms/compound terms) ---
|
|
||||||
type_number :- _ = number.
|
|
||||||
type_string :- _ = string.
|
|
||||||
type_boolean :- _ = boolean. % Represents the type 'boolean'
|
|
||||||
type_list_nil :- _ = list_nil. % AST node for empty list literal '()'
|
|
||||||
type_list(_T) :- _ = list(_). % _T is intentionally a singleton, structure check: list(Anything)
|
|
||||||
type_tuple(Ts) :- _ = tuple(Ts), is_list(Ts). % Ts is used, not singleton
|
|
||||||
type_union(T1, T2) :- _ = union(T1, T2). % T1, T2 are used, not singletons
|
|
||||||
type_intersection(T1, T2) :- _ = intersection(T1, T2). % T1, T2 are used, not singletons
|
|
||||||
type_negation(T) :- _ = negation(T). % T is used, not singleton
|
|
||||||
type_any :- _ = any. % Top type
|
|
||||||
type_never :- _ = never. % Bottom type, result of failed branches or contradictions
|
|
||||||
|
|
||||||
% --- Environment ---
|
|
||||||
% Env is a list of Var:Type pairs.
|
|
||||||
initial_env([]).
|
|
||||||
|
|
||||||
get_type(Var, [Var:Type | _], Type) :- !.
|
|
||||||
get_type(Var, [_ | RestEnv], Type) :- get_type(Var, RestEnv, Type).
|
|
||||||
get_type(Var, [], _) :-
|
|
||||||
log(error, unbound_variable(Var, 'unknown_location')), % Location should be passed
|
|
||||||
fail.
|
|
||||||
|
|
||||||
refine_env(Var, Type, EnvIn, [Var:Type | EnvSansOldBinding]) :-
|
|
||||||
delete(EnvIn, Var:_, EnvSansOldBinding), % EnvSansOldBinding is EnvIn with all Var:_ bindings removed.
|
|
||||||
log(type_refinement, env_refined(Var, Type)).
|
|
||||||
|
|
||||||
|
|
||||||
% --- Type Inference ---
|
|
||||||
infer_type(int(_), _Env, number) :- log(type_inference, 'Integer literal -> number').
|
|
||||||
infer_type(string_val(_), _Env, string) :- log(type_inference, 'String literal -> string').
|
|
||||||
infer_type(bool(_), _Env, boolean) :- log(type_inference, 'Boolean literal -> boolean').
|
|
||||||
infer_type(list_nil, _Env, list(never)) :- log(type_inference, 'Empty list literal -> list(never)'). % Or a polymorphic list type
|
|
||||||
|
|
||||||
infer_type(id(Var), Env, Type) :-
|
|
||||||
( get_type(Var, Env, Type) ->
|
|
||||||
log(type_inference, id(Var) -> Type)
|
|
||||||
; log(error, type_error(unbound_variable(Var), id(Var))),
|
|
||||||
Type = never, % Or fail, depending on error handling strategy
|
|
||||||
explain_error(unbound_variable(Var, id(Var)), _Msg)
|
|
||||||
).
|
|
||||||
|
|
||||||
infer_type(let(Var, ValueAst, BodyAst), EnvIn, BodyType) :-
|
|
||||||
log(type_inference, 'Inferring type for let expression'),
|
|
||||||
infer_type(ValueAst, EnvIn, ValueType),
|
|
||||||
log(type_inference, let_value(Var, ValueType)),
|
|
||||||
refine_env(Var, ValueType, EnvIn, EnvMid),
|
|
||||||
infer_type(BodyAst, EnvMid, BodyType),
|
|
||||||
log(type_inference, let_body(BodyType)).
|
|
||||||
|
|
||||||
infer_type(if(CondAst, ThenAst, ElseAst), EnvIn, IfType) :-
|
|
||||||
log(type_inference, 'Inferring type for if expression'),
|
|
||||||
infer_type(CondAst, EnvIn, CondType),
|
|
||||||
( CondType == boolean -> true
|
|
||||||
; log(error, type_error(expected_boolean_condition, CondAst)), IfType = never, fail
|
|
||||||
),
|
|
||||||
% Flow-sensitive refinement example:
|
|
||||||
( CondAst = is_number(id(X)) -> % If condition is is_number(X)
|
|
||||||
log(type_inference, flow_refinement_condition(is_number(id(X)))),
|
|
||||||
refine_env(X, number, EnvIn, EnvThen) % X is number in Then branch
|
|
||||||
; EnvThen = EnvIn % No specific refinement from condition structure
|
|
||||||
),
|
|
||||||
infer_type(ThenAst, EnvThen, ThenType),
|
|
||||||
% For Else branch, if CondAst was `is_number(X)`, then X is `not(number)`
|
|
||||||
( CondAst = is_number(id(X)) ->
|
|
||||||
get_type(X, EnvIn, OriginalXType), % Get original type of X before refinement
|
|
||||||
refine_env(X, intersection(OriginalXType, negation(number)), EnvIn, EnvElse)
|
|
||||||
; EnvElse = EnvIn
|
|
||||||
),
|
|
||||||
infer_type(ElseAst, EnvElse, ElseType),
|
|
||||||
unify_types(ThenType, ElseType, IfType), % Branches must have compatible types
|
|
||||||
log(type_inference, if_expression(CondType, ThenType, ElseType) -> IfType).
|
|
||||||
|
|
||||||
% Example: is_number/1 predicate (built-in)
|
|
||||||
infer_type(is_number(ArgAst), Env, boolean) :-
|
|
||||||
log(type_inference, 'Inferring type for is_number/1 call'),
|
|
||||||
infer_type(ArgAst, Env, _ArgType). % ArgType can be anything, is_number checks it.
|
|
||||||
|
|
||||||
% Lambda expressions (placeholder - full function type inference is complex)
|
|
||||||
infer_type(lambda(_Params, _BodyAst), _Env, any) :- % For (lambda (params...) body)
|
|
||||||
% A proper implementation would construct a function type: fun_type(ParamTypes, ReturnType)
|
|
||||||
% This requires inferring types for params (possibly from annotations) and body.
|
|
||||||
log(type_inference, 'Lambda expression -> any (placeholder)').
|
|
||||||
|
|
||||||
% General function application (placeholder - requires function type for FunctorSExpr)
|
|
||||||
infer_type(apply(FunctorSExpr, ArgsSExprs), Env, any) :- % For ((lambda ...) arg) or (f arg) where f is complex
|
|
||||||
log(type_inference, 'General application (apply/2) -> any (placeholder)'),
|
|
||||||
infer_type(FunctorSExpr, Env, FunctorType),
|
|
||||||
% Infer types of ArgsSExprs
|
|
||||||
maplist(infer_type_arg(Env), ArgsSExprs, ArgTypes),
|
|
||||||
log(type_inference, apply_functor_type(FunctorType)),
|
|
||||||
log(type_inference, apply_arg_types(ArgTypes)).
|
|
||||||
% A proper implementation would:
|
|
||||||
% 1. Ensure FunctorType is a function type, e.g., fun_type(ExpectedParamTypes, ReturnType).
|
|
||||||
% 2. Check Arity and if ArgTypes are subtypes of ExpectedParamTypes.
|
|
||||||
% 3. Return ReturnType.
|
|
||||||
% For now, it's 'any'.
|
|
||||||
|
|
||||||
infer_type_arg(Env, ArgSExpr, ArgType) :- infer_type(ArgSExpr, Env, ArgType).
|
|
||||||
|
|
||||||
|
|
||||||
% Example: validate_user/1 (hypothetical predicate that narrows type)
|
|
||||||
% Assume validate_user/1 takes 'any' and if it succeeds, the arg is a 'user_record'.
|
|
||||||
% This would typically be declared elsewhere (e.g. function signatures)
|
|
||||||
% For now, we simulate its effect.
|
|
||||||
infer_type(validate_user(ArgAst), Env, boolean) :- % validate_user returns boolean
|
|
||||||
log(type_inference, 'Inferring type for validate_user/1 call'),
|
|
||||||
infer_type(ArgAst, Env, _ArgType).
|
|
||||||
% The actual refinement happens in the 'then' branch of an 'if' or similar construct
|
|
||||||
% e.g., if validate_user(x) then ... (x is now user_record)
|
|
||||||
|
|
||||||
% Pattern Matching
|
|
||||||
infer_type(match(ExprAst, Clauses), EnvIn, MatchType) :-
|
|
||||||
log(type_inference, 'Inferring type for match expression'),
|
|
||||||
infer_type(ExprAst, EnvIn, ExprType),
|
|
||||||
infer_clause_types(Clauses, ExprType, EnvIn, ClauseTypes),
|
|
||||||
( ClauseTypes = [] -> MatchType = never % Or error: non-exhaustive match if not desired
|
|
||||||
; reduce_types(ClauseTypes, MatchType) % Unify all clause body types
|
|
||||||
),
|
|
||||||
log(type_inference, match_result_type(MatchType)).
|
|
||||||
|
|
||||||
infer_clause_types([], _ExprType, _EnvIn, []).
|
|
||||||
infer_clause_types([clause(Pattern, _Guard, BodyAst) | RestClauses], ExprType, EnvIn, [BodyType | RestBodyTypes]) :-
|
|
||||||
log(type_inference, inferring_clause_pattern(Pattern)),
|
|
||||||
refine_env_from_pattern(Pattern, ExprType, EnvIn, EnvPattern),
|
|
||||||
( EnvPattern == fail -> % Pattern doesn't match ExprType or is contradictory
|
|
||||||
log(type_warning, pattern_will_not_match(Pattern, ExprType)),
|
|
||||||
BodyType = never % This branch is effectively dead
|
|
||||||
; infer_type(BodyAst, EnvPattern, BodyType)
|
|
||||||
),
|
|
||||||
infer_clause_types(RestClauses, ExprType, EnvIn, RestBodyTypes).
|
|
||||||
|
|
||||||
% refine_env_from_pattern/4: (+Pattern, +MatchedExprType, +EnvIn, -EnvOutOrFail)
|
|
||||||
refine_env_from_pattern(pvar(Name), MatchedExprType, EnvIn, EnvOut) :-
|
|
||||||
!, refine_env(Name, MatchedExprType, EnvIn, EnvOut).
|
|
||||||
refine_env_from_pattern(pwild, _MatchedExprType, EnvIn, EnvIn) :- !.
|
|
||||||
refine_env_from_pattern(pint(_), MatchedExprType, EnvIn, EnvIn) :-
|
|
||||||
( unify_types(MatchedExprType, number, number) -> true % Check if MatchedExprType is compatible with number
|
|
||||||
; log(type_error, pattern_type_mismatch(pint, MatchedExprType)), fail
|
|
||||||
).
|
|
||||||
refine_env_from_pattern(pstring(_), MatchedExprType, EnvIn, EnvIn) :-
|
|
||||||
( unify_types(MatchedExprType, string, string) -> true % Check if MatchedExprType is compatible with string
|
|
||||||
; log(type_error, pattern_type_mismatch(pstring, MatchedExprType)), fail
|
|
||||||
).
|
|
||||||
refine_env_from_pattern(pbool(_), MatchedExprType, EnvIn, EnvIn) :- % Added for boolean patterns
|
|
||||||
( unify_types(MatchedExprType, boolean, boolean) -> true % Check if MatchedExprType is compatible with boolean
|
|
||||||
; log(type_error, pattern_type_mismatch(pbool, MatchedExprType)), fail
|
|
||||||
).
|
|
||||||
refine_env_from_pattern(ptuple(Patterns), MatchedExprType, EnvIn, EnvOut) :-
|
|
||||||
( MatchedExprType = tuple(ElementTypes) ; MatchedExprType = any ), % Allow matching 'any' as a tuple
|
|
||||||
( var(ElementTypes) -> % MatchedExprType was 'any' or tuple(_)
|
|
||||||
length(Patterns, L), length(ElementTypes, L), % Infer arity
|
|
||||||
maplist(=(any), ElementTypes) % Assume elements are 'any' if not specified
|
|
||||||
; length(Patterns, L1), length(ElementTypes, L2), L1 == L2 % Check arity
|
|
||||||
), !,
|
|
||||||
refine_env_from_patterns(Patterns, ElementTypes, EnvIn, EnvOut).
|
|
||||||
refine_env_from_pattern(ptuple(_Patterns), MatchedExprType, _EnvIn, fail) :-
|
|
||||||
log(type_error, pattern_type_mismatch(ptuple, MatchedExprType)), fail.
|
|
||||||
|
|
||||||
refine_env_from_pattern(plist(Patterns), MatchedExprType, EnvIn, EnvOut) :-
|
|
||||||
( MatchedExprType = list(ElementType) ; MatchedExprType = any ),
|
|
||||||
( var(ElementType) -> ElementType = any ), % If MatchedExprType was 'any' or list(_), treat element type as 'any'
|
|
||||||
!,
|
|
||||||
length(Patterns, Len),
|
|
||||||
length(TypesForPatterns, Len), % Create a list of unbound variables of the same length as Patterns
|
|
||||||
maplist(=(ElementType), TypesForPatterns), % Unify each variable in TypesForPatterns with ElementType
|
|
||||||
refine_env_from_patterns(Patterns, TypesForPatterns, EnvIn, EnvOut).
|
|
||||||
refine_env_from_pattern(plist(_Patterns), MatchedExprType, _EnvIn, fail) :-
|
|
||||||
\+ (MatchedExprType = list(_); MatchedExprType = any), % Fail only if not a list or any
|
|
||||||
log(type_error, pattern_type_mismatch(plist, MatchedExprType)),
|
|
||||||
fail.
|
|
||||||
|
|
||||||
|
|
||||||
refine_env_from_patterns([], [], Env, Env).
|
|
||||||
refine_env_from_patterns([P|Ps], [T|Ts], EnvIn, EnvOut) :-
|
|
||||||
refine_env_from_pattern(P, T, EnvIn, EnvMid),
|
|
||||||
( EnvMid == fail -> EnvOut = fail, !
|
|
||||||
; refine_env_from_patterns(Ps, Ts, EnvMid, EnvOut)
|
|
||||||
).
|
|
||||||
|
|
||||||
|
|
||||||
% --- Type Unification ---
|
|
||||||
unify_types(T, T, T) :- !, log(unification, identical(T)).
|
|
||||||
unify_types(any, T, T) :- !, log(unification, any_with(T) -> T).
|
|
||||||
unify_types(T, any, T) :- !, log(unification, T -> T). % Corrected T_with_any to T
|
|
||||||
unify_types(never, _T, never) :- !, log(unification, 'never involved'). % Or should it be T? Depends on meaning.
|
|
||||||
unify_types(_T, never, never) :- !, log(unification, 'never involved').
|
|
||||||
|
|
||||||
unify_types(list(T1), list(T2), list(TU)) :- !,
|
|
||||||
unify_types(T1, T2, TU),
|
|
||||||
log(unification, list(T1, T2) -> list(TU)).
|
|
||||||
|
|
||||||
unify_types(tuple(Ts1), tuple(Ts2), tuple(TUs)) :- !,
|
|
||||||
length(Ts1, L), length(Ts2, L), % Tuples must have same arity
|
|
||||||
maplist(unify_types, Ts1, Ts2, TUs),
|
|
||||||
log(unification, tuple(Ts1, Ts2) -> tuple(TUs)).
|
|
||||||
|
|
||||||
% Union Type Unification (simplified: create a canonical union)
|
|
||||||
unify_types(union(A, B), C, union(A, union(B,C))) :- \+ is_union(C), !. % Simplistic, needs canonical form
|
|
||||||
unify_types(A, union(B, C), union(A, union(B,C))) :- \+ is_union(A), !.
|
|
||||||
unify_types(union(A1,B1), union(A2,B2), union(A1,union(B1,union(A2,B2)))) :- !. % Very naive
|
|
||||||
|
|
||||||
is_union(union(_,_)).
|
|
||||||
|
|
||||||
% Intersection (placeholder)
|
|
||||||
unify_types(intersection(A,B), C, intersection(A,intersection(B,C))) :- !. % Needs proper logic
|
|
||||||
|
|
||||||
% Helper to reduce a list of types to a single type (e.g., for match clauses)
|
|
||||||
reduce_types([T], T) :- !.
|
|
||||||
reduce_types([T1, T2 | Ts], ResultType) :-
|
|
||||||
unify_types(T1, T2, UnifiedHead),
|
|
||||||
( UnifiedHead == never -> ResultType = never, ! % Propagate failure
|
|
||||||
; reduce_types([UnifiedHead | Ts], ResultType)
|
|
||||||
).
|
|
||||||
reduce_types([], never). % Consistent with match behavior for no clauses.
|
|
||||||
|
|
||||||
|
|
||||||
% --- Example Predicate for Type Narrowing ---
|
|
||||||
% This would be part of function signature definitions.
|
|
||||||
% For 'validate_user(X)', if it returns true, X's type is refined.
|
|
||||||
% This is usually handled by the 'if' construct using the boolean result.
|
|
||||||
% e.g. if validate_user(x) then (env refined for x) else (env not refined or negated refinement)
|
|
||||||
|
|
||||||
% Example:
|
|
||||||
% ?- initial_env(Env), infer_type(if(is_number(id(x)), id(x), string_val("no")), [x:union(number,string)], Type).
|
|
||||||
% Type = number (because string_val("no") is string, unified with number from then branch, if x is number then x is number, else x is string.
|
|
||||||
% The unification of number and string should fail, or result in union(number, string).
|
|
||||||
% The example above needs careful check of unify_types.
|
|
||||||
% Let's assume unify_types(T1,T2,union(T1,T2)) if they are different base types.
|
|
||||||
|
|
||||||
% Corrected unification for disparate types (common supertype or union)
|
|
||||||
unify_types(T1, T2, union(T1, T2)) :-
|
|
||||||
% This is a fallback if no other rule matches and T1, T2 are not 'any' or 'never'
|
|
||||||
T1 \= any, T2 \= any, T1 \= never, T2 \= never,
|
|
||||||
T1 \= T2, % Not identical
|
|
||||||
% Ensure not to double-wrap unions unnecessarily (basic check)
|
|
||||||
\+ (T1 = union(_,_) ; T2 = union(_,_)),
|
|
||||||
log(unification, disparate_types(T1, T2) -> union(T1,T2)).
|
|
||||||
Loading…
x
Reference in New Issue
Block a user