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]]
.
head
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)
.