LIJP is a member of lisp programming languages designed to be a backend for a statically typed, lazy, pure functional programming language.
Describing the abstract syntax for the language with Haskell:
data Term = Var Natural
| App Term Term
| Abs Term
| Let [Term] Term
| Enum Natural Natural
| Case Term [Term]
Syntax is simple enough:
- Variables are denoted with sequences of digits.
- Parentheses are used as grouping element.
- Sequences of terms are interpreted as application
- Lambda abstraction is denoted by lambda symbol(
λ
). - Let expression is expressed with terms separated by colon(
:
), multiple binds happening in parallel are separated by comma(,
). - Data constructor is an index separated from arity with a slash(
/
). The index and arity are digit sequences. - Case expression is a term followed by braces (
{}
), enclosing cases separated by semicolon (;
) - Hash (
#
) is used as a single-line comment. - Whitespace is ignored unless it separates two terms.
2020-09-30 update: Let Term Term
changed to Let [Term] Term
.
The parallel bind informs that the bound terms won't reference each other.
These combinators should be relatively familiar to everybody: Here's the S -combinator.
λλλ2 0 (1 0)
The K -combinator:
λλ1
The I -combinator:
λ0
λ (λ1(0 0)) (λ1(0 0))
(λλ1{ 0; 1/0 }) # or
, (λλ1{ 0/0; 0 }) # and
, (λ0{ 1/0; 0/0 }) # not
: 0 (0 (0 (0 (2 0/0 0/0)))) # not (not (not (not (or false false))))
The runtime implements some barebones functionality for evaluating lijp programs.
- The
runtime/term.py
reads terms and implements the lijp syntax. - The
runtime/evaluator.py
implements a lazy evaluator.
- Ensure you have Python installed.
- Download the pypy runtime and extract it.
- Locate the rpython and run it on the
runtime/goal_standalone.py
.
Sequence of terminal commands that do the trick:
wget https://downloads.python.org/pypy/pypy3.6-v7.3.2-src.tar.bz2
tar -xf pypy3.6-v7.3.2-src.tar.bz2
pypy3.6-v7.3.2-src/rpython/bin/rpython runtime/goal_standalone.py
On successful compile you obtain the lijp
-executable.
You can try out some of the samples by running it there:
./lijp samples/boolean-arithmetic.ij
./lijp samples/s-combinator.ij
It doesn't produce particularly interesting outputs. It currently just prints "Returned a tag 0" or "Returned non-integer non-tag".
Both the evaluator and term reader could be accessible from the runtime.
The obj.RuntimeTypeError
is raised whenever there's any kind of an error.
There are primitives obj.Integer
and obj.Data
to pass around primitive data types
plus some functions to typecheck the structures,
called obj.to_integer
and obj.to_data
.
It's expected that lists are built from constructors,
expecting tag 0 for the empty list, and tag 1 for the populated list.
The obj.from_list
/obj.to_list
convert
The prelude is passed to the program as a script. It's either coming through standard input, or from a file given as the first argument.
There is no I/O driver in the runtime. Program is just evaluated and it tries to read it as a value.
The startup process is implemented by the runtime/goal_standalone.py
.
The interpreter is using the push/enter evaluation model.
Internally object.enter(argument_list)
is expected to be used.
This procedure may return a primitive or then a evaluator.PAP
-structure.
In case it does neither then it likely crashes or hangs up.
evaluator.Prog(env, expr)
is an unevaluated expression object.
The evaluator.PAP(obj, args)
is a partially applied function object,
this is produced whenever a function is entered with too few arguments.
The evaluator.Thunk(obj)
implements lazy evaluation.
Upon evaluation the thunk rewrites itself into the computation result
procuded by the heap object it refers to.
The evaluator has several entry points,
though I prefer the evaluator.activate(env, expr)
would be used.
This procedure produces a construct to evaluate an expression
and determines whether a thunk is needed in the expression.
The evaluator.examine(expr)
determines whether to
build a thunk for the expression or not.
The evaluator.evaluate(env, expr, args)
starts an on-demand
evauluation of an expression,
inside the given environment with the given arguments.
The loader is considerably uninteresting.
It is used by constructing a term.Reader()
-state.
Then run term.read_character(reader, char)
through every character and finally call term.read_done(reader)
to terminate reading and obtain the read term as the result.
The state is discarded after use.
Reader procudes term.ReadError(char)
upon a failure.
The term produced by the reader is a runtime-readable data structure closely following the abstract syntax encoding of lijp.
Lijp is an exploration tool based on the observation that some dynamically typed languages form a feasible runtime for a statically typed pure functional programming language.
The focus of this project is the use of types as user interface elements and treating of typechecking as something that verifies that some plan is feasible to apply.
Large programs would become systems for running many small programs written by the users of mentioned systems.