-
Notifications
You must be signed in to change notification settings - Fork 1
Type System
Runiq is interpreted by JavaScript, and Runiq doesn't add very much of its own type-related logic. So the tl;dr of Runiq's type system is see JavaScript.
But, for the curious and/or concerned, it's worth going into more detail about how typing in Runiq works. Note that Runiq is a work-in-progress and much of this may be subject to change.
Runiq's type system is:
- Pretty much just like JavaScript
- Unsafe
- Dynamic
- Weak
Runiq's type system operates over four layers:
- Parsing Layer
- Interpretation Layer
- Function Layer
- Library Layer
When a Runiq program such as (foo 1 2 "hi" (bar baz '(qux))) is parsed, first a series of token objects are generated, e.g.:
open-paren, identifier, whitespace, number,
whitespace, number, whitespace, string,
open-paren, identifier, identifier,
quote, identifier, close-paren, close-paren,
close-paren
Each token object retains the original source string of the found token. Token objects look like this:
{ type: 'identifier', string: 'foo' },
{ type: 'number', string: '1' }, ...
That sequence of tokens is handed to a secondary parser function that arranges them into the Runiq AST format, which is a plain JavaScript array that looks like this:
["foo", 1, 2, "hi", ["bar", "baz", {"'": ["qux"]}]]
The values that end up in the Runiq AST are plain-old-JavaScript types:
- Array (denoting lists) (via
Array.isArray()) - String (denoting strings and identifiers)
- Object (denoting quoted lists)
- Number
The AST is then passed to the interpreter.
The interpreter consumes the AST generated in the parsing step. It walks the AST, and looks at the structure of each list to decide whether to treat it like a function, or like a sequence:
LIST CHECK:
is the first element a string?
does the string match a defined library function?
treat as a function invocation
else
treat as a sequence
else
treat as a sequence
For function invocations, elements in the tail of the list evaluated and passed as arguments to the function named by the head element. For sequences, all elements are evaluated, and the last element is returned.
When evaluating the elements of a list -- which may be members of sequences or function arguments -- each element is type-checked using JavaScript's type system, and a decision is made on what value it represents:
ELEMENT CHECK:
is this element an array?
do the LIST CHECK
is this element a number?
give the number
is this element an object?
does it look like a quote object? ({"'":["foo"]})
give the quoted array (["foo"])
and do the LIST CHECK
else
give the object
is this element a string?
does the string match a defined constant? e.g. PI
give the constant value
else
give the string
Once a function list has been reduced to the point that it contains only numbers, strings, and objects, the tail elements are passed to the named function as arguments.
In the following example, consider the inner function foo:
(bar (foo 1.23 "2" 3 '(4 5 6)))
Under the hood, this might result in the following function being run:
lib.functions['foo'] = function(num1, str, num2, arr, cb) {
// num1 => 1.23
// str => "2"
// num2 => 3
// arr => [4,5,6]
return cb(null, 89.33);
};
Which in turn gives us the following list:
(bar 89.33)
Or, in AST form:
["bar", 89.33]
Then bar is invoked, and so on.
Although library functions can technically return any type of value, only are JSON-serializable values -- i.e., strings, numbers, arrays, (acyclic) objects, null, true, false -- are valid. If you choose to define your own library functions via the DSL builder, stick to this constraint or you may get errors.
In some cases, a library function may choose to return undefined or null. The interpreter treats undefined and null as a signal that a function gave "no meaningful result", and these are removed from the top-level list before the next step in computation is run.
[undefined,1,2,null,"3"] => [1,2,"3"]
Runiq's core library contains a handful of functions that work with types. Assuming you are using the core library and not defining your own, the following are available:
- bool
(bool token)- casttokento boolean - number
(number token)- casttokento number - string
(string token)- casttokento string - list
(list tokens...)- cast arguments to list (array) - hash
(hash tokens...)- cast arguments to hash (object)
Under the hood, these functions do basic JavaScript type checking and casting to produce result values.
I would like to see Runiq do more interesting things with regard to type systems. Ideally, I would like it if the type system for Runiq could be pluggable at both the global and local level.