Flint

Flint is a small computer programming language, based on the LISP family of languages.

The Flint programming language contains a restricted set of language constructs — integers, linked lists, symbol definitions, and symbol references — all of which are evaluatable expressions, accepting a list of arguments and returning a value. Values can be mapped to a symbol and can be referenced later by that name, though all values in Flint are immutable and the value of a defined symbol can only be altered through the re-definition of that symbol to a different value. A handful of built-in primitive functions (accessed as pre-defined symbols) provide a starting point for the construction of more complex expressions.

Flint was created as an investigation into the tradeoffs between complexity, expressiveness, and computational efficiency in extremely minimal languages.

A future project would be to design a similarly-constrained language that operates on flat arrays instead of linked lists, for a possible performance improvement while keeping the existing list semantics.

Types

Integers

Integers are implemented as 16-bit unsigned integers (with values ranging from 0 to 65535, inclusive), although this is specific to my implementation and can be readily altered with no impact to the semantics of the language. Integer literals are written in base 10, as a series of one or more consecutive ASCII digits (for example, 1, 65, and 972). When evaluated, an integer will return itself (for example, the integer expression 3 will return the value 3 when evaluated).

Lists

Lists are implemented as linked lists. List literals are written as a series of values wrapped in parentheses. For example, an empty (zero-length) list is written (), a list containing the numbers 1, 2, and 3 is written (1 2 3), and a list containing three other lists is written ((1 2) (3 4) (5 6)).

Internally, lists are implemented as a combination of two hidden types, the null type and the pair type. There is no concrete list type in the implementation; what the user sees as a list is really an abstraction built from a nesting of nulls and pairs. For the following sections we will notate the null type as ! (a single exclamation mark) and the pair type as {1 2} (exactly two expressions wrapped in curly braces, to differentiate from a two-element list).

The left slot of a pair is allowed to contain a value of any type (integer, pair, null), but the right slot can only ever contain a pair or a null type.

The empty list (a list containing no values) is implemented as a single null value. Thus, when the user writes (), the internal representation will be !.

A list containing a single item is implemented as a single pair value. When the user writes (1) (a list containing the number 1), the internal representation will be the pair {1 !}. The left value of the pair is the item being stored, and the right value of the pair is the null value.

A list containing two items is implemented as a pair value inside a pair value. When the user writes (1 2) (a list containing the numbers 1 and 2), the internal representation will be {1 {2 !}}. The left value of the pair is the first list item, and the right value of the pair is another pair, containing the second list item and a null.

Every time that an item is added to the end of the list, we replace the right-most null with the pair {item !}. The list (1 2 3 4 5), for example, will be implemented as the pair {1 {2 {3 {4 {5 !}}}}}. The rules hold when the list contains a nesting of lists, though it can be difficult to read if you’re unfamiliar with the format. The list (()) (a one-length list that contains an empty list) will be implemented as the pair {! !}, where the left value is the item (an empty list, or null) and the right value is null. Likewise, the list (() () ()) (a list containing three empty lists) is implemented as {! {! {! !}}}, the list ((1) (2)) is written as {{1 !} {{2 !} !}}, and the list ((1 2) (3 4) (5 6)) is implemented as {{1 {2 !}} {{3 {4 !}} {{5 {6 !}} !}}}.

When a list is evaluated, the first element of the list will be evaluated, with the remainder of the list being used as the list of arguments. For example, the list expression (3) will return the value 3 when evaluated, and the list expression (add 3 5) will return the value 8 when evaluated. All values that are used as arguments are evaluated before being passed to the expression. The symbols $1, $2, ..., $8 are placeholders for the nth passed argument, and the symbol $@ is a placeholder for the full argument list (enabling the definition of variadic functions). Arguments should normally be quoted to prevent them from being evaluated twice (once when passed and once as used). The value of an unprovided argument is (), and an empty list will return itself.

Built-in functions

quote

The quote function returns the first argument without evaluating it. For example, (1 2 3) will evaluate to 1, but (quote (1 2 3)) will evaluate to (1 2 3).

This must be implemented as a special case, as all arguments are normally always evaluated during a function evaluation.

As this function is used prevalently by most programs, there exists a terser syntax for readability and convenience. Any expression can be quoted by directly prefixing it with a character. For example, the expression ‘item is equivalent to the expression (quote item).

A familiar syntax is also available for quoted lists. The expression (quote (1 2 3)) can be written as [1 2 3], resembling an array from a C-style language. Do note that the equivalent of the expression (quote ((1 2) (3 4) (5 6))) will be [(1 2) (3 4) (5 6)], rather than [[1 2] [3 4] [5 6]].

The head function will return the first element of a list, or null if the list is empty. For example, (head ‘(1 2 3)) will evaluate to 1.

If the argument is an integer, the function will return that integer. For example, (head 3) will evaluate to 3.

tail

The tail function will return a list with the first element removed, or null if the list is empty. For example, (tail ‘(1 2 3)) will evaluate to (2 3).

If the argument is an integer, the function will return null. For example, (tail 3) will evaluate to ().

merge

The merge function appends a value to the front of a list. The pair {x y} if y is a pair, else the pair {x {y !}}

cond

The cond function iterates over all provided arguments, evaluating the head of each one. If the result is truthy, returns the head of the tail of that argument. If no arguments evaluate to true, returns ().

0 is false, all other integers are true. () is false, all other lists are true.

int?

Returns 1 if the argument is an integer, otherwise returns 0.

list?

Returns 1 if the argument is a list, otherwise returns 0.

null?

Returns 1 if the argument is an empty list, otherwise returns 0.

eq?

Returns 1 if the two arguments are integers and their values are equal, otherwise returns 0.

A null argument is treated as a 0.

lth?

Returns 1 if the two arguments are integers and the first integer has a value less than the other, otherwise returns 0.

gth?

Returns 1 if the two arguments are integers and the first integer has a value greater than the other, otherwise returns 0.

add

Returns the sum of the two arguments. Non-integer arguments are treated as integers with value 0.

sub

Returns the difference of the two arguments. Non-integer arguments are treated as integers with value 0.

mul

Returns the product of the two arguments. Non-integer arguments are treated as integers with value 0.

div

Returns the quotient of the two arguments. Non-integer arguments are treated as integers with value 0.

Other syntax

Symbol definition

Assigning a value to an identifier is accomplished with the syntax (#ident value). For example, the expression (#t 1) will alias the integer 1 to the identifier t. Any value with any data type can be aliased, so (#sum add) will alias the function add to the identifier sum. The expression (sum 1 2) will now be equivalent to (add 1 2).

Syntactic sugar for strings

ASCII strings as “abcdef”, linked lists. The expression “abc def” is equivalent to the list (97 98 99 32 100 101 102).

Example function implementations

Most of the symbol definitions in this section will build on top of previously defined symbols.

Aliases for true and false

To make our programs clearer when we use truthy and falsey values, we can alias 1 (a truthy value) to the symbol t, and 0 (a falsey value) to the symbol f. It will also aid readability in the future to alias 1 to the symbol else, for use in the default branch of a cond expression.

(#t     1)
(#f     0)
(#else  1)

Boolean normalisation

The bool function will take a single argument and convert it into a number based on its truthyness, returning 1 if the value is truthy or 0 if the value is falsey.

(#bool
  [cond
    [’$1   t]
    [else  f]
  ]
)

>>> (bool [1 2 3])
1
>>> (bool [])
0

Not

The not function is the inverse of the bool function, returning 1 if the argument is falsey, or 0 if the argument is truthy. This can be used to invert a boolean value.

(#not
  [cond
    [’$1   f]
    [else  t]
  ]
)

>>> (bool [1 2 3])
0
>>> (bool [])
1

Diadic if

The if function chooses between two functions to call based on the truthyness of the first argument. It is a less verbose alternative to cond for this situation.

(#if
  [cond
    [’$1   ‘$2]
    [else  ‘$3]
  ]
)

>>> (if [1 2 3] 20 30)
20
>>> (if [] 20 30)
30

Increment and decrement

(#inc [add $1 1])
(#dec [sub $1 1])

Sequence generation

(#seq
  [cond
    [’$1   (seq (dec ‘$1) (merge ‘$1 ‘$2))]
    [else  ‘$2]
  ]
)

>>> (seq 5)
(1 2 3 4 5)
>>> (seq 0)
()

Reverse a list

(#rev
  [cond
    [’$1   (rev (tail ‘$1) (merge (head ‘$1) ‘$2))]
    [else  ‘$2]
  ]
)

Get the length of a list

(#len
  [cond
    [’$1   (inc (len (tail ‘$1)))]
    [else  0]
  ]
)

Logical OR over a list of values

Takes a list of values as an argument. Returns true if any of the list members are true, otherwise returns false. Returns false if the list is empty.

(#any
  [cond
    [(null? ‘$1)  f]
    [(head  ‘$1)  t]
    [else  (any (tail ‘$1))]
  ]
)
(#or  ‘any)

>>> (or [0 0 0])
0
>>> (or [0 5 0])
1

Logical AND over a list of values

Takes a list of values as an argument. Returns true if all of the list members are true, otherwise returns false. Returns true if the list is empty.

(#all
  [cond
    [(null? ‘$1)       t]
    [(not (head ‘$1))  f]
    [else  (all (tail ‘$1))]
  ]
)
(#and  ‘all)

>>> (and [1 2 0])
0
>>> (and [1 2 3])
1

Map a function over a list

Takes a function as the first argument and a list of values as the second argument. Note that the $1 in the else branch is unquoted, as we’re wanting to evaluate it as a function.

(#map
  [cond
    [(null? ‘$2)   ()]
    [else   (merge ($1 (head ‘$2)) (map ‘$1 (tail ‘$2)))]
  ]
)

Reduce

(#reduce
  [cond
    [(null? ‘$2)  ‘$3]
    [else  (reduce ‘$1 (tail ‘$2) ($1 (head ‘$2) ‘$3))]
  ]
)

Filter

(#filter
  [cond
    [(null? ‘$2)   ()]
    [($1 (head ‘$2))   (merge (head ‘$2) (filter ‘$1 (tail ‘$2)))]
    [else   (filter ‘$1 (tail ‘$2))]
  ]
)

Sum, product, factorial

(#sum       [reduce ‘add ‘$1])
(#product   [reduce ‘mul ‘$1 1])
(#factorial [product (seq ‘$1)])

Repeat

(#repeat
  [cond
    [(gth? ‘$2 0)   (merge ‘$1 (repeat ‘$1 (dec ‘$2)))]
    [else   ()]
  ]
)

Exponent

(#exp  [product (repeat ‘$1 ‘$2)])

Triangular numbers

(#triangle  [sum (seq ‘$1)])

Less-than-or-equal, greater-than-or-equal

(#<= [or [
  (lth? ‘$1 ‘$2)
  (eq? ‘$1 ‘$2)
]])

(#>= [or [
  (gth? ‘$1 ‘$2)
  (eq? ‘$1 ‘$2)
]])