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

u8

1 byte

Unsigned 8-bit integer, 0..255.

u16

2 bytes

Unsigned 16-bit integer, 0..65535.

i8

1 byte

Signed 8-bit integer, -128..127 (two’s complement).

i16

2 bytes

Signed 16-bit integer, -32768..32767.

bool

1 byte

Boolean. true is 1, false is 0.

char

1 byte

A character / byte value.

r8

1 byte

Q4.4 fixed-point real number.

r16

2 bytes

Q8.8 fixed-point real number.

void

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:

  • r8 is Q4.4 — 4 integer bits and 4 fractional bits in one byte.

  • r16 is 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.

  • bool and char are byte-sized and interchangeable with u8 in 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.