From 968145d934e2169b2478687bb9d3439925947104 Mon Sep 17 00:00:00 2001
From: Ardi <113623122+hardlyardi@users.noreply.github.com>
Date: Thu, 13 Mar 2025 05:16:32 -0500
Subject: [PATCH 1/3] squash
---
...abstract-module-paths-and-init-dot-luau.md | 142 ++++++++++++++++++
docs/method-type-issubtypeof.md | 116 ++++++++++++++
docs/noinfer-type-operator.md | 102 +++++++++++++
docs/types-library-optional.md | 19 +++
4 files changed, 379 insertions(+)
create mode 100644 docs/abstract-module-paths-and-init-dot-luau.md
create mode 100644 docs/method-type-issubtypeof.md
create mode 100644 docs/noinfer-type-operator.md
create mode 100644 docs/types-library-optional.md
diff --git a/docs/abstract-module-paths-and-init-dot-luau.md b/docs/abstract-module-paths-and-init-dot-luau.md
new file mode 100644
index 00000000..240848cf
--- /dev/null
+++ b/docs/abstract-module-paths-and-init-dot-luau.md
@@ -0,0 +1,142 @@
+# Abstract module paths and `init.luau`
+
+## Summary
+
+We will redefine `foo/init.luau` to be the contents of the module named foo for
+the purpose of resolving relative `require()` calls within its body.
+
+We will do this because a significant portion of our users today rely on the
+idiom that `init.luau` represents the module's contents, especially when using
+Rojo to build for Roblox.
+
+## Motivation
+
+First, consider an example filesystem:
+
+```
+./foo.luau
+./package
+./package/init.luau
+./package/foo.luau
+./package/dependency.luau
+```
+
+Luau maps this filesystem into a module tree. Module paths largely correspond
+to filesystem paths, but they are distinct ideas. Given the filesystem above,
+Luau considers the following module paths to be valid:
+
+```
+foo
+package
+package/init
+package/foo
+package/dependency
+```
+
+When Luau is running on a conventional filesystem, we presently offer some
+special behaviour to afford the ability for modules to both contain other
+modules, and for them to themselves export values and types: The module path
+`package` is treated as an alias to `package/init`.
+
+Library authors can use this feature to afford a package hierarchy with a root
+package that exports all of the most important, basic functionality, plus
+submodules that either contain implementation details or additional public APIs.
+
+This feature works well, but it's incompatible with the idiom that Rojo, a
+popular tool in our ecosystem today, employs when building artifacts for Roblox.
+
+This incompatibility stems from how requires are resolved within the body of
+`init.luau`.
+
+Because we consider `package/init` to be an ordinary Luau module, it is
+considered to be rooted at `package`. Therefore, a relative
+`require('./foo')` call will resolve to `./package/foo.luau` rather than
+`./foo.luau`.
+
+Rojo, by contrast, considers `./package/init.luau` to belong to the containing
+directory. A developer using Rojo must instead write
+`require('./package/foo')`. This is only necessary when the script is named
+`init.luau`.
+
+Because of this incompatibility, scripts with require-by-string uses in Rojo
+projects will not work outside of Roblox places and vice versa. Since
+cross-runtime compatibility is an important goal of the require-by-string
+project, we would like to rectify this.
+
+## Design
+
+First, we introduce a new abstraction: a _module path_. Module paths refer
+either to _modules_ or _directories_.
+
+For the purposes of navigation, both modules and directories are functionally
+identical: modules and directories can both have children, which could
+themselves be modules or directories, and both types can have at most one
+parent, which could also be either a module or a directory.
+
+The thing that separates a module from a directory is precisely that modules
+represent source code that can be imported via the `require()` function.
+Directories, by contrast, are merely organizational units.
+
+The central feature of this RFC is about how module paths correspond to
+filesystem paths: A module path refers to a module if it corresponds either to a
+`.luau` file or to a directory that contains a file named `init.luau`. A module
+path refers to a directory if it refers to a filesystem directory that lacks an
+`init.luau` file.
+
+More concretely, Luau will no longer consider relative requires from a package
+`init.luau` file to resolve relative to the script itself. It will instead
+resolve relative to the location of the abstract module it represents, i.e. the
+location of its parent folder in a filesystem context.
+
+Secondly, we recognize an unfortunate side effect of this change: code within
+`package/init.luau` is forced to write `require('./package/dependency')` when it
+specifically wants to carry out the ordinary task of importing a subordinate
+module.
+
+We propose to alleviate this with a special import alias `@self` that resolves
+to the path to the current module.
+
+```lua
+-- package/init.luau
+
+local foo = require('./foo') -- This pulls in the outer foo!
+local foo = require('@self/foo') -- import package/foo
+```
+
+## Drawbacks
+
+On a filesystem, Rojo's behaviour is frankly very weird! A reader must know the
+name of the current source file in order to know what `require('./foo')` will
+do, and if a developer renames a file to or from `init.luau`, they will be
+forced to rewrite every `require()`.
+
+We could someday mitigate this with a new lint: If a module is the parent of other
+modules, it is poor style for it to directly import sibling or parent modules.
+Files named `init.luau` should never issue a require that resolves to a sibling
+module. In our example, we would warn if `package/init.luau` were to
+`require('./foo')` because that reaches outside of its folder.
+
+Further, as require-by-string has been live on Roblox for a little while, there
+is some risk that changing things now will break existing code. Roblox will use
+live telemetry to assess the impact of this change before we move forward.
+
+## Alternatives
+
+### Patching Rojo
+
+Adjusting Rojo to match what Luau does today without breaking any existing
+application code is tricky and most likely requires something far weirder than
+the change outlined in this RFC.
+
+Crucially, most code written using Rojo predates string-based requires. All of
+this code must continue to run exactly as-is.
+
+This puts us in quite a bind:
+
+* If we map `foo/init.luau` to an actual `ModuleScript` at `foo/init` in the
+ Roblox data model, then instance-based requires break. This rules out clever
+ solutions like aliasing modules and compatibility shims.
+* If we don't, then we need some other magic Roblox rule that changes the
+ require resolution rules
+ 1. Only for scripts that, on the Rojo side, are named `init.luau`, and
+ 2. Only for string-based requires
diff --git a/docs/method-type-issubtypeof.md b/docs/method-type-issubtypeof.md
new file mode 100644
index 00000000..6b77bc5c
--- /dev/null
+++ b/docs/method-type-issubtypeof.md
@@ -0,0 +1,116 @@
+# type:issubtypeof
+
+## Summary
+
+New method on the [`type`](./user-defined-type-functions.md#type-instance) userdata in type functions for performing subtype checks.
+
+## Motivation
+
+Currently there is no built in way to do subtype checks in User-Defined Type Functions. This means the developer has to write the following if they wanted to do a subtype check. Subtype checks are useful for type functions, because it makes code like in the following 2 example cases unneeded.
+
+Case 1: Checking if a type is a string or a string singleton
+```luau
+local is_string = t:is("string") or (t:is("singleton") and type(t:value()) == "string")
+```
+
+Case 2: Checking if a type is an enum
+```luau
+type function isenum(t)
+ if t:is("union") then
+ local components = t:components()
+ local components_that_are_string = 0
+
+ for _, component in components do
+ if tv:is("string") or (tv:is("singleton") and type(tv:value()) == "string") then
+ components_that_are_string += 1
+ end
+ end
+ return types.singleton(
+ components_that_are_string == #components
+ )
+ else
+ return types.singleton(false)
+ end
+end
+```
+
+Additionally it would be a pain for users to write a type function to be able to do subtype checks for any type. As the following example type function is already very long, despite not covering everything.
+
+```luau
+type function issubtypeof(a, b)
+ local false_type = types.singleton(false)
+ local true_type = types.singleton(true)
+ local is_subtype = false
+ local a_tag = a.tag
+ local b_tag = b.tag
+
+ if
+ (a_tag == b_tag) or
+ (b_tag == "singleton" and type(b:value()) == a_tag) or
+ (b_tag == "negation" and type(b:inner()) == a_tag)
+ then
+ is_subtype = true
+ elseif b_tag == "union" or b_tag == "intersection" then
+ local components = b:components()
+ local conmponents_that_are_subtype = 0
+
+ for _, component in components do
+ if issubtypeof(component):value() then
+ conmponents_that_are_subtype += 1
+ end
+ end
+
+ return types.singleton(#components == conmponents_that_are_subtype)
+ elseif b_tag == "table" and a_tag == "table" then
+ local b_mt = b:metatable()
+ local a_mt = a:metatable()
+
+ if b_mt == a_mt then
+ return true_type
+ elseif b_mt and a_mt then
+ local b_mts = { b_mt }
+ local next_b_mt
+ local next_a_mt = a_mt
+
+ while next_b_mt do
+ table.insert(b_mts, next_b_mt)
+ next_b_mt = next_b_mt:metatable()
+ end
+
+ while next_a_mt do
+ for _, mt in b_mts do
+ if mt == next_a_mt then
+ return true_type
+ end
+ end
+ next_a_mt = next_a_mt:metatable()
+ end
+ else
+ --[[
+ For the purposes of this rfc, my point has already been made. And I do not want to write the rest of this type function that nobody should ever use,
+ as its probably going to be an easy way to hit the type function time limit.
+ --]]
+ end
+ end
+
+ return types.singleton(is_subtype)
+end
+```
+
+## Design
+
+The [`type`](./user-defined-type-functions.md#type-instance) will gain a new method: `issubtypeof`:
+
+```luau
+type:issubtypeof(arg: type): boolean
+```
+
+This method will return a boolean indicating if the type is a subtype of `arg`, using luau's existing subtyping algorithm.
+
+## Drawbacks
+
+Adds another method to the type userdata in User-Defined type functions.
+
+## Alternatives
+
+Do nothing, and let users write the checks themselves as it is currently.
diff --git a/docs/noinfer-type-operator.md b/docs/noinfer-type-operator.md
new file mode 100644
index 00000000..0c891fb1
--- /dev/null
+++ b/docs/noinfer-type-operator.md
@@ -0,0 +1,102 @@
+# `noinfer` Type Operator
+
+## Summary
+
+This RFC builds on an alternative listed in [#106](https://github.com/luau-lang/rfcs/pull/106).
+ This RFC proposes the addition of a new type function, `noinfer`, which could be used to block type inference from various contexts.
+
+## Motivation
+
+When a user is working with a function which contains a generic type, they may wish for one or more inputs of the function not to contribute to type inference. E.g., a potential version of `table.insert`:
+
+```luau
+local function insert(tbl: { V }, value: V)
+ -- ...
+end
+```
+
+Take the following example in the current version of Luau's Type Inference Engine V2:
+
+```luau
+local some_table = { 1, 2, 3 }
+insert(some_table, true)
+```
+
+The above doesn't produce a type error, although it would have in The V1 Type Inference Engine. This is "expected" of Luau's Type Inference Engine V2, but might not be what a user wants.
+The purpose of this RFC is to allow users to annotate a type which will not contribute to type inference, such that a polymorphic type's value isn't inferred from an input to a `noinfer` type.
+
+## Design
+
+For the purpose of this RFC, additions to Luau's type function runtime will not be considered in the main body. This is because relevant changes to the types runtime or `types` library could be investigated in a future RFC.
+
+This RFC proposes a solution in the form of a type function, `noinfer`, which will block type inference on its input when a generic type is instantiated implicitly. This RFC allows, i.e., a `greedy_insert` function to be defined:
+
+```luau
+local function greedy_insert(tbl: { V }, value: noinfer)
+ -- ...
+end
+local a: "a"
+local b: "b"
+local some_table = { a, b }
+local c: "c"
+greedy_insert(some_table, c) -- TypeError: Type '"c"' could not be converted into '"a" | "b"'
+```
+
+In the above code, `greedy_insert` is instantiated as type `'(tbl: { "a" | "b" }, value: "a" | "b") -> ()'`, which causes a type error.
+This would behave similarly in a type alias:
+
+```luau
+type SomeAlias = {
+ foo: T,
+ bar: noinfer
+}
+local function test(input: SomeAlias)
+ -- ...
+end
+test({
+ foo = "hello",
+ bar = false
+}) -- TypeError: Type 'boolean' could not be converted to 'string' in an invariant context
+```
+
+If all provided generic inputs are represented by `noinfer` generic type(s), the generic should instantiate with `'unknown'`.
+
+## Drawbacks
+
+- Introduces a new keyword in type contexts.
+- Introducing a new "modifier" to types in luau could potentially increase the complexity of the type inference engine.
+- The behavior proposed here could be 'icky' in the context of some types & type aliases. Consider if a user has an alias with multiple optional properties they wish to belong to identical types. Someone might try:
+
+ ```luau
+ type Foo = {
+ foo: T?,
+ bar: noinfer?,
+ baz: noinfer?
+ }
+ local test: (Foo): T
+ ```
+
+ The above code would instantiate the generic with parameter `T` as `'unknown'` if the user tries to pass values `{bar = 123}`, and would not produce an error given the input `{bar = 123, baz = true}`. This RFC does not remove existing behaviour and does not block future solutions to this problem.
+
+## Alternatives
+
+### Greedy Annotation for Generic Types
+
+New syntax could be introduced to allow any inference of a generic type `T` to modify subsequent occurences to follow the behaviour of `noinfer`, expecting a more "precise" type match. This could look something like `greedy T` or `eager T`:
+
+```luau
+local function test(foo: T?, bar: T?, baz: T?)
+
+end
+test("hi", 1) -- TypeError: Type 'number' could not be converted into 'string?'
+```
+
+Because this error is dependant on the "order" of generic inputs traversed during instantiation, this alternative exposes an implementation detail in a way that provides error messages to the user. However, this alternative could solve some drawbacks of this RFC, while following similar motivation. Such a modifier for generic types isn't *exclusive* to this RFC, and could be investigated at a later date.
+
+### Do Nothing
+
+Introduce no new type function or syntax, leave Luau's Type Inference Engine V2 as-is. Due to the reasoning in the motivation section, this doesn't seem desireable.
+
+### Default Behaviour
+
+By default, assume similar behavior to the greedy/eager behaviour mentioned before, and add an annotation to allow polymorphic types to be "non-greedy". This defeats a lot of the purpose of bi-directional type inference in Luau's Type Inference Engine V2, as new features predating this RFC would become essentially opt-in.
diff --git a/docs/types-library-optional.md b/docs/types-library-optional.md
new file mode 100644
index 00000000..7221f179
--- /dev/null
+++ b/docs/types-library-optional.md
@@ -0,0 +1,19 @@
+# types.optional
+
+## Summary
+
+This RFC proposes adding a new method to the `types` library in type functions, that provides a shorthand for `types.unionof(type, types.singleton(nil))`.
+
+## Motivation
+
+Optional types occur very commonly, and it is annoying to have to always type `types.unionof(meow, types.singleton(nil))` to make a optional type, this leads to having the following appear in codebases that make use of optional types a lot in type functions:
+
+```luau
+type function optional(type)
+ return types.unionof(type, types.singleton(nil))
+end
+```
+
+## Design
+
+As a solution, a new method to the `types` library should be added called `optional`. When called on a union, this will add `nil` to the components. Otherwise, this new method will act the same as `types.unionof(type, types.singleton(nil))`.
From 6801974c3946b80551e982e0cce2394acff5274a Mon Sep 17 00:00:00 2001
From: Ardi <113623122+hardlyardi@users.noreply.github.com>
Date: Wed, 7 May 2025 04:03:54 -0500
Subject: [PATCH 2/3] Add alternative: Type Attributes
---
docs/noinfer-type-operator.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/noinfer-type-operator.md b/docs/noinfer-type-operator.md
index 0c891fb1..61a706f0 100644
--- a/docs/noinfer-type-operator.md
+++ b/docs/noinfer-type-operator.md
@@ -80,6 +80,10 @@ If all provided generic inputs are represented by `noinfer` generic type(s), the
## Alternatives
+### Type Attributes
+
+Luau could introduce syntax for 'type attributes', consistent with current function attribute syntax. Instead of masking a `noinfer` type as a type function, you could use `@noinfer T` as a type.
+
### Greedy Annotation for Generic Types
New syntax could be introduced to allow any inference of a generic type `T` to modify subsequent occurences to follow the behaviour of `noinfer`, expecting a more "precise" type match. This could look something like `greedy T` or `eager T`:
From b727d1995a84dd5758d129ba8b2e3d3dd6fe34eb Mon Sep 17 00:00:00 2001
From: Ardi <113623122+hardlyardi@users.noreply.github.com>
Date: Wed, 7 May 2025 21:30:53 -0500
Subject: [PATCH 3/3] Update noinfer-type-operator.md
---
docs/noinfer-type-operator.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/noinfer-type-operator.md b/docs/noinfer-type-operator.md
index 61a706f0..28cde845 100644
--- a/docs/noinfer-type-operator.md
+++ b/docs/noinfer-type-operator.md
@@ -82,7 +82,7 @@ If all provided generic inputs are represented by `noinfer` generic type(s), the
### Type Attributes
-Luau could introduce syntax for 'type attributes', consistent with current function attribute syntax. Instead of masking a `noinfer` type as a type function, you could use `@noinfer T` as a type.
+Luau could allow 'type attributes', consistent with current function attribute syntax. Instead of masking a `noinfer` type as a type function, you could use `@noinfer T` as a type with an attribute.
### Greedy Annotation for Generic Types