Type system¶
ik is strongly and statically typed. Every variable, parameter, and function return has a type fixed at compile time, and the compiler checks that values flow between compatible types. There is no implicit heap, no boxing, and no runtime type information — a type is purely a compile-time description of the bytes a value occupies.
Primitive types¶
Type |
Size |
Description |
|---|---|---|
|
1 byte |
Unsigned 8-bit integer, 0..255. |
|
2 bytes |
Unsigned 16-bit integer, 0..65535. |
|
1 byte |
Signed 8-bit integer, -128..127 (two’s complement). |
|
2 bytes |
Signed 16-bit integer, -32768..32767. |
|
1 byte |
Boolean. |
|
1 byte |
A character / byte value. |
|
1 byte |
Q4.4 fixed-point real number. |
|
2 bytes |
Q8.8 fixed-point real number. |
|
— |
No value. Only valid as a function return type. |
The integer width and signedness directly determine the AVR instructions the
compiler emits, so choosing u8 over u16 where a byte suffices produces
smaller, faster code.
Note
The naming is deliberate: u/i for unsigned/signed integers and r
for the real (fixed-point) types, each followed by its bit width. bool
and char are byte-sized and interchangeable with u8 in most contexts.
Fixed-point reals (r8, r16)¶
ik has no floating-point unit and no float type. Fractional values are
represented in fixed-point:
r8is Q4.4 — 4 integer bits and 4 fractional bits in one byte.r16is Q8.8 — 8 integer bits and 8 fractional bits in two bytes. The stored integer is the real value multiplied by 256.
A source literal with a decimal point (3.14) is converted to this scaled
representation at compile time. Arithmetic on fixed-point values is ordinary
integer arithmetic, except that multiplication and division must rescale; the
std/math — Fixed-point math library provides correctly scaled r16 operations and
the full set of math functions.
Because the format is fixed-point, range and precision are limited. Q8.8
r16 represents roughly -128.0 .. +127.996 in steps of 1/256. There is no
NaN or infinity; the math library’s @isnan/@isinf predicates exist for
API completeness and reflect this (a fixed-point value is always finite).
Per-core support
Fixed-point multiplication and division call a small runtime that requires
the hardware multiplier: a program that multiplies r8/r16 values is
rejected at compile time on cores without MUL (AVRe), and the
fixed-point runtime is not available at all on the reduced AVRrc core.
Arrays¶
An array type is a primitive type followed by a compile-time length in brackets:
ram mut $buf: u8[16] = 0
ram imut $luts: u16[4] = 0
The length is a constant Number; arrays are not dynamically sized. A single
initialiser value fills every element. Indexing with $buf[i] is valid in any
expression position, including as an assignment target, and the index may be a
runtime expression. See Statements for declaration syntax and
Expressions for indexing.
Pointer types¶
A pointer type names both that it is a pointer and the memory space it points into:
ptr ram u8 # pointer to a u8 in SRAM
ptr flash u8 # pointer to a u8 in program memory
ptr eeprom u8 # pointer to a u8 in EEPROM
The pointed-to space (ram, eeprom, flash) is part of the type, which
is how the compiler knows whether a dereference must use a normal load, an LPM
from flash, or an EEPROM access sequence. The pointer variable itself always
lives in SRAM. Pointer declaration syntax is covered in Memory model.
String types¶
A string is a NUL-terminated byte sequence. As a type specifier — for a
parameter or return — only str ram is accepted:
@strlen($s: str ram) -> u16 { ... }
The flash str form exists only as a variable declaration (a string literal
placed in program memory), not as a parameter type. See Memory model.
Function types¶
A function-pointer type is written with the fn keyword, a parenthesised
parameter-type list, and an optional return type:
fn(u8) # takes a u8, returns nothing
fn(u8) -> u8 # takes a u8, returns a u8
fn(u8, u8) -> u8 # takes two u8s, returns a u8
fn() # takes nothing, returns nothing
A function type can be the declared type of a mut/imut variable, and you
populate it with &@name. Calls go through @$var(...). See
Functions.
Type compatibility¶
ik does not silently mix unrelated types. The general rules are:
Integer types of the same width and signedness are compatible.
boolandcharare byte-sized and interchangeable withu8in arithmetic and comparison contexts.Mixing signed and unsigned, or different widths, follows the operator rules in Expressions; be explicit about widths to avoid surprises in wrapping behaviour.
Pointers are only compatible when their pointee type and memory space match.
A function pointer is compatible with a function whose parameter and return types match the
fn(...)type exactly.
Note
Not every type token that appears in the grammar is fully implemented in the current compiler. The toolchain includes a guardrail that rejects programs using a type whose code paths are not yet implemented, with a clear error, rather than miscompiling. If the compiler refuses a type, believe it.
Conversions, literal ranges and truncation¶
Integer values are stored in two’s complement and conversions operate on the raw bit pattern. The exact rules are:
Integer literals must fit the bit width of their context. A literal initializing or being assigned to a typed location is checked against that type’s width at compile time; a value that does not fit in the width at all is a compile error:
ram mut $a: u8 = 300 # error: literal 300 does not fit in type 'u8'
ram mut $b: u16 = 70000 # error: literal 70000 does not fit in type 'u16'
Both interpretations of a fitting bit pattern are accepted, so the two common embedded idioms remain valid:
ram mut $c: u8 = -15 # ok: stores 0xF1 (two's complement)
ram mut $d: i8 = 0xFF # ok: stores -1 (same bit pattern)
Widening is implicit and lossless. Assigning a narrower value to a wider location zero-extends unsigned sources and sign-extends signed sources.
Narrowing is implicit but truncates. Assigning a wider value to a narrower location keeps only the low byte(s):
ram mut $w: u16 = 0x1234
ram mut $n: u8 = 0
$w -> $n # $n becomes 0x34; the compiler warns once per target
Because accidental narrowing is a common source of bugs, a plain ->
assignment that truncates emits a one-time warning per target. Two forms state
the intent explicitly and never warn:
A declaration with the narrower type written at the site — this is the language’s explicit-conversion idiom:
ram imut $lo: u8 = $w # explicit: the u8 is written right here
A mask or shift that provably fits the target width:
$w & 0xFF -> $n # low byte, explicit $w >> 8 -> $n # high byte, explicit $w % 10 -> $n # remainder < 10, fits
Deliberate truncation (e.g. sending the low byte of a 16-bit baud divisor to a UART register) is well-defined; write it in one of the two forms above.