Core syntax Static typing

Typing and Constants

Klyn is statically typed from top to bottom. Type inference keeps declarations compact, but the inferred type is still a real compile-time type. This page explains the practical rules that matter when you declare variables, annotate APIs, and rely on constants.

Klyn Is Always Typed
count = 10          # inferred as Int
ratio = 0.5         # inferred as Double
name = "Klyn"       # inferred as String

count = 12          # valid
count = "twelve"    # invalid

Inference is convenient syntax, not dynamic typing. The compiler remembers the type and checks every later assignment against it.

Inference is compile-time work

An inferred variable does not carry a dynamic type tag that changes with each assignment. The compiler resolves its type once, generates code for that type, and rejects incompatible uses before execution.

Explicit Type Annotations
import klyn.collections

count as Int = 10
price as Double = 19.99
names as ArrayList<String> = ["Ada", "Grace"]

Use explicit annotations when the type is part of the design, when a public API must stay self-documenting, or when you want a specific numeric type instead of the default literal type.

In local implementation code, inference is usually the clearest choice. Public properties, method parameters, return values, collection boundaries, and persisted models should normally expose their types explicitly.

Declaration Before Use

A variable must be declared before its first use. Klyn does not create an implicit global with a neutral value merely because a declaration appears later in the file. This keeps name resolution deterministic and catches initialization-order mistakes at compile time.

limit as Int = 10
print(limit)       # valid

print(otherLimit)  # invalid: otherLimit is not declared yet
otherLimit = 20
Numeric Types
Type Width Range or precision Typical literal
SByte 8 bits -128 to 127 SByte(10)
Byte 8 bits 0 to 255 Byte(10)
Short 16 bits -32,768 to 32,767 Short(10)
UShort 16 bits 0 to 65,535 UShort(10)
Int 32 bits -2,147,483,648 to 2,147,483,647 10
UInt 32 bits 0 to 4,294,967,295 10u
Long 64 bits -263 to 263-1 10l
ULong 64 bits 0 to 264-1 10ul
Float 32 bits IEEE 754 single precision 0.5f
Double 64 bits IEEE 754 double precision 0.5

Int and Double are the default integer and floating-point types. Choose another width when the domain, binary format, native ABI, or memory footprint requires it.

No silent narrowing

Klyn rejects assignments that would lose precision or violate the target range. Use an explicit conversion such as Float(value) or UInt(value) when the conversion is intentional.

Numeric Literal Forms
decimal = 1_000_000
binary = 0b1010
octal = 0o755
hexadecimal = 0xFF

unsigned = 42u
wide = 42l
wideUnsigned = 42ul
singlePrecision = 3.14f
doublePrecision = 3.14

Underscores improve readability and do not affect the value. Integer bases may use the same unsigned and width suffixes as decimal integers. Suffix letters are case-insensitive, but lowercase forms are easier to read consistently.

NaN and Infinity

Klyn provides four special floating-point words. They are literals with fixed compile-time types, not variables and not function calls.

Word Type Meaning
nan Double Quiet not-a-number value.
nanf Float Single-precision quiet not-a-number value.
inf Double Positive infinity; write -inf for negative infinity.
inff Float Single-precision positive infinity; write -inff for negative infinity.
assert type(nan) == Double
assert type(nanf) == Float
assert type(inf) == Double
assert type(inff) == Float

assert nan != nan
assert not (nan < 0.0)
assert not (nan >= 0.0)
assert inf > 1.0
assert -inff < 0.0f

# Readable IEEE identity predicates preserve the literal precision.
assert nan is nan
assert nanf is nanf
assert inf is inf
assert -inff is -inff
assert 42.0 is not nan

NaN is unordered: it is unequal to every value, including itself, and every ordered comparison involving NaN returns false. Use is nan or is nanf when testing its category rather than trying equality. The corresponding is inf, is -inf, is inff, and is -inff predicates distinguish the sign and precision of infinity. Runtime division or remainder by zero still raises ZeroDivisionException; use these words when an infinite or NaN value is explicitly part of the model.

Import klyn.math and use Math.isClose() when two floating-point results should be compared with relative or absolute tolerances. Its defaults are rel_tol=1e-9 and abs_tol=0.0; negative tolerances raise ValueException. IEEE special values keep the rules above.

import klyn.math

assert Math.isClose(0.1 + 0.2, 0.3)
assert Math.isClose(0.0, 1e-12, abs_tol=1e-9)
assert not Math.isClose(nan, nan)
assert Math.isClose(inf, inf)
Conversions, Equality, and Hash Keys
small as Float = Float(3.5)
count as UInt = UInt(42)

assert 1l == 1ul
assert 1l == 1.0
assert 0.0 == -0.0

Numeric comparisons are mathematical across primitive numeric types and preserve full integer precision. This does not make differently typed boxed values the same hash key. Maps and sets retain the primitive key type, following the strict key semantics commonly expected from a statically typed runtime.

import klyn.collections

values = HashMap<Object, String>()
values[1l] = "signed"
values[1ul] = "unsigned"

assert values.size == 2
assert values[1l] == "signed"
assert values[1ul] == "unsigned"
const and readonly

const declares a binding that cannot be assigned again. It does not imply that every initializer is evaluated by the compiler, nor that every referenced object becomes deeply immutable. readonly instead describes instance state initialized once or a property that callers can read but cannot set.

Contract const readonly
Primary guarantee The declared binding cannot be reassigned The field is initialized once, or the property has no writable setter
Initialization Mandatory; any type-compatible expression is accepted At declaration or during instance construction
Class member storage Implicitly static Normally per instance; a static readonly property may expose a computed class value
Reference types The reference is fixed; the referenced object may remain mutable A returned reference may still designate a mutable object
Constant Bindings
const PI = 3.141592653589793
const APP_NAME as String = "Klyn"

class ApplicationInfo:
    public const BASE as Int = 40
    public const ANSWER as Int = BASE + 2

def demo():
    const RETRY_COUNT as Int = 3
    print(ApplicationInfo.ANSWER, RETRY_COUNT)

def describe(const value as Int) as String:
    # value = 0                 # Compile-time error: the parameter is fixed.
    return String(value)

A const declaration requires an initializer; a const parameter receives its value from the call and cannot be rebound inside the function. Class constants belong to the type itself, so ApplicationInfo.ANSWER is available without an instance. Do not write static const: static would be redundant and the compiler rejects it. Constant bindings cannot be reassigned, incremented, decremented, or modified through compound assignment.

Value and Reference Semantics
import klyn.collections
import klyn.time

class Dates:
    public const BIRTH_DAY = DateTime(1973, 8, 26)
    public const LABELS = ArrayList<String>()

assert Dates.BIRTH_DAY.year == 1973
Dates.LABELS.add("birthday")        # Allowed: the reference is constant.

# Dates.BIRTH_DAY = DateTime(2000, 1, 1)  # Compile-time error.
# Dates.LABELS = ArrayList<String>()      # Compile-time error.

For a ValueType such as DateTime, the constant denotes the value itself. For a reference type, const has final-reference semantics: the reference cannot change, but the object's public mutation operations remain available. Klyn String values are currently mutable reference objects, so a const String fixes the referenced string and does not provide deep immutability of its contents.

Compile-Time Evaluation Is Independent
class Constants:
    public const ANSWER = 40 + 2
    public const STARTED_AT = DateTime.now

The compiler folds ANSWER because its initializer is a constant expression. STARTED_AT is nevertheless legal: it is initialized exactly once at runtime and stored as a class constant. Whether an initializer can be folded is an optimization decision and does not change the source-level meaning of const.

Read-Only Instance State and Properties
import klyn.collections

class Project:
    private readonly _identifier as Int
    public readonly property files as ArrayList<String>

    public Project(identifier as Int):
        this._identifier = identifier
        this.files = []

project = Project(7)
project.files.add("main.kn")         # Allowed: the property is shallow.
# project.files = []                 # Compile-time error: no setter.

Keep readonly for instance fields that are assigned during construction and for getter-only properties. Use const for a local binding, a parameter, or a class-owned value whose binding must never change.

Null References
message as String = null

if message is null:
    print("no message")
else:
    print(message)

def lengthOf(notnull value as String) as UInt:
    return value.size

Reference values may be null. Use is null and is not null for clear null checks. A notnull parameter expresses and enforces a non-null API contract at the call boundary. The compiler rejects a literal null or a local value proven to be null. When static analysis cannot prove the runtime value, the generated call guard raises TypeError before the function body executes.

Function and Lambda Types
square as (Int) -> Int =
    lambda(value as Int) as Int: value * value

power = lambda(value as Double, exponent as Double) as Double:
    return value ** exponent

assert type(square) == (Int) -> Int
assert type(power) == (Double, Double) -> Double

A function type lists its parameter types before -> and its return type after it. Klyn can infer the variable type from a typed lambda, while an explicit function annotation is useful for callbacks, fields, and public method parameters.

CodeUnit, Char, and Unicode

Klyn separates UTF-16 representation from Unicode text semantics. A CodeUnit is exactly one 16-bit UTF-16 code unit. A Char is exactly one 32-bit Unicode scalar value and can therefore represent a supplementary character without splitting it into a surrogate pair.

Type Literal Storage Use
CodeUnit `K`, `\u03C0` 16-bit UTF-16 code unit UTF-16 buffers, SAX APIs, native interoperability
Char 'K', 'πŸ˜€', '\U0001F600' 32-bit Unicode scalar String indexing, text algorithms, Unicode characters
letter as CodeUnit = `K`
letterScalar as Char = letter
smile as Char = 'πŸ˜€'

assert letterScalar == 'K'
assert smile.toInt() == 0x1F600
assert smile.utf16SequenceLength == 2u

# Narrowing is explicit and only valid for a Char that fits in one UTF-16 unit.
assert CodeUnit(letterScalar) == letter
A surrogate is not a Char

A supplementary character such as πŸ˜€ cannot be stored in one CodeUnit. UTF-16 represents it with a high and low surrogate, while Char stores the complete scalar. Klyn converts CodeUnit to Char implicitly, but validates the code unit: an isolated surrogate raises ValueException. Narrowing a supplementary Char to CodeUnit also raises ValueException.

Strings Are Scalar Text, Not Byte Buffers
text = "AπŸ˜€Ο€B"
bytes as Byte[] = f:[Byte(75), Byte(108), Byte(121), Byte(110), Byte(0), Byte(1)]

assert text.size == 4u
assert text[1u] == 'πŸ˜€'
assert text[1:2] == "πŸ˜€"
assert bytes.size == 6ul

A Klyn String stores zero-terminated UTF-8 text. Its size, indexing, iteration, and slicing operate on Unicode scalar values; indexing therefore returns a Char. The code point U+0000 is reserved as the end marker and cannot be stored inside a string. Use Byte[] for binary payloads, embedded NUL bytes, native buffers, and file or network data that has not yet been decoded as text.

Scalar and UTF-16 views
text = "AπŸ˜€B"

characters = text.codePoints
assert characters.size == 3
assert characters[1] == 'πŸ˜€'

# String indexing is scalar, so assignment expects a Char.
text[0] = 'B'
assert text == "BπŸ˜€B"

# codeUnits exposes the 16-bit UTF-16 compatibility view.
units = text.codeUnits
high as CodeUnit = units[1]
low as CodeUnit = units[2]
assert units.size == 4
assert high == `\uD83D`
assert low == `\uDE00`

codePoints returns a new Char[] snapshot and codeUnits returns a new CodeUnit[] UTF-16 snapshot. The scalar count is available through size; the UTF-16 length is the snapshot's size. Keep that snapshot in a local variable when several code units are needed because repeatedly evaluating either array property would allocate a new array each time.

Character formatting is textual by default. Width and alignment therefore count displayed scalar text rather than formatting the numeric code point.

assert f"[{'Γ©':<3}]" == "[Γ©  ]"
assert f"[{`Γ©`:<3}]" == "[Γ©  ]"
FFI and I/O boundary

Validate the external encoding before creating a String. Keep arbitrary C buffers and protocol frames as Byte[]; converting them to text may reject or truncate data at U+0000 by design.

Generic Types in Declarations
import klyn.collections

names as ArrayList<String> = ["Ada", "Grace"]
config as Map<String, Int> = {"port": 8080}
values as IList<Int> = i:[1, 2, 3]
linked as List<Int> = l:[1, 2, 3]
pair as Tuple<String, Int> = ("age", 42)

Generic arguments are part of the declared type. Literals often infer them automatically, but explicit generic annotations are usually clearer in public APIs. In Klyn, IList<T> is the read-only indexed homogeneous collection contract, List<T> is the mutable indexed contract, Map<K,V> is the non-instantiable associative contract, and Tuple<...Ts> denotes a fixed positional tuple. A map literal supplies a concrete implementation assignable to that contract.

Inspecting a Type Quickly
value = [10, 20, 30]
metadata = type(value)
print(metadata.fullName)

The full reflection API is documented later, but type(...) is already useful when you want to verify what the compiler or runtime considers the real type of a value.

Common Mistakes
  • Expecting inference to allow later assignments of unrelated types.
  • Writing redundant static const instead of a class-level const.
  • Assuming that const recursively freezes a reference type.
  • Confusing a constant binding with an expression that the compiler can evaluate at compile time.
  • Using readonly for a class-owned field instead of const.
  • Relying on implicit numeric narrowing instead of converting explicitly.
  • Testing NaN with equality instead of using its unordered comparison semantics.
  • Assuming mathematically equal values of different primitive types are the same boxed map key.
  • Leaving externally visible properties untyped because β€œthe compiler can infer it anyway”.