Current section
Files
Jump to
Current section
Files
lib/mix/tasks/compile_stdlib.ex
defmodule Mix.Tasks.CompileStdlib do
@moduledoc """
Attempts to compile all 123 Hamler .hm standard library files in topological
order (dependencies first), sharing the accumulated type environment across
compilations.
Usage: mix compile_stdlib
"""
use Mix.Task
@shortdoc "Compile all Hamler stdlib .hm files in dependency order"
def run(_args) do
Mix.Task.run("compile", [])
files = Path.wildcard("lib/**/*.hm") |> Enum.sort()
total = length(files)
IO.puts("\n#{IO.ANSI.bright()}=== Phi Stdlib Compilation Report ===#{IO.ANSI.reset()}")
IO.puts("Found #{total} .hm files\n")
# Build dependency graph and sort topologically
module_infos = Enum.map(files, &extract_module_info/1)
name_to_file = Map.new(module_infos, fn {file, mod, _} -> {mod, file} end)
sorted_files = topological_sort(module_infos, name_to_file)
IO.puts("Compiling in topological order...\n")
results = compile_in_order(sorted_files, name_to_file)
# Summary
passed = Enum.count(results, fn {_, status, _} -> status == :ok end)
failed = total - passed
IO.puts("\n#{IO.ANSI.bright()}=== Summary ===#{IO.ANSI.reset()}")
IO.puts("#{IO.ANSI.green()}PASS: #{passed}/#{total}#{IO.ANSI.reset()}")
if failed > 0 do
IO.puts("#{IO.ANSI.red()}FAIL: #{failed}/#{total}#{IO.ANSI.reset()}")
IO.puts("\n#{IO.ANSI.bright()}Failed files:#{IO.ANSI.reset()}")
results
|> Enum.filter(fn {_, status, _} -> status == :error end)
|> Enum.each(fn {file, _, reason} ->
short_reason = reason |> String.slice(0, 120) |> String.replace("\n", " ")
IO.puts(" #{IO.ANSI.red()}✗#{IO.ANSI.reset()} #{file}: #{short_reason}")
end)
end
IO.puts("")
if failed > 0 do
IO.puts("#{IO.ANSI.bright()}Error categories:#{IO.ANSI.reset()}")
results
|> Enum.filter(fn {_, status, _} -> status == :error end)
|> Enum.map(fn {_, _, reason} -> categorize_error(reason) end)
|> Enum.frequencies()
|> Enum.sort_by(fn {_, count} -> -count end)
|> Enum.each(fn {category, count} ->
IO.puts(" #{count}x #{category}")
end)
IO.puts("")
end
end
# ---------------------------------------------------------------------------
# Dependency extraction
# ---------------------------------------------------------------------------
defp extract_module_info(file) do
source = File.read!(file)
stripped = strip_comments(source)
mod_name =
case Regex.run(~r/^module\s+([\w.]+)/m, stripped, capture: :all_but_first) do
[name] -> name
_ -> file_to_module_name(file)
end
imports =
Regex.scan(~r/^import\s+([\w.]+)/m, stripped, capture: :all_but_first)
|> List.flatten()
{file, mod_name, imports}
end
# Remove block comments {- ... -} and line comments -- ...
# Handles one level of nesting; sufficient for import scanning.
defp strip_comments(source) do
source
|> remove_block_comments()
|> String.replace(~r/--[^\n]*/, "")
end
defp remove_block_comments(source) do
case Regex.run(~r/\{-/, source, return: :index) do
nil ->
source
[{start, _}] ->
before = String.slice(source, 0, start)
rest = String.slice(source, start, String.length(source))
case Regex.run(~r/-\}/, rest, return: :index) do
nil ->
# Unclosed block comment — drop to end
before
[{close_start, close_len}] ->
after_close = String.slice(rest, close_start + close_len, String.length(rest))
before <> remove_block_comments(after_close)
end
end
end
# Fallback: derive module name from file path (e.g. lib/Data/List.hm -> Data.List)
defp file_to_module_name(file) do
file
|> String.replace_prefix("lib/", "")
|> String.replace_suffix(".hm", "")
|> String.replace("/", ".")
end
# ---------------------------------------------------------------------------
# Topological sort — Kahn's algorithm
# ---------------------------------------------------------------------------
defp topological_sort(module_infos, name_to_file) do
# Build: file -> list of dependency files (only those present in stdlib)
edges =
Map.new(module_infos, fn {file, _mod, imports} ->
deps =
imports
|> Enum.map(&Map.get(name_to_file, &1))
|> Enum.reject(&is_nil/1)
|> Enum.reject(&(&1 == file))
{file, deps}
end)
all_files = Enum.map(module_infos, fn {file, _, _} -> file end)
# in-degree per file = number of dependencies it has (incoming edges)
in_degree = Map.new(all_files, fn file -> {file, length(edges[file] || [])} end)
# reverse edges: dep -> list of files that depend on dep
# When dep is compiled, decrement in-degree of each dependent file
rev_edges =
Enum.reduce(all_files, %{}, fn file, acc ->
Enum.reduce(edges[file] || [], acc, fn dep, a ->
Map.update(a, dep, [file], &[file | &1])
end)
end)
queue = all_files |> Enum.filter(&(Map.get(in_degree, &1, 0) == 0)) |> :queue.from_list()
kahn(queue, in_degree, rev_edges, [])
end
defp kahn(queue, in_degree, rev_edges, acc) do
case :queue.out(queue) do
{:empty, _} ->
# Cycle or remaining — append anything unvisited at the end
remaining = in_degree |> Enum.filter(fn {_, d} -> d > 0 end) |> Enum.map(&elem(&1, 0))
if remaining != [] do
IO.puts(
"#{IO.ANSI.yellow()}Warning: cycle detected among #{length(remaining)} file(s), appending at end#{IO.ANSI.reset()}"
)
end
Enum.reverse(acc) ++ remaining
{{:value, file}, rest_queue} ->
dependents = Map.get(rev_edges, file, [])
{new_queue, new_in_degree} =
Enum.reduce(dependents, {rest_queue, in_degree}, fn dep, {q, deg} ->
new_deg = Map.update!(deg, dep, &(&1 - 1))
q2 = if new_deg[dep] == 0, do: :queue.in(dep, q), else: q
{q2, new_deg}
end)
kahn(new_queue, new_in_degree, rev_edges, [file | acc])
end
end
# ---------------------------------------------------------------------------
# Compilation with shared environment
# ---------------------------------------------------------------------------
defp compile_in_order(sorted_files, name_to_file) do
# We also need the file->mod map for printing
file_to_mod =
Map.new(name_to_file, fn {mod, file} -> {file, mod} end)
{results, _final_env} =
Enum.reduce(sorted_files, {[], Phi.Typechecker.Env.new()}, fn file, {acc, env} ->
{status, reason, new_env} = compile_file(file, env)
label =
if status == :ok,
do: "#{IO.ANSI.green()}✓#{IO.ANSI.reset()}",
else: "#{IO.ANSI.red()}✗#{IO.ANSI.reset()}"
mod_hint = Map.get(file_to_mod, file, "")
IO.puts(" #{label} #{file}#{if mod_hint != "", do: " (#{mod_hint})", else: ""}")
{[{file, status, reason} | acc], new_env}
end)
Enum.reverse(results)
end
defp compile_file(file, env) do
result =
try do
source = File.read!(file)
case Phi.Compiler.compile_module(source, source_path: file, env: env) do
{:ok, mod, bin, _ast, new_env} ->
Phi.Compiler.load_module(mod, bin)
{:ok, "", new_env}
{:error, reason} ->
{:error, inspect(reason), env}
other ->
{:error, inspect(other), env}
end
rescue
e -> {:error, "#{inspect(e.__struct__)}: #{Exception.message(e)}", env}
catch
kind, value -> {:error, "#{kind}: #{inspect(value)}", env}
end
case result do
{:ok, _, new_env} -> {:ok, "", new_env}
{:error, reason, kept_env} -> {:error, reason, kept_env}
end
end
# ---------------------------------------------------------------------------
# Error categorisation
# ---------------------------------------------------------------------------
defp categorize_error(reason) do
cond do
String.contains?(reason, "FunctionClauseError") -> "FunctionClauseError"
String.contains?(reason, "MatchError") -> "MatchError"
String.contains?(reason, "erl_compile") -> "Erlang compile error"
String.contains?(reason, "erl_lint") -> "Erlang lint error"
String.contains?(reason, "parse") or String.contains?(reason, "Parser") -> "Parse error"
String.contains?(reason, "KeyError") -> "KeyError"
String.contains?(reason, "UndefinedFunctionError") -> "UndefinedFunctionError"
String.contains?(reason, "CaseClauseError") -> "CaseClauseError"
String.contains?(reason, "BadMapError") -> "BadMapError"
true -> "Other"
end
end
end