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.{Layout, Desugar, Typechecker, AST}
@shortdoc "Per interactive REPL"
def run(args) do
{opts, _, _} = OptionParser.parse(args, switches: [syntax: :string])
syntax = case Keyword.get(opts, :syntax) do
"agda" -> :agda
_ -> :lean
end
IO.puts("🧊 Per Programming Language version 0.4.0\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, [syntax: syntax]) do
{:ok, new_env} ->
IO.puts("Loaded: #{mod_name}")
new_env
{:error, _err} ->
acc_env
end
end)
loop(env, syntax)
end
defp loop(env, syntax) do
syntax_name = if syntax == :agda, do: "Agda", else: "Lean"
input = IO.gets("per [#{syntax_name}]> ")
case input do
nil -> :ok
":q\n" -> :ok
"\n" -> loop(env, syntax)
":syntax agda\n" ->
IO.puts("Switched to Agda syntax")
loop(env, :agda)
":syntax lean\n" ->
IO.puts("Switched to Lean syntax")
loop(env, :lean)
":check " <> rest ->
handle_introspection(String.trim(rest), :check, env, syntax)
loop(env, syntax)
":eval " <> rest ->
handle_introspection(String.trim(rest), :eval, env, syntax)
loop(env, syntax)
":print " <> rest ->
handle_introspection(String.trim(rest), :print, env, syntax)
loop(env, syntax)
"import " <> rest ->
mod_name = String.trim(rest)
case Per.Compiler.load_module_to_env(mod_name, env, [syntax: syntax]) do
{:ok, new_env} ->
IO.puts("Loaded: #{mod_name}")
loop(new_env, syntax)
{:error, err} ->
IO.puts("Error: #{inspect(err)}")
loop(env, syntax)
end
_ ->
case eval(input, env, syntax) do
{:ok, result} ->
IO.puts("Result: #{AST.to_string(result)}")
loop(env, syntax)
{:error, err} ->
IO.puts("Error: #{inspect(err)}")
loop(env, syntax)
end
end
end
defp handle_introspection(input, mode, env, syntax) do
case parse_and_desugar(input, env, syntax) 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, syntax) do
input = String.trim(input)
if input == "" do
{:error, :empty_input}
else
lexer = if syntax == :agda, do: Per.Lexer.Agda, else: Per.Lexer
parser = if syntax == :agda, do: Per.Parser.Agda, else: Per.Parser
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, syntax) do
case parse_and_desugar(input, env, syntax) do
{:ok, term} -> {:ok, Typechecker.normalize(env, term)}
err -> err
end
end
end