=========== 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 :doc:`/library/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). .. admonition:: 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: .. code-block:: text 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 :doc:`statements` for declaration syntax and :doc:`expressions` for indexing. Pointer types ============= A pointer type names both that it is a pointer and the memory space it points into: .. code-block:: text 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 :doc:`memory`. String types ============ A string is a NUL-terminated byte sequence. As a *type specifier* — for a parameter or return — only ``str ram`` is accepted: .. code-block:: text @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 :doc:`memory`. Function types ============== A function-pointer type is written with the ``fn`` keyword, a parenthesised parameter-type list, and an optional return type: .. code-block:: text 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 :doc:`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 :doc:`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.