Intrinsic Expressions
An intrinsic expression is a builtin that appears in expression position and produces a value.
intrinsic = "@" IDENT "(" [ intrinsic_arg { "," intrinsic_arg } ] ")" ;
intrinsic_arg = expression | type ;
Intrinsics MAY accept expressions, types, or a combination of both as arguments, depending on the specific intrinsic.
Each intrinsic has a fixed signature specifying the number and types of arguments it accepts.
It is a compile-time error to call an intrinsic with the wrong number of arguments.
It is a compile-time error to use an unknown intrinsic name.
Quick Reference
The following tables list every intrinsic the compiler recognizes, grouped by whether the intrinsic may appear in any expression position (expression intrinsics) or only inside a checked block (unchecked intrinsics, specified in §9.2). This inventory is kept in sync with the compiler's intrinsic registry: the pre-interned names in crates/rue-air/src/sema/known_symbols.rs and the dispatch on them in crates/rue-air/src/sema/analysis.rs. A name that is absent from that registry is rejected as an unknown intrinsic (rule 4.13:5), so any intrinsic the compiler accepts MUST appear here.
Expression intrinsics (usable in any expression position):
| Intrinsic | Purpose | Arguments | Return Type |
|---|---|---|---|
@dbg | Print debug output | 1 expression (int, bool, or string) | () |
@size_of | Get type size in bytes | 1 type | i32 |
@align_of | Get type alignment in bytes | 1 type | i32 |
@offset_of | Get a struct field's byte offset | 1 type, 1 field name | u64 |
@intCast | Convert between integer types | 1 expression (integer) | inferred integer type |
@to_string | Format an integer as its decimal String | 1 expression (any integer) | String |
@drop | Run a value's drop glue and consume it (RUE-187) | 1 expression (any type) | () |
@read_line | Read line from stdin | none | Option(String) |
@parse_i32 | Parse string to i32 | 1 expression (String) | Option(i32) |
@parse_i64 | Parse string to i64 | 1 expression (String) | Option(i64) |
@parse_u32 | Parse string to u32 | 1 expression (String) | Option(u32) |
@parse_u64 | Parse string to u64 | 1 expression (String) | Option(u64) |
@random_u32 | Generate random u32 | none | u32 |
@random_u64 | Generate random u64 | none | u64 |
@target_arch | Get target architecture | none | Arch |
@target_os | Get target OS | none | Os |
@import | Import module | 1 expression (string literal) | module type |
Unchecked intrinsics (only valid inside a checked block; see §9.2 for their full semantics):
| Intrinsic | Purpose | Arguments | Return Type |
|---|---|---|---|
@syscall | Direct system call | 1–7 expressions (u64) | i64 |
@raw | const pointer to a place | 1 place expression | ptr const T |
@raw_mut | mut pointer to a place | 1 place expression | ptr mut T |
@field_ptr | mut pointer to a struct field place | 1 field-access expression | ptr mut F |
@ptr_read | Read through a pointer | 1 expression (ptr const T/ptr mut T) | T |
@ptr_write | Write through a pointer | 2 expressions (ptr mut T, T) | () |
@ptr_offset | Pointer arithmetic | 2 expressions (ptr T, integer) | ptr T |
@ptr_to_int | Pointer to integer | 1 expression (pointer) | u64 |
@int_to_ptr | Integer to pointer | 1 expression (u64) | inferred ptr mut T |
@alloc | Allocate a heap block | 1 expression (u64 count) | inferred ptr mut T |
@free | Free a heap block | 2 expressions (ptr mut T, u64 count) | () |
@realloc | Resize a heap block | 3 expressions (ptr mut T, u64, u64) | ptr mut T |
The compiler frontend additionally reserves the names @cast, @panic, and @assert. Their surface syntax is not yet fully stabilized, so they are omitted from the normative inventory above, but they are no longer no-ops (RUE-319):
@panic(msg?)aborts the process. It writespanic: <msg>(or justpanicwhen called with no argument) to standard error and exits with status 101 — the same abort discipline as the@intCastoverflow, division-by-zero, and bounds-check traps.@assert(cond, msg?)evaluates the booleancond. Whencondisfalseit aborts exactly like@panic: with a message it writespanic: <msg>, otherwise it writesassertion failed, and in both cases exits with status 101. Whencondistrueit has no effect.@castis rejected at compile time with a diagnostic directing the programmer to@intCast; it never had a working inference rule and is fully redundant with@intCast. Use@intCastfor integer conversions.
@dbg
The @dbg intrinsic prints a value to standard output for debugging purposes. It borrows its argument: the value is read but not consumed, so a non-Copy argument (such as a String) remains valid after the @dbg call and is dropped by its owner at the end of the enclosing scope, exactly as if the @dbg call had not occurred.
@dbg accepts exactly one argument of integer, boolean, or string type.
@dbg prints the value followed by a newline character.
The textual form @dbg prints for its argument is determined by the argument's type: an integer is printed in base 10, with a leading - when a signed integer is negative and no sign otherwise; a boolean is printed as true or false; a String is printed as its exact bytes, byte-for-byte, with no quoting or escaping (mirroring print, 3.7). The newline of 4.13:8 follows this text.
The return type of @dbg is ().
fn main() -> i32 {
@dbg(42); // prints: 42
@dbg(-17); // prints: -17
@dbg(true); // prints: true
@dbg(false); // prints: false
@dbg(10 + 5); // prints: 15
@dbg("hello"); // prints: hello
0
}
@dbg is useful for inspecting values during development:
fn factorial(n: i32) -> i32 {
@dbg(n); // trace each call
if n <= 1 {
1
} else {
n * factorial(n - 1)
}
}
fn main() -> i32 {
factorial(5)
}
@size_of
The @size_of intrinsic returns the size of a type in bytes.
@size_of accepts exactly one argument, which MUST be a type.
The return type of @size_of is i32.
The value returned by @size_of is determined at compile time.
fn main() -> i32 {
@size_of(i32) // 8 (one 8-byte slot)
}
struct Point { x: i32, y: i32 }
fn main() -> i32 {
@size_of(Point) // 16 (two 8-byte slots)
}
@align_of
The @align_of intrinsic returns the alignment of a type in bytes.
@align_of accepts exactly one argument, which MUST be a type.
The return type of @align_of is i32.
The value returned by @align_of is determined at compile time.
All types in Rue currently have 8-byte alignment.
fn main() -> i32 {
@align_of(i32) // 8
}
@offset_of
The @offset_of intrinsic returns the byte offset of a field within a struct type, mirroring Rust's core::mem::offset_of!.
@offset_of accepts exactly two arguments: the first MUST be a struct type, and the second MUST be the name of one of that struct's fields.
The return type of @offset_of is u64.
The value returned by @offset_of is the offset the compiler assigns to the field under the layout it chooses for the struct, determined at compile time. Because the value comes from the compiler's own layout rather than a hand-computed constant, @offset_of remains correct even if the struct layout is implementation-defined. The offset of a field is the sum of the sizes of all preceding fields under the current layout (§3.6).
It is a compile-time error to apply @offset_of to a non-struct type, or to name a field that the struct does not declare.
struct Mixed { a: i32, b: i64, c: bool }
fn main() -> i32 {
let off_a: u64 = @offset_of(Mixed, a); // 0
let off_b: u64 = @offset_of(Mixed, b); // 8 (a occupies one 8-byte slot)
let off_c: u64 = @offset_of(Mixed, c); // 16 (a and b occupy two slots)
let sum: u64 = off_a + off_b + off_c; // 24
@intCast(sum)
}
@intCast
The @intCast intrinsic converts an integer value from one integer type to another.
@intCast accepts exactly one argument, which MUST be an integer type (any of i8, i16, i32, i64, u8, u16, u32, u64).
The target type of the conversion is inferred from the context where @intCast is used.
It is a compile-time error if the target type cannot be inferred or is not an integer type.
If the source value cannot be exactly represented in the target type, a runtime panic occurs.
fn main() -> i32 {
let x: i32 = 100;
let y: u8 = @intCast(x); // OK: 100 fits in u8
@intCast(y) // Convert back to i32
}
fn takes_u8(x: u8) -> u8 { x }
fn main() -> i32 {
let x: i32 = 50;
takes_u8(@intCast(x)); // Target type inferred from parameter
0
}
// This panics at runtime: 256 doesn't fit in u8
fn main() -> i32 {
let x: i32 = 256;
let y: u8 = @intCast(x); // panic: integer cast overflow
0
}
// This panics at runtime: negative values don't fit in unsigned types
fn main() -> i32 {
let x: i32 = -1;
let y: u32 = @intCast(x); // panic: integer cast overflow
0
}
@read_line
The @read_line intrinsic reads a line of text from standard input.
@read_line accepts no arguments.
The return type of @read_line is Option(String), where Option is the ordinary comptime-generic library enum (enum { Some(T), None }, ADR-0038). The concrete Option(String) type is taken from the surrounding context — a let binding annotation, or the arms of a match on the result — so Option must be in scope (e.g. imported) at the call site. As a special case, when @read_line() is the operand of the ? operator (§4.15), the context supplies no annotation, so the intrinsic instantiates its own Option(String) directly (see rule 4.15:9).
@read_line reads bytes from standard input until a newline character (\n) is encountered or end-of-file is reached.
On a successful read the result is Some(line), where the line String does not include the trailing newline character.
If end-of-file is reached with some data read, the partial line is returned as Some(line).
If end-of-file is reached with no data read, the result is None (this is not an error; a read-until-end-of-input loop terminates by observing None).
If a read error occurs, a runtime panic occurs with the message "input error". (This behavior is documented but not tested, as I/O errors cannot be reliably simulated in portable test environments.)
fn main() -> i32 {
let Opt = @import("std/option.rue").Option(String);
@dbg("What is your name?");
match @read_line() {
Opt::Some(name) => @dbg(name),
Opt::None => @dbg("(no input)"),
}
0
}
Reading every line until end-of-input:
fn main() -> i32 {
let Opt = @import("std/option.rue").Option(String);
loop {
let line: Opt = @read_line();
match line {
Opt::None => break,
Opt::Some(text) => @dbg(text),
}
}
0
}
Integer Parsing Intrinsics
The integer parsing intrinsics convert a string to an integer value.
Each parsing intrinsic returns Option(T) for its target integer type T, where Option is the ordinary comptime-generic library enum (ADR-0038):
@parse_i32returnsOption(i32)@parse_i64returnsOption(i64)@parse_u32returnsOption(u32)@parse_u64returnsOption(u64)
The concrete Option(T) type is taken from the surrounding context (a let annotation, or the arms of a match on the result), so Option must be in scope at the call site. As a special case, when a parsing intrinsic is the operand of the ? operator (§4.15), the intrinsic instantiates its own Option(T) directly (see rule 4.15:9).
Each parsing intrinsic accepts exactly one argument, which MUST be of type String.
The string argument is borrowed, not consumed. The original string remains valid after parsing.
A successful parse yields Some(n). The parsed string is parsed successfully when it matches the following grammar:
integer_string = [ "-" ] digit { digit } ;
digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
Leading minus signs are only allowed for signed types (@parse_i32, @parse_i64); a negative value for an unsigned type is a parse failure (yields None).
The result is None (a recoverable parse failure, not a panic) if:
- The string is empty
- The string contains non-digit characters (other than an optional leading minus)
- The value overflows the target type
- A negative value is parsed for an unsigned type
fn main() -> i32 {
let Opt = @import("std/option.rue").Option(i32);
match @parse_i32("42") {
Opt::Some(n) => n, // returns 42
Opt::None => 0,
}
}
fn main() -> i32 {
let Opt = @import("std/option.rue").Option(i32);
match @parse_i32("-17") {
Opt::Some(n) => n, // returns -17
Opt::None => 0,
}
}
fn main() -> i32 {
let Opt = @import("std/option.rue").Option(i32);
let s = "42";
// String is borrowed, not consumed
let parsed: Opt = @parse_i32(s);
@dbg(s); // s is still valid
match parsed {
Opt::Some(n) => n,
Opt::None => 0,
}
}
// An invalid character is a recoverable failure: `None`, not a panic.
fn main() -> i32 {
let Opt = @import("std/option.rue").Option(i32);
match @parse_i32("12abc") {
Opt::Some(n) => n,
Opt::None => -1, // taken: "12abc" is not an integer
}
}
// A negative value for an unsigned type is a recoverable failure: `None`.
fn main() -> i32 {
let Opt = @import("std/option.rue").Option(u32);
match @parse_u32("-17") {
Opt::Some(n) => @intCast(n),
Opt::None => 0, // taken: "-17" is negative
}
}
@random_u32
The @random_u32 intrinsic generates a random unsigned 32-bit integer.
@random_u32 accepts no arguments.
The return type of @random_u32 is u32.
Each call to @random_u32 returns a non-deterministic value using a platform-provided cryptographically-secure entropy source.
If the platform entropy source is unavailable or fails, a runtime panic occurs.
fn main() -> i32 {
let secret: u32 = (@random_u32() % 100) + 1; // Random number 1-100
@dbg(secret);
0
}
Using @random_u32 in a guessing game:
fn main() -> i32 {
let OptStr = @import("std/option.rue").Option(String);
let OptU32 = @import("std/option.rue").Option(u32);
let secret: u32 = (@random_u32() % 100) + 1; // 1-100
@dbg("Guess the number between 1 and 100!");
let mut guesses = 0;
loop {
let input: OptStr = @read_line();
match input {
OptStr::None => break, // end of input
OptStr::Some(text) => {
match @parse_u32(text) {
OptU32::Some(guess) => {
guesses = guesses + 1;
if guess < secret {
@dbg("Too low!");
} else if guess > secret {
@dbg("Too high!");
} else {
@dbg("You got it!");
break;
}
},
OptU32::None => @dbg("not a number, try again"),
}
},
}
}
@intCast(guesses)
}
@random_u64
The @random_u64 intrinsic behaves identically to @random_u32 but returns a random unsigned 64-bit integer.
@random_u64 accepts no arguments.
The return type of @random_u64 is u64.
fn main() -> i32 {
let large_random = @random_u64();
@dbg(large_random);
0
}
@target_arch
The @target_arch intrinsic returns the target architecture as an Arch enum value.
@target_arch accepts no arguments.
The return type of @target_arch is Arch.
The Arch enum is a built-in enum with the following variants:
Arch::X86_64- x86-64 architectureArch::Aarch64- ARM64/AArch64 architecture
The value returned by @target_arch is determined at compile time based on the compilation target.
fn main() -> i32 {
match @target_arch() {
Arch::X86_64 => 1,
Arch::Aarch64 => 2,
}
}
@target_os
The @target_os intrinsic returns the target operating system as an Os enum value.
@target_os accepts no arguments.
The return type of @target_os is Os.
The Os enum is a built-in enum with the following variants:
Os::Linux- Linux operating systemOs::Macos- macOS operating system
The value returned by @target_os is determined at compile time based on the compilation target.
fn main() -> i32 {
match @target_os() {
Os::Linux => 1,
Os::Macos => 2,
}
}
Combining @target_arch and @target_os for platform-specific code:
fn main() -> i32 {
match @target_arch() {
Arch::X86_64 => {
match @target_os() {
Os::Linux => 99,
Os::Macos => 88,
}
},
Arch::Aarch64 => {
match @target_os() {
Os::Linux => 77,
Os::Macos => 66,
}
},
}
}
@import
The @import intrinsic imports a module from another source file.
@import accepts exactly one argument, which MUST be a string literal specifying the module path.
The return type of @import is a module struct type containing all pub declarations from the imported file.
Module path resolution follows this order:
- Standard library:
@import("std")resolves to the bundled standard library - A file
{path}.ruerelative to the importing file's directory - A directory module: a directory
{path}/containing the facade file_{basename}.rue, which is the module's root
It is a compile-time error if the module path does not resolve to an existing file.
It is a compile-time error to pass a non-string-literal argument to @import.
// math.rue
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn sub(a: i32, b: i32) -> i32 { a - b }
fn helper() -> i32 { 42 } // private, not exported
// main.rue
fn main() -> i32 {
let math = @import("math");
math.add(1, 2) // returns 3
}
Private declarations (those without pub) are not visible to importers:
// main.rue
fn main() -> i32 {
let math = @import("math");
// math.helper() // Error: `helper` is not visible
0
}
The imported module can be bound to any name:
fn main() -> i32 {
let m = @import("math");
m.add(1, 2)
}
Nested paths are supported for importing from subdirectories:
fn main() -> i32 {
let strings = @import("utils/strings");
0
}
It is a compile-time error if a module path resolves to both a file module {path}.rue and a directory module {path}/_{basename}.rue — the import is ambiguous, and neither form takes precedence.
It is a compile-time error to access a member of a module that is not declared in the imported file. Module membership is per-file: even though all compiled files share one global function namespace, a declaration from some other file is not a member of the module and MUST NOT be reachable through it.