/jevkalk

A Jevko-based interpreter.

Primary LanguageJavaScript

Jevkalk

a little hedgehog aka yezhyk in his natural habitat

An experimental interpreter for a crazy little language called Jevkalk.

The language is similar in spirit to Lisps in that the written syntax mirrors abstract syntax, providing homoiconicity, and it is very minimal and flexible. Otherwise Jevkalk is peculiar and not compatible in any way with any Lisp or other language. It's an experiment to test some exciting (to me) programming language design ideas.

It is made for fun and to inspire other designers.

Features

Goals

  • Extremely simple implementation -- easy to port, explain, hack on
  • Explore original programming language design ideas
  • Fun

Installation

Maybe later.

Running tests

Deno required.

To run tests use deno test --allow-read.

Examples

To better understand the examples, see Explanation of syntax and Operators.

Fibonacci sequence

The following program:

  • defines fib to be a one-argument function which computes the n-th Fibonacci number,
  • computes the 10th Fibonacci number using fib
bind [
  [fib] fun [[n]
    ? [
      < [[n][2]] [n]
      + [
        fib [
          - [[n][1]]
        ]
        fib [
          - [[n][2]]
        ]
      ]
    ]
  ]
]

fib [10]

Native identifiers with spaces

The following:

bind [[sum primes] fun [[[a][b]]
  accumulate [
    [+]
    [0]
    filter [
      [prime?]
      enumerate interval [[a][b]]
    ]
  ]
]]

is a translation of this Scheme fragment:

(define (sum-primes a b)
  (accumulate +
              0
              (filter prime? (enumerate-interval a b))))

from SICP 3.5.1, demonstrating identifiers with spaces.

Here sum primes and enumerate interval are two double-word identifiers.

Explanation of syntax

An application of a procedure or a special form looks like this:

operator [...operands]

Where each operand has the same shape.

For example:

factorial [1]

Would apply factorial to 1.

operator can be empty:

[5]

The empty operator simply returns the value of its argument. In this case it's the number 5.

Whether operands are evaluated at all, depends on the operator. For example:

+ [[a][b]]

Here we have the operator + called on 2 operands. + happens to evaluate its operands. In this case the operands are [a] and [b]. Both of these have empty operators. The empty operator called on an identifier returns the value assigned to that identifier. So if a and b were defined to be 1 and 2, these values will be returned.

After evaluating its operands, + adds them together and returns the result. Which in this example would be 3.

Caveats

Calling functions returned from functions

Suppose we have a function make adder which returns another (anonymous function) which takes in two numbers and returns their sum plus some secret number (unimportant).

We want to call it with 1 and 2 as arguments.

This would be written:

((make-adder) 1 2)

in an S-expression-based language.

In Jevkalk straight support for such a thing could look like:

make adder[][[1][2]]

However implementing this is unnecessarily complicated and it may seem a bit confusing to use.

Therefore it is implemented like this instead (ap stands for apply partial):

ap [make adder[][[1][2]]]

i.e. the syntax is as above, but wrapped in ap to make it explicit what's going on as well as simple to implement (see Goals).

Operators

The empty operator

The empty operator evaluates its argument and returns the result.

The rules it uses for evaluation are as follows.

  1. Whatever looks like a number evaluates to a number. E.g.:
[1]

evaluates to the number 1.

  1. Anything else is treated as an identifier. An identifier is evaluated by looking up its value in the current context and returning it. E.g. suppose we have defined a to be:
bind [[a] [5]]

then evaluating:

[a]

will return 5.