test262 icon indicating copy to clipboard operation
test262 copied to clipboard

Import cycles through star exports

Open ghost opened this issue 2 years ago • 3 comments

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 avatar Jul 26 '23 01:07 ghost

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

ghost avatar Jul 05 '24 02:07 ghost

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?

ptomato avatar Jul 12 '24 17:07 ptomato

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 []
    • Middle [Left]
      • Left [Left Middle] -> cycle
      • Right [Left Middle]
        • Middle [Left Middle Right] -> cycle
      • Substance [Left Middle Right] -> binding
// 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.

ghost avatar Jul 12 '24 20:07 ghost