Skip to content

Type System

Matthew Trost edited this page Dec 19, 2015 · 5 revisions

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.

Summary

Runiq's type system is:

  • Pretty much just like JavaScript
  • Unsafe
  • Dynamic
  • Weak

Layers

Runiq's type system operates over four layers:

  • Parsing Layer
  • Interpretation Layer
  • Function Layer
  • Library Layer
Parsing 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.

Interpretation Layer

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
Function Layer

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"]
Library Layer

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) - cast token to boolean
  • number (number token) - cast token to number
  • string (string token) - cast token to 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.

Future

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.

Clone this wiki locally