Current section

Files

Jump to
per lib mix tasks per.repl.ex
Raw

lib/mix/tasks/per.repl.ex

defmodule Mix.Tasks.Per.Repl do
use Mix.Task
alias Per.{Lexer, Layout, Parser, Desugar, Typechecker, AST}
@shortdoc "Per interactive REPL"
def run(_) do
IO.puts("🧊 Per Programming Language version 0.4.0 [Lean Syntax]\n" <>
"Copyright (c) 2016-2026 Groupoid Infinity\n" <>
"https://groupoid.github.io/per/\n"
)
env = %Typechecker.Env{}
# Auto-load foundations
foundations = ["mltt", "inductive", "univalent", "homotopical"]
env =
Enum.reduce(foundations, env, fn mod_name, acc_env ->
case Per.Compiler.load_module_to_env(mod_name, acc_env) do
{:ok, new_env} ->
IO.puts("Loaded: #{mod_name}")
new_env
{:error, _err} ->
acc_env
end
end)
loop(env, "Lean")
end
defp loop(env, syntax_name) do
input = IO.gets("per> ")
case input do
nil -> :ok
":q\n" -> :ok
"\n" -> loop(env, syntax_name)
":check " <> rest ->
handle_introspection(String.trim(rest), :check, env)
loop(env, syntax_name)
":eval " <> rest ->
handle_introspection(String.trim(rest), :eval, env)
loop(env, syntax_name)
":print " <> rest ->
handle_introspection(String.trim(rest), :print, env)
loop(env, syntax_name)
"import " <> rest ->
mod_name = String.trim(rest)
case Per.Compiler.load_module_to_env(mod_name, env) do
{:ok, new_env} ->
IO.puts("Loaded: #{mod_name}")
loop(new_env, syntax_name)
{:error, err} ->
IO.puts("Error: #{inspect(err)}")
loop(env, syntax_name)
end
_ ->
case eval(input, env) do
{:ok, result} ->
IO.puts("Result: #{AST.to_string(result)}")
loop(env, syntax_name)
{:error, err} ->
IO.puts("Error: #{inspect(err)}")
loop(env, syntax_name)
end
end
end
defp handle_introspection(input, mode, env) do
case parse_and_desugar(input, env) do
{:ok, term} ->
# For :check and :eval, we need the type
case mode do
:check ->
try do
ty = Typechecker.infer(env.ctx, term)
IO.puts("TYPE: #{AST.to_string(Typechecker.readback(ty))}")
rescue
e -> IO.puts("Type Error: #{inspect(e)}")
end
:eval ->
try do
ty = Typechecker.infer(env.ctx, term)
norm = Typechecker.normalize(env, term)
IO.puts("TYPE: #{AST.to_string(Typechecker.readback(ty))}")
IO.puts("TERM: #{AST.to_string(norm)}")
rescue
e -> IO.puts("Error: #{inspect(e)}")
end
:print ->
try do
ty = Typechecker.infer(env.ctx, term)
IO.puts("TYPE: #{AST.to_string(Typechecker.readback(ty))}")
IO.puts("TERM: #{AST.to_string(term)}")
rescue
e -> IO.puts("Error: #{inspect(e)}")
end
end
{:error, err} ->
IO.puts("Error: #{inspect(err)}")
end
end
defp parse_and_desugar(input, env) do
input = String.trim(input)
if input == "" do
{:error, :empty_input}
else
case Lexer.lex(input) do
{:error, _} = err -> err
tokens ->
resolved = Layout.resolve(tokens)
case Parser.parse_expression(resolved) do
{:ok, expr, _} ->
{:ok, Desugar.desugar_expression(expr, env)}
err -> {:error, err}
end
end
end
end
defp eval(input, env) do
case parse_and_desugar(input, env) do
{:ok, term} -> {:ok, Typechecker.normalize(env, term)}
err -> err
end
end
end