Esta página aún no está traducida al español. Leer la versión en inglés

Larva HIR

The language-neutral program model every frontend writes - assemblies, types, members, generics, typed method bodies, its text form, the walker and the HIR linker.

Última actualización: 20 min de lectura Editar en GitHub

En esta página

Larva HIR (high-level intermediate representation) is the model of a program that every language on Larva produces. A frontend reads its own syntax and writes HIR; from there on, everything is Larva's: linking, lowering to the IR, the back ends, reflection, the catalog the editor and larva show use, and the .lva files libraries ship as.

The model lives in the Larva.Hir project: HirAssembly (assemblies, types, members, constants), HirTypes (types and tokens), HirBody (statements, expressions, patterns), HirWalker, Text/HirPrinter, Encoding/ (the binary format) and Linking/HirLinker. It depends on nothing else in Larva.

Why HIR exists

Before HIR, the Antril frontend did almost everything: classes, interfaces, inheritance, closures, async, reflection, boxing, enums, strings and projects, all lowered straight to Larva's low-level IR (structs, gc handles, basic blocks). Libraries were referenced by reparsing the text of their declarations. The design document lists the principles of the rearchitecture that replaced this:

  • HIR is the truth of the program. Types, members, generics, attributes and typed bodies, with full metadata. Every frontend, tool and library speaks HIR.
  • The frontend does not decide run-time semantics. It resolves names, overloads, types and inference, and emits HIR that is already typed. How a virtual method dispatches, how a closure is built, how an async method becomes a state machine: Larva decides, once, for every language.
  • Libraries are referenced by their HIR metadata, never by reparsing declarations. A library's declarations are never rewritten as source text.
  • Everything native is extern. The runtime exposes services (memory, GC, threads, files, network, time) as extern functions with stable names; the base library is written in a language on top of them.
  • Ahead of time first. No JIT: generics are specialized, reflection is generated from metadata.
  • Tools belong to Larva. Build, packages, the language server and the object formats serve any frontend; a language brings only its syntax.

The level of HIR follows from those principles. It is above IR (it still has classes, interfaces, virtual calls, lambdas, try, patterns, generics) so that Larva lowers them with full context, and it is below source (names are resolved, every expression knows its type, overloads are chosen, operators of user types are calls of their methods) so that Larva never has to understand a language.

The model at a glance

HirAssembly ── Name, Version, EntryPoint (MemberToken?)
 ├─ References : AssemblyRef(Name, Version[])           other assemblies it uses
 ├─ TypeRefs   : TypeRef(Assembly, Namespace, Name, Arity)      their types, by name
 ├─ MemberRefs : MemberRef(Kind, Parent, Name, GenericArity, Parameters, Result)
 ├─ Types      : TypeDef ──┬─ Namespace, Name, Kind, Flags, Access, DeclaringType
 │                         ├─ Generics : GenericParam (constraints)
 │                         ├─ Base, Interfaces, Underlying
 │                         ├─ Fields     : FieldDef(Name, Type, Flags, Access, Value?)
 │                         ├─ Methods    : MethodDef(Name, Kind, Flags, Parameters, Result, Body?)
 │                         ├─ Properties : PropertyDef(Getter, Setter = method indices)
 │                         ├─ Events     : EventDef(Adder, Remover)
 │                         ├─ Operators  : OperatorDef(Symbol, Fixity, Precedence, Method)
 │                         └─ Attributes, Location, Doc
 ├─ Attributes : HirAttribute(Constructor, Arguments, Named)
 ├─ Resources  : HirResource(Name, Data)                linked into the program
 ├─ LanguageData : HirResource(Name, Data)              kept for the language, never linked
 └─ Files      : source paths that SourceLocation.File indexes

Tokens: how HIR names things

Inside an assembly, types and members are named by small integer tokens, not by strings:

Token Value Printed Meaning
TypeToken.Def(i) i (≥ 0) T3 Types[i] of this assembly.
TypeToken.Ref(i) ~i (< 0) TR3 TypeRefs[i]: a type of another assembly, by namespace, name and arity.
MemberToken.Def(t, m) (t, m) T3.1 Member m of Types[t] (field, method, property or event, by context).
MemberToken.Ref(i) (-1, i) MR4 MemberRefs[i]: a member of another assembly's type, by name and signature.

A reference names its target the way a person would: a TypeRef by assembly, namespace, name and generic arity; a MemberRef by its kind, parent type, name, generic arity, parameter types and result type (the signature tells overloads apart). HirAssembly.Reference creates each reference once and reuses it. When assemblies are loaded together, references are resolved by those names; the HIR linker turns them into definitions.

Types

HirType is a small closed hierarchy, compared structurally (two NamedTypes with the same token and arguments are equal):

HIR type Example (HIR text) Meaning
PrimitiveType i32, f64, bool, char, void The built-in values: bool, char (a Unicode scalar, 32 bits), i8…i256, u8…u256, f16…f256, void.
NamedType [System.Core]System.String<u8> A class, struct, interface, enum or named func type, with type arguments.
ArrayType i32[], f64[,] Rank 1 to 8; jagged arrays are arrays of arrays.
InlineArrayType fixed u8[4], fixed u8[sizeof(T)] Elements stored inline, in a struct or a field.
NullableType T?
ReferenceType &T, &mut T A borrow, checked by the borrow checker.
PointerType T*, T const* A raw pointer (unsafe code).
FuncType func<i32> : bool A function value: a closure, a bound method, a static function.
NativeFuncType func*<void*, void*> : i32 The address of a C function.
GenericParamType !0 (type's), !!0 (method's) A type parameter by owner and index; a nested type's parameters come after its outer types'.
PackExpansionType T... A variadic parameter's pack.

str and any are not primitives: they are classes of the core library (System.String<u8> and System.Any), named like any other type. Lowering finds the ones it needs by their full names. HirTypes.Substitute replaces type parameters with arguments; IsGeneric says whether a type mentions one.

Type definitions

TypeDef part Values
Kind Class, Struct, Interface, Enum, Bitflags, Func
Flags Abstract, Final, Static, Record, Unsafe, Generated (closures and other types a frontend made: hidden from reflection and completion), Primitive (the value type that gives a primitive its members, as Int32 does for i32), Extension (an ext(…) block whose static methods extend Underlying)
Access Public, Internal, Friendly, Inherited, Private, File
DeclaringType The index of the enclosing type, or -1.
Base, Interfaces The base class (null for the root) and the interfaces it declares.
Underlying An enum's integer type; a primitive's type for Primitive; the extended type for Extension.

A namespace has no entity of its own: it is a prefix of type names. The free functions and fields of a namespace live in a class named $Module (a program's Main is App.$Module.Main).

Members

Member Holds Flags
FieldDef name, type, access, constant Value, attributes, location, doc Static, Readonly, Const (no storage), ThreadStatic, Generated (the storage of get; set;), InPlace (a fixed T[] array kept inside the object)
MethodDef name, Kind, generics, parameters, result, Body, ExternName, Library, attributes, location, doc Static, Virtual, Override, Abstract, Final, Async, Iterator, Unsafe, Extern (a runtime service), Import (a C function from Library), Export, Generated, Implicit (conversions), Inline, NoInline, Cold
PropertyDef type, Indices (an indexer), Getter and Setter (indices into Methods, -1 for none) Static, Virtual, Override, Abstract
EventDef type, Adder, Remover (method indices) same as properties
OperatorDef symbol, Fixity (prefix, postfix, binary), precedence, associativity, implementing method

MethodKind is Normal, Constructor (named .new), StaticConstructor, Finalizer, Getter, Setter, Adder, Remover, Operator or Conversion. Accessors are ordinary methods of the type; a property only points at them. Operator definitions travel in the metadata so that a frontend can parse code that uses another library's own operators.

Parameter has a name, a type, flags (Params: the last parameter takes any number of arguments as an array; Optional) and a Default constant.

Generics and constraints

Types and methods carry a list of GenericParam. A parameter states everything an argument must satisfy, so that lowering can specialize generic code without asking the frontend:

GenericParam field Constraint Antril
Kinds Value, Plain, Copy, Unmanaged, Managed, FixedSize, Numeric, Integer, SignedInteger, UnsignedInteger, Float, New, Enum, Bitflags, Object T : value, T : copy, T : new()…
Types A base class and interfaces the argument must have. T : IComparable<T>, T : number (INumber)
OneOf A closed list: the argument is one of these. T : [u8, u16, u32]
Operators Operators the argument supports, as symbol(operands) : result. operator constraints
ConvertsTo, ConvertsFrom Conversions the argument has, in each direction. promised by casts such as (T)n
Constructors Exact constructor signatures. T : new(i32, str)
FixedSize The argument's size in bytes. T : fixed(8)
Variadic, Default A pack (T...), a default argument.

Attributes, resources and language data

  • Attributes (HirAttribute) are a constructor token, positional constant arguments and named ones. They can sit on the assembly, types, fields, methods, parameters, properties and events.
  • Constants (HirConstant) appear as field values, parameter defaults, attribute arguments, literals and patterns: integers of any width (BigInteger plus the primitive), floats as their IEEE bits, bool, char, strings, null, types, enum members (with their text for an enum based on str) and arrays.
  • Resources (HirResource: name and bytes) are files the program carries. The linker concatenates them into the program, and a body reads one by name with ResourceExpr (Antril's $resource("name")).
  • Language data is also name and bytes, but it is the language's own: what it keeps next to a library for its own compiler, read back when a program references the library and never linked into a program. Antril stores there, as antril.template:<file>, the text of each variadic template (such as its tuple types), which a program expands for the sizes it uses.
  • Debug information: Files lists source paths and SourceLocation(File, Line, Column) places types, members and statements. Documentation comments are kept in Doc.

Bodies

A method body (HirBody) is its locals and a block. It is a structured, fully typed tree, at the level of a bound syntax tree, not of a stack machine: it keeps loops, try, switch and patterns so that Larva lowers them with all the context. Every expression carries its type, and every call points at the exact member already chosen.

Parameters are not locals: ParamExpr(i) reads parameter i. A lambda's parameters and the locals it declares are locals of the enclosing body; the lambda (LambdaExpr) lists them, and lowering finds what it captures.

Statements
Structure BlockStmt, LocalStmt (declares a local, with its first value), ExprStmt
Control IfStmt, WhileStmt, DoWhileStmt, ForStmt, ForeachStmt (arrays and ranges directly, anything else through the core enumerable contract), SwitchStmt (sections of patterns and guards), BreakStmt, ContinueStmt (loops carry a label, -1 for none), ReturnStmt
Errors and cleanup ThrowStmt (null rethrows), TryStmt with CatchClauses (type, local, filter) and finally, UsingStmt
Memory DeleteStmt (free now), RegionStmt (an arena), UnsafeStmt
Other YieldStmt (iterators), CheckedStmt (overflow checked or wrapping)
Expressions
Values LiteralExpr, LocalExpr, ParamExpr, ThisExpr, DefaultExpr, TypeOfExpr, SizeOfExpr, ResourceExpr
Members FieldExpr (with the instantiation for a static field of a generic type), PropertyExpr, ElementExpr, ArrayLengthExpr
Calls CallExpr (static, instance, virtual, interface; NonVirtual for base.M()), InvokeExpr (a function value), MethodRefExpr, LambdaExpr, AwaitExpr
Creation NewExpr (constructor, arguments, member initializers), NewArrayExpr, ArrayLiteralExpr, WithExpr (a record copy)
Operators UnaryExpr, BinaryExpr (built-in operators on primitives only; And/Or short-circuit), AssignExpr (optionally compound)
Conversions ConvertExpr with a ConversionKind: Numeric, Upcast, Downcast, Box, Unbox, User (with its method), Wrap/Unwrap (nullable), Pointer, Enum, Function
Tests and choices IsExpr (with a pattern), AsExpr, SwitchExpr, ConditionalExpr, CoalesceExpr, ConditionalAccessExpr with its ReceiverExpr
Text and ranges InterpolatedExpr, RangeExpr
Unsafe and borrows BorrowExpr (&x, &mut x), AddressOfExpr, DerefExpr
Sugar BlockExpr (statements, then a value: what frontends expand sugar into), CheckedExpr

Patterns: DiscardPattern, ConstantPattern, RelationalPattern, RangePattern, TypePattern, BindingPattern, AndPattern, OrPattern, NotPattern, PropertyPattern, PositionalPattern (through a deconstructor), ListPattern (with a slice).

What stays in the frontend: pure syntax. Sections such as public:, into namespace, counting loops, reactive and effect are expanded by the frontend into ordinary HIR or library calls. For example, Antril writes loop (Point p in points) over an array as a ForStmt over an index, as the printed body below shows.

Method bodies are decoded lazily: MethodDef.Body reads the body from the file the first time it is asked for (BodyPending says whether it still has to). Referencing a large library costs only its metadata.

The text form

HirPrinter (Larva.Hir/Text/HirPrinter) writes an assembly as text, for people and for tests. With types: true every expression also shows its type (x:i32), so two assemblies print the same exactly when they are the same. HirAssembly.ToString() returns this text; there is no larva command for the full form yet (see larva show --lang hir for the declarations).

Take this library:

Antril
into namespace Tiny
{
	public class Calc
	{
		public:
			static Twice(i32 n) : i32 => n * 2;
	}
}

Compiled with larva lib -o Tiny.lva Tiny.ant, its HIR prints as follows (211 typeref lines cut: the Antril frontend references every type of System.Core it may need by name, and the .lva keeps those references):

assembly Tiny v1.0
reference System.Core
typeref TR0 = [System.Core]System.Type`0
typeref TR1 = [System.Core]System.String`1
typeref TR2 = [System.Core]System.Any`0
…
file 0 "docs-larva-hir/Tiny.ant"

public class Tiny.Calc : [System.Core]System.Any @0:3:15
{
    #0 public static Twice(i32 n) : i32 @0:6:11
    {
        return (%0 * 2i32) @0:6:33
    }
}

With types on every expression, the body line is return (%0:i32 * 2i32:i32):i32 @0:6:33. The file name is the folder and the file, not the full path: a library names its files that way because it leaves the machine it was built on.

A larger example, with a struct, a constructor, a call into System.Core and a loop:

Antril
import System;

into namespace Geometry
{
	/// A point on a plane.
	public value Point
	{
		public:
			f64 X;
			f64 Y;

			new(f64 x, f64 y)
			{
				X = x;
				Y = y;
			}

			/// How far the point is from the origin.
			Length() : f64 => Math.Sqrt(X * X + Y * Y);
	}

	public class Shapes
	{
		public:
			static Farthest(Point[] points) : Point
			{
				Point best = points[0];
				loop (Point p in points)
				{
					if (p.Length() > best.Length())
						best = p;
				}
				return best;
			}
	}
}
assembly Geometry v1.0
reference System.Core
…
memberref MR0 = method [System.Core]System.Math::Sqrt`1(!!0) : !!0
file 0 "docs-larva-hir/Geometry.ant"

public struct Geometry.Point @0:6:15
{
    field public f64 X
    field public f64 Y
    #0 constructor public .new(f64 x, f64 y) : void @0:12:4
    {
        (this.Geometry.Point::X = %0) @0:14:5
        (this.Geometry.Point::Y = %1) @0:15:5
    }
    #1 public Length() : f64 @0:19:4
    {
        return [System.Core]System.Math::Sqrt~0<f64>(((this.Geometry.Point::X * this.Geometry.Point::X) + (this.Geometry.Point::Y * this.Geometry.Point::Y))) @0:19:22
    }
}

public class Geometry.Shapes : [System.Core]System.Any @0:22:15
{
    #0 public static Farthest(Geometry.Point[] points) : Geometry.Point @0:25:11
    {
        local $0 mut Geometry.Point best
        local $1 mut Geometry.Point[] $items1
        local $2 mut i32 $index2
        local $3 mut Geometry.Point p
        let $0 = %0[0i32] @0:27:5
        for @0:28:5
            let $1 = %0
            let $2 = 0i32
        cond ($2 < len $1); step ($2++)
        {
            let $3 = $1[$2]
            { @0:29:5
                if ($3.Geometry.Point::Length#1() > $0.Geometry.Point::Length#1()) @0:30:6
                    ($0 = $3) @0:31:7
            }
        }
        return $0 @0:33:5
    }
}

Things to notice:

  • Math.Sqrt is generic (Sqrt<T>(T) : T), so the call names the reference MR0 with the type argument <f64>: the frontend inferred it, lowering will specialize it.
  • p.Length() is resolved to method #1 of Point (Length#1); overloads share a name, so the printer adds the index.
  • The loop became a for over a hidden array local and index ($items1, $index2): that is the frontend's sugar, expanded before HIR.
  • A value type's constructor is a constructor method named .new whose this is the value being built.

Notation

Text Meaning
assembly N v1.0, reference R The assembly and the assemblies it references.
typeref TRi = [Asm]Ns.Namen| Type referencei`, with its arity.
memberref MRi = kind [Asm]Type::Nameg(params) : result| Member referencei` with its generic arity and signature.
file i "path", @f:l:c Source files, and a location (file, line, column).
#i Method index within its type; get #1 set #-1 on a property.
%i Parameter i.
$i Local i (local $i mut T name declares it; let $i = … is its LocalStmt).
!i, !!i Type parameter i of the type, of the method.
Type::Member, Type::Method#i, Type::Name~i A member by name; a method with its index; a member reference.
2i32, 0x…f64, Store.Color(1) Constants: integers with their type, floats as bit patterns, enum members.
numeric<T>(e), upcast<T>(e), enum<i32>(e)… A ConvertExpr and its kind.
invoke f(args), &Type::M, lambda($1) … Calling a function value, a method as a value, a lambda.
len a, (a ?-> b), <receiver> Array length, conditional access and its receiver.

A third example shows an enum, a property, a generic class with a constraint and a function value. Note how Antril's T : number became an interface constraint INumber<!0>, and how the enum got a synthesized ToString:

public enum Store.Color : i32 @0:5:14
{
    field public static const Store.Color Red = Store.Color(0)
    field public static const Store.Color Green = Store.Color(1)
    field public static const Store.Color Blue = Store.Color(2)
    #0 public ToString() : [System.Core]System.String<u8> @0:5:14
    {
        local $0 mut Store.Color $enum0
        return this switch { Store.Color(0) => "Red"; Store.Color(1) => "Green"; Store.Color(2) => "Blue"; $0 => enum<i32>($0).[System.Core]System.Int32::ToString~0() }
    }
}

public class Store.Counter<T : [System.Core]System.INumber<!0>> : [System.Core]System.Any @0:7:15
{
    field private !0 total
    property public !0 Total get #1 set #-1
    #0 constructor public .new() : void @0:13:4
    {
        (this.Store.Counter::total = [System.Core]System.INumber::get_Zero~1<!0>()) @0:15:5
    }
    #1 getter public get_Total() : !0 @0:18:14
    {
        return this.Store.Counter::total @0:18:21
    }
    …
}

larva show --lang hir

larva show --lang hir prints a type through the HIR view of the catalog: declarations only, with every type fully qualified, in any language's library:

Terminal
larva show --lang hir Geometry.Point Geometry.ant
// A point on a plane.
public struct Geometry.Point
{
    field X: f64
    field Y: f64
    constructor Point(x: f64, y: f64)
    // How far the point is from the origin.
    method Length() -> f64
}

This view is described in The catalog and language views.

HirWalker

HirWalker visits every statement, expression and pattern of a body, a parent before its children. A subclass overrides Visit(Stmt?), Visit(Expr?) or Visit(Pattern?), looks at the node, and calls the base method to go on into the children (or does not, to stop there). Lambda bodies are visited as part of the expression that holds them.

Larva's own passes use it where they need facts about whole bodies before lowering:

Walker Where What it collects
Reachable Larva.Hir.Lowering/StaticInit The types a program can use from its entry and its exported functions (calls, fields, objects made, function values, instance methods of types in use): only those types get their static constructors run.
UsedTypes StaticInit The types a method's body uses, following the methods it calls (each once, up to 4096).
ReflectionUse Larva.Hir.Lowering/Reflection Whether some body says typeof or calls GetType(), and whether one asks for members (GetFields, GetMethod…): a program that asks for none gets no member tables.

Frontends use it too: the Antril binder walks bodies to find uses of in-place arrays (InPlaceUses).

Linking HIR assemblies

A program is compiled against libraries (System.Core, a project's .dll dependencies, bindings) that are separate HIR assemblies. Before lowering, HirLinker.Link(name, inputs) (Larva.Hir/Linking/HirLinker) merges them into one assembly with no references left, so lowering sees the whole program. larva build links [System.Core, external domains…, libraries…, program]:

Terminal
larva build --timings --backend x64 -o app.exe App.ant Tiny.dll
app.exe (x64 back end, entry app::App.$Module.Main)
timings: System.Core (compiled library) 45 ms, Tiny.dll 4 ms, frontend 1146 ms, HIR link 55 ms, lowering 640 ms, IR checks 366 ms, IR link 118 ms, native (x64) 1410 ms; total 3805 ms

How it works:

inputs (.lva bytes, any order)
   │  pass 1: read each one's metadata ("its own view": references unresolved)
   ▼
order: each assembly after those it references (depth-first; a cycle is an error)
   │  number every type: offset[a] = types of the assemblies before it
   │  byName[(assembly, namespace, name, arity)] = merged index
   ▼
   │  pass 2, per assembly in order:
   │    MapType:   T5  -> T(offset + 5)        TR2 -> byName[[System.Core]System.Any]
   │    MapMember: T3.1 -> T(offset + 3).1     MR0 -> the definition with that name and signature
   │    re-read the .lva with that TokenMap (LvaReader), file indices moved too
   ▼
merged HirAssembly: Types, Files, Attributes, Resources concatenated; EntryPoint = the last input's
                    Origins: which input each run of types came from

Details that matter:

  • Order. The assemblies are sorted so each comes after the ones it references. An assembly that references one not given ("App references Lib, which is not linked"), or that reaches itself through others, is an error.
  • Type references resolve by (assembly, namespace, name, arity). A missing one is "Ns.Name is not in Lib".
  • Member references resolve by name and signature, after mapping the signature's types: a method must match name, generic arity, parameter types and result exactly; a field, property or event matches by name. A miss is "Type has no method Name with that signature". All member references are resolved up front, because bodies are read later, on several threads, and only look them up.
  • Bodies stay unread. The merged assembly's methods still point into each input's bytes; a body is decoded, already mapped, when lowering first asks for it.
  • Duplicates. Two types with the same identity in one assembly fail ("is defined twice"); two inputs with the same assembly name fail before anything else:
Terminal
larva build --check -o Tiny.exe App.ant Tiny.dll
larva: two assemblies are named `Tiny` (a program and a library it uses, say): rename one (a project's PROJ name, or a library's assembly)
  • What is merged. Types, files, assembly attributes and resources. LanguageData is not: it belongs to the language's compiler, not to the program.
  • Origins records, in memory only, which input each run of types came from (HirAssembly.OriginOf). Lowering uses it to tell which types come from an assembly built for a GC domain of its own: those are lowered as declarations that the IR linker binds to that library's object (assembly::name), not as definitions.

HIR linking is the first of two linkers. After lowering, the IR linker joins IR objects (for example a library built for another GC); see IR objects and linking.

¿Te resultó útil esta página?

No se guarda ninguna dirección; tu nombre de usuario sí, si iniciaste sesión.

Esc