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.
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.
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.
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.
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
| 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.
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.
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.
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)
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 |
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.
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.
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.
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.
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.
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 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.
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.
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}]" == "[Γ© ]"
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.
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.
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.
- Expecting inference to allow later assignments of unrelated types.
- Writing redundant
static constinstead of a class-levelconst. - Assuming that
constrecursively freezes a reference type. - Confusing a constant binding with an expression that the compiler can evaluate at compile time.
- Using
readonlyfor a class-owned field instead ofconst. - 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β.