Objeck Language Features
Comprehensive guide to Objeck's language features with examples.
Object-Oriented Programming
Inheritance
class Triangle from Shape {
New() {
Parent();
}
}
Interfaces
class Triangle from Shape implements Color {
New() {
Parent();
}
method : public : GetRgb() ~ Int {
return 0xadd8e6;
}
}
interface Color {
method : virtual : public : GetRgb() ~ Int;
}
Default and Named Arguments
Trailing parameters can have default values. An argument can be passed by name, name: value, after any positional ones. Named arguments may come in any order, and they may skip any parameter that has a default, not just the trailing ones:
function : Train(data : Int[], epochs : Int := 10, rate : Float := 0.01, verbose : Bool := false) ~ Nil { ... }
Train(data); # every default
Train(data, verbose: true); # skip epochs and rate
Train(data, rate: 0.1, epochs: 50); # any order
Point->New(y: 2, x: 1); # constructors too
Arguments are evaluated in the order written. Named arguments work for the functions, methods and constructors of the program being compiled, and for those of libraries, which record their parameter names:
"abc"->Pad(num: 2, char: '*', is_left: true); # "**abc"
Int->Clamp(value: v, min: 0, max: 10);
Virtual methods, function values and calls on a primitive value (v->Clamp(...)) do not take them: a call that tries is a compile error, never a silent positional call. A library's parameter names are part of its API. See NAMED_ARGUMENTS_DESIGN.md.
Reference Parameters
A ref parameter lets a function change its caller's variable. The call site says ref too, so a reader sees which arguments a call may change:
function : Swap(ref a : Int, ref b : Int) ~ Nil { t := a; a := b; b := t; }
function : Divmod(a : Int, b : Int, ref remainder : Int) ~ Int { remainder := a % b; return a / b; }
Swap(ref x, ref y);
q := Divmod(17, 5, ref r); # q is 3, r is 2
The value is copied in and copied back after the statement, as Swift's inout is. A local, a parameter or a field can be passed. A call with ref arguments is a statement of its own or the right side of an assignment. See REFERENCE_PARAMETERS_DESIGN.md.
Array Literals
An array literal can hold expressions as well as constants; it is built when it is evaluated, the elements in the order written. Objects of several classes make a Base[], and primitives among objects are boxed:
x := 3;
squares := [x, x * x, Square(x + 1)]; # Int[]
row := ["id", x, 2.5, true]; # Base[]: String, IntRef, FloatRef, BoolRef
PrintEach([name, title->ToUpper()]); # the variadic idiom: an array at the call site
Jagged Arrays
T[][] is an array of T[] rows of any length, at any depth:
rows := Int[]->New[3]; # three rows, each Nil until set
rows[0] := [1, 2, 3];
rows[1] := Int->New[5];
rows[2] := [x, y];
total := rows[0][1] + rows[2][0]; # rows[i] is an Int[]; rows[i][j] an Int
triangle := [[1], [1, 1], [1, 2, 1]]; # a ragged literal is an Int[][]
function : Sum(rows : Int[][]) ~ Int { ... }
Type Inference
value := "Hello World!";
value->Size()->PrintLine();
Nil Safety
?-> calls a method only when the receiver is non-Nil, yielding Nil instead of faulting. ?? supplies a value when the left side is Nil.
# without them
city := Nil;
if(person <> Nil) {
address := person->GetAddress();
if(address <> Nil) {
city := address->GetCity();
};
};
# with them
city := person?->GetAddress()?->GetCity();
# '??' supplies a default; the right side is evaluated only when needed
name := lookup ?? "(unknown)";
port := config_port ?? ExpensiveDefault();
A single ?-> guards the whole rest of the chain, so you do not need one at every step:
# both calls are guarded by the one '?->'
text := maybe?->Trim()->ToUpper();
A chain that ends in a value type needs ?? to supply the result, since an Int cannot itself be Nil:
size := maybe?->ToUpper()->Size() ?? -1;
For a guarded value -- Bool, Byte, Char or Int -- the default replaces only a failure: size above is -1 when maybe is Nil and 0 when it is "". For a guarded object, ?? takes the default when the chain failed or yielded Nil.
Both build on the Try()/Otherwise() intrinsics, so a?->b() is exactly a->Try()->b() and a ?? b is a->Otherwise(b). One consequence worth knowing: Try() guards against most runtime errors in the chain, not only a Nil dereference — a?->Get(999) yields Nil on an out-of-range index rather than faulting. An invalid object cast is recovered too, in interpreted and in JIT-compiled code alike — the same program behaves the same way whether or not the failing method happened to be compiled. A call-stack overflow is recovered in interpreted code only: compiled frames consume the real C stack, so the process dies in the OS before any frame-count guard can fire.
A guarded call works as a statement too: t?->Close(); runs Close() unless t is Nil, and recovers a runtime error inside it. A method that returns nothing yields Nil through the guard.
Reading a recovered error
After a ?-> recovers, the error it took is recorded for the thread:
value := table->Get(key)?->Parse() ?? 0;
if(Runtime->HasError()) {
kind := Runtime->GetErrorKind(); # NIL_DEREFERENCE, INDEX_OUT_OF_BOUNDS,
# DIVIDE_BY_ZERO, INVALID_CAST, OTHER
"skipped {$key}: {$(Runtime->GetError())}"->ErrorLine();
Runtime->ClearError();
};
A Nil receiver is not an error: x?->M() with x Nil calls nothing. See RECOVERED_ERRORS_DESIGN.md.
Errors: raise, try, on, finally
An error is an object: System.Error, or a subclass of your own. Runtime->Raise raises one; a try block catches errors by type, and finally runs on every way out:
class ParseError from Error {
New(message : String) { Parent(message); }
}
try {
rows := Load(path);
model := Fit(rows);
}
on(e : ParseError) {
"bad row: {$(e->GetMessage())}"->ErrorLine();
}
on(e : DivideByZeroError) {
model := Nil;
}
finally {
log->Close();
};
onclauses are tested in order; an error none of them names is raised again afterfinally, so a typedtrynever swallows what it did not ask for.on(e : Error)takes anything.- The runtime's own errors are
NilError,IndexError,DivideByZeroError,CastErrorandRuntimeError(a failedRuntime->Assert, a failing library call). Runtime->Raise(e)in a handler passes the error on. An error raised in a handler still runsfinallyfirst.return,breakandcontinuemay leave atrybody or a handler, throughfinally; they may not leavefinallyitself.try,onandfinallyare contextual: they can still be used as names.?->still recovers from any error below a guarded call, andRuntime->GetLastError()returns the one it took.- An error nothing catches ends the program with
Unhandled <class>: <message>.
A method with a try block runs interpreted (the JIT compiles its callees), as one with a ?-> does. See ERROR_HANDLING_DESIGN.md.
Tail calls
A method's call to itself on @self in tail position (Down(); } or return Down();) is compiled as a jump at -opt s1 and above, so unbounded tail recursion runs in constant stack. The transform is skipped for Main, constructors, virtual methods, methods containing &/|, and methods with a Try() region of their own. A call on another instance of the class is a real call, and so is any call at -opt s0.
Anonymous Classes
interface Greetings {
method : virtual : public : SayHi() ~ Nil;
}
class Hello {
function : Main(args : String[]) ~ Nil {
hey := Base->New() implements Greetings {
New() {}
method : public : SayHi() ~ Nil {
"Hey..."->PrintLine();
}
};
}
}
Reflection
klass := "Hello World!"->GetClass();
klass->GetName()->PrintLine();
klass->GetMethodNumber()->PrintLine();
Dependency Injection
value := Class->Instance("System.String")->As(String);
value += "510";
value->PrintLine();
Generics
map := Collection.Map->New()<IntRef, String>;
map->Insert(415, "San Francisco");
map->Insert(510, "Oakland");
map->Insert(408, "Sunnyvale");
map->ToString()->PrintLine();
Type-parameter bounds
A type parameter can require an interface bound (T : Bound); a concrete argument must implement it. Multiple bounds combine with &, and a bound may be generic (F-bounded, e.g. self-comparable):
# single bound
class SortedBox<T : Compare> { ... }
# compound bound: T must satisfy BOTH interfaces
class Entry<T : Compare & Stringify> { ... }
# F-bounded: T must be comparable to itself
class Sorter<T : Compare<T>> { ... }
Variance (out / in)
Declaration-site variance relaxes the default invariance. A covariant parameter (out T) lets Producer<Dog> be used where Producer<Animal> is expected; a contravariant parameter (in T) lets Consumer<Animal> be used where Consumer<Dog> is expected. Omitting the modifier keeps the safe, invariant default.
class Animal {}
class Dog from Animal { New() { Parent(); } }
class Producer<out T> { @v : T; New() {} method : public : Get() ~ T { return @v; } }
class Test {
function : Use(p : Producer<Animal>) ~ Nil {}
function : Main(args : String[]) ~ Nil {
pd := Producer->New()<Dog>;
Use(pd); # accepted: Producer<Dog> is a Producer<Animal>
}
}
Generic Methods
A function or method can declare its own type parameters, inferred from the arguments at each call:
function : Max<T : Compare>(values : Vector<T>) ~ T { ... }
function : Swap<T>(a : T[], i : Int, j : Int) ~ Nil { ... }
best := Max(scores); # scores is a Vector<IntRef>, so best is an IntRef
Swap(names, 0, 2); # T is String
A type parameter takes an object (box primitives as IntRef, ...). Library methods take type parameters too: Vector, List and CompareList have Map<R>((H) ~ R), so a map can change the element type:
lengths := words->Map(\(String) ~ IntRef : (s) => IntRef->New(s->Size())); # Vector<IntRef>
upper := words->Map(\(s) => s->ToUpper()); # R from the body
An inferred lambda for a type parameter must return an object. See METHOD_GENERICS_DESIGN.md.
Type Boxing
list := Collection.List->New()<IntRef>;
list->AddBack(17);
list->AddFront(4);
(list->Back() + list->Front())->PrintLine();
Static Import
use function Int;
class Test {
function : Main(args : String[]) ~ Nil {
Abs(-256)->Sqrt()->PrintLine();
}
}
Serialization
serializer := System.IO.Serializer->New();
serializer->Write(map);
serializer->Write("Fin.");
bytes := serializer->Serialize();
bytes->Size()->PrintLine();
Closures and Lambda Expressions
A lambda is written one of two ways, and which you need depends on whether it takes parameters.
No parameters — the shorthand \() => body. When the surrounding context expects a FuncRef<R> — an assignment target, a return, a method argument, or a collection element — the return type is inferred and the lambda auto-wraps into a FuncRef<R> (no explicit FuncRef->New(...)<R> or ~ R : annotation). Call a FuncRef directly with v() — no ->Get()/->Call() step — and chain on the result (v()->Get()).
With parameters — the annotated form \(types) ~ R : (names) => body, which targets a function type rather than a FuncRef<R>. FuncRef<R> wraps a () ~ R and has no parameter slots, so a lambda taking an argument cannot be a FuncRef. See Lambdas with parameters below.
# bare lambda; return type inferred from the FuncRef<IntRef> target, called directly
f : FuncRef<IntRef> := \() => IntRef->New(5);
f()->Get()->PrintLine(); # 5
# block body: an arbitrary statement list (inferred locals, loops) with a return
sum := \() => {
acc := IntRef->New(0);
for(k := 0; k < 5; k += 1;) { acc->Set(acc->Get() + k); };
return acc;
};
sum()->Get()->PrintLine(); # 0+1+2+3+4 = 10
# auto-wraps when stored in a generic collection, then called directly
funcs := Vector->New()<FuncRef<IntRef>>;
each(i : 5) { funcs->AddBack(\() => IntRef->New(i * 7)); };
each(i : funcs) {
f := funcs->Get(i);
f()->Get()->PrintLine();
};
A bare lambda also auto-wraps when returned or passed as an argument:
function : Mk(n : Int) ~ FuncRef<IntRef> {
x := IntRef->New(n);
return \() => x; # auto-wraps to FuncRef<IntRef>
}
The verbose form with an explicit annotation is still supported and equivalent: FuncRef->New(\() ~ IntRef : () => x)<IntRef>.
Lambdas with parameters
The shorthand is zero-parameter only. A lambda that takes arguments uses the annotated form — parameter types, the return type, then parameter names — and targets a function type:
f : (Int) ~ Int := \(Int) ~ Int : (x) => x + 1;
f(41)->PrintLine(); # 42
The two parenthesised groups are types and then names, in that order. Writing \(x : Int) => ... does not compile: the shorthand has no parameter list, and the error points at the parameter name rather than saying so.
A parameterised lambda captures exactly as a zero-parameter one does, so a function can return one and the captures travel with it — independently per lambda, with no shared state:
function : MakeAdder(n : Int) ~ (Int) ~ Int {
return \(Int) ~ Int : (x) => x + n;
}
add3 := MakeAdder(3);
add100 := MakeAdder(100);
add3(10)->PrintLine(); # 13
add100(10)->PrintLine(); # 110
add3(10)->PrintLine(); # 13 — unaffected by the second
This is what makes currying, partial application and function composition expressible; see First-Class Functions below for composition.
What a lambda can capture
A lambda can use the locals and parameters of the method that creates it: values, strings, arrays, objects, function references and other FuncRefs. Inside the body you can call methods on a captured object (including virtual methods) and call a captured function or FuncRef directly.
c := Counter->New();
inc : FuncRef<IntRef> := \() => c->Inc(); # instance method on a captured object
r : FuncRef<IntRef> := \() => IntRef->New(3);
next : FuncRef<IntRef> := \() => IntRef->New(r()->Get() + 1); # captured FuncRef called
Captures are by copy, taken when the lambda is created:
- A later assignment to the captured local is not seen: with
n := 1;, then a lambda that readsn, thenn := 2;, the lambda still sees1. A loop variable captured in each iteration keeps that iteration's value. - An object is copied as a reference, so the lambda and the enclosing method share it:
c->Inc()inside the lambda is visible asc->Get()outside. Rebinding the local (c := Counter->New();) does not change which object the lambda uses.
Not available in lambda bodies yet: self, @field access, and calling an instance method of the enclosing class without a receiver (Inc()), which is rejected with "Cannot reference an instance method from this context". Capture the object in a local and call through it instead.
First-Class Functions
A named function is passed as Name(types) ~ Return, and a function type is written the same way. Compose two by returning a parameterised lambda, which closes over them:
function : Compose(f : (Int) ~ Int, g : (Int) ~ Int) ~ (Int) ~ Int {
return \(Int) ~ Int : (x) => f(g(x));
}
function : F(a : Int) ~ Int { return a + 14; }
function : G(a : Int) ~ Int { return a + 15; }
function : Main(args : String[]) ~ Nil {
compose := Compose(F(Int) ~ Int, G(Int) ~ Int);
compose(13)->PrintLine(); # 42
}
Each call to Compose produces an independent function, so several may be live at once.
f and g — and it is wrong: ``ruby @f : static : (Int) ~ Int; @g : static : (Int) ~ Int; function : native : BadCompose(x : Int) ~ Int { return @f(@g(x)); } function : BadComposer(f : (Int) ~ Int, g : (Int) ~ Int) ~ (Int) ~ Int { @f := f; @g := g; return BadCompose(Int) ~ Int; } ` Every result refers to the same BadCompose`, which reads whichever pair was stored last. Build two and the first silently changes meaning — no error, no warning, just a wrong number. Use the lambda above.These 40 words cannot be used as identifiers:
alias and break bundle class consts continue critical do each else enum false for from function if implements in interface label leaving method native not or other private public readonly record return reverse select static true use virtual while xor
Using one reports Expected identifier at that position, without naming the word. Four of them read as ordinary identifiers and are worth knowing about:
| word | reserved for | commonly wanted as |
|---|---|---|
other | the default label in select | Equals(other), Compare(other), Add(other) |
label | select case labels | a caption, a graph vertex, a class label |
from | the inheritance clause, class X from Y | a date range, a graph edge |
reverse | each(... reverse ...) | a sort direction |
self, parent and value are not reserved as identifiers.
- Width:
Intis 64-bit two's-complement;ByteandCharwiden toInt. - Overflow:
+ - *wrap and never trap. - Shifts: use
n and 63.>>sign-fills,>>>zero-fills. - Division:
/truncates toward zero;%takes the dividend's sign. - Traps: a zero divisor is the only integer trap;
MIN / -1 = MINandMIN % -1 = 0. - Byte narrowing: a
Byteis unsigned, 0 to 255. A value that lands in a scalarByte-- a local, field or parameter, a~ Bytereturn,->As(Byte)orInt->ToByte-- keeps its low 8 bits, as aByte[]element store does:255 + 1is 0,0 - 1is 255, 300 is 44. The arithmetic runs atIntwidth and wraps only where its result lands in aByte: withbat 255,b + 1 = 256is true, but(b + 1)->PrintLine()prints 0, becausePrintLinetakes aByteparameter. - Char is not narrowed: a scalar
Charholds a whole Unicode code point on every platform, so0x1F600->As(Char)keeps its value andString->Appendwrites the surrogate pair where strings are UTF-16 (Windows). Only aChar[]element store is limited to the platform's character width (16 bits on Windows, 32 elsewhere). - Printing a Byte: always its decimal value.
ToString,Print/PrintLine, interpolation, concatenation andString->Append(Byte)all give "65" for 65; useb->As(Char)for the character. - Consistency: identical in the interpreter, both JITs and at every
-optlevel.
min := -9223372036854775807 - 1;
(9223372036854775807 + 1 = min)->PrintLine(); # true: wraps
(min / -1 = min)->PrintLine(); # true: no trap
(1 << 65)->PrintLine(); # 2: the count is 65 and 63 = 1
(-8 >>> 64)->PrintLine(); # -8: a count of 64 shifts by 0
(-7 / 2)->PrintLine(); # -3
(-7 % 3)->PrintLine(); # -1
b : Byte := 255;
b += 1;
b->PrintLine(); # 0: a Byte wraps on store
Int->ToByte(300)->PrintLine(); # 44
(b - 1 = -1)->PrintLine(); # true: the arithmetic is Int-wide
Interpolation
Embed a value in a string literal with {$...}. The braces may hold any expression — a bare variable, a field, an arithmetic / comparison / logical expression, an array access, or a method call:
name := "World";
i := 41;
"Hello, {$name}!"->PrintLine(); # Hello, World!
"next = {$i + 1}"->PrintLine(); # next = 42
"even? {$i % 2 = 0}"->PrintLine(); # even? false
"upper = {$name->ToUpper()}"->PrintLine(); # upper = WORLD
A String or object that is Nil interpolates as Nil, so a message can show a value that is missing: s : String; "s={$s}"->PrintLine(); prints s=Nil. Only interpolation does this; String->Append(Nil) and + with a Nil operand still raise a Nil error.
Any object can be interpolated or concatenated. It prints through the nearest ToString in its class chain, ending at System.Base->ToString. That method prints a Stringify instance's own ToString, and for any other object the class name and instance ID (Point@0x1f). So a mixed Base[] prints without a cast. Methods with a body are bound statically. To make an object print its own way through a Base reference, implement Stringify rather than only declaring a ToString:
class Point implements Stringify {
@x : Int; @y : Int;
New(x : Int, y : Int) { @x := x; @y := y; }
method : public : ToString() ~ String { return "({$@x}, {$@y})"; }
}
items := Base->New[2];
items[0] := Point->New(1, 2);
items[1] := "text";
each(i : items) { items[i]->ToString()->PrintLine(); }; # (1, 2) / text
Format specifiers
Append :spec to an interpolated expression for precision, width, alignment, or radix (Python / .NET–style):
| Spec | Example | Result |
|---|---|---|
.N — precision | "{$pi:.2}" | 3.14 |
N — min width (right-justified) | "[{$n:5}]" | [ 42] |
0N — zero-pad | "{$n:05}" | 00042 |
<N — left-justify | "[{$s:<6}]" | [hi ] |
>N — right-justify | "[{$s:>6}]" | [ hi] |
x — hexadecimal | "{$v:x}" | 0xff |
b — binary | "{$v:b}" | 11111111 |
| width + precision | "[{$pi:8.2}]" | [ 3.14] |
pi := 3.14159; n := 42; v := 255;
"pi = {$pi:.2}"->PrintLine(); # pi = 3.14
"id = {$n:05}"->PrintLine(); # id = 00042
"hex = {$v:x}"->PrintLine(); # hex = 0xff
String->Format
For a positional template — handy when the format string is reused or built at runtime — use String->Format. Placeholders {0}, {1}, … are replaced by the matching argument's ToString; {{ and }} produce literal braces:
String->Format("{0} = {1}", "x", "y")->PrintLine(); # x = y
String->Format("{1} before {0}", "a", "b")->PrintLine(); # b before a
String and object arguments are passed directly; primitive values are boxed with their holder type (IntRef, FloatRef, …):
use System;
String->Format("n = {0}", IntRef->New(42))->PrintLine(); # n = 42
"{$x}" / "{$x:.2}") is usually more convenient than Format.Building strings manually
String also provides Append, Pad / PadTo, Justify, and Truncate for incremental construction:
buffer := String->New();
buffer->Append("count=");
buffer->Append(7);
buffer->ToString()->PrintLine(); # count=7
"42"->PadTo(5, '0', true)->PrintLine(); # 00042
Unicode
"Καλημέρα κόσμε"->PrintLine();
"こんにちは 世界"->PrintLine();
File System
content := System.IO.Filesystem.FileReader->ReadFile(filename);
content->Size()->PrintLine();
System.IO.Filesystem.File->Size(filename)->PrintLine();
Sockets
socket->WriteString("GET / HTTP/1.1\nHost:google.com\nUser Agent: Mozilla/5.0 (compatible)\nConnection: Close\n\n");
line := socket->ReadString();
while(line <> Nil & line->Size() > 0) {
line->PrintLine();
line := socket->ReadString();
};
socket->Close();
Named Pipes
pipe := System.IO.Pipe->New("foobar", Pipe->Mode->CREATE);
if(pipe->Connect()) {
pipe->ReadLine()->PrintLine();
pipe->WriteString("Hi Ya!");
pipe->Close();
};
Threading with Mutexes
class CalculateThread from Thread {
...
@inc_mutex : static : ThreadMutex;
New() {
@inc_mutex := ThreadMutex->New("inc_mutex");
}
method : public : Run(param : System.Base) ~ Nil {
Compute();
}
method : native : Compute() ~ Nil {
y : Int;
while(true) {
critical(@inc_mutex) {
y := @current_line;
@current_line+=1;
};
...
};
}
}
Structured Concurrency
A TaskScope (nursery) spawns closures as worker Threads. Wrapped in a leaving { scope->JoinAll(); } block it can never exit until every child has joined — the structured-concurrency invariant (no task outlives its scope), on any exit path (normal, early return, or unwind). Library: System.Concurrency (link -lib gen_collect,concurrent).
use System.Concurrency;
scope := TaskScope->New();
leaving { scope->JoinAll(); } # joins all children on every exit path
a := scope->Spawn(FuncRef->New(\() ~ System.Base : () => { return Work(1); })<System.Base>);
b := scope->Spawn(FuncRef->New(\() ~ System.Base : () => { return Work(2); })<System.Base>);
# after the block: a->GetResult(), b->GetResult(); scope->Cancel() for boundary cancel
TaskScope: Spawn(FuncRef<System.Base>)~Task, JoinAll(), Cancel(), IsCancelled(), Size(). Task: GetResult()~System.Base, IsDone()~Bool, DidRun()~Bool. Each Task is a real Thread, so mutator register/unregister and the GC stop-the-world contract are handled for free. Cancellation is cooperative and boundary-only — a lambda body can call static functions but not instance methods, so it can't poll the scope; Task.Run checks cancellation before invoking the body.
Runtime Diagnostics
System.Concurrency.Monitor gives typed access to live VM/GC/thread statistics (thin wrappers over System.Runtime->GetProperty("runtime.*")); all are lock-free reads that do not affect GC behavior.
use System.Concurrency;
Monitor->MajorCollections()->PrintLine(); # GC count
Monitor->LastPauseMicros()->PrintLine(); # last stop-the-world pause (us)
Monitor->ActiveThreads()->PrintLine(); # registered mutator threads
Monitor->MemoryOverhead()->PrintLine(); # RSS - live heap (fragmentation gauge)
Or read keys directly: System.Runtime->GetProperty(key)->ToInt().
| key | meaning |
|---|---|
runtime.memory.used / .peak / .allocated / .max / .overhead | process RSS / peak RSS / live heap / GC threshold / RSS−heap |
runtime.gc.minor / .major / .total | collection counts |
runtime.gc.pause.last_us / .max_us / .avg_us | stop-the-world pause times (µs) |
runtime.gc.nursery.used / .capacity / .occupancy_permille | young-generation fill / limit (--nursery) |
runtime.gc.promoted.last / .total / .bytes / runtime.gc.old.bytes | promotion rate (objects; real bytes copied) / old-gen size |
runtime.gc.remembered / .contention | cross-gen writes since minor GC / collector-lock contention |
runtime.alloc.since_gc | bytes allocated since the last collection |
runtime.threads.active / .parked / .running | mutator threads (.parked includes STW/blocked) |
runtime.gc.stw | 1 while a collection is stopping the world |
runtime.cpu.count / .time / runtime.uptime_ms | logical cores / process CPU ms / uptime |
runtime.call.depth_max | the deepest calls may nest, in frames (--call-depth, default 16384) |
Date/Time Handling
yesterday := System.Time.Date->New();
yesterday->AddDays(-1);
yesterday->ToString()->PrintLine();