tc39 / test262

Official ECMAScript Conformance Test Suite
Other
2.39k stars 465 forks source link

Import cycles through star exports #3884

Open ghost opened 1 year ago

ghost commented 1 year ago

Import cycles have some interaction with star exports. Where one star export finds a cycle and another finds a binding, the binding should override and resolve successfully.

It was pointed out to me that typically a cycle will cause a crash elsewhere, when InitializeEnvironment detects it. But that shouldn't happen in the case where a star export is in the middle of the cycle. There is inconsistency in implementations on this point. I wonder if it should be in the test suite.

There are eshost outputs below, plus browser demos. The browser demos log/error to the console.


Cycle on both sides, wrapped in named exports.

https://bojavou.github.io/star-export-cycle/biname/

https://github.com/bojavou/star-export-cycle/tree/main/biname

// entry.mjs
import { value } from './left.mjs'
print(value)

// left.mjs
export { value } from './middle.mjs'

// middle.mjs
export * from './left.mjs'
export * from './right.mjs'
export * from './bind.mjs'

// right.mjs
export { value } from './middle.mjs'

// bind.mjs
export const value = 'bind'
$ eshost entry.mjs
#### engine262
bind

#### JavaScriptCore
bind

#### SpiderMonkey
bind

#### V8

SyntaxError: Detected cycle while resolving name 'value' in './middle.mjs'

#### XS

SyntaxError: (host): import value ambiguous

Cycle on one side, wrapped in named exports.

https://bojavou.github.io/star-export-cycle/uniname/

https://github.com/bojavou/star-export-cycle/tree/main/uniname

// entry.mjs
import { value } from './left.mjs'
print(value)

// left.mjs
export { value } from './middle.mjs'

// middle.mjs
export * from './right.mjs'
export * from './bind.mjs'

// right.mjs
export { value } from './middle.mjs'

// bind.mjs
export const value = 'bind'
$ eshost entry.mjs
#### engine262
bind

#### JavaScriptCore
bind

#### SpiderMonkey
bind

#### V8
bind

#### XS

SyntaxError: (host): import value ambiguous

Cycle on both sides, wrapped in star exports.

https://bojavou.github.io/star-export-cycle/bistar/

https://github.com/bojavou/star-export-cycle/tree/main/bistar

// entry.mjs
import { value } from './left.mjs'
print(value)

// left.mjs
export * from './middle.mjs'

// middle.mjs
export * from './left.mjs'
export * from './right.mjs'
export * from './bind.mjs'

// right.mjs
export * from './middle.mjs'

// bind.mjs
export const value = 'bind'
$ eshost entry.mjs
#### engine262
bind

#### JavaScriptCore
bind

#### SpiderMonkey
bind

#### V8
bind

#### XS
bind

Cycle on one side, wrapped in star exports.

This behaves identically to bistar.

https://bojavou.github.io/star-export-cycle/unistar/

https://github.com/bojavou/star-export-cycle/tree/main/unistar

// entry.mjs
import { value } from './left.mjs'
print(value)

// left.mjs
export * from './middle.mjs'

// middle.mjs
export * from './right.mjs'
export * from './bind.mjs'

// right.mjs
export * from './middle.mjs'

// bind.mjs
export const value = 'bind'
$ eshost entry.mjs
#### engine262
bind

#### JavaScriptCore
bind

#### SpiderMonkey
bind

#### V8
bind

#### XS
bind
ghost commented 4 months ago

Here's a diagram of this situation, since I was doing it for my own work. I called it a hyperjunction in tests. Importing value from left, middle, or right shows the condition.

hyperjunction

When the arms are stars it works everywhere.

hyperjunction-star

When 1 arm is chopped off it works everywhere.

hyperjunction-chopped

ptomato commented 4 months ago

Do you know if this behaviour is specified? i.e. can we unambiguously say that one or the other of the behaviours is wrong? Or is it a gap in the specification?

ghost commented 4 months ago

By my understanding, it should work. It's kind of awkward to get your head around because it's only there in the implications. I'll give what I think is a correct trace of an import.

This is the traversal that should happen if you import value from Left. ResolveExport defines it. The seen list is in [].

// Left.mjs
export { value } from './Middle.mjs'

// Middle.mjs
export * from './Left.mjs'
export * from './Right.mjs'
export * from './Substance.mjs'

// Right.mjs
export { value } from './Middle.mjs'

// Substance.mjs
export const value = 'value'

When I first raised it someone pointed out to me that a cycle will normally throw at module evaluation time. This happens in InitializeEnvironment. It pulls all imports to try to create the internal bindings. When one of them is a cycle, it has to fail. In this graph one of the distal modules will throw.

distal-cycle

But star exports on their own don't create bindings and so they never hit this evaluation-time error. There's a certain kind of cycle through stars that is entirely binding free.

If you have a list of star exports where some have these pure star cycles but one of them has a binding, the spec seems to say it should resolve. A list of star exports is kind of like a disjunction operator. It any of these stars gives a binding, that's the one, ignore cycles on the other stars.