Skip to content

Function semantics and generics ​

Functions make parameter, result, and failure effects visible in their signature.

Function declarations ​

ts
function greet(name: string): string {
  return "Hello, " + name;
}

The general shape is:

text
function name [<type parameters>] (parameters) : ResultType { body }

Parameters and results are explicit. void means the function produces no ordinary value. A non-void function must return on every continuing path.

Parameters are local bindings ​

Parameters are initialized mutable local bindings. Reassigning a value parameter never changes the caller's binding. Shared effects come from reference-bearing values or explicit pointers.

ts
function rename(user: User): void {
  user.name = "Aki"; // class instance is shared
}

function move(point: Point): Point {
  point.x++;
  return point; // caller receives a changed copy
}

Return ​

ts
function absolute(value: int): int {
  if (value < 0) { return -value; }
  return value;
}

return; is the void form. return expression; must be assignable to the result type. Returned structs/arrays/objects copy; slices/maps/classes/pointers retain their documented reference-bearing behavior.

Arrow functions ​

ts
const double = (value: int): int => value * 2;
const clamp = (value: int): int => {
  if (value < 0) { return 0; }
  return value;
};

Arrows have statically checked function types. Captured bindings use lexical scope; captures do not create a JavaScript runtime or dynamic closure environment beyond the generated Go closure.

Arrow definitions and main ​

Arrow-style entry points and function definitions are supported:

ts
import go { Println } from "fmt"

export const factorial = (value: int): int => {
  if (value <= 1) { return 1 }
  return value * factorial(value - 1)
}

const twice: (value: int) => int = (value) => {
  return value * 2
}

const main = () => {
  Println(factorial(5), twice(21))
}

Running this example prints 120 42. A module-level const initialized directly with an arrow, with an inferred or unnamed function type, is a callable declaration. It emits an ordinary Go function and can be exported with export const. Its name follows the same Go capitalization rules as function. main must have no parameters and return void; const main = () => { ... } supplies that result context automatically.

Calls to later module-level arrows infer their results by following declaration dependencies, including later inferred globals. Definition order does not change the inferred result or reorder runtime initialization:

ts
import go { Println } from "fmt"

const answer = twice(21)
const read = () => later
const twice = (value: int) => value * 2
const later = 42

const main = () => {
  Println(answer, read())
}

This prints 42 42. Dependencies use their own module's lexical scope, not the locals of the function that references them. This also works for explicitly imported module-level arrows.

Give a callable declaration an explicit result annotation, or a complete function-type annotation on the binding, to break a recursive result-inference cycle. Calls before a declaration do not otherwise require result annotations. Its body can refer to later globals. For example, const twice: (n: int) => int = (n) => { return n * 2 } declares the whole signature without repeating it on the arrow.

Callable declarations cannot be reassigned or addressed with &. Since v0.4.0, Go consumers receive a function rather than an assignable function variable. let bindings remain function storage that can be reassigned. An explicit named Go function type retains its named storage/type contract; local arrows continue to be lexical function values.

main is not a keyword: parameters and local constants, variables and functions may use that name. A top-level arrow named main in an explicitly selected input file must be a const with no parameters and a void result. Since v0.4.3, an imported module's main is consistently an ordinary module-local binding, including data and value-returning functions. It does not become the importing application's entry point; the application must declare its own entry. Importing main and declaring another main in the same source module is still a duplicate binding; a re-export alias can give the imported symbol a different name.

Local recursive arrows ​

Local const and let arrows can call themselves when the binding has a complete function type or the arrow has explicit parameter and result types:

ts
import go { Println } from "fmt"

const main = () => {
  const factorial: (n: int) => int = (n) => {
    if (n <= 1) { return 1 }
    return n * factorial(n - 1)
  }

  let step = (n: int): int => {
    if (n == 0) { return 1 }
    return step(n - 1) + 1
  }
  const saved = step
  step = (n: int): int => 10

  Println(factorial(5), saved(2))
}

This prints 120 11. A recursive call reads the same binding as any other use: after a let is reassigned, even a previously saved closure sees the replacement through that name. const still rejects reassignment. Captures remain valid when a closure is returned from its enclosing function.

A direct arrow initializer sees its own binding, shadowing an outer binding of the same name. An arrow parameter or inner local can shadow that name in turn. Other initializers keep their existing scope: const n = n + 1 inside a nested block still reads an outer n.

An inferred recursive result requires an annotation.

Recursive loop-initializer arrows ​

A three-clause for initializer can also declare a recursive arrow:

ts
import go { Println } from "fmt"

function main(): void {
  for (const factorial = (n: int): int => {
    if (n <= 1) { return 1 }
    return n * factorial(n - 1)
  }; factorial(0) == 1;) {
    Println(factorial(5))
    break
  }
}

This prints 120. The closure is created once before the first condition is evaluated. As in other three-clause loops, each iteration has its own binding; the next binding copies the previous value before the post statement runs. An escaped closure keeps the binding from the iteration where it was created. In particular, the initializer's self-reference retains the first iteration's binding; copying the function into later iterations does not rebuild it or retarget its captures. A let can be reassigned, while const remains immutable. The loop binding is not visible after the loop.

Local mutually recursive arrow groups ​

Consecutive direct arrow declarations form a group. Each arrow body can refer to any peer in the group, including later declarations:

ts
import go { Println } from "fmt"

function main(): void {
  const even = (n: int): boolean => {
    if (n == 0) { return true }
    return odd(n - 1)
  }
  const odd = (n: int): boolean => {
    if (n == 0) { return false }
    return even(n - 1)
  }

  Println(even(8), odd(8), replace())
}

function replace(): int {
  const first = (): int => second()
  let second = (): int => 1

  second = (): int => 7
  return first()
}

This prints true false 7. Both const and let participate; a peer reference reads the same storage, so replacing a let also changes what earlier or escaped closures call.

Group names shadow outer bindings throughout the group, including in earlier arrow bodies. Arrow parameters and inner locals can shadow a peer in turn. Recursive or forward-referenced local arrows must not share a name with a type, type parameter, or Go package namespace; rename the local binding if diagnosed. Closure storage is prepared before the group and closures are initialized in source order. Only direct arrow declarations participate: comments and blank lines do not break a group, but a call, assignment, ordinary variable initializer, label, or other statement does. A body cannot use this feature to capture a later ordinary local, and calls before a group do not see its declarations.

Keep mutually recursive definitions together, and call or publish them after the group. This avoids exposing a not-yet-initialized peer.

Local forward result inference ​

Within a group, results can also be inferred through references to later peers:

ts
import go { Println } from "fmt"

function main(): void {
  const offset = 1
  const first = (offset: string) => last(21)
  const last = (value: int) => value * 2 + offset

  Println(first("shadow"))
}

This prints 43. The later function captures the group's offset, not the earlier function's same-named parameter. Each body is checked once in that shared lexical environment; runtime initialization order does not change. Generic type parameters, receiver access, nested closures, and capture-write checks are preserved. Parameters still need annotations or a matching function type on the binding. A cycle whose result cannot be determined requires an explicit result or binding function type to break the inference dependency.

Contextual types and block results ​

When a binding, field, callback parameter, assignment, or return position supplies a matching function type, arrow parameters can omit their type annotations and the result annotation can be omitted for either body form:

ts
function apply(value: int, transform: (n: int) => int): int {
  return transform(value)
}

const result = apply(21, (n) => { return n * 2 })

The parameter count and rest-parameter shape must match the expected function type. Without such a context, parameter types are explicit. An explicit arrow annotation is still checked against the expected function type.

Without a result context, a block arrow infers its result from its returns: the first return establishes the default value type and subsequent returns must be assignable to it. A block with no value returns is void. Mixed bare/value returns, incompatible results, and inference from nil/null require a correction or an explicit result annotation. Non-void arrows must return on every continuing path. Nested arrows have independent return inference; try/finally keeps the enclosing arrow's inferred result and cleanup behavior.

Callbacks ​

ts
function apply(value: int, transform: (value: int) => int): int {
  return transform(value);
}

const result = apply(21, (value: int): int => value * 2);

Generic callback inference ​

Direct arrow arguments can also omit parameter types when a generic call supplies their context, including imported Go generic functions:

ts
import go { Println } from "fmt"
import go { IndexFunc } from "slices"

function transform<T, U>(value: T, convert: (item: T) => U): U {
  return convert(value)
}

const main = () => {
  const doubled = transform(21, (n) => n * 2)
  const position = IndexFunc([10, 20, 30], (n) => n == 20)
  const length = transform("yes", (text) => { return len(text) })
  Println(doubled, position, length)
}

This prints 42 1 3. Other arguments, explicit type arguments, callback signature annotations, and dependent constraints supply the callback's input types. The callback may appear before the argument that determines its input type. An inferred callback result can determine another type parameter, including the input type of another callback in the same call. Generic methods and variadic callbacks use the same rules.

Ready typed callbacks take precedence over defaulting untyped numeric constants. If a callback needs a numeric default to determine its input type, that default unlocks checking its body. Runtime argument order and single evaluation do not change; creating an arrow does not execute its body.

Inference does not guess parameter types from operations inside a body. If no argument or annotation supplies an input type, or callbacks depend on each other without an initial type, add a parameter annotation or explicit type arguments. Constraint failures, incompatible results, numeric overflow, and unsafe nullable captures remain errors.

Imported Go callbacks connect when the generated function shape is representable. Parameter/result types, variadic status, and named Go type identity must match.

Variadic parameters ​

ts
function sum(prefix: int, ...values: int[]): int {
  let total = prefix;
  for (const value of values) { total += value; }
  return total;
}

The rest parameter must be final and has slice type inside the function. Call with individual arguments or one final spread slice:

ts
sum(10, 1, 2);
sum(10, values...);

Methods, constructors, interfaces, arrows, and function types use the same rule.

The expanded slice element type must match the rest parameter:

ts
function sum(...values: int[]): int { return len(values); }

function invalid(values: string[]): int {
  return sum(values...);
}

Generic functions ​

ts
function identity<T>(value: T): T { return value; }
function pair<T, U>(left: T, right: U): { left: T, right: U } {
  return { left: left, right: right };
}

Calls may infer all type arguments:

ts
const value = identity("hello");

Or supply a leading partial/full list:

ts
identity<string>("hello");
identity[string]("hello");
pair<int>(1, "one");

Every uninferred parameter must be supplied. An uninstantiated generic function cannot be stored as a function value.

A value-returning body must end in a terminating statement, as required by Go. Remove unreachable statements after a final return. A switch or select that can exit through break does not by itself prove that a value is returned.

Constraints ​

ts
function choose<T extends comparable>(value: T, fallback: T): T {
  if (value === fallback) { return fallback; }
  return value;
}

comparable follows Go comparability, including contained array/struct fields. Slices, maps, and functions do not satisfy it. A source declaration such as constraint Numeric = ~int | ~float64 names a union of permitted types. Constraint references can be reused in other unions; overlapping terms and cycles are rejected.

Constraint terms that need a source struct's value storage before it is finalized are currently diagnosed, including generic struct instances. Imported Go concrete types do not have this source declaration-order limitation.

Recursive source bounds whose array/struct comparability depends on the same unresolved parameter are currently diagnosed. For example, with constraint Pair<E> = comparable & ~[1]E, avoid T extends Pair<T>; use an independent E extends comparable and T extends Pair<E> where appropriate. Recursive pointer terms and method contracts do not have this restriction.

Generic constraints can describe collection element relationships:

ts
constraint Slice<E> = ~E[];
function size<S extends Slice<E>, E>(values: S): int {
  let count = 0;
  for (const _ of values) { count++; }
  return count;
}

Bounds may refer to later parameters. Calls infer dependent parameters from typed arguments and constraints before defaulting untyped numeric constants. Mixed untyped numeric arguments select a common numeric kind, while every actual constant must remain representable in the inferred type. T(value) explicitly converts to an in-scope type parameter when its bound permits the conversion.

Intersect source type sets with & to accept only types satisfying both:

ts
import go { Println } from "fmt"

constraint Number = ~int | ~int8
constraint Scalar = ~int | ~string
constraint Integer = Number & Scalar
type Score = distinct int
constraint OnlyScore = Integer & Score

constraint Storage<E> = ~E[]
constraint Slice<E> = Storage<E> & ~E[]

function twice<T extends Integer>(value: T): T {
  return value * 2
}

function next<T extends OnlyScore>(value: T): T {
  return value + 1
}

function copy<S extends Slice<E>, E>(values: S): E[] {
  let result: E[] = []
  for (const value of values) { result = append(result, value) }
  return result
}

const main = (): void => {
  Println(twice(6))
  Println(next(Score(7)))
  Println(len(copy([1, 2, 3])))
}

The program prints 12, 8, and 3. An exact term such as Score narrows ~int to that one nominal type. Intersections emit separate Go interface embeddings, and can be reused through imports, re-exports, and export aliases. Matching generic collection terms retain element inference and nullable types.

A declaration uses either | or &; name intermediate constraints to combine the operators. Empty intersections and conflicting nullable shapes are rejected. Since v0.4.2, intersections can discard terms whose shapes cannot overlap under any substitution. For example, intersecting writable and readable channel bounds keeps only bidirectional channels. Slices and arrays, different array lengths, channel directions, and distinct nominal declarations can also be distinguished, including stable mismatches inside collection elements:

ts
import go { Println } from "fmt"

constraint Writable<E> = ~GoChannel<E> | ~GoSendChannel<E>
constraint Readable<E> = ~GoChannel<E> | ~GoReceiveChannel<E>
constraint Duplex<E> = Writable<E> & Readable<E>

function roundTrip<E, C extends Duplex<E>>(channel: C, value: E): E {
  channel <- value
  return <-channel
}

constraint Storage<E> = ~E[] | ~[2]E
constraint Slice<E> = Storage<E> & ~E[]

function first<E, S extends Slice<E>>(values: S): E {
  return values[0]
}

function main(): void {
  const channel = goChannel<int>(1)
  Println(roundTrip(channel, 42))
  Println(first([7, 8]))
}

If terms might overlap after substitution, such as E[] and int[], they remain rejected; instantiate those operands with concrete types first:

ts
// E could become int, so the intersection cannot be normalized yet.
constraint Unresolved<E> = ~E[] & ~int[]

The expanded union limit remains 100 terms, but repeated & operands do not consume that union limit. Constraints cannot be stored as runtime values.

This intersection has no common types:

ts
constraint Impossible = ~int & string

Ordinary Go interfaces can contribute methods to a constraint:

ts
import go fmt from "fmt"
import go strconv from "strconv"

constraint Printable = ~int & fmt.Stringer
type Score = distinct int

public function string(this: Score): bstring {
  return strconv.Itoa(int(this))
}

function show<T extends Printable>(value: T): bstring {
  return (value + value).String()
}

const main = (): void => {
  fmt.Println(show(Score(21)))
}

This prints 42. Both the underlying integer type and the String() method are required. constraint Stream = io.Reader & io.Closer combines two method interfaces; constraint Named = fmt.Stringer names just a method contract. Generic Go interfaces can be composed too, for example constraint Access<E> = api.Getter<E> & api.Setter<E>. Calls retain Go's method capitalization, and method values and dependent type inference are supported.

Repeated identical methods are valid; conflicting signatures are errors. Interfaces with methods cannot be operands of |, including via a named constraint. Go's private methods retain their package identity and cannot be called from Kinmokusei. An argument must satisfy all method requirements when the constraint is instantiated; declaring a constraint does not guarantee that an implementor exists.

Nullable or native-only arguments in Go method signatures are diagnosed if conversion would lose source type information, including through later generic substitutions. Existing collection-only nullable inference is unchanged.

Use &, not a union, to combine method contracts:

ts
import go io from "io"
constraint Invalid = io.Reader | io.Closer

Reusing Go type sets ​

You can narrow an imported constraint, then reuse it in functions and other constraints:

ts
import go { Ordered } from "cmp"
import go fmt from "fmt"

constraint Integer = Ordered & ~int
constraint Key = comparable

function twice<T extends Integer>(value: T): T {
  return value * 2
}

function equal<T extends Key>(left: T, right: T): boolean {
  return left === right
}

const main = (): void => {
  fmt.Println(twice(21), equal("hello", "hello"))
}

This prints 42 true. Imported generic collection constraints also compose: constraint Items<E> = api.Slice<E> & ~E[]. The element type remains available for inference and null checks. Go method interfaces can be combined with these type sets using &.

An explicit comparable requirement survives constraint reuse. It may be named on its own or combined with &, but cannot appear in a multi-term | union, including indirectly through an imported or source constraint. Empty intersections are errors. Native source interfaces are not constraint operands.

For example, an ordered type cannot also have boolean as its underlying type:

ts
import go cmp from "cmp"

constraint Invalid = cmp.Ordered & ~boolean

Generic named types ​

ts
struct Page<T> {
  public items: T[];
  public function size(): int { return len(this.items); }
}

const page: Page<string> = Page<string> { items: ["one", "two"] };

Classes, structs, interfaces, and defined types may have type parameters. Named type positions require full explicit instantiation. Methods may use the enclosing parameters and introduce separate method-local parameters. Those methods lower to standalone Go helpers; they are excluded from virtual dispatch and Go interface method sets.

Generic class inheritance, virtual methods using class parameters, static methods, and generic aliases are available. Class type parameters and generic method parameters must not hide the enclosing type name in generated helper signatures. Instance properties can use class parameters; static fields and accessors are shared across instantiations and cannot use them. See properties and shared class storage.

Abstract classes and dependency injection ​

Since v0.4.0, an abstract class can share state and concrete methods while requiring descendants to implement selected methods. It is also a type for parameters, fields, results, and dependency injection:

ts
import go fmt from "fmt"

abstract class Repository<T> {
  public abstract function find(id: int): T
  public function first(): T { return this.find(0) }
}

class Names extends Repository<string> {
  public override function find(id: int): string { return "Kinmokusei" }
}

class Service {
  constructor(private repository: Repository<string>) {}
  public function run(): string { return this.repository.first() }
}

function main(): void {
  const service = new Service(new Names())
  fmt.Println(service.run())
}

Abstract methods have no body and are implicitly virtual. They must be public or protected; implementations require override with the same visibility and full signature. Abstract intermediate classes may retain unresolved methods or use abstract override to require a new implementation. Class generics are supported, but virtual methods cannot have their own type parameters. Static/final abstract methods and final abstract classes are rejected.

Every concrete class must implement all inherited abstract methods. Abstract classes cannot be instantiated, including those with no abstract methods:

ts
abstract class Repository {
  public abstract function find(id: int): string
}

function main(): void {
  const repository = new Repository()
}

Interfaces remain state-free method/accessor contracts; abstract classes may additionally own state and behavior and use single class inheritance. An abstract class declaring implements must explicitly declare (or inherit) each required method signature. super cannot access an abstract method without a base implementation.

Constructors keep phase-local virtual dispatch. Direct abstract-method access on this in a constructor is an error. Indirect access through a helper or callback to an unimplemented slot during construction panics; call abstract-dependent behavior after construction. Generated Go has no public constructor factory for abstract classes. Manually created zero-value Go structs do not establish source class invariants and also panic on unimplemented abstract slots.

Multiple results ​

Source functions can declare separate results, just as imported Go functions expose their actual result list:

ts
import go { Println } from "fmt"

function pair(): (int, boolean) {
  return 7, true
}

function forward(): (int, boolean) {
  return pair()
}

function describe(value: int, present: boolean): int {
  if (present) { return value + 1 }
  return 0
}

const main = () => {
  const [value, present] = forward()
  Println(value, present, describe(pair()))
}

This prints 7 true 8. Destructuring remains function-local; source result signatures and forwarding do not enable top-level multiple bindings.

Methods, interface signatures, arrows, and function types can also declare a result list. A multiple-result call expands only when it is the sole argument of a call; its result types and count must match the parameters. It cannot be combined with additional arguments or stored as one tuple value.

Imported Go results use the same explicit binding rules:

ts
const [value, err] = strconv.Atoi(text);

There is no hidden error discard and no tuple wrapper. Bind every result or use _ explicitly, or forward a matching result list. Use Result<T> and ? when the signature should expose a checked error effect instead of an ordinary list.

Result functions ​

Result<T> is a return effect, lowering to (T, error); Result<void> lowers to error. The Result itself cannot be stored, nested, or used as a field/parameter. ok, fail, and ? make the result paths explicit.

A result-producing function therefore advertises the effect at the boundary:

ts
function validatePort(value: int): Result<int> {
  if (value < 1 || value > 65535) {
    return fail(errors.New("port out of range"));
  }
  return ok(value);
}

Result-returning function values ​

Since v0.4.0, a function returning Result is an ordinary value. Store it in a binding, pass it as a callback, return a closure, or use it in fields and collections. The callback's type keeps the error path visible to its callers:

ts
import go { Println } from "fmt"

function apply(value: int, transform: (value: int) => Result<int>): Result<int> {
  const transformed = transform(value)?
  return ok(transformed)
}

function main(): void {
  const [value, err] = apply(21, (n) => { return ok(n * 2) })
  if (err !== nil) {
    Println(err)
    return
  }
  Println(value)
}

The matching callback context supplies the arrow's parameter and Result types. Without such a context, write an explicit return annotation, such as (value: int): Result<int> => { return ok(value); }. Result arrows require a block body and explicit returns; their call results must still be propagated with ?, split into value/error bindings, or forwarded by return.

Native aliases and defined function types can carry this signature, including generic payloads. Explicitly annotating an ABI-compatible Go function also works: const parse: (text: string) => Result<int> = strconv.Atoi;. This adds no wrapper and forwards the original Go values and error. An unannotated Go function keeps its raw Go result list.

Methods as functions with receivers ​

Class methods use implicit reference this. Struct methods default to a value receiver and may declare pointer function for shared mutation. External receiver syntax keeps a native named type's methods near other functions:

ts
public function label(this: UserID): string {
  return "user:" + string(this);
}

The receiver type must be declared in the same module; this is not extension syntax for imported packages.

Structs, classes, and interfaces applies these call rules to value receivers, object identity, and contracts.

Kinmokusei is a pre-1.0 project. Documentation describes implemented behavior.