Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Top level await #4007

Merged
merged 3 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion js/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,10 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*BundleInstance
env: b.preInitState.RuntimeOptions.Env,
moduleVUImpl: vuImpl,
}
var result *modules.RunSourceDataResult
callback := func() error { // this exists so that Sobek catches uncatchable panics such as Interrupt
var err error
bi.mainModule, err = modSys.RunSourceData(b.sourceData)
result, err = modSys.RunSourceData(b.sourceData)
return err
}

Expand All @@ -346,6 +347,14 @@ func (b *Bundle) instantiate(vuImpl *moduleVUImpl, vuID uint64) (*BundleInstance

<-initDone

if err == nil {
var finished bool
bi.mainModule, finished, err = result.Result()
if !finished {
return nil, errors.New("initializing the main module hasn't finished, this is a bug in k6 please report it")
olegbespalov marked this conversation as resolved.
Show resolved Hide resolved
}
}

if err != nil {
var exception *sobek.Exception
if errors.As(err, &exception) {
Expand Down
17 changes: 16 additions & 1 deletion js/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func TestNewBundle(t *testing.T) {
t.Run("InvalidExports", func(t *testing.T) {
t.Parallel()
_, err := getSimpleBundle(t, "/script.js", `module.exports = null`)
require.EqualError(t, err, "GoError: CommonJS's exports must not be null\n") // TODO: try to remove the GoError from herer
require.EqualError(t, err, "CommonJS's exports must not be null")
})
t.Run("DefaultUndefined", func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -983,3 +983,18 @@ func TestGlobalTimers(t *testing.T) {
})
}
}

func TestTopLevelAwaitErrors(t *testing.T) {
t.Parallel()
data := `
const delay = (delayInms) => {
return new Promise(resolve => setTimeout(resolve, delayInms));
}

await delay(10).then(() => {something});
export default () => {}
`

_, err := getSimpleBundle(t, "/script.js", data)
require.ErrorContains(t, err, "ReferenceError: something is not defined")
}
10 changes: 10 additions & 0 deletions js/modules/require_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/grafana/sobek"

"go.k6.io/k6/loader"
)

Expand Down Expand Up @@ -47,6 +48,7 @@ func (ms *ModuleSystem) Require(specifier string) (*sobek.Object, error) {
} else {
panic(fmt.Sprintf("expected sobek.CyclicModuleRecord, but for some reason got a %T", m))
}
promisesThenIgnore(rt, promise)
switch promise.State() {
case sobek.PromiseStateRejected:
err = promise.Result().Export().(error) //nolint:forcetypeassert
Expand Down Expand Up @@ -195,3 +197,11 @@ func getPreviousRequiringFile(vu VU) (string, error) {
}
return result, nil
}

// sets the provided promise in such way as to ignore falures
// this is mostly needed as failures are handled separately and we do not want those to lead to stopping the event loop
func promisesThenIgnore(rt *sobek.Runtime, promise *sobek.Promise) {
cal, _ := sobek.AssertFunction(rt.ToValue(promise).ToObject(rt).Get("then"))
handler := rt.ToValue(func(_ sobek.Value) {})
_, _ = cal(rt.ToValue(promise), handler, handler)
}
mstoykov marked this conversation as resolved.
Show resolved Hide resolved
33 changes: 24 additions & 9 deletions js/modules/resolution.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,7 @@ func NewModuleSystem(resolver *ModuleResolver, vu VU) *ModuleSystem {
// RunSourceData runs the provided sourceData and adds it to the cache.
// If a module with the same specifier as the source is already cached
// it will be used instead of reevaluating the source from the provided SourceData.
//
// TODO: this API will likely change as native ESM support will likely not let us have the exports
// as one big sobek.Value that we can manipulate
func (ms *ModuleSystem) RunSourceData(source *loader.SourceData) (sobek.ModuleRecord, error) {
func (ms *ModuleSystem) RunSourceData(source *loader.SourceData) (*RunSourceDataResult, error) {
specifier := source.URL.String()
pwd := source.URL.JoinPath("../")
if _, err := ms.resolver.resolveLoaded(pwd, specifier, source.Data); err != nil {
Expand All @@ -256,14 +253,32 @@ func (ms *ModuleSystem) RunSourceData(source *loader.SourceData) (sobek.ModuleRe
}
rt := ms.vu.Runtime()
promise := rt.CyclicModuleRecordEvaluate(ci, ms.resolver.sobekModuleResolver)
switch promise.State() {

promisesThenIgnore(rt, promise)

return &RunSourceDataResult{
promise: promise,
mod: mod,
}, nil
}

// RunSourceDataResult helps with the asynchronous nature of ESM
// it wraps the promise that is returned from Sobek while at the same time allowing access to the module record
type RunSourceDataResult struct {
promise *sobek.Promise
mod sobek.ModuleRecord
}

// Result returns either the underlying module or error if the promise has been completed and true,
// or false if the promise still hasn't been completed
func (r *RunSourceDataResult) Result() (sobek.ModuleRecord, bool, error) {
switch r.promise.State() {
case sobek.PromiseStateRejected:
return nil, promise.Result().Export().(error) //nolint:forcetypeassert
return nil, true, r.promise.Result().Export().(error) //nolint:forcetypeassert
case sobek.PromiseStateFulfilled:
return mod, nil
return r.mod, true, nil
default:
// TODO(@mstoykov): this will require having some callbacks through the code, but should be doable, just not pretty
panic("TLA not supported in k6 at the moment")
return nil, false, nil
}
}

Expand Down
2 changes: 2 additions & 0 deletions js/tc39/breaking_test_errors-experimental_enhanced.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"test/language/module-code/instn-star-err-not-found.js-strict:true": "test/language/module-code/instn-star-err-not-found.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/parse-err-hoist-lex-fun.js-strict:true": "test/language/module-code/parse-err-hoist-lex-fun.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/parse-err-return.js-strict:true": "test/language/module-code/parse-err-return.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/top-level-await/new-await.js-strict:true": "test/language/module-code/top-level-await/new-await.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/top-level-await/no-operand.js-strict:true": "test/language/module-code/top-level-await/no-operand.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/reserved-words/await-module.js-strict:true": "test/language/reserved-words/await-module.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/statements/class/class-name-ident-await-escaped-module.js-strict:true": "test/language/statements/class/class-name-ident-await-escaped-module.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/statements/class/class-name-ident-await-module.js-strict:true": "test/language/statements/class/class-name-ident-await-module.js: error is not an object (Test262: This statement should not be evaluated.)",
Expand Down
2 changes: 2 additions & 0 deletions js/tc39/breaking_test_errors-extended.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@
"test/language/module-code/instn-star-err-not-found.js-strict:true": "test/language/module-code/instn-star-err-not-found.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/parse-err-hoist-lex-fun.js-strict:true": "test/language/module-code/parse-err-hoist-lex-fun.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/parse-err-return.js-strict:true": "test/language/module-code/parse-err-return.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/top-level-await/new-await.js-strict:true": "test/language/module-code/top-level-await/new-await.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/module-code/top-level-await/no-operand.js-strict:true": "test/language/module-code/top-level-await/no-operand.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/reserved-words/await-module.js-strict:true": "test/language/reserved-words/await-module.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/statements/class/class-name-ident-await-escaped-module.js-strict:true": "test/language/statements/class/class-name-ident-await-escaped-module.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/statements/class/class-name-ident-await-module.js-strict:true": "test/language/statements/class/class-name-ident-await-module.js: error is not an object (Test262: This statement should not be evaluated.)",
Expand Down
11 changes: 9 additions & 2 deletions js/tc39/tc39_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ var (
featuresBlockList = []string{
"IsHTMLDDA", // not supported at all
"async-iteration", // not supported at all
"top-level-await", // not supported at all
"String.prototype.replaceAll", // not supported at all, Stage 4 since 2020
"dynamic-import", // not support in k6

// from Sobek
"Symbol.asyncIterator",
Expand Down Expand Up @@ -737,10 +737,17 @@ func (ctx *tc39TestCtx) runTC39Module(name, src string, includes []string, vm *s
moduleRuntime.VU.InitEnvField.CWD = base

early = false
_, err = ms.RunSourceData(&loader.SourceData{
result, err := ms.RunSourceData(&loader.SourceData{
Data: []byte(src),
URL: u,
})
if err == nil {
var finished bool
_, finished, err = result.Result()
if !finished {
panic("tc39 has no tests where this should happen")
}
}

return early, err
}
Expand Down