RFC: Support cyclic requires.#205
Conversation
We augment the require() function, the module interface, and the type checker to allow modules to cyclically require one another in many cases.
| 2. Look up the requested module in the cache to see if it has already been loaded or begun loading | ||
| 3. If a module is in the cache, return it immediately. Otherwise, | ||
| 4. Populate the cache with an empty table. |
There was a problem hiding this comment.
Might be better to indicate this is really one step with two alternatives.
| 2. Look up the requested module in the cache to see if it has already been loaded or begun loading | ||
| 3. If a module is in the cache, return it immediately. Otherwise, | ||
| 4. Populate the cache with an empty table. | ||
| 5. Pass this new table to the target script as its sole argument and evaluate it. This table can be accessed within the script via `...` at the top level. |
There was a problem hiding this comment.
Are there any backwards compatibility issues here?
There was a problem hiding this comment.
how would hot-reloading a module affect everything, since it talks about cache
|
|
||
| We will augment this class to instead work on one strongly-connected component\* at a time. All modules within an SCC use the same arena and are typechecked together in a single pass through the solver. | ||
|
|
||
| \* A "strongly connected component" is a set of modules that all mutually `require()` one another. |
There was a problem hiding this comment.
Wording nit: if I didn't know what an SCC was (or forgot because it's been a bit since my graph theory class), I might assume this only meant direct mutually recursive requires, e.g.:
--- A.luau
local B = require('B')
--- B.luau
local A = require('A')... and not just any set of modules where every one depends on every other via some chain of requires.
| For instance, the following code will fail: | ||
|
|
||
| ```luau | ||
| --- A.luau | ||
|
|
||
| local B = require("B") | ||
|
|
||
| class ClassAOne extends B.ClassOne ... end | ||
| class ClassATwo ... end | ||
|
|
||
| --- B.luau | ||
|
|
||
| local A = require("A") | ||
|
|
||
| class ClassBOne ... end | ||
| class ClassBTwo extends A.ClassATwo ... end | ||
| ``` |
There was a problem hiding this comment.
Food for thought, I think this would get tricker with a (sketched out) import syntax:
--- A.luau
import ClassBOne from B
export class ClassAOne extends ClassBOne ... end
export class ClassATwo ... end
--- B.luau
import ClassATwo from A
class ClassBOne ... end
class ClassBTwo extends ClassATwo ... end
... if we're considering this a serious drawback, this seems like the real danger.
|
|
||
| Today, typechecking is driven by a class called `Frontend`. It accepts a set of modules that need checking, builds a DAG from that, and checks modules one after another. | ||
|
|
||
| We will augment this class to instead work on one strongly-connected component\* at a time. All modules within an SCC use the same arena and are typechecked together in a single pass through the solver. |
There was a problem hiding this comment.
You could also keep them separate, and instead dispatch one constraint solver in the same component, and dispatch the next one if that one is stuck, and so on, until either all of them completes or all of them are stuck.
This would avoid a constraint from module A making its way into some random metavariable's bounds in module B.
|
The ceremony required to make modules that don't use static exports participate in cycles seems silly to me. As the RFC mentions, cycles are already rare for projects prior to classes, and since classes go hand-in-hand with static exports, I see no sense in adding an escape hatch for this edge case. Just support cycles only for modules that use static exports. That way you don't even have to worry about if a module returns a different value since it's impossible. |
| In the absence of `export`, a script must be updated to support cyclic requires by making a small edit: Instead of creating an export table directly with `{}`, the script should accept it from `...` like so: | ||
|
|
||
| ```luau | ||
| local exports = ... |
There was a problem hiding this comment.
So, this is GETVARARGS. Now, the thing I know about it, is that it's often used within function bodies.
Doing local var = ... would mean that it's taking 1 element from the var arg alone.
If I do local a,b,c = ... it would take 3 and not more.
What would the design have to change here? Or what would this syntax mean if it is within function bodies?
Or does nothing change about GETVARARGS and it will stay 1 element, it's just to accept the export table
|
Will there be a new kind of type? I know there's modules that do the following -- now we could require("Main.luau") for the varargs?
--- A.luau
return function (varargs)
print(varargs.OurEnvironment) -- inheritance from upper module, I guess.
endbut I guess cyclic requires would solve this too, e.g. for Require Tracer? -- Main.luau
local PortionA = require("A.luau")
local Main = {
OurEnvironment = {}
}
PortionA(Main) |
|
Wouldn't it be better if a separate keyword was used instead (i.e., A second benefit to this is that it handles the case where |
Rendered
We augment the
require()function, the module interface, and the type checker to allow modules to cyclically require one another in many cases.