Loom is an object-oriented systems programming language that combines the control, performance, and portability of low-level languages with the developer experience of high-level languages.
module MyModule
{
// Classes are reference types that are allocated on the heap and are interacted with via the Loom smart pointer memory model
export class MyClass
{
// Properties are wrappers for fields that run custom logic when getting or setting.
public i32 Value { get; set; }
public MyClass(i32 value)
{
Value = value;
}
}
// Structs are value types that are allocated on the stack and reconstructed between scopes
export struct MyStruct
{
public i32 Value { get; set; }
}
// Returning reference type + no pointer type = error!
export MyClass ReturnImplicitOwnedRefType()
{
MyClass obj = new MyClass(); // No pointer type + reference type = error!
return obj;
}
export MyStruct ReturnStackAllocatedValueType()
{
MyStruct obj = new MyStruct(); // No pointer type + value type = stack alloc
return obj;
}
export unique MyClass ReturnUniqueRefType()
{
unique MyClass obj = new MyClass(); // Unique pointer type = heap alloc
return obj;
}
// Ref = Read from unique pointer without taking ownership
export i32 BorrowUniqueRef(ref MyClass obj)
{
return obj.Value;
}
// Ref Mut = Read/Write from unique pointer without taking ownership
export i32 BorrowMutableUniqueRef(ref mut MyClass obj)
{
obj.Value = 100;
return obj.Value;
}
unsafe
{
export raw MyClass ReturnRaw()
{
raw MyClass obj = new MyClass();
return obj;
}
}
}
The Loom Compiler works via a Linear Pipeline system where each step in the pipeline mutates a compilation context using data provided by the previous steps.
The first step is the Parser which takes raw source code, tokenizes it, and converts it into an in-memory Abstract Syntax Tree (AST). The Loom parser uses modular 'Rules' to determine how to parse tokens and syntax.
publicrecordReturnStatementNode(ExpressionNode?Expression=null):StatementNode{/// <inheritdoc/>publicoverrideIEnumerable<ASTNode>Children=>Expressionis not null?new[]{Expression}:Enumerable.Empty<ASTNode>();}[ParserRule]publicclassReturnStatementRule:ParserRule<ReturnStatementNode>{/// <inheritdoc/>publicReturnStatementRule(LoomParserparser):base(parser){}/// <inheritdoc/>publicoverrideReturnStatementNodeParseNode(){Parser.Reader.Expect(TokenType.Return);// returnif(Parser.Reader.Check(TokenType.Semicolon))returnnewReturnStatementNode();returnnewReturnStatementNode(RunRule<ExpressionRule,ExpressionNode>());}}The analyzer walks the AST and maps its nodes to Symbols, which serve two purposes: enforcing language rules through semantic analysis, and driving LIR compilation in the next step.
/// <summary>/// Checks for imports that do not exist./// </summary>[LoomAnalyzer]publicclassUnresolvedImportAnalyzer:Analyzer{publicstaticDiagnosticUnresolvedImportDiagnostic=new(Diagnostic.DiagnosticLevel.Error,"The module '{0}' could not be resolved.");[Visitor]publicvoidVisit(ImportNodenode){ModuleSymbol?symbol=Context.GetSymbol(node.ModuleName).SymbolasModuleSymbol;if(symbol==null)Context.DiagnosticContext?.Report(UnresolvedImportDiagnostic,node.ModuleName.BaseName);}}Loom then compiles its AST higher representation down to a linear Intermediate Representation called LIR. This lowered form makes cross-target compilation very easy, the same LIR can target both LLVM and .NET.
[Name: Test]
define Consumer::Main() -> i32 {
entry:
%0 = alloca i32
%1 = call i32 @Consumer::Add, 2, 2
store i32 %1, %0
%2 = load i32, %0
return %2
}
define Consumer::Add(i32 %a, i32 %b) -> i32 {
entry:
%0 = alloca i32
store i32 %a, %0
%1 = alloca i32
store i32 %b, %1
%2 = load i32, %0
%3 = load i32, %1
%4 = add i32 %2, %3
return %4
}
The constructed LIR is then passed into a Loom.CodeGen.* project. The default implementation uses LLVM to support extreme portability and code optimisation.