- Larva
- Cómo funciona Larva
- Reducir las características del lenguaje
Esta página aún no está traducida al español. Leer la versión en inglés
Lowering language features
How Larva lowers closures, iterators, async functions, exceptions, reflection, C interop, regions and destructors from HIR to IR.
En esta página
- Closures and lambdas
- Function values
- What is captured, and where it lives
- Iterators
- Async and await
- The coroutine pass
- Exceptions
- try, catch and finally
- How the back ends unwind
- Panics are not exceptions
- Reflection
- reflection: none | layout | full
- Interop
- Imports
- Exports, function pointers and pointers
- Regions and arenas
- The arena signature
- RegionChecker
- Destructors
- Reactive code
Lowering HIR to IR covered objects, dispatch and generics. This page covers the features that need more than a type mapping: code that captures, pauses, unwinds, describes itself or leaves Larva's world. Most of them follow one rule from the design: lower a feature to ordinary IR (objects, functions, blocks) whenever that is enough, so the IR checks and every back end get it for free. Few of these reach the IR as constructs of their own: exceptions (throw, resume, unwind edges), regions (region_enter, region_exit), finalizers on allocations (finalized), and coroutines (a function attribute and one IR pass).
The listings were printed with Larva's HIR and IR printers on the lowered program (the larva command prints neither; see Seeing what lowering makes).
Most examples come from this program:
import System;
import System.Threading.Tasks;
into namespace Feat
{
Adder(i32 offset) : func<i32> : i32 => x => x + offset;
Squares(i32 count) : Iterator<i32>
{
loop (i32 i in 0..count)
yield i * i;
}
async Later(i32 x) : Task<i32>
{
await Task.Yield();
return x * 2;
}
Safe(i32[] items, i32 i) : i32
{
try
{
return items[i];
}
catch (IndexError e)
{
return -1;
}
finally
{
Console.WriteLine("checked");
}
}
Main()
{
func<i32> : i32 add = Adder(10);
Console.WriteLine(add(5));
loop (i32 s in Squares(4))
Console.WriteLine(s);
Console.WriteLine(Later(21).Result);
Console.WriteLine(Safe([1, 2], 5));
}
}
larva build -o feat.exe feat.ant
feat.exe (llvm back end, O3, entry feat::Feat.$Module.Main)
feat.exe
15
0
1
4
9
42
larva panic: index 5 out of bounds for length 2
The last line is not a mistake in the example: see Panics are not exceptions.
Closures and lambdas
Function values
A func<A…> : R value is an object: Larva.Func{n}<A…, R>, declared once by HirLowerer.DeclareRoot for 0 to 8 parameters.
struct Larva.Func1<P0, R> { $class: i32, $vt: *mut Larva.Object$vt, $fn: fn(gc Larva.Object, P0) -> R, $env: gc Larva.Object } // base Larva.Object
$fn is a plain function that takes the environment first; $env is the object holding what the lambda captured (null when it captures nothing). Because a function value is an ordinary object, the collector, reference counting and the borrow checker treat it like any other.
Each lambda becomes a function of its own, {method}$lambda{k}(env, parameters…), with the method's type parameters and their requirements. Adder's HIR:
#0 internal static Adder(i32 offset) : func<i32> : i32 @0:6:2
{
local $0 mut i32 x
return lambda($0) return ($0 + %0) @0:6:41
}
becomes a display object holding the captured parameter, and a Func1 that points at the lambda's function:
struct Feat.$Module.Adder$display { $class: i32, $vt: *mut Larva.Object$vt, offset: i32 } // base Larva.Object
fn Feat.$Module.Adder(mut _1: i32) -> gc Larva.Func1<i32, i32> {
let mut _0: gc Larva.Func1<i32, i32>;// return
let mut _2: gc Feat.$Module.Adder$display;// $display
let mut _3: Feat.$Module.Adder$display;
let mut _4: Larva.Func1<i32, i32>;
let mut _5: gc Larva.Func1<i32, i32>;
bb0: {
_3 = filler Feat.$Module.Adder$display;
_3.0 = classid Feat.$Module.Adder$display;
_2 = gc move _3;
(*_2).2 = copy _1;
_4 = filler Larva.Func1<i32, i32>;
_4.0 = classid Larva.Func1<i32, i32>;
_4.2 = fn Feat.$Module.Adder$lambda0;
_4.3 = copy _2 as gc Larva.Object;
_5 = gc move _4;
_0 = copy _5;
return;
}
}
unsafe fn Feat.$Module.Adder$lambda0(_1: gc Larva.Object, mut _2: i32) -> i32 {
let mut _0: i32; // return
let mut _3: gc Feat.$Module.Adder$display;// $display
bb0: {
_3 = copy _1 as gc Feat.$Module.Adder$display;
_0 = Add(copy _2, copy (*_3).2);
return;
}
}
Calling a function value (BodyLowerer.Invoke) reads $fn and $env and makes an indirect call with the environment first:
bb13: {
_26 = copy (*_7).2;
_27 = copy (*_7).3;
_25 = call copy _26(copy _27) -> bb14;
}
What is captured, and where it lives
Closures first scans the body (Scan): contexts are the method (0) and each lambda (k + 1, in the order they appear). A local belongs to the context that declares it and is captured when another context names it; the method's parameters and this are captured when a lambda names them.
What lambdas capture lives in display objects that the method and its lambdas share, not in copies. There is one display per method call, made at the method's entry, for what is declared outside loops, and one per pass of each loop of the method's own body, for what that body declares. A loop's display is made at the start of each pass and points to the display around it ($parent). The lambda's environment is the innermost display.
This is the "capture per pass" rule: a lambda made inside a loop keeps that pass's variables, while variables declared outside the loop stay shared.
import System;
into namespace Capture
{
Main()
{
List<func : i32> made = new List<func : i32>();
i32 total = 0;
loop (i32 i in 0..3)
{
i32 square = i * i;
made.Add(() => square + total);
}
total = 100;
loop (func : i32 f in made)
Console.WriteLine(f());
}
}
cap.exe
100
101
104
Each lambda kept its own square (0, 1, 4) and saw the one total, changed to 100 after the loop. The two displays:
struct Capture.$Module.Main$display { $class: i32, $vt: *mut Larva.Object$vt, total: i32 } // base Larva.Object
struct Capture.$Module.Main$display1 { $class: i32, $vt: *mut Larva.Object$vt, $parent: gc Capture.$Module.Main$display, square: i32 } // base Larva.Object
and the loop body, which makes a new $display1 each pass before making the lambda's object:
bb4: {
_14 = filler Capture.$Module.Main$display1;
_14.0 = classid Capture.$Module.Main$display1;
_14.2 = copy _1;
_2 = gc move _14;
(*_2).3 = Mul(copy _5, copy _5);
_16 = copy _4;
_18 = filler Larva.Func0<i32>;
_18.0 = classid Larva.Func0<i32>;
_18.2 = fn Capture.$Module.Main$lambda0;
_18.3 = copy _2 as gc Larva.Object;
_19 = gc move _18;
_17 = copy _19;
_15 = System.List.Add::<gc Larva.Func0<i32>>(copy _16, copy _17) -> bb7;
}
The lambda reaches total through $parent:
unsafe fn Capture.$Module.Main$lambda0(_1: gc Larva.Object) -> i32 {
let mut _0: i32; // return
let mut _2: gc Capture.$Module.Main$display1;// $display
let mut _3: gc Capture.$Module.Main$display;
bb0: {
_2 = copy _1 as gc Capture.$Module.Main$display1;
_3 = copy (*_2).2;
_0 = Add(copy (*_2).3, copy (*_3).2);
return;
}
}
A loop counter that is captured is counted in a local and copied into the pass's display each pass, so the loop itself does not pay for the indirection. A scope that keeps nothing captured gets no display at all.
Iterators
A function that returns Iterator<T> and uses yield is split in two by the frontend: the function itself makes a hidden class holding its arguments, and that class's $Step() holds the body. In Antril's HIR for Squares:
#1 internal static Squares(i32 count) : [System.Core]System.Iterator<i32> @0:8:2
{
return upcast<[System.Core]System.Iterator<i32>>(new Feat.$Module.Squares$iterator1() { Feat.$Module.Squares$iterator1::$p0 = %0 })
}
internal final generated class Feat.$Module.Squares$iterator1 in Feat.$Module : [System.Core]System.Iterator<i32> @0:8:2
{
field private generated i32 $p0
#0 private final iterator generated $Step() : bool @0:8:2
{
local $0 mut i32 i
foreach $0 in (0i32..%0) @0:10:3
yield ($0 * $0) @0:11:4
}
#1 public virtual override final generated Next() : bool
{
return base this.Feat.$Module.Squares$iterator1::$Step#0()
}
}
$Step carries MethodFlags.Iterator, and inside it %0 still names the function's parameter: Iterators maps each parameter to its field ($p{i}, and $this for an instance method) while lowering. A yield value stores the value in the base's current field, sets the result to true and calls a pause function; the end of the body (or yield break) sets false and pauses for good, so every later Next() gives false again:
[Larva.Coroutine("1", "Feat.$Module.Squares$iterator1.$Step$suspend")] unsafe fn Feat.$Module.Squares$iterator1.$Step(_1: gc Feat.$Module.Squares$iterator1) -> bool {
…
bb2: {
(*_1).2 = Mul(copy _2, copy _2);
_0 = const true;
_5 = Feat.$Module.Squares$iterator1.$Step$pause(copy _1) -> bb5;
}
…
bb6: {
_0 = const false;
_6 = Feat.$Module.Squares$iterator1.$Step$pause(copy _1) -> bb7;
}
bb7: {
goto -> bb6;
}
}
As written, this function would run to the end and never return between values: $Step$pause is an empty function marked [Larva.Await]. That is on purpose. The body is still ordinary IR, so the type checker and borrow checker check it like any function. Making it resumable is the job of one IR pass, shared with async.
Async and await
An async function is lowered the same way, with the hidden class deriving from Task or Task<T>: the frame is the task the function returns.
#2 internal static Later(i32 x) : [System.Core]System.Threading.Tasks.Task<i32> @0:14:8
{
local $0 mut Feat.$Module.Later$async2 $task
let $0 = new Feat.$Module.Later$async2() { Feat.$Module.Later$async2::$p0 = %0 }
base $0.Feat.$Module.Later$async2::Run#1()
return upcast<[System.Core]System.Threading.Tasks.Task<i32>>($0)
}
internal final generated class Feat.$Module.Later$async2 in Feat.$Module : [System.Core]System.Threading.Tasks.Task<i32> @0:14:8
{
field private generated i32 $p0
#0 private final async generated $Step() : void @0:14:8
{
await [System.Core]System.Threading.Tasks.Task::Yield~0() @0:16:3
return (%0 * 2i32) @0:17:3
}
#1 public virtual override final generated Run() : void
{
local $0 mut [System.Core]System.Any failure
try
{
base this.Feat.$Module.Later$async2::$Step#0()
}
catch [System.Core]System.Any $0
{
this.[System.Core]System.Threading.Tasks.Task::Fail~7($0)
}
}
}
Run catches whatever the step throws and stores it in the task (Fail), so an exception crosses the await: whoever awaits the task gets it thrown again. In $Step, each await t becomes a call to an await function made for that place ($Step$await0, marked [Larva.Await]), which calls Task.Check (or GetResult for a Task<T>): it gives the value or throws the task's error. A return completes the task (SetResult / Complete):
[Larva.Coroutine("1", "Feat.$Module.Later$async2.$Step$suspend")] unsafe fn Feat.$Module.Later$async2.$Step(_1: gc Feat.$Module.Later$async2) -> () {
…
bb0: {
_3 = System.Threading.Tasks.Task.Yield() -> bb1;
}
bb1: {
_2 = Feat.$Module.Later$async2.$Step$await0(copy _3) -> bb2;
}
bb2: {
_4 = Mul(copy (*_1).7, const 2_i32);
_5 = copy _1 as gc System.Threading.Tasks.Task`1<i32>;
_6 = System.Threading.Tasks.Task`1.SetResult::<i32>(copy _5, copy _4) -> bb3;
}
…
}
The Larva.Coroutine("1", "…$suspend") attribute names the frame (argument 1) and a suspend function of the frame, suspend(frame, task) -> bool. For async it is Task.Pause(awaited), which registers the frame to run again once the awaited task is done and says whether to pause; for an iterator it always says to pause:
unsafe fn Feat.$Module.Later$async2.$Step$suspend(_1: gc Feat.$Module.Later$async2, _2: gc System.Threading.Tasks.Task) -> bool {
…
_0 = System.Threading.Tasks.Task.Pause(copy _3, copy _2) -> bb1;
…
}
The coroutine pass
Larva.IR.Lowering.Coroutines.Lower runs at the start of Monomorphizer.Run, so every back end gets resumable code, and after every check, so what it rewrites is what the checkers approved, with its storage moved. For each function marked Larva.Coroutine it:
- Adds fields to the frame's struct:
$state, then one per local (except the result and the frame itself). The function that makes the frame zeroes them. - Roots every place that was a local at its field instead, so the locals survive a return.
- Starts the function with a
switchon$state: 0 runs from the start, k resumes after the k-th pause. - At each await, calls
suspend(frame, task)first. If it says to pause, it stores the next state and returns; otherwise, and when resumed, it calls the await function, which finds the task done.
The iterator's frame after the pass:
struct Feat.$Module.Squares$iterator1 { $class: i32, $vt: *mut Larva.Object$vt, current: i32, $p0: i32, $state: i32, $i2: i32, $l3: i32, $l4: bool, $l5: (), $l6: () } // base System.Iterator<i32>
and its step:
[Larva.Coroutine("1", "Feat.$Module.Squares$iterator1.$Step$suspend")] unsafe fn Feat.$Module.Squares$iterator1.$Step(_1: gc Feat.$Module.Squares$iterator1) -> bool {
…
bb0: {
_11 = copy (*_1).4;
switch copy _11 -> [0: bb1, 1: bb12, 2: bb15, otherwise: bb16];
}
…
bb2: {
(*_1).5 = const 0_i32;
(*_1).6 = copy (*_1).3;
goto -> bb3;
}
bb3: {
(*_1).7 = Lt(copy (*_1).5, copy (*_1).6);
if copy (*_1).7 -> [true: bb4, false: bb6];
}
bb4: {
(*_1).2 = Mul(copy (*_1).5, copy (*_1).5);
_0 = const true;
_7 = copy _1 as gc Feat.$Module.Squares$iterator1;
_8 = Feat.$Module.Squares$iterator1.$Step$suspend(copy _1, move _7) -> bb10;
}
…
bb10: {
if copy _8 -> [true: bb11, false: bb12];
}
bb11: {
(*_1).4 = const 1_i32;
return;
}
bb12: {
(*_1).8 = Feat.$Module.Squares$iterator1.$Step$pause(copy _1) -> bb7;
}
…
}
The loop counter i (_2) now lives in field 5 ($i2), count was copied into field 6, and the first yield sets $state to 1 and returns true. The next Next() enters at bb12, right after the pause, and goes on round the loop.
Iterators and async share this one mechanism. Before HIR, Antril's async was an AST rewrite followed by an IR pass; now the frontend only marks the body, and Larva does the rest. The async scheduler is single-threaded and lives in System.Threading.Tasks, which is why await Task.Yield() measured 45 ms for a million awaits against 640 ms on a thread-pool runtime.
Exceptions
Exceptions are objects, thrown as the root handle. A throw is an IR terminator:
import System;
into namespace Errors
{
public exception Negative
{
public:
new() : base("negative") { }
}
Check(i32 x) : i32
{
if (x < 0)
throw new NegativeException();
return x;
}
Safe(i32 x) : i32
{
try
{
return Check(x);
}
catch (NegativeException e)
{
return 0;
}
finally
{
Console.WriteLine(x);
}
}
Main()
{
Console.WriteLine(Safe(-5));
}
}
bb4: {
_4 = copy _7;
_3 = copy _4 as gc Larva.Object;
throw copy _3;
}
try, catch and finally
BodyLowerer.Try gives each try one handler that takes every exception and tests the catch clauses in order, plus, when there is a finally, a catch-all. A call inside the protected region carries the handlers of every try around it, innermost first, as its unwind edge:
bb0: {
_0 = Errors.$Module.Check(copy _1) -> bb5 unwind [catch gc Larva.Object -> bb4, catch _ -> bb2];
}
The handler reads the exception (caught) and runs the type test of each clause: for a class, the class-id range. No clause matching, resume passes the exception on, to the finally handler here:
bb4: {
_4 = caught;
_5 = copy _4;
_6 = copy _5;
_7 = copy (*_6).0;
_8 = classid Errors.NegativeException;
_10 = Ge(copy _7, copy _8);
if copy _10 -> [true: bb9, false: bb8];
}
…
bb7: {
_2 = copy _4 as gc Errors.NegativeException;
_0 = const 0_i32;
_12 = System.Console.WriteLine$39(copy _1) -> bb10;
}
bb8: {
resume [catch _ -> bb2];
}
bb9: {
_9 = classid.end Errors.NegativeException;
_11 = Lt(copy _7, copy _9);
if copy _11 -> [true: bb7, false: bb8];
}
finally is copied: once into the catch-all handler, which runs the block and re-raises (bb2, then resume), and once on every normal way out of the try. A return, break or continue that leaves a try runs the finally blocks it crosses, innermost first (RunFinally). In Safe, the block Console.WriteLine(x) appears three times: after the try body returns (bb5), after the catch returns (bb7), and in the catch-all (bb2). The catch bodies stay protected by the finally.
Lowering also tracks which handlers can be reached (usedHandlers): a catch whose try body contains nothing that can throw becomes unreachable, and the back ends emit no handler for it.
How the back ends unwind
The IR only says where an exception goes. Each back end implements it:
| Back end | Mechanism |
|---|---|
| LLVM (Windows) | Zero-cost SEH. throw raises an exception with Larva's code whose information carries a cell holding the value and its type id; calls with catch clauses become invokes into catchswitches whose catchpads use generated filter functions; one cleanuppad per function releases what the frame owns, closes the arena regions it opened and unpins. The personality is __C_specific_handler. |
| x64 | Its own Win64 unwind info per function, with __C_specific_handler scope tables, so the same runtime path finds the handlers. |
| WebAssembly | The exception in flight is kept in globals; after a call, the site checks it and dispatches to its first matching clause, else the frame's cleanup. |
| VM | The interpreter unwinds its own frames. |
On Windows, throw goes through larva_raise in the runtime instead of RaiseException: it walks the frames with the unwind tables and asks the Larva filters itself, so the common case never enters the kernel. See Runtime, LLVM back end and x64 back end.
Panics are not exceptions
An array index out of bounds, a division by zero, an invalid array length or a null dereference is a panic, not an exception: the back end calls larva_panic, which prints the reason and ends the program with exit code 101. A panic is never caught by catch. That is why Safe([1, 2], 5) in the first program ends the program: items[i] cannot throw an IndexError, and lowering marked the catch unreachable because nothing in the try body throws. Library collections that check their indexes themselves throw real exceptions (List.At throws IndexError).
Reflection
Reflection is written as HIR before lowering, so the rest of lowering treats it like any other code (Reflection). It is generated only when some body (outside the reflection library) says typeof or calls GetType(); a program that does neither carries none of it.
Each described type X gets a nested class X.$Describer, with X's type parameters, derived from System.Reflection.TypeDescriber:
| Member | What it does |
|---|---|
static $type, $Type() |
X's System.Type, made once (one per instance of a generic type) and kept in the static field. |
$Make() |
Builds the Type: name, namespace, kind, and a describer for the rest. Marked cold and not inlined. |
Base, Arguments, Interfaces, EnumNames, EnumValues, Attributes |
Answered on demand, overriding the describer's virtual methods. |
GetField, SetField, Invoke, Construct |
Members by number, values converted to and from any?. One small function per member: a single large one made the IL grow far worse than linearly (22 s against 0.7 s). |
$TypeOf(X*), $Box(X), $Unbox(any?) |
The implementations generic code reaches through an IR impl once T is known: typeof(T), a T as any and back. |
Classes override Object.RuntimeType(), which GetType() calls. typeof(i32).Name lowers to a call of the describer:
bb12: {
_21 = System.Int32.$Describer.$Type() -> bb13;
}
bb13: {
_22 = copy _21;
_20 = System.Type.get_Name(copy _22) -> bb14;
}
global System.Int32.$Describer.$type: ?gc System.Type
fn System.Int32.$Describer.$Type() -> gc System.Type {
let mut _0: gc System.Type; // return
let mut _1: bool;
let mut _2: ?gc System.Type;
let mut _3: ?gc System.Type;
bb0: {
_2 = System.Int32.$Describer.$type;
_1 = is_null _2;
if copy _1 -> [true: bb1, false: bb2];
}
bb1: {
_0 = System.Int32.$Describer.$Make() -> bb3;
}
bb2: {
_3 = System.Int32.$Describer.$type;
_0 = unwrap copy _3;
return;
}
bb3: {
return;
}
}
Member tables are made only when the program asks for members (GetFields, GetMethod, Activator…), and only for the program's own types, Object, and library types the program names in a typeof. Each member table reaches the types of its members, so giving every type one pulled the whole library into every program: 2.4 MB against 300 KB. With only typeof/GetType(), an executable grows by about 50 KB.
typeof(A) == typeof(B) compares typeids, which are constants once generics are resolved, so no Type is made.
Not implemented yet: Type.GetType(name) and MakeGenericType; classes the compiler makes (iterators, async) report their base's Type; setting a field of a boxed value through reflection.
reflection: none | layout | full
The .queen key reflection is a different, lower level: the IR's ReflectionLevel, the metadata a module gives its runtime about IR structs (CompilationOptions.Reflection, default layout). It is used when lowering to IL for the VM (MetadataBuilder):
| Level | What a struct exposes |
|---|---|
none |
Nothing beyond what the GC needs; names trimmed. A struct that owns gc handles never drops below layout. |
layout |
Name, field names, offsets and layout (enough to serialize). |
full |
Everything, including invocable methods; functions are reflected only with #[reflect(full)], and such functions are roots that are never dropped as dead code. |
The language-level reflection above (typeof, GetType(), FieldInfo, Activator) does not depend on this key: it is code, generated from HIR metadata.
Interop
Imports
A method with [Import("lib", "Entry")] and no body is an import: an import method with extern "Entry" from "lib" in HIR. Lowering gives it a body that finds the C function's address the first time (native.symbol), keeps it in a global, and calls it indirectly from then on:
import System;
import System.Runtime.InteropServices;
into namespace Native
{
[Import("kernel32.dll")]
GetCurrentProcessId() : u32;
[Import("kernel32.dll", "lstrlenA")]
Length(str text) : i32;
Main()
{
Console.WriteLine(GetCurrentProcessId() > 0);
Console.WriteLine(Length("hello"));
Console.WriteLine(typeof(i32).Name);
}
}
nat.exe
true
5
Int32
The HIR declarations:
#0 internal static import GetCurrentProcessId() : u32 extern "GetCurrentProcessId" from "kernel32.dll" @0:7:2
#1 internal static import Length([System.Core]System.String<u8> text) : i32 extern "lstrlenA" from "kernel32.dll" @0:10:2
and the import:
global $import.kernel32.dll.GetCurrentProcessId: u64
[Larva.Import("kernel32.dll", "GetCurrentProcessId")] unsafe fn Native.$Module.GetCurrentProcessId() -> u32 {
let mut _0: u32; // return
let mut _1: u64; // address
let mut _2: bool; // missing
let mut _3: &[u16]; // library
let mut _4: &[u16]; // entry
let mut _5: fn() -> u32; // code
bb0: {
_1 = $import.kernel32.dll.GetCurrentProcessId;
_2 = Eq(copy _1, const 0_u64);
if copy _2 -> [true: bb1, false: bb2];
}
bb1: {
_3 = static#302;
_4 = static#303;
_1 = intrinsic "native.symbol"(copy _3, copy _4);
$import.kernel32.dll.GetCurrentProcessId = copy _1;
goto -> bb2;
}
bb2: {
_5 = copy _1 as fn() -> u32;
_0 = call copy _5() -> bb3;
}
bb3: {
return;
}
}
The IR attribute Larva.Import tells the VM to make the native call itself; the native back ends compile the body as written.
A string parameter travels as a temporary zero-terminated copy: UTF-8 for str, UTF-16 for String<u16>, and a null str? as a null pointer. The import is then a wrapper around a raw import (Length$native) that takes pointers. The copy goes into a 128-byte buffer on the stack when it fits, and into native memory, freed after the call, when it does not:
unsafe fn Native.$Module.Length(mut _1: gc System.String<u8>) -> i32 {
…
let mut _2: [u8; 128]; // cstring
…
bb0: {
_2 = zeroed [u8; 128];
_3 = &mut _2;
_4 = move _3 as *mut [u8; 128];
_5 = copy _4 as *mut u8;
_7 = System.Runtime.InteropServices.CStrings.Utf8Into(copy _1, copy _5, const 128_i32) -> bb2;
}
bb1: {
_8 = Native.$Module.Length$native(copy _6) -> bb6;
}
…
bb4: {
_6 = System.Runtime.InteropServices.CStrings.Utf8(copy _1) -> bb5;
}
…
bb7: {
_14 = System.Runtime.InteropServices.CStrings.Free(copy _6) -> bb9;
}
…
}
Imports and func* helpers have no safepoint on entry: C does not allocate on Larva's heap, and a Larva function called from C polls on its own.
Exports, function pointers and pointers
| Construct | Lowering |
|---|---|
[Export("name")] |
The function is exported, with IR attribute Larva.Export giving its C name in native images. larva build --dll makes a library whose exported functions run the static constructors on first call. |
func*<A…> : R |
An IR function type. &F is the address of a static function. |
calling a func* |
Through a helper per signature, $NativeCall{n}(code, args…), marked Larva.NativeCall, so the VM knows it is calling C and C callbacks (for example qsort's comparer) can run in the interpreter. |
p + n, p - n, p - q, p[i], p->f |
Through u64, in whole elements (the size of the pointee; 1 for void*). |
Structs cross to C by pointer, not by value, and variadic C functions cannot be called. How [ValueLayout] structs are laid out is in Object layout; how larva bind produces imports is in Bindings internals.
Regions and arenas
With gc: arena, objects are freed together when the region that made them closes. HIR has a RegionStmt; Antril writes it region { … }.
BodyLowerer lowers it only when the GC mode has GcCapabilities.Regions. It emits region_enter(rN) and region_exit(rN) (a counter per function) and pushes a TryFrame whose Region is rN, so RunFinally, the same code that runs finally blocks on return, break and continue, closes the region on every way out. In other modes the block runs as a plain block, and its objects live as the collector decides.
import System;
into namespace Mem
{
public class Handle
{
i32 id;
public:
new(i32 id) { this.id = id; }
destroy() { Console.WriteLine(id); }
}
Main()
{
loop (i32 i in 1..4)
{
region
{
Handle h = new Handle(i);
if (i == 2)
break;
}
}
Console.WriteLine(0);
}
}
In a project whose .queen says gc: arena:
larva build -o Arena.exe
Arena.exe (llvm back end, O3, entry Arena::Mem.$Module.Main)
Arena.exe
1
2
0
The region closes at the end of the first pass and on the break of the second, running each object's destructor. Both exits are visible in the IR:
fn Mem.$Module.Main<'a>() -> () in 'a {
…
bb2: {
region_enter(r0);
_5 = filler Mem.Handle;
_5.0 = classid Mem.Handle;
_6 = Mem.Handle.$vtable() -> bb5;
}
…
bb5: {
_5.1 = copy _6 as *mut Larva.Object$vt;
_8 = fn Mem.Handle.$destroy;
_7 = gc move _5 finalized move _8;
_9 = Mem.Handle..new(copy _7, copy _1) -> bb6;
}
bb6: {
_2 = copy _7;
_10 = Eq(copy _1, const 2_i32);
if copy _10 -> [true: bb7, false: bb8];
}
bb7: {
region_exit(r0);
goto -> bb4;
}
bb8: {
region_exit(r0);
goto -> bb3;
}
…
}
The arena signature
Note <'a> and in 'a on Main. In arena mode, HirLowerer.ArenaSignature gives every function one region parameter 'a: its arguments and result are in 'a, and IrFunction.CallerRegion is 'a, so what a function allocates outside its own regions lands in its caller's. Objects then flow between arguments, fields and results as in the other modes, and only what a region { } allocates is checked not to leave it. Without that, the whole core failed the region checker (storing arguments in this, returning arguments).
String literals are normally made once and cached in a global; under arena they are not, because a literal first reached inside a region would die with it.
RegionChecker
Larva.IR.Analysis.RegionChecker runs as part of the IR checks, only for modes with regions, and is always on. It tags every value that can point into a region (a body region rN, a region parameter 'a, an argument private to the call, or the caller's region), follows tags through copies, moves, borrows, fields and calls, and rejects:
- a local tagged
rNstill used afterregion_exit(rN); - a store that would outlive the memory it points to;
- a result carrying more than its signature allows (
-> T in 'a, plus the caller's region); - unbalanced regions: they must close innermost first, never be re-entered while open, all be closed at
return, and every path into a block must see the same open regions.
Calls are checked against the callee's signature rather than trusted. Returning an object that escaped its region fails the build:
larva build
larva: error[RegionEscape]: `kept` holds a value from region r0 and is still used after region_exit(r0)
--> Mem.$Module.Make bb4[2]
(from Make() : Handle assigning kept = h; inside a region and returning kept). The error comes from the IR checker, without an Antril diagnostic code of its own. In a program that mixes GC domains, RegionChecker.CheckMixed also refuses passing a value that carries a region to a function of another domain.
Known limit: a region frame has no handlers, so an exception thrown inside a region and caught in the same function does not close the region on its way to the catch. The region is closed when the exception leaves the function (the frame's cleanup).
Destructors
A class's destroy() is its destructor. The frontend makes each one virtual and wraps its body so the base's runs after it, whatever happens (in HIR, a try … finally { base.destroy() }):
#1 public virtual override destroy() : void @0:11:4
{
try
{
[System.Core]System.Console::WriteLine~0(this.Mem.Handle::id) @0:11:16
}
finally
{
base this.[System.Core]System.Any::destroy~1()
}
}
HirLowerer.DestructorOf finds the nearest destroy in a class's chain. If it is not Object's empty one, the class gets a synthetic $destroy(self) that calls self.destroy() (dispatched, so the real class's runs), and TypeInfo.Finalizer remembers it. BodyLowerer.New names it as the allocation's finalizer: gc move _5 finalized move _8 above. The IR has carried finalizers for a long time (AllocRvalue.Finalizer, a fn(gc T)), and the VM, the native back ends and larva_rt.c already ran them; this is how a language reaches them.
When the destructor runs depends on the GC mode, not on lowering:
| Mode | When destroy() runs |
|---|---|
default (tracing) |
At a collection that finds the object unreachable (the runtime's two-phase finalization). Objects still alive when the program ends are not destroyed. |
refcount |
When the last reference is released. |
arena |
When the region that made the object closes (region_exit), before its memory is freed. |
manual |
At delete x: lowering calls destroy (dispatched) and then frees the object. No finalizer is attached, since nothing else would run it: under manual, handles are *mut T and new is a plain alloc. |
In the managed modes delete does nothing beyond evaluating its operand. Under manual, the same new Handle(1) lowers to:
bb1: {
_2.1 = copy _3 as *mut Larva.Object$vt;
_4 = alloc move _2;
_5 = Mem.Handle..new(copy _4, const 1_i32) -> bb2;
}
destroy is inherited in Object, so it does not appear among a type's public methods through reflection.
Reactive code
reactive fields, computed values, effect { } and reacts are not a Larva feature. Antril's parser and binder expand them into private fields, properties and calls to System.Reactive (Source, Effect, Reactive.Watch) in System.Core, so the HIR Larva receives contains only ordinary properties, lambdas and calls, and nothing in HIR or lowering is specific to them. Another language could offer the same feature over the same library. See Async and reactive code.
Editar esta página en GitHub Reportar un problema