Molten
An LLVM compiler for an ML-like language (written in Rust)
Install / Use
/learn @transistorfet/MoltenREADME
Molten
Started November 06, 2017
Molten is a programming language which borrows from the ML family of languages, as well as from Rust and Python. The compiler is written in Rust and uses LLVM to generate IR which can be compiled to machine code.
I originally started this project in order to learn Rust. It is intended to be a high level language with a full object system that facilitates both functional and object-oriented programming. Some syntax elements have been changed from typical ML languages to follow conventions found in more common languages, such as C++, Rust, and Python (eg. parenthesis-delimited blocks, conventional class definitions, generics/type parameters with angle brackets, etc). For more info on the internals, see An Overview Of Molten Internals
Installing
You will need rustc and cargo installed. It's recommended that you use
rustup to install these. I've most recently tested it with rustc version
1.52. You will also need LLVM 11 installed, as well as libgc
(Boehm-Demers-Weiser's Garbage Collector), and clang for linking, although clang
can be replace with gcc by editing the molten python script.
On Debian/Ubuntu, run:
sudo apt-get install llvm-11 llvm-11-runtime llvm-11-dev clang libgc-dev
On macOS, run:
brew install llvm@11
You may need to add /usr/local/opt/llvm@11/bin to your path, and you will probably need to install libgc separately
Running
The molten script helps with compiling and linking IR files. To run an example:
./molten run examples/fac.mol
This will run cargo to build the compiler if needed, then compile the fac.mol
file, as well as all of its dependencies (in this case, the libcore.mol
library), link them together using clang, along with libgc, and then run the
binary. It can also compile to LLVM IR, and run LLVM bitcode by using the -S
flag. The resulting .bc file can be run using lli-11.
Example
fn fac(x) {
if x < 1 then
1
else
x * fac(x - 1)
}
println(str(fac(10)))
Types
() // unit type
Nil
Bool
Byte
Char // UCS-4 character
Int
Real
String
(Int, Int) -> Int // function type
'a // universal type variable
Array<Int> // array of integers
(Int, Real) // tuple type
{ a: Int, b: Real } // record type
Declarations
let foo = 0
let bar: String = "Hey"
Functions
fn foo(x, y) => x + y // named inline function
fn foo(x, y) { x + y } // named block function
let foo = fn x, y => x + y // anonymous function
fn foo(x: Int, y) -> Int { x + y } // with optional type annotations
Invoking Functions
Unlike in ML, the brackets of a function call are not elidable. This is a design decision to improve readability of the code and to make the parser simpler and more predictable.
foo(1, 2)
Blocks
A block is a collection of expressions which return the result of the last expression in the block. They can be used in place of a single expression. They do not create their own local scope, at least at the moment, so variables defined inside blocks will appear in the parent scope (usually the function the block is in). Each expression in the block must end in a newline or semi-colon character (or be the last expression in the block). This applies to the top level.
let is_zero = if self.x <= 0 then {
self.x = 0
true
} else {
false
}
Math
Infix operators are evaluated using order of operations. Both sides of an infix
operation must be on the same line, or a \ character can be used to
continue the line.
5 + 12 * 2 // equals 29
42 * 4 \
% 5 // equals 3
And / Or
The keyword operators and and or have side-effects and will not execute the
second expression if the result can be determined from the first expression.
The expressions must have the same type, and the returned value is the first
expression that returns a non-zero value.
let is_cat = true
let result = is_cat and println("It's a cat") == ()
Since is_cat is Bool, both sides of the and must be Bool. Since the
println() function returns Unit, we compare it with itself which will always
be true. The println() will only execute if is_cat is true, and result
will be true if is_cat is true, or false if is_cat is false.
Tuples
let tup = (1, "String", 4.5)
println(tup.1) // prints "String"
Records
Records are like tuples but with named fields. Record literals use the equals sign ("=") to assign a value to a field. Specifying a record type uses a colon (":") to separate the field name from the type.
let rec = { i = 1, s = "String", r = 4.5 }
println(rec.s) // prints "String"
let rec: { i: Int, s: String, r: Real }
Records can be updated, which will copy all fields of the record into a new record, but with some of the fields modified.
let rec = { i = 1, s = "String", r = 4.5 }
let newrec = { rec with s = "Updated" }
println("New Value: " + newrec.s) // prints "Updated"
println("Old Value: " + rec.s) // prints "String"
Arrays
let array1 = [ 1, 3, 6 ]
for x in array1
println(str(x))
let array2 = new Array<String>();
array2.insert(0, "Hello")
println(array2[0])
The Array type is defined in libcore, which must be imported if arrays are used.
Flow Control
The return value of an if expression is the result of evaluating either the
then clause or else clause. The types of both clauses must match. The
else clause can be left out as long as the true clause evaluates to Nil.
if x == 5 then
"It's five"
else
"It's not five"
A match expression allows pattern matching, which can unpack refs, tuples, records, and enums. It can bind values to named variables in the pattern and creates a new scope for each arm of the match expression.
match x with
| 1 => "It's one"
| 5 => "It's five"
| num => "It's not one or five, it's " + str(num)
Values can be unpacked with match as well, including records, tuples, refs, and
enum variants. A underscore _ will match any value, and an identifier (eg.
value) will match anything and bind that value to the name in the process, so
that it can be referenced inside the match arm. Here's an example using a
record:
match { num = 10, name = "Ten" } with
| { num = 10, name = value } => println(value)
| { num = _, name = value } => println("Something else named " + value)
Loops
while true
println("looping")
for i in iter([ 1, 2, 3 ])
println("counting " + i)
For loops take an instance of Iterator<'item> and calls the .next() method
on it, running the body for each Option::Some('item) returned. The iter
function is defined for different types to convert them into an appropriate
iterator. In the case of arrays, it will return the result of new ArrayInterator<'item>(input_array)
Refs
A ref is an indirect reference to some data. It can be passed around as a value, and dereferenced to get or set the data inside of it. The internal value of a reference is always mutable
let r = ref 42
println(str(*r)) // prints 42
*r = 65
println(str(*r)) // prints 65
fn foo(x: ref Int) { } // ref types look similar to ref constructors
let r = ref { a = 42, b = "The Answer" }
println(*r.b) // prints "The Answer"
Classes
class Foo {
// A field with type String
val mut name: String
fn new(self, name) {
self.name = name
}
fn get(self) => self.name
fn static(x) => x * 2
}
class Bar extends Foo {
fn get(self, title) => self.name + " " + title
}
let bar = new Bar("Mischief")
bar.get("The Cat") // returns "Mischief The Cat"
Foo::static(5)
All methods are both closures, and virtual methods, so they can access variables in their parents' scopes and also be overridden by a child class's implementation of the same method, accessible with a reference to the parent class type.
Fields can have an optional type, but not an initializer. If a class has at
least one field, it must have at least one "new" constructor, which must assign
to each field an initial value. The type can be inferred from this assignment
if the optional type is not supplied. If a field is declared as mutable, it can
be reassigned to, but if the mut keyword is absent, the field can only be
assigned to within the constructor, and will be immutable after the constructor
has returned. Every constructor must also call the Super::new method of its
parent class, if it has a parent that has a constructor.
Enums (Tagged Unions)
An enum can either have no arguments, or a tuple of arguments. Constructing an
enum variant requires using the Resolve (::) notation. Pattern matching is
currently the only way to get values out of a variant. Unlike with classes,
which allocate memory for a new instance, enums are immediate data types like
tuples and records. In order to store it in a memory location, a ref must be
used. A ref is required in order to make a recursive enum.
enum Value =
| None
| Integer(Int)
| String(String)
| Pair(String, String)
| Reference(ref Value) // A recursive reference
let val = Value::String("Hey")
match val with
| Value::String(s) => println(s)
| _ => ()
Methods can be added to enums using a methods body. Currently it can only be
used with enums.
methods Value {
fn is_some(val: Value) {
match val with
| Value::None => false
| _ => true
}
}
val.is_some()
Traits and Trait Objects
A trait can be defined with method declarations in the body, with the predefined type alias "Self" used to refer to the current trait object
trait Add {
decl add(Self, Self) -> Self
}
A trait
Related Skills
himalaya
350.8kCLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language).
node-connect
350.8kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
taskflow
350.8kname: taskflow description: Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layer
frontend-design
110.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
