Add structured coroutines and explicit error handling

This commit is contained in:
pavel 2026-08-27 16:26:40 +02:00
commit fe62e81152
31 changed files with 1701 additions and 325 deletions

113
README.md
View file

@ -12,7 +12,7 @@ This is the practical boundary of the prototype:
- `fun` declarations
- `val` and `var`
- `Int`, `String`, `Boolean`, `Unit`
- `Int`, `Long`, `String`, `Boolean`, `Unit`
- function types like `(String) -> Unit`
- `if`, `else`, `while`, and `for (item in items)`
- function calls
@ -23,8 +23,11 @@ This is the practical boundary of the prototype:
- `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
@ -118,6 +121,58 @@ fun describe(result: PaymentResult): String {
Enum matches must contain each variant exactly once. Variant payload arity is
checked during Gotlin compilation.
## 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<T, Error>` flows.
An explicit `Result` return or variable type converts the Go return directly:
```kotlin
fun parse(value: String): Result<Int, Error> {
return strconv.atoi(value)
}
fun ping(db: *sql.DB): Result<Unit, Error> {
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<User>`. Explicit pointer syntax is
still supported for compatibility. Go interop remains explicit, for example
`*http.Request` and `*pgxpool.Pool`.
Enums whose variants carry no payload are represented as string-backed values.
The exact variant identifier is used for JSON and PostgreSQL text values:
@ -148,11 +203,11 @@ When the surrounding expression provides a target type, the type argument is
optional:
```kotlin
fun response(account: *AccountEntity): *AccountResponse {
fun response(account: AccountEntity): AccountResponse {
return account.mapTo()
}
val response: *AccountResponse = account.mapTo()
val response: AccountResponse = account.mapTo()
val envelope = Envelope(account.mapTo())
```
@ -192,6 +247,28 @@ Run directly:
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` and bare `go` forms are not
valid Gotlin syntax.
```kotlin
suspend fun load(): Int {
delay(10)
return 42
}
fun main() {
runBlocking {
val value = async<Int> { 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`.
@ -262,17 +339,20 @@ Projection targets are local data classes. The constructor must contain one dire
```kotlin
data class AccountSummary(var id: String, var balance: Double)
fun summaries(pool: *pgxpool.Pool, ctx: context.Context): List<*AccountSummary> {
fun summaries(pool: *pgxpool.Pool, ctx: context.Context): List<AccountSummary> {
return sql.from<AccountRow>()
.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 `List<*AccountRow>` and closes the pgx rows after reading and scanning every result in data-class field declaration order:
Select chains can execute directly against a pgx/v5 pool. `fetch(pool, ctx)`
returns `Result<List<AccountRow>, Error>` and closes pgx rows after reading and
scanning every result in data-class field declaration order:
```kotlin
import context
@ -282,33 +362,38 @@ fun accountsFor(
pool: *pgxpool.Pool,
ctx: context.Context,
customerId: String
): List<*AccountRow> {
): List<AccountRow> {
return sql.from<AccountRow>()
.where { it.customerId == customerId }
.orderBy { it.accountType }
.fetch(pool, ctx)
.unwrap()
}
```
`single(pool, ctx)` returns `*AccountRow`. It closes the rows and panics through Gotlin's auto-throw path unless the query produces exactly one row:
`single(pool, ctx)` returns `Result<AccountRow, Error>` 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 {
fun account(pool: *pgxpool.Pool, ctx: context.Context, id: String): AccountRow {
return sql.from<AccountRow>()
.where { it.id == id }
.single(pool, ctx)
.unwrap()
}
```
`iterator(pool, ctx)` streams pgx rows. Call `next()` before each `value()`, always arrange an explicit `close()` (normally with `defer`), and call `err()` after iteration. Assigning the error result invokes the existing auto-throw convention:
`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<AccountRow>().iterator(pool, ctx)
val rows = sql.from<AccountRow>().iterator(pool, ctx).unwrap()
defer rows.close()
while (rows.next()) {
val account: *AccountRow = rows.value()
val account: AccountRow = rows.value()
println(account.customerId)
}
@ -316,7 +401,11 @@ fun printAccounts(pool: *pgxpool.Pool, ctx: context.Context) {
}
```
Query and scan failures panic through `gotlinAutoThrow`. `fetch` and `single` close rows internally; an iterator leaves lifecycle control with the caller, and `value()` panics unless the immediately preceding `next()` succeeded. None of the execution terminals require an intermediate `build()`, `query.sql`, or `query.args` access. `build()` remains available when a query value is needed for manual execution.
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