Add structured coroutines and explicit error handling
This commit is contained in:
parent
de1262b4cf
commit
fe62e81152
31 changed files with 1701 additions and 325 deletions
113
README.md
113
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue