From d398a080c454fa98a6adac5a5b2cca4f27b64e91 Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Mon, 26 Oct 2026 23:52:51 +0100 Subject: [PATCH] fix(compiler): await an async call that returns a range iterator A range over the result of an async call, such as for i := range seq(x) where seq returns an iter.Seq, was lowered as await seq(x)!(yield), which calls the promise of seq(x) as the iterator: TypeError "seq(x) is not a function". The awaited call is now parenthesized, as for a map range. Signed-off-by: Randall C. O'Reilly --- compiler/lowering.go | 4 +- .../expected.log | 5 ++ .../range_func_async_method_value.go | 33 +++++++++++ .../range_func_async_method_value.gs.ts | 55 +++++++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) diff --git a/compiler/lowering.go b/compiler/lowering.go index bfed684aa..5931165a4 100644 --- a/compiler/lowering.go +++ b/compiler/lowering.go @@ -8657,7 +8657,9 @@ func (o *LoweringOwner) lowerRangeStmt(ctx lowerFileContext, stmt *ast.RangeStmt if signature == nil { return loweredStmt{}, append(diagnostics, loweringUnsupportedAt(ctx, stmt, "statement", ctx.semPkg.pkgPath, "unsupported function range signature")) } - lowered, funcDiagnostics := o.lowerRangeFuncStmt(ctx, stmt, rangeValue, signature) + // An async call that returns the iterator is awaited before the + // iterator is called. + lowered, funcDiagnostics := o.lowerRangeFuncStmt(ctx, stmt, parenthesizeAwaitedExpr(rangeValue), signature) diagnostics = append(diagnostics, funcDiagnostics...) return lowered, diagnostics } diff --git a/tests/tests/range_func_async_method_value/expected.log b/tests/tests/range_func_async_method_value/expected.log index 27ba77dda..b17b7ce9e 100644 --- a/tests/tests/range_func_async_method_value/expected.log +++ b/tests/tests/range_func_async_method_value/expected.log @@ -1 +1,6 @@ true +key: 0 +key: 1 +key: 2 +pair: 0 0 +pair: 1 0.5 diff --git a/tests/tests/range_func_async_method_value/range_func_async_method_value.go b/tests/tests/range_func_async_method_value/range_func_async_method_value.go index 0e2d77f02..0cb53babb 100644 --- a/tests/tests/range_func_async_method_value/range_func_async_method_value.go +++ b/tests/tests/range_func_async_method_value/range_func_async_method_value.go @@ -1,5 +1,7 @@ package main +import "iter" + type provider interface { Items() []*Group } @@ -31,9 +33,40 @@ func (g *Group) Build() { } } +var ch = make(chan int, 1) + +// count is async, as it receives from a channel. +func count() int { + ch <- 3 + return <-ch +} + +// seq is async, as it calls count, so the iterator that it returns is +// awaited before it is called. +func seq(v any) iter.Seq2[int, float64] { + n := count() + if s, ok := v.([]float64); ok { + n = len(s) + } + return func(yield func(int, float64) bool) { + for i := range n { + if !yield(i, float64(i)*0.5) { + return + } + } + } +} + func main() { child := &Group{} root := &Group{provider: &listProvider{items: []*Group{child}}} root.Build() println(child.seen) + + for i := range seq(nil) { + println("key:", i) + } + for i, f := range seq([]float64{1, 2}) { + println("pair:", i, f) + } } diff --git a/tests/tests/range_func_async_method_value/range_func_async_method_value.gs.ts b/tests/tests/range_func_async_method_value/range_func_async_method_value.gs.ts index da4cf2699..fa288e1f9 100644 --- a/tests/tests/range_func_async_method_value/range_func_async_method_value.gs.ts +++ b/tests/tests/range_func_async_method_value/range_func_async_method_value.gs.ts @@ -2,6 +2,8 @@ // Updated when compliance tests are re-run, DO NOT EDIT! import * as $ from "@goscript/builtin/index.js" +import * as iter from "@goscript/iter/index.js" +import "@goscript/iter/index.js" export type provider = { Items(): $.Slice | null> @@ -98,13 +100,66 @@ export class Group { ) } +export let ch: $.Channel | null = $.makeChannel(1, 0, "both") + +export function __goscript_set_ch(__goscriptValue: $.Channel | null): void { + ch = __goscriptValue +} + +export async function count(): globalThis.Promise { + await $.chanSend(ch, 3) + return await $.chanRecv(ch) +} + +export async function seq(v: any): globalThis.Promise | null> { + let n = await count() + { + let __goscriptTuple0: any = $.typeAssertTuple<$.Slice>(v, /* @__PURE__ */ $.sliceType(/* @__PURE__ */ $.basicType("float64"))) + let s: $.Slice = __goscriptTuple0[0] + let ok: boolean = __goscriptTuple0[1] + if (ok) { + n = $.sliceLen(s) + } + } + return $.functionValue(async (_yield: ((_p0: number, _p1: number) => boolean | globalThis.Promise) | null) => { + for (let __goscriptRangeCount0 = n, i = 0; i < __goscriptRangeCount0; i++) { + if (!(__goscriptAwait = _yield!(i, i * 0.5), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait)) { + return + } + } + }, ({ kind: $.TypeKind.Function, params: [({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.basicType("int"), /* @__PURE__ */ $.basicType("float64")], results: [/* @__PURE__ */ $.basicType("bool")] } as $.FunctionTypeInfo)], results: [] } as $.FunctionTypeInfo)) +} + export async function main(): globalThis.Promise { let child = new Group() let root = new Group(false, $.interfaceValue(new listProvider(false, $.arrayToSlice | null>([child])), "*main.listProvider", /* @__PURE__ */ $.pointerType("main.listProvider"))) void (root !== null && (root as any).__isVarRef !== true && (root as any).__goValue === undefined ? ((root as any) as Group).Build() : Group.prototype.Build.call(root)) await $.println($.pointerValue(child).seen) + + let __goscriptRangeReturn1 = false + ;await (async () => { + await (await seq(null))!(async (i, __goscriptRange1_1) => { + await $.println("key:", i) + return true + }) + })() + if (__goscriptRangeReturn1) { + return + } + let __goscriptRangeReturn2 = false + ;await (async () => { + await (await seq($.interfaceValue($.typedSliceOf(Float64Array, [1, 2]) as $.Slice, "[]float64", /* @__PURE__ */ $.sliceType(/* @__PURE__ */ $.basicType("float64")))))!(async (i, f) => { + await $.println("pair:", i, f) + return true + }) + })() + if (__goscriptRangeReturn2) { + return + } } +var __goscriptAwait: any + if ($.isMainScript(import.meta)) { await main() }