Skip to content

KodaScript Semantic Typing Notes

This document is the current implementation-facing contract for KodaScript static typing. It is intentionally lean. It exists to guide compiler/type-checker work without freezing the full language design too early.

Repository boundary:

  • pure language/compiler implementation belongs in kodascript-core/
  • server/runtime integration belongs in server-node/

New language/compiler features should land in kodascript-core first, and only touch server-node when host/runtime integration is actually required.

Scope

This document covers:

  • internal compile-time type forms
  • assignability rules
  • async / await typing
  • statically-known API surface used by the semantic checker
  • current boundaries of compile-time checking

This document does not define the full language grammar or runtime behavior in exhaustive detail.

Internal Type Forms

The semantic checker currently reasons about these internal type categories:

  • Primitive types: int, long, float, double, bool, char, string, void
  • Array types: T[]
  • API canonical types: OSApi, FileSystem, Window, Node, FileNode, FolderNode, DriveNode, AppNode, ProcessInfo, UserInfo, GroupInfo, ServiceInfo, Connection, ProbeResult, ComponentInfo
  • built-in exception object type: Exception
  • runtime exception subtype names such as RuntimeError, TypeError, PermissionError, AuthenticationError, FileSystemError, NetworkError, TimeoutError, and IndexError
  • User class types: class name such as Player
  • null
  • unknown
  • Awaitable<T> for async expressions

Awaitable<T> is compiler-internal only. Players do not write it directly.

Async Typing

Async expressions produce Awaitable<T> at compile time.

Examples:

  • Device.Connect(...) -> Awaitable<Connection>
  • Device.Probe(...) -> Awaitable<ProbeResult>
  • os.GetProcesses() -> Awaitable<ProcessInfo[]>
  • os.Restart() -> Awaitable<bool>
  • term.GetOutput() -> Awaitable<string>

Applying await to Awaitable<T> yields T.

Using await on a non-awaitable expression is a compiler error.

Using an Awaitable<T> where T is required is a compiler error.

Examples:

kodascript
Connection conn = Device.Connect(ip, 22, user, pass);      // compile error
Connection conn = await Device.Connect(ip, 22, user, pass); // OK

var conn = Device.Connect(ip, 22, user, pass); // inferred as Awaitable<Connection>
conn.GetFileSystem(); // compile error

Assignability

Current assignability rules:

  • exact type matches are allowed
  • primitive widening conversions are allowed using existing numeric rules
  • null is assignable to reference-like types in current practice
  • API subtype to API supertype assignment is allowed using the API type registry
  • array assignment requires compatible element types
  • Awaitable<T> is not assignable to T

The checker does not currently model general union types.

Statically-Known API Surface

The semantic checker uses a minimal API signature registry.

Current purpose:

  • determine whether a known API/class member exists
  • infer return types for known calls
  • mark async calls as Awaitable<T>
  • reject obvious missing-await mistakes
  • reject deterministic assignment mismatches such as:
kodascript
OSApi os = Device.GetOS();
ProcessInfo[] procs = os.GetFileSystem(); // compile error

Current non-goals:

  • exhaustive argument-type validation for every API method
  • full static modeling of all dynamic library exports
  • full union/error-string typing

Dynamic library note:

  • await Device.Load("/path/to/lib.so") is compile-time unknown
  • the compiler validates the Load(...) call boundary only
  • exported library members remain runtime-driven until the language gains library signatures, headers, or import metadata

Current Compile-Time Guarantees

The compiler/type-checker should catch:

  • missing await when assigning async results into concrete types
  • member access on awaitable values before await
  • calls to nonexistent methods on statically-known API types
  • calls to nonexistent methods on statically-known user class types
  • field/property access on statically-known API data objects such as ProcessInfo, UserInfo, GroupInfo, and ServiceInfo
  • deterministic return-type mismatches from statically-known calls
  • statically-known members on caught Exception values in catch (Exception ex)
  • statically-known members on typed caught exception values such as catch (RuntimeError ex)
  • catch-filter expressions in catch (Exception ex) when (...), type-checked in the catch scope

Runtime checks still remain as a backstop.

Exceptions And Catch Filters

Script catches only handle normalized KodaScript runtime exceptions. Unexpected internal engine errors still bypass script catch blocks and terminate the script.

Current exception hierarchy:

  • Exception
  • RuntimeError : Exception
  • TypeError : RuntimeError
  • PermissionError : RuntimeError
  • AuthenticationError : RuntimeError
  • FileSystemError : RuntimeError
  • NetworkError : RuntimeError
  • TimeoutError : RuntimeError
  • IndexError : RuntimeError

This means catch (RuntimeError ex) acts as a broad script-runtime catch, while catch (PermissionError ex) only handles permission-class failures.

C#-style catch filters are supported:

kodascript
try {
    int x = 1 / 0;
} catch (Exception ex) when (ex.Message.Contains("Division")) {
    Print(ex.Message);
}

Filters are evaluated in catch order after type matching. The first catch whose type matches and whose when (...) filter evaluates truthy runs.

Scripts can also throw explicit built-in exception objects:

kodascript
throw new FileSystemError("File not found", "E_FILE_NOT_FOUND");

Current constructor shape:

  • new Exception(message)
  • new Exception(message, code)
  • same for RuntimeError, TypeError, PermissionError, AuthenticationError, FileSystemError, NetworkError, TimeoutError, and IndexError

Errorable Returns

Some APIs intentionally return branchable domain results such as bool | string, object-or-string, or object-or-null.

Current approach:

  • keep deterministic compile-time checks strict where the signature is fully known
  • keep runtime checks as the backstop for errorable/union-like returns
  • allow the signature registry to carry lightweight metadata about errorable calls in preparation for a later union phase
  • emit warning W008 when a known errorable call is assigned directly into a concrete declared type without prior branching or checking
  • emit warning W009 when a previously stored errorable value is later used or assigned as if it were already narrowed
  • reject user-method return statements at compile time when the declared return type is concrete but the returned expression is still statically known to be nullable or errorable

Current metadata fields in the signature registry:

  • returnType
  • mayReturnNull
  • mayReturnErrorString
  • errorCodes

This avoids overfitting the language too early while keeping room for a future explicit result/union model.

Example:

kodascript
OSApi os = Device.GetOS();
bool started = await os.StartService(8080, "/c/home/alice/server.b");

StartService() is now success-or-throw rather than bool|string, so it no longer participates in W008 string-risk analysis.

For user-defined method returns, deterministic risk is stricter than ordinary local use:

kodascript
public async Window Open(OSApi os) {
    return await os.OpenNode("/c/secret.txt"); // compile error
}

That return expression is still known to be Window | null, so it cannot satisfy a concrete Window return type without narrowing first.

Branch-local narrowing is now recognized for simple checks such as:

kodascript
var conn = await Device.Connect("10.0.0.5", 22, "admin", "pass");
if (conn != null) {
    string name = conn.GetName(); // no W009 in this branch
}

Device.Connect(...) remains a nullable API for reachability/service absence, but invalid credentials now throw AuthenticationError instead of being modeled as a nullable-or-string result.

It also recognizes nullability guards for nullable object results:

kodascript
var win = await os.OpenNode("/c/secret.txt");
if (win != null) {
    string title = await win.GetTitle();
}

if (win is Window) {
    string title = await win.GetTitle();
}

if (win == null) {
    Print("blocked");
} else {
    string title = await win.GetTitle();
}

Explicit Non-Goals For This Phase

This phase does not fully solve:

  1. Static member validation for the entire language and runtime
  2. Tooling/IDE display of inferred var types
  3. Union return types such as ProcessInfo[] | string
  4. Static typing of dynamically loaded .so library exports
  5. Full overload resolution and argument-type validation everywhere

Phase 2 Direction

The intended next expansion is:

  1. full member existence validation
  2. argument count and argument type validation
  3. return-type assignability across more expression forms
  4. better nullability rules
  5. union/error-result modeling where useful

Until then, this document should stay small and track what the compiler actually enforces.