- Odin 99.9%
- Makefile 0.1%
I fixed the bug by tracking strings used as keys in a separate map in the Object struct. I know this is not a very performant or memory efficient solution. I considered doing string interning, but that seems too annoying to implement for now. This is something I can consider in the future. |
||
|---|---|---|
| src | ||
| tests | ||
| .gitignore | ||
| Makefile | ||
| README.md | ||
Chroma Interpreter
Chroma is an interpreted, procedural, garbage-collected scripting language.
It features a fully inferred static type system inspired by OCaml. The implementation is based on the Hindley-Minler type system, extended and modified to support a procedural code style, and additional data structures such as tagged unions.
This is a toy project made for learning and exploration purposes. The implementation is still work in progress. There are many bugs and missing features.
The implementation is loosely based on the wonderful book "Writing An Interpreter In Go" by Thorsten Ball.
Overview
Basic Data Types
There are only four basic data types.
1234 // integer
12.4 // float
"23" // string
true // boolean
false // boolean
Comments
Chroma only supports single line comments using the // prefix
// A comment
my_int := 1 // Another comment
Operators
Operators combine operands into expressions. For binary operations, operand types must be identical.
// numeric operators work for integer, and float types
1 + 1 - 1 * -1 / 1
// boolean operators
!true
true != false
true == true
1 < 2
1 > 2
// string operators
"Hello, " + "World!"
Variables
Variables are created using the := operator, and can be updated using the =
operator. A variable always has strictly a single data type.
my_value := 1
my_value = 2
my_value = "string" // Error!
Variables can be shadowed to change the data type.
myValue := 1
myValue := "string"
myValue := process(myValue)
If Statements
If statement conditions do not require parentheses ( ), but do require
braces { }.
if x > 0 {
print("x is positive")
}
// optional else statements
if x > 0 {
print("x is positive")
} else if x == 0 {
print("x is zero")
} else {
print("x is negative")
}
Arrays
Arrays are created using the [] syntax. An array can only hold a single
data type, it is not possible to mix types, for that see named types.
Any array element can be retrieved or updated using the x[i] syntax.
my_array := [1, 2, 3]
print(my_array[0]) // prints: 1
my_array[0] = 10
print(my_array[0]) // prints: 10
To add a value to an array, the builtin append function can be used.
my_array := []
append(my_array, 1)
print(my_array) // prints: [1]
Objects
Objects are created using the {} syntax. It is a container of key to value pairs.
All values in an object must have the same data type, it is not possible to mix
types, for that see named types. The same rule applies to keys.
Any object value can be retrieved and updated using the x[key] syntax.
my_object := { "a" = 1, "b" = 2 }
print(my_object["a"]) // prints: 1
my_object["a"] = 2
my_object["c"] = 3
print(my_object) // prints: { "a" = 2, "b" = 2, "c" = 3 }
For Loops
// Traditional for loops
for i := 0; i < 10; i = i + 1 {}
for ;; {} // infinite loop
// while loops
for x > 0 {}
The condition in a for loop is optional.
// For loop with no condition is an infinite loop
for {
// Break and continue statements can be used to control loops
if condition {
break
}
continue
}
To iterate over arrays, objects, or strings, the for .. in syntax can be used.
for key, value in my_object {}
for element, index in my_array {}
for character, index in my_string {}
// The second value is optional
for key in my_object {}
for element in my_array {}
for character in my_string {}
Functions
Functions are declared using the fun keyword. Values are returned using the
return keyword.
fun add_one(x) {
return x + 1
}
Function parameters do not require type declarations. The type signature of the function is automatically inferred from code.
print(add_one) // prints: fun(int) -> int
Named Types
A named type in Chroma is a tagged union.
Every named type consists of one or more variants, also called constructors. Each variant can be directly invoked in code without having to specify the used named type.
The simplest form of a named type is an enumeration.
type Direction {
North,
East,
South,
West,
}
dir := North
Each variant in a named type can optionally hold a value.
type Color {
Red,
Green,
Blue,
Rgb([int])
}
red := Red
rgb := Rgb([255, 255, 255])
Named types can hold polymorphic parameters.
The simplest example is the builtin Option type.
type Option(T) {
None,
#unwrap Some(T),
}
missing := None
value := Some("Hello, World!")
The #unwrap directive allows a variant's inner value to be accessed directly using the .? suffix operator. This extracts the value without needing a switch statement.
variant := Some(42)
print(value) // Option.Some(42)
value := variant.?
print(value) // prints: 42
Switch Statements
A switch statement can be used to match a value.
switch x {
case 1:
case 2:
case foo(): // the foo() only get's called when the first two cases fail
}
A switch statement can also be used to match a named type.
switch optional {
case None:
case Some(v):
print(v) // prints innner value of Some(T)
}
Interfaces
An interface, also called a constraint, is used to specify that a type can be used in specific situations.
Interfaces are used when the type inference system cannot determine the exact type of a value.
fun add(x, y) { return x + y }
print(add) // prints: fun('a, 'a) -> 'a where 'a: addable
// The addable interface is implemented by integers, floats, and strings
add(1, 2)
add(1.2, 2.3)
add("Hello, ", "World!")
// But not by other data types
add([1], [2]) // Error!
add({1=1}, {2=2}) // Error!
add(None, Some(1)) // Error!
The possible interfaces are:
iterable- Allows type to be iterated in a range loop, and indexed. Supported by types string, array, map.addable- Allows type to use the+operator. Supported by types string, int, float.numeric- Allows type to use the+,-,*, and/operators. Supported by types int, float.equal- Allows type to use the==and!=operators. Supported by types int, float, string, boolean.comparable- Allows type to use the==,!=,<, and>operators. Supported by types int, float.
Imports
Code from other files, called modules, can be imported using the import keyword.
import "./another_file.chroma"
// .chroma can be omitted
import "./another_file"
An imported module is stored in an object with the same name as the file.
To change that name, the import .. as .. syntax can be used.
import "./another_file"
another_file.someFunction()
// imported file can be renamed
import "./another_file" as lib
lib.someFunction()
Builtins
Chroma has a few builtin functions and types.
Functions:
print :: fun('a)- prints a single value.append :: fun(['a], 'a)- appends a single value to an array.len :: fun('a) -> int where 'a: iterable- returns the length of an array, object, or string.
Types:
Option(T)with variantsNone, andSome(T). Option is often simplified toT?by the type system.