# Gotlin `Gotlin` is a small Kotlin-like frontend implemented in Go that targets the Go toolchain. This is the practical boundary of the prototype: - It is a Kotlin-flavored language frontend. - It targets the Go toolchain by generating valid Go source and building through `go build`. - It is not a direct integration into Go's internal `cmd/compile` backend APIs. The compiler keeps source spelling in its syntax AST, then builds lexical symbols, structural semantic types, resolved expression meanings, and typed HIR before Go emission. Class reference semantics live in `ClassType`; only the semantic Type-to-Go mapping turns a class such as `User` into `*User`. ## Supported language slice - `fun` declarations - `val` and `var` - `Int`, `Long`, `String`, `Boolean`, `Unit` - function types like `(String) -> Unit` - `if`, `else`, `while`, and `for (item in items)` - function calls - lambdas like `{ x: Int -> println(x) }` and `{ println(it) }` - `class` with primary-constructor fields and methods - `interface` with method signatures - Rust-style algebraic `enum` declarations with payload variants and exhaustive `match` - `println(...)` - arithmetic, comparison, and boolean operators - decimal literals, `defer`, and Go address-of expressions such as `&value` - explicit nullable types (`Type?`), safe access (`?.`), and non-null assertions (`!!`) - Gotlin classes are reference types by default; `*` is only needed for external Go pointer types - named external Go struct construction, for example `http.Client(timeout = 3 * time.second)` - top-level embedded resources such as `@embed("assets/*") val assets: embed.FS` - structured coroutines with `suspend fun`, `runBlocking`, `coroutineScope`, `launch`, `async`, `await`, `delay`, `withTimeout`, and `isActive` ## Example ```kotlin package demo fun fib(n: Int): Int { if (n < 2) { return n } return fib(n - 1) + fib(n - 2) } fun main() { println(fib(8)) } ``` Imports from Go packages are supported: ```kotlin package demo import strings fun main() { println(strings.ToUpper("gotlin")) } ``` HTTP server example: ```kotlin package demo.web import fmt import net.http fun helloHandler(w: http.ResponseWriter, r: *http.Request) { fmt.Fprintln(w, "hello from gotlin") } fun main() { http.HandleFunc("/", helloHandler) fmt.Println("serving http://localhost:8080") http.ListenAndServe(":8080", http.DefaultServeMux) } ``` Classes and interfaces: ```kotlin package demo interface Greeter { fun greet(name: String): String } class ConsoleGreeter(val prefix: String) { fun greet(name: String): String { return prefix + name } } fun main() { val greeter: Greeter = ConsoleGreeter("hello, ") println(greeter.greet("gotlin")) } ``` Rust-style enums: ```kotlin enum PaymentResult { Accepted(String) Rejected(String) Pending } fun describe(result: PaymentResult): String { return match (result) { PaymentResult.Accepted(id) -> id PaymentResult.Rejected(reason) -> reason PaymentResult.Pending -> "pending" } } ``` Enum matches must contain each variant exactly once. Variant payload arity is checked during Gotlin compilation. A match used as an expression also requires every arm to return the same type. Block-style statement matches remain available for side effects. ## Null safety Types are non-nullable by default. Add `?` explicitly when `null` is valid: ```kotlin fun email(user: User?): String? { return user?.email } ``` The compiler rejects `null` in non-nullable arguments, fields, local variables, and return values. Nullable receivers cannot be dereferenced directly. Gotlin smart-casts values after `value != null` branches and guard clauses such as `if (value == null) { return }`. Use `!!` only when an invariant cannot be expressed through control flow: ```kotlin val required: User = optionalUser!! ``` ## Error handling Gotlin adapts Go `(T, error)` returns into Rust-style `Result` flows. An explicit `Result` return or variable type converts the Go return directly: ```kotlin fun parse(value: String): Result { return strconv.atoi(value) } fun ping(db: *sql.DB): Result { return db.ping() } ``` Use `?` to propagate a Go error or chain a Gotlin function returning `Result`. Explicit panic and fallback operations are available when appropriate: ```kotlin val required = parse("42").unwrap() val fallback = parse("invalid").unwrapOr(0) ``` Gotlin never inserts implicit panic wrappers. A Go call returning `(T, error)` must have an explicit `Result` context or use `?`, explicit `value, error` destructuring, or `.unwrap()`. Gotlin-defined `class` and `data class` values are references automatically, including nested generic types such as `List`. Explicit pointer syntax is reserved for Go interop, for example `*http.Request` and `*pgxpool.Pool`; applying `*` to a Gotlin class is a compile error. Enums whose variants carry no payload are represented as string-backed values. The exact variant identifier is used for JSON and PostgreSQL text values: ```kotlin enum PaymentStatus { PENDING_RESERVATION INITIATED } ``` This reads and writes `"PENDING_RESERVATION"` and `"INITIATED"` directly. Payload-carrying enums remain algebraic sum types. ## Structural mapping Compatible classes and enums can be converted with `mapTo()`: ```kotlin data class AddressEntity(var city: String) data class AddressResponse(var city: String) data class AccountEntity(var id: String, var address: *AddressEntity) data class AccountResponse(var address: *AddressResponse, var id: String) val response = account.mapTo() ``` When the surrounding expression provides a target type, the type argument is optional: ```kotlin fun response(account: AccountEntity): AccountResponse { return account.mapTo() } val response: AccountResponse = account.mapTo() val envelope = Envelope(account.mapTo()) ``` Use explicit `mapTo()` when assigning to an untyped local or when no target type can be inferred. Fields are matched by Gotlin name rather than declaration order. Mapping is recursive across nested classes, pointers, nullable values, lists, mutable lists, maps, and enum payloads. Enum variants are matched by name. Extra source fields and extra target enum variants are allowed; every target field and every source enum variant must be compatible. Payloadless enums also map recursively to and from `String`. String-to-enum mapping validates the runtime value and panics for an unknown variant string. Incompatible mappings fail compilation with a complete path, for example: ```text cannot map Account.address.zip: String is incompatible with Int ``` ```bash go run ./cmd/gotlinc build ./examples/hello.gt ./hello ``` Emit Go source instead: ```bash go run ./cmd/gotlinc build -src ./examples/hello.gt -o /tmp/hello.go go run /tmp/hello.go ``` Run directly: ```bash go run ./cmd/gotlinc run ./examples/hello.gt ``` ## Structured coroutines Gotlin coroutines use Go goroutines underneath, but expose only structured scopes. A scope waits for its children, propagates child failures, and cancels sibling coroutine contexts. The removed `worker`, bare `go`, and channel `select` forms are not valid Gotlin syntax; use coroutine scopes, `delay`, and explicit channel `read()`/`send()` operations. ```kotlin suspend fun load(): Int { delay(10) return 42 } fun main() { runBlocking { val value = async { return load() } launch { println("loading") } println(value.await()) } } ``` ## Type-checked SQL queries Gotlin recognizes a PostgreSQL SQL DSL at compile time. SQL row mappings must be data classes annotated with `@table`. Fields map from lower-camel Gotlin names to `snake_case` columns by default and can override the SQL name with `@column`. Conflict keys use `@id`; database-generated or defaulted fields use `@generated`. ```kotlin import time @table("accounts") data class AccountRow( @generated @id var id: String, var customerId: String, @column("kind") var accountType: String, var balance: Double, var closedAt: time.Time? ) fun accountsFor(customerId: String): GotlinSQLQuery { return sql.from() .where { row -> row.customerId == customerId && row.closedAt == null } .orderByDescending { row -> row.balance } .limit(100) .build() } ``` The compiler emits a Go value with this generated support type: ```go type GotlinSQLQuery struct { SQL string Args []any } ``` The example selects every field, including `@generated` fields, and produces PostgreSQL `$n` placeholders. Arguments are emitted in SQL traversal order. A literal `limit(100)` is embedded after validation; a typed non-literal `Int` limit uses the next placeholder. Nullable types use a `?` suffix, currently including forms such as `String?` and `time.Time?`; generated Go fields use pointers. Comparing a nullable field with `null` lowers to `IS NULL` or `IS NOT NULL`. Ordering comparisons support numeric values and `time.Time`; typed `now()` emits `CURRENT_TIMESTAMP` without an argument: ```kotlin sql.from() .where { it.publishedAt == null && (it.claimedUntil == null || it.claimedUntil < now()) } .orderBy { it.createdAt } .limit(batchSize) .forUpdate() .skipLocked() .build() ``` The canonical select method order is: ```text sql.from() [.select { ... }] [.where { ... }] [.orderBy { ... } | .orderByDescending { ... }] [.limit(Int)] [.forUpdate()] [.skipLocked()] .build() | .fetch(pool, ctx) | .single(pool, ctx) | .iterator(pool, ctx) ``` Each optional method may occur at most once. `select` must be first, `skipLocked` requires `forUpdate`, and clauses are emitted as `WHERE`, `ORDER BY`, `LIMIT`, `FOR UPDATE`, `SKIP LOCKED` in PostgreSQL order. ### Typed projections Projection targets are local data classes. The constructor must contain one direct source-row field per target field, in target declaration order, with exact matching types: ```kotlin data class AccountSummary(var id: String, var balance: Double) fun summaries(pool: *pgxpool.Pool, ctx: context.Context): List { return sql.from() .select { row -> AccountSummary(row.id, row.balance) } .orderBy { it.balance } .fetch(pool, ctx) .unwrap() } ``` The compiler emits `SELECT id, balance`, scans in projection declaration order, and makes `fetch`, `single`, and `iterator` target `AccountSummary` rather than `AccountRow`. The row parameter in later `where` and ordering methods still represents `AccountRow`. Select chains can execute directly against a pgx/v5 pool. `fetch(pool, ctx)` returns `Result, Error>` and closes pgx rows after reading and scanning every result in data-class field declaration order: ```kotlin import context import pgxpool "github.com/jackc/pgx/v5/pgxpool" fun accountsFor( pool: *pgxpool.Pool, ctx: context.Context, customerId: String ): List { return sql.from() .where { it.customerId == customerId } .orderBy { it.accountType } .fetch(pool, ctx) .unwrap() } ``` `single(pool, ctx)` returns `Result` and closes the rows. Use `?` to propagate zero/multiple-row errors or call `.unwrap()` explicitly: ```kotlin fun account(pool: *pgxpool.Pool, ctx: context.Context, id: String): AccountRow { return sql.from() .where { it.id == id } .single(pool, ctx) .unwrap() } ``` `iterator(pool, ctx)` returns a `Result` around a streaming pgx iterator. Call `next()` before each `value()`, arrange an explicit `close()`, and inspect `err()` after iteration: ```kotlin fun printAccounts(pool: *pgxpool.Pool, ctx: context.Context) { val rows = sql.from().iterator(pool, ctx).unwrap() defer rows.close() while (rows.next()) { val account: AccountRow = rows.value() println(account.customerId) } val checked = rows.err() } ``` Query and scan failures are returned as `Result` errors. `fetch` and `single` close rows internally; an iterator leaves lifecycle control with the caller, and `value()` panics only when called without a successful `next()`. None of the execution terminals require an intermediate `build()`, `query.sql`, or `query.args` access. `build()` remains available for manual execution. ### Inserts and returning Inserts omit every `@generated` field. If all fields are generated, the compiler emits `INSERT ... DEFAULT VALUES`. Conflict handling is optional; conflict fields must have `@id`: ```kotlin sql.insert(row) .onConflict { it.id } .doNothing() .build() ``` Composite conflict keys use `listOf`, for example `.onConflict { listOf(it.tenantId, it.id) }`. Updates use a typed lower-camel `set` form because Gotlin does not currently implement Kotlin callable references (`AccountRow::balance`): ```kotlin sql.insert(row) .onConflict { it.id } .doUpdate { excluded -> set(AccountRow.balance, excluded.balance) } .build() ``` Multiple `set(...)` expressions may appear in one `doUpdate` lambda. Update targets must exist and be `var`; excluded fields must exist and have a compatible type. An insert can return the full row or a typed projection. Write chains can use `fetch`, `single`, or `iterator` only after `returning`: ```kotlin sql.insert(row) .returning { it } .single(pool, ctx) sql.insert(row) .onConflict { it.id } .doNothing() .returning { value -> AccountSummary(value.id, value.balance) } .single(pool, ctx) ``` The exact insert forms are: ```text sql.insert(row) [.onConflict { field | listOf(fields...) }.doNothing() | .doUpdate { ... }] [.returning { it | Projection(it.field, ...) }] .build() | returning execution terminal ``` ### Updates and deletes Typed updates use one `set` lambda. Targets are mutable source-row fields. Values may be typed names/selectors, literals, `null` for nullable targets, `now()` for timestamps, source-row fields, or numeric `+` and `-` expressions: ```kotlin sql.update() .set { row -> set(row.balance, row.balance + amount) set(row.closedAt, now()) } .where { it.id == accountId } .returning { row -> AccountSummary(row.id, row.balance) } .single(pool, ctx) ``` Deletes support a typed predicate and the same returning forms: ```kotlin sql.delete() .where { it.id == accountId } .returning { it } .single(pool, ctx) ``` The exact write forms are: ```text sql.update() .set { row -> set(row.field, value); ... } [.where { predicate }] [.returning { it | Projection(it.field, ...) }] .build() | returning execution terminal sql.delete() [.where { predicate }] [.returning { it | Projection(it.field, ...) }] .build() | returning execution terminal ``` `where` is intentionally optional for updates and deletes, so omitting it affects the whole table. There is no write `execute` terminal yet; use `build()` for manual `Exec`, or add `returning` and use a query execution terminal. At compile time the DSL checks that the generic row type exists, is a data class with `@table`, has unique mapped columns, and contains every referenced field. It checks known predicate operand types, nullable operations, insert row types, `@id` conflict metadata, update mutability and value compatibility, method cardinality/order, lock dependencies, and projection field counts/types. Current SQL limitations: - Predicates support `&&`, `||`, `!`, `==`, `!=`, and numeric/timestamp `<`, `<=`, `>`, `>=`. Values must have a compiler-known type; external values become `$n` arguments. - Projections and `returning` do not support scalar results, aliases, computed expressions, aggregates, external structs, or reordered/coerced target types. They accept a full row or direct fields passed to a local data-class constructor. - Standalone update values do not support SQL functions other than `now()`, string expressions, intervals, casts, subqueries, or arbitrary SQL fragments. Upsert `doUpdate` values still come only from the `excluded` row. - Inserts emit one row at a time. Bulk/multi-row inserts are not implemented. - Table and column annotations are validated unquoted SQL identifiers. Joins, aliases, grouping, aggregates, `OFFSET`, lock strengths other than `FOR UPDATE`, and conflict predicates are not implemented. - The compiler does not perform migrations, schema generation, database connections, or schema introspection. Language server: ```bash go build -o ./bin/gotlin-lsp ./cmd/gotlin-lsp ./bin/gotlin-lsp ``` VS Code extension: ```bash cd ./tools/vscode-gotlin npm install npm run build ``` ## Notes - `val` is immutable after initialization; `var` can be reassigned. - Type inference is local to declarations without an explicit type. - Top-level declarations currently support functions, classes, and interfaces. - `gotlinc build` produces an executable by default. If `-o` is omitted, the output name is derived from the input file name. - `gotlinc build -src` emits Go source instead of a binary. - `gotlinc` supports `build` and `run`, and defaults to `build` if no subcommand is given. - Gotlin source files use the `.gt` extension. - `gotlin-lsp` provides diagnostics, hover, and go-to-definition over stdio. - `gotlin-lsp` can optionally use `gopls` for hover and definition on Go-imported symbols. - the VS Code extension adds syntax highlighting, snippets, and launches the LSP for `.gt` files.