Syntax guide
Files
The standard extension is .kofun.
UTF-8 is the standard, and identifiers may use Unicode.
let 合計 = 40 + 2
Two distinct identifier spellings with the same UTS #39 confusable skeleton
in one compilation unit are a hard error (EUNICODE006). This security check
does not change identifier equality or name resolution. Cross-module
confusable collision detection is not implemented. Its accepted resolver
contract checks only each module's effective visible bindings, per semantic
namespace, and will use the distinct hard-error code EUNICODE008; private or
unselected dependency names do not participate. Implementation is tracked by
#1018.
Comments
# line comment
Nested block comments are planned:
/* outer
/* inner */
*/
Bindings
let name = "Ada"
let mut count = 0
let own file = File.open(path)
The default is immutable.
Functions
fn add(left: Int, right: Int) -> Int {
return left + right
}
single-expression form:
fn add(left: Int, right: Int) -> Int = left + right
parameter mode:
fn inspect(read file: File) -> Metadata
fn update(edit file: File) -> Void
fn consume(take file: File) -> Void
The parameter list fixes the function's exact arity. A two-parameter
declaration has callable type (A, B) -> R; it does not accept one argument
and return a partially applied function. Declaration result types continue to
use ->.
Lambdas
Kofun accepts three lambda spellings. The canonical form starts with fn:
fn(x) => x + 1
fn(x: Int, y: Int) => x + y
A block body is planned for the canonical form, and the active compiler does not parse one:
fn(x: Int) {
let squared = x * x
return squared + 1
}
Every lambda the compiler accepts today has an expression body introduced by
=>. A block body is not rejected by name — the parameter list is not
recognised as one, so the parameter never binds and the reader is told about a
symbol instead: fn(x) { return x + 1 } reports
error[E2S35]: unknown lexical binding `x` , and the annotated spelling
above reports the same for Int. An implementation owes a named refusal here
before it owes the feature.
An expression-body lambda may omit fn. Parentheses remain available for
multiple parameters or annotations; a bare lambda has exactly one unannotated
parameter:
(x, y) => x + y
value => value * 2
x => expression is a lambda in expression position and a match arm at an arm
boundary. The parser uses that position, rather than token shape alone, to keep
the two meanings distinct. Call-arguments v1 additionally restricts the lambda
written after a closed call to the canonical fn(...) spelling.
Conditionals
if score >= 90 {
print("A")
} else if score >= 80 {
print("B")
} else {
print("C")
}
if is an expression.
let label = if ready { "ready" } else { "waiting" }
Nullability
let port: Int? = null
let effective = port ?? 8080
nil and None are not keywords.
Lists and tuples
let numbers = [1, 2, 3]
let pair = ("answer", 42)
planned map/set literals:
let counts = { "a": 1, "b": 2 }
let ids = set { 10, 20, 30 }
The literal syntax will be settled by UX testing the ambiguity against blocks. Records are already settled and do not constrain it: record construction uses no braces (see Records).
Operators
Precedence, roughly from highest to lowest.
postfix: call, member, index
unary: ! + -
power: **
multiplicative:* / // %
additive: + -
range: ..
comparison: < <= > >=
equality: == !=
logical: && ||
coalescing: ??
pipeline: |>
assignment: =
Division
// is the integer quotient and takes the floor, so it disagrees with C-style
truncation on negative operands. % is paired with that quotient.
7 // 2 # 3
-7 // 2 # -4, not -3
-7 % 2 # 1, the divisor's sign
/ is not defined on Int. Kofun performs no implicit numeric conversion, so
/ cannot produce a fractional value from two Int operands; 7 / 2 is a
compile error rather than either 3 or 3.5. See docs/TYPE_SYSTEM.md.
Pipelines
let answer = values
|> filter(fn(x) => x > 0)
|> map(fn(x) => x * x)
|> sum()
rewrite rule:
value |> function => function(value)
value |> function(a, b) => function(value, a, b)
Ranges
0 .. 10
The upper bound is exclusive.
planned inclusive range:
0 ..= 10
Loops
for index in 0 .. len(values) {
print(values[index])
}
while queue.len() > 0 {
# ...
}
Pattern matching
planned syntax:
match message {
Quit => stop()
Move(x, y) if x >= 0 => move_to(x, y)
Move(_, _) => error("negative coordinate")
}
Types
Int
Int?
List[Int]
Map[Text, Int]
Result[User, Error]
Int -> Text
(Int, Text) -> Bool
() -> Int
Int -> (Text -> Bool)
Tuple[Int, Text] -> Bool
(read File, take Buffer) -> Result[Response, Error]
-> has the lowest precedence in a type and associates to the right.
Consequently A -> B? means A -> (B?); an optional callable is written
(A -> B)?.
Callable domains have fixed arity:
A -> Rtakes one argument;(A, B) -> Rtakes two arguments;() -> Rtakes no arguments;A -> (B -> R)takes one argument and returns a callable; andTuple[A, B] -> Rtakes one tuple argument.
There is no implicit currying, partial application, or conversion between a
multi-argument callable and a tuple-taking callable. Ownership modes such as
read, edit, and take are part of each callable parameter type. Parameter
names are not part of callable type identity in v1.
The former Fn[...] notation is not an alias. Migration diagnostics must
offer a targeted rewrite from Fn[A, R] to A -> R and from historical
multi-argument forms to a parenthesized fixed-arity domain. Fn is otherwise
an ordinary identifier.
Module constants
A top-level let declares an immutable module constant. The initializer is
exactly one integer literal, with an optional leading -:
let MAX_RETRIES = 3
let ERROR_LOW = -4095
A constant is visible to every function in its compilation unit and lowers to
one file-scope C constant, so it is not a lexical binding: a function-local
let of the same name shadows it inside that scope only. Constants carry no
visibility modifier in this slice, so they stay internal to their compilation
unit and add nothing to KIF.
Four shapes are refused, each naming the constant it is about:
| Written | Diagnostic |
|---|---|
let LIMIT = 1 + 2 | E2S159 — the initializer is not one integer literal |
let helper = 1 beside fn helper() | E2S159 — the name collides with a declaration |
let LIMIT = 1 twice | E2S160 — duplicate module constant |
let mut LIMIT = 1 | E2S161 — a top-level let is immutable |
task module-constants is the gate. Mutable module state, non-Int
constants, and constant expressions are outside this slice.
ADT declarations
The executable Stage 2 C11 checkpoint accepts concrete enums with zero or one
named Int payload:
type Signal =
| Red
| Yellow(code: Int)
| Green
The checkpoint requires explicit local typing (let signal: Signal = Green),
passes and returns enum values through same-typed functions, and supports
exhaustive statement-position matching with Yellow(code), Yellow(_), _,
or a binding catch-all. Type parameters and wider payloads remain planned:
type Result[T, E] =
| Ok(T)
| Err(E)
See spec/enum-match-exhaustiveness.md for the bounded constructor namespace,
coverage, diagnostic, and implementation limits.
Records
Accepted design, gated by a bounded frontend rather than the production
compiler. See spec/records-v1.md.
type Point = {
x: Float,
y: Float,
}
let point = Point(x: 1.0, y: 2.0)
let x = point.x
Construction is always the parenthesized labelled call form. Point { ... } is
rejected, so { never starts an expression and blocks, control-flow
conditions, and the planned map literal cannot collide with a record. Every
declared field is supplied exactly once, in any written order; arguments
evaluate left to right in written order and are stored in declaration order.
Fields are immutable in v1, take moves a whole record, and take point.x is
rejected.
Traits and implementations
planned:
trait Show[T] {
fn show(read value: T) -> Text
}
impl Show[Point] {
fn show(read value: Point) -> Text =
"Point(${value.x}, ${value.y})"
}
Compile-time law declarations
The accepted target separates a law family, a named implementation, and a named check/model request:
law Monad[A, MA] {
operation pure(value: A) -> MA
operation bind(value: MA, next: A -> MA) -> MA
equation left_identity(
value: A in values,
next: A -> MA in functions,
) = bind(pure(value), next) == next(value)
equation right_identity(
value: MA in monads,
) = bind(value, pure) == value
}
impl OptionalBoolMonad: Monad[Bool, Bool?] {
pure = optional_pure
bind = optional_bind
}
check laws OptionalBoolMonadEvidence {
instance = OptionalBoolMonad
domain values: Bool = all
domain monads: Bool? = all
domain functions: Bool -> Bool? = all_functions(values, monads)
equality Bool? = structural
require assurance = proven_finite
budget = standard
}
law and check laws are contextual top-level constructs. operation,
equation, the colon form of impl, domain, equality,
require assurance, and budget are contextual only inside those constructs.
Monad is an ordinary library identifier; monad is not a keyword or a
compiler-known law family.
The schema parameters are first-order slots. Every first executable check
substitutes ground types before evaluation; higher-kinded types are not
required. Equation parameters quantify over named typed domains, and equality
is selected per operand type. all and all_functions require
compiler-certified complete finite carriers.
Law checking is compile-time-only, has an empty effect set, and runs under the
versioned kofun.law-eval/standard-v1 budget. The active compiler does not yet
implement this syntax: it rejects the retained historical law monad examples
with E2S02. See Law system for the full normative target,
assurance levels, evidence identity, and historical migration boundary.
Visibility
The normative contract is
spec/modules/visibility.md. Stage 2 accepts
the four basic forms on top-level functions:
# Public API is intentional. Omission is private.
pub fn create_user(name: Text) -> User
internal fn generate_id() -> UserId
private fn validate_name(name: Text)
fn normalize_name(name: Text) -> Text
internal means the package/build unit defined by the package-root contract,
not a directory or textual module. pub(to ancestor.path) is specified for
restricted module access but is deferred beyond the first executable slice.
There is no protected modifier, and capitalization does not affect access.
The executable syntax slice records visibility, implicit versus explicit
origin, and modifier/declaration spans. A separate top-level declaration gate
now assigns production module-scoped SymbolId values to bounded functions,
ADTs, and constructors, while the identity-only access engine enforces
private, internal, public, and enclosing boundaries for already-resolved IDs.
General imports and public-signature checking are still not routed through the
active compiler. pub, internal, and private remain ordinary identifier
tokens outside a supported declaration-prefix position.
Modules
Manifest source files use an explicit module header under the normative
spec/modules/source-file-mapping.md
contract:
module user.service
The manifest chooses which files belong to the package; the header alone chooses the semantic module path. Paths and directories are not fallback module names. Anonymous single-file builds instead use one synthetic root and do not accept a module header in v1. This is an accepted design contract; the active compiler does not yet parse a general multi-file module graph.
Semantic namespaces
The normative
spec/modules/namespaces.md contract assigns
every named declaration to exactly one of four namespaces:
| Namespace | Examples |
|---|---|
| value | functions, constants, locals, constructors, methods |
| type | nominal types, aliases, type parameters, traits |
| module | declared modules and imported module qualifiers |
| meta | macros, meta functions, and named laws |
The same spelling may coexist across different namespaces but is rejected twice in the same namespace and scope. The syntactic use site chooses one namespace, so lookup never retries another namespace after failing. A selective import may bind all exported meanings of one spelling, one per namespace. Capitalization has no effect on namespace assignment. This is an accepted design contract; the active compiler does not yet implement the general resolver.
Imports and re-exports
The ordinary qualified, qualified-alias, and selective forms below have bounded executable Stage 2 checkpoints. They are not yet routed through the general CLI/package resolver:
import science
import data.csv as csv
from collections import Map, Set
Public forwarding uses the normative
spec/modules/re-exports.md forms:
pub import collections
pub from collections import Map, Set
An ordinary import or module alias never becomes public implicitly. A
re-export preserves the original target identity, creates a distinct
ExportBindingId, and fails if any target/enclosing/signature boundary is less
than public. Re-export aliases, wildcards, internal forwarding, and an
export keyword are not part of the first re-export slice;
pub import ... as ... is rejected.
Semicolons
Not required. Statement boundaries are determined by newlines and }.
Line continuation is either inside a delimiter or made explicit with an operator.
let answer = values
|> map(transform)
|> sum()
The Stage 0 lexer treats a leading pipeline as a continuation as a special case.