Skip to content

std.core.conv

std.core.conv — Type Conversion Intrinsics

Section titled “std.core.conv — Type Conversion Intrinsics”

Profile: :core Spec: SPEC-026

Janus does not silently coerce between integer types. Every conversion is an explicit call. std.core.conv provides three generic conversion functions backed by dedicated LLVM intrinsics. The compiler injects type-specific constants at monomorphization time — no runtime dispatch, no boxing.


as[T] — Widening / Same-Width Pass-Through

Section titled “as[T] — Widening / Same-Width Pass-Through”
func as[T](value: i64) -> T do
@intrinsic("as", value)
end

Zero-cost identity or widening cast. No LLVM instruction is emitted — the value flows through unchanged.

Use when: You want an explicit, readable type annotation at a widening boundary.

func run() !i64 do
let narrow = toInt[u8](200)?
return as[i64](narrow) // widen u8 → i64 explicitly
end

truncate[T] — Unchecked Bit-Mask Truncation

Section titled “truncate[T] — Unchecked Bit-Mask Truncation”
func truncate[T](value: i64) -> T do
@intrinsic("truncate", value)
end

Retains only the low N bits of value, where N is the bit-width of T. Equivalent to value & ((1 << N) - 1). No error on overflow — values wrap silently.

The lowerer injects the bit_width constant from the concrete type at monomorphization:

Typebit_width
u8, i88
u16, i1616
u32, i3232
u64, i6464

Use when: You want intentional wrapping — bitmask operations, hash functions, protocol byte extraction.

func main() do
let masked = truncate[u8](0x1FF) // 511 → low 8 bits = 255
print_int(masked) // 255
let wrapped = truncate[u8](256) // 0x100 → low 8 bits = 0
print_int(wrapped) // 0
end

toInt[T] — Checked Narrowing with Range Enforcement

Section titled “toInt[T] — Checked Narrowing with Range Enforcement”
func toInt[T](value: i64) !T do
@intrinsic("int_cast_checked", value)
end

Narrows value to type T. Returns an error union !T — either the value, or an error if it falls outside T’s representable range.

The lowerer injects min and max from the concrete type’s range:

Typeminmax
u80255
i8-128127
u16065535
i16-3276832767
u3204294967295
i32-21474836482147483647
i64-92233720368547758089223372036854775807
u64018446744073709551615

Use when: You are narrowing from user input, protocol data, or array indices and correctness requires range verification.

? propagation is only valid in functions that return !T. Never use ? on toInt directly inside main()main() has a void/i32 return type and ? would emit an incompatible ret { i8, i64 } in LLVM.

Always wrap in a helper:

func toInt[T](value: i64) !T do
@intrinsic("int_cast_checked", value)
end
func parse_byte(n: i64) !i64 do
return toInt[u8](n)? // OK: this function returns !i64
end
func main() do
let b = parse_byte(200) catch -1
print_int(b) // 200
let bad = parse_byte(999) catch -1
print_int(bad) // -1 (out of range → catch sentinel)
end

Narrowing and widening back is lossless for values within the target type’s range:

func run() !i64 do
let narrow = toInt[u8](200)? // u8, value=200
return as[i64](narrow) // i64, value=200
end
func main() do
print_int(run() catch -1) // 200
end

The :core integer model treats i64 as the universal integer type. Type-specific operations on u8, i16, etc. are always entered through an explicit conversion call. This makes the conversion intent visible at every call site — a direct expression of Syntactic Honesty.

Design: Why Intrinsics Rather Than Zig FFI?

Section titled “Design: Why Intrinsics Rather Than Zig FFI?”

The lowerer injects type constants after monomorphization, before LLVM emission. A regular extern call would receive these as runtime arguments. The intrinsic path lets LLVM emit optimal code — a simple and for truncate, a two-compare bounds check for toInt — with the constants folded at compile time.