/quilt

A dynamically typed interpreted scripting language.

Primary LanguageRustMIT LicenseMIT

quilt hero banner

A dynamically typed interpreted scripting language.

the name came from the language originally being created to edit images progamatically

About

Quilt is a hobby project of mine. I designed it to be a simple, with a syntax similar to rust and python. I needed a language that I could use within my own projects that could be easily embedded and extended. I also wanted to learn more about how to build a programming language.

crafting interpreters was a great resource during the development of the backend.

Features

  • Dynamically typed
  • Bytecode interpreter
  • First class functions
  • Closures
  • Extendable native rust builtins
  • Relatively fast (about 1.5x - 2.5x slower than python to calculate fib(30) & fib(40))
  • Python style dissasembler

Roadmap

  • optimizations (ie, loop unrolling, tail call optimization, etc)
  • Spread operator
  • Short circuiting
  • break and continue statements
  • error handling (try /catch)
  • repl

Installation

clone the repo

git clone https://github.com/nathanielfernandes/quilt

install the binary

cargo install --path ./cli

Usage

once installed, you can run the binary with a file path

quilt main.ql

the binary also as a few options, check them out with

quilt --help

Examples

// includes
include "std/math.ql"

// recursion
fn fib(n) {
    if n < 2 {
        return n
    }

    return fib(n - 1) + fib(n - 2)
}

fib(30)


// switch case statements
fn fib(n) {
    switch n {
        case 0, 1 {
            n
        }

        default {
            fib(n - 1) + fib(n - 2)
        }
    }
}

fib(20)

// loops
let i = 0
// assignements are expressions (as well as if statements and loops)
let res = while (i = i + 1) < 10 {
    i * 20
}

// native rust function
@print(res)

// for loops
for i in 0:10 {
    for j in [1, 2, 3, i] {
        @print(i, j)
    }
    @print(i)
}

// destructuring
let a, b = (1, 2)
for a, b, c in [[1, 2, 3], [4, 5, 6], [7, 8, 9]] {
    @print(a, b, c)
}

// closures
fn NewCounter() {
    let count = 0

    fn Increment() {
        count += 1
    }

    fn Decrement() {
        count -= 1
    }

    // lambdas
    let Get = fn() { count }

    // implicit return
    [Increment, Decrement, Get]
}

let incr, decr, get = NewCounter()

Complete errors, with tracebacks: disassembler

Disassembler Output

fn fib(n) {
    if n < 2 {
        n
    } else {
        fib(n - 1) + fib(n - 2)
    }
}

fib(30)

disassembler

Builtin Functions

Builtins can be created with an easy to use macro. The macro will generate the callable functions and put them all into the same module. The module can then be added to the vm.

generate_builtins! {
    [export=math] // builtin module name

    ///Compute the sqrt of a number.
    fn @sqrt(a: num) -> float {
        Value::Float(a.sqrt())
    }

    ///Get a random number between 0 and 1.
    fn @random() -> float {
        Value::Float(rand::random())
    }

    ///Get a random integer between two values.
    fn @randint(start: int, end: int) -> int {
        if start >= end {
            Err(error("start must be less than end"))?
        }

        Value::Int(rand::thread_rng().gen_range(start..end))
    }

    ///Get a random float between two values.
    fn @randfloat(start: double, end: double) -> float {
        if start >= end {
            Err(error("start must be less than end"))?
        }

        Value::Float(rand::thread_rng().gen_range(start..end))
    }

    ///Compute the luminance of a color.
    fn @luma(c: color) -> float {
        Value::Float(c[0] as f64 * 0.2126 + c[1] as f64  * 0.7152 + c[2] as f64  * 0.0722)
    }

     ///Truncate a string to a given length and add a custom ellipsis.
    fn @truncate(s: str, len: int, ?ellipsis: str = String::from("...")) -> str {
        ...
    }
}

// custom mutable state
let state: i32 = 0;
let ops = VmOptions::default();

let mut vm: VM<i32> = VM::new(state, script, ops);

vm.add_builtins(math);

ByteCode

Instructions u8 Args Description
Halt Stops the program
Return Pops the top value from the stack, returns from the current function, and pushes the value back to the stack
Pop Pops the top value from the stack
PopMany n: u16 Pops the top n values from the stack
Swap Swaps the top two values on the stack
SwapPop Swaps the top two values on the stack, then pops the top value
LoadConst idx: u16 Pushes the constant at idx (in the constant pool) to the stack
LoadLocal offset: u16 Pushes the local variable at offset (on the stack) to the stack
SetLocal offset: u16 Peeks the top value from the stack, and sets the local variable at offset to the value
DefineGlobal id: u16 Pops the top value from the stack, and defines a global variable with the value
LoadUpvalue offset: u16 Pushes the upvalue at offset to the stack
SetUpvalue offset: u16 Peeks the top value from the stack, and sets the upvalue at offset to the value
LoadGlobal id: u16 Pushes the global variable with the id id to the stack
SetGlobal id: u16 Peeks the top value from the stack, and sets the global variable with the id id to the value
CloseUpvalue offset: u16 Closes the upvalue at offset
CreateFunction idx: u16 Creates a function from the constant at idx and pushes it to the stack
CreateClosure idx: u16, repeats(offset: u16, is_local: u8) for n captured variables Creates a closure from the function at idx and pushes it to the stack
CreatePair Pops the top two values from the stack, and pushes a pair of the values to the stack
CreateArray n: u8 Pops the top n values from the stack, and pushes an array of the values to the stack
CreateRange Pops the top two values from the stack, and pushes a range of the values to the stack
CallFunction n: u8 Pops the top n values from the stack, and calls the function at the top of the stack with the values
CallBuiltin id: u16, n: u8 Pops the top n values from the stack, and calls the builtin function with the id id with the values
EnterContext idx: u16, n_args: u8 Creates a new context with builtin idx, and n_args arguments, and pushes it to the stack
ExitContext Pops the top context from the stack
BlockResult Pops the top value from the stack, and pushes it to the parent context
BlockReturn Pops the return value from the parent context, and pushes it to the stack
JumpIfFalse offset: u16 Pops the top value from the stack, and jumps to offset if the value is false
JumpForward offset: u16 Jumps forward offset instructions
JumpBackward offset: u16 Jumps backward offset instructions
Unpack n: u8 Pops the top value from the stack, and unpacks it into n values, pushing them to the stack
LoadNone Pushes None to the stack
LoadNoneMany n: u8 Pushes None to the stack n times
NewLoopCtx Creates a new loop context, and pushes it to the stack
IterNext end: u16, loopctx: u16 uses the iterable at the top of the stack, and pushes the next value to the stack, or jumps to end if the iterable is exhausted
UnaryNegate Pops the top value from the stack, and pushes the negated value to the stack
UnaryNot Pops the top value from the stack, and pushes the boolean negated value to the stack
UnarySpread Pops the top value from the stack, and pushes the spread value to the stack
BinaryAdd Pops the top two values from the stack, and pushes the sum to the stack
BinarySubtract Pops the top two values from the stack, and pushes the difference to the stack
BinaryMultiply Pops the top two values from the stack, and pushes the product to the stack
BinaryDivide Pops the top two values from the stack, and pushes the quotient to the stack
BinaryModulo Pops the top two values from the stack, and pushes the remainder to the stack
BinaryPower Pops the top two values from the stack, and pushes the power to the stack
BinaryEqual Pops the top two values from the stack, and pushes the equality to the stack
BinaryNotEqual Pops the top two values from the stack, and pushes the inequality to the stack
BinaryGreater Pops the top two values from the stack, and pushes the greater than to the stack
BinaryGreaterEqual Pops the top two values from the stack, and pushes the greater than or equal to to the stack
BinaryLess Pops the top two values from the stack, and pushes the less than to the stack
BinaryLessEqual Pops the top two values from the stack, and pushes the less than or equal to to the stack
BinaryAnd Pops the top two values from the stack, and pushes the boolean and to the stack
BinaryOr Pops the top two values from the stack, and pushes the boolean or to the stack
BinaryJoin Pops the top two values from the stack, and pushes the joined value to the stack