Kilogram Design

This document serves as a living design document for how I feel the language should look / work internally.

Syntax

Variables

Variables are defined using the let keyword.

let x: int = 100
let y: string = "hello world"
let z: float = 3.14

An interesting thing to note is that variables are immutable. This is similar to languages like Haskell or Elixir.

Functions

There are no named functions, but instead lambda functions assigned to variables. Functions are treated like any other variable, thus they are declared like any other variable: using a let binding.

let sum: (int, int) -> int =
function(x: int, y: int): int
	x + y
end

sum(10, 5)

Note that for all variable declarations, you can optionally drop the type declaration. So, the above expression can be simplified to the following.

let sum = function(x: int, y: int): int
	x + y
end

We still need to specify the types of x and y, however we can infer the type of sum based on its value.

Records

Records are immutable “bags” of data, similar to a struct in C.

record Player
	x: int,
	y: int,
	w: int,
	h: int
end

let my_player = Player { x: 0, y: 5, w: 8, h: 16 }
my_player

Enums

Enums behave similarly to algebraic data types in OCaml and Haskell. They can be used to construct more complex data types, such as linked lists and trees.

enum List
	Cons(int, List),
	Nil
end

let my_list = Cons(1, Cons(2, Cons(3, Nil)))

If

Instead of the classic, C-style if statements, Kilogram uses “if expressions”.

let demo: string = if 5 < 10 then "hello" else "world"

TODO

Implementation

Kilogram is a compiled language using C as its compilation target. Since C is not a functional language, thought must be put into how we represent functional concepts.

Lambda functions

A core component of functional languages is lambda functions, otherwise known as anonymous functions. Let’s consider two equivalent chunks of code, one in Javascript and the other in Kilogram.

Javascript:

let sum = (x, y) => x + y;

Kilogram:

let sum = function(x: int, y: int): int 
	x + y
end

Internally, we are assigning the variable sum to a lambda function that takes integers x, y and returns an integer. Since functions are just another value, we do not want to handle them drastically different from how we handle another primitives. To do this we need to use a couple clever tricks.

The first issue is handling free variables within the function body. Consider the following code:

let z = 10
let sum = function(x: int, y: int): int 
	x + y + z
end

As you can see, the value of sum depends on the value of z, but z is not passed as an argument. Instead, it is captured by the lambda function. Let’s say we pass this sum variable to another function. How do we know what z is?

To solve this, we add z to a function context. When we create an instance of a lambda function, we capture the values of each free variable and store it within the function context. When we call the lambda function, we’ll load each of the variables from the context into scope; that way, all of the necessary variables will be in scope to evaluate the function’s body.