Spencer-Comin / Fern

A toy programming language built with LLVM and Prolog

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Caution

I have abandoned this iteration of Fern. I've been slowly rewriting and reimagining Fern in OCaml. That repo is still private, but if you want to see what's going on there please reach out and I may add you as a collaborator.

Fern

A toy programming language built with LLVM and Prolog

A quick rundown

Let's look at an example to see how everything works. To start off, here's the whole thing:

{:
    [Int32, Int32 -> Int32]
        sum;

    [End -> IO]
        main;
    
    [(Int32, Int32 -> Int32), Int32, Int32 -> Int32]
        apply;
:}

std::putInt32;
std::debug;

sum := a, b -> a + b;

apply := f, a, b -> f(a, b);

main := putInt32(debug(),
    apply(sum, 1, 2)
);

Let's break it down to look at the individual components.

Type info

At the top of the example there's a section wrapped in {: :}. This is the type info section of the file. In here there are a few type declarations that look like

[Foo]
    bar;

This declaration says that this file defines a bar with type Foo. If we wanted both a bar and a baz with type Foo we could do

[Foo]
    bar;
    baz;

Now that we know that, we can look at the actual type declarations from the example. First:

[Int32, Int32 -> Int32]
    sum;

Int32 is the 32 bit signed integer type. , is the structuring operator. -> is the function definition operator. Putting those together, the type we have here is a function that takes a structure* of two Int32s as input and outputs an Int32. Later in the example there will be a definition of sum which has this type.

* In Fern, functions types are a mapping from just one input type (on the left of the ->) to just one output type (on the right of the ->), and therefore functions are mappings from just one input argument to just one output value. To get the effect of multiple arguments or returns, we use structures and structure types.

On to the next type declaration:

[End -> IO]
    main;

End is Fern's terminal or singleton type; as an input type it's used to indicate that a function takes no arguments. IO is the input/output stream type. Later, when we see the definition of main, we'll see that it doesn't take an (explicit**) argument, which is why its input type is End, and it prints to an output stream, which is why its output type is IO.

Now for the last type declaration:

[(Int32, Int32 -> Int32), Int32, Int32 -> Int32]
    apply;

The type for apply is a bit more involved. First, in the parentheses, we see the same type that we had for sum (this is intentional), a function that takes a structure of two Int32s as input and outputs an Int32. Structured together with this function type we have two more Int32s. At the output of our function type we have a single Int32. All together, apply will have a type of a function taking a structure containing a function and two Int32s and outputting an Int32.

Imports

Importing in Fern is super simple, you just write the name you are importing and the namespace you are importing it from.

std::putInt32;
std::debug;

In our example we are importing putInt32 and debug from the std namespace. We don't need to include their types in the type info section, as the compiler finds the types while resolving the imports. If you want to know their types, you can look in std.types.frn. putInt32 takes a structure of an IO output stream and an Int32 value and returns the output stream with the value put on it. debug gives us an output stream for debugging (technically, it gives us the stderr output stream).

Definitions

Definitions look like

a := x;

which would mean a is defined as x (whatever x is).

In our example, we have

sum := a, b -> a + b;

sum is defined as a function that takes a structure of a and b and returns their sum (duh).

Next,

apply := f, a, b -> f(a, b);

apply is a function that takes a structure of f, a, and b, passes a structure built with a and b to f, and return the result.

Last,

main := putInt32(debug(),
    apply(sum, 1, 2)
);

You might notice that there is no -> here, so how can main be a function? Remember that mains input type is End, so it takes no argmuments. In Fern, a non-function definition is equivalent to a function definition with an End input. Alternatively we could have indicated this with an empty parentheses input:

main := () -> putInt32(debug(),
    apply(sum, 1, 2)
);

To explain what main is doing, we'll start from the inside and work our way out. We call debug without arguments** to get a debug output stream. Our sum function from earlier is structured together with number literals 1 and 2 and passed to apply (This is why the type of sum shows up in the type declaration of apply). Next we structure together the output debug stream with the result of apply and pass that to putInt32.

main is a special function name, as it indicates the entry point to the program. When we run this example, Fern calls main with a nil*** argument, and on the output we will see 3 printed.

** The compiler implicitly puts in a nil*** argument, to satisfy the End input type. There must always be one input and one output.

*** nil is the only value that has the End type.

TODO

  • Generate executables
  • Closures
  • Match expressions
  • String literals
  • Floating point literals
  • Type sums

About

A toy programming language built with LLVM and Prolog


Languages

Language:C++ 59.7%Language:Prolog 35.4%Language:CMake 3.6%Language:Python 1.3%