cluaupp 1.5.2 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +29 -41
  3. package/api/generated/bench-latest.json +43 -21
  4. package/api/generated/lsp-index.json +1 -1
  5. package/api/manifests/roblox-target.manifest.json +5 -5
  6. package/api/normalized/roblox-target.profile.json +8729 -267
  7. package/api/roblox/luau-builtins.d.luau +185 -0
  8. package/api/roblox/surface.json +1 -0
  9. package/api/roblox-api.lock.json +11 -2
  10. package/bin/cluaupp.js +38 -0
  11. package/editors/vscode/README.md +1 -1
  12. package/editors/vscode/package.json +50 -15
  13. package/editors/vscode/snippets/flare.json +1 -1
  14. package/editors/vscode/snippets/hive.json +1 -1
  15. package/editors/vscode/syntaxes/clpp.tmLanguage.json +211 -0
  16. package/examples/README.md +9 -9
  17. package/examples/game/cluaupp.config.json +2 -2
  18. package/examples/game/default.project.json +4 -15
  19. package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau +8 -0
  20. package/examples/game/out/ReplicatedStorage/Shared/GameInfo.luau.map.json +13 -0
  21. package/examples/game/out/ServerScriptService/Leaderstats.server.luau +26 -0
  22. package/examples/game/out/ServerScriptService/Leaderstats.server.luau.map.json +13 -0
  23. package/examples/game/out/ServerScriptService/Main.server.luau +15 -0
  24. package/examples/game/out/ServerScriptService/Main.server.luau.map.json +13 -0
  25. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau +10 -0
  26. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Main.client.luau.map.json +13 -0
  27. package/examples/game/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
  28. package/examples/game/src/ServerScriptService/Leaderstats.server.clp +18 -0
  29. package/examples/game/src/ServerScriptService/Main.server.clp +9 -0
  30. package/examples/game/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
  31. package/generated/api/build-profile.js +1 -1
  32. package/generated/api/loader.js +2 -2
  33. package/generated/api/surface/build.js +328 -0
  34. package/generated/api/surface/luau-defs.js +595 -0
  35. package/generated/api/surface/model.js +8 -0
  36. package/generated/api-cli.js +2 -2
  37. package/generated/cli.js +18 -17
  38. package/generated/clpp/contract.js +6 -3
  39. package/generated/clpp/handshake.js +36 -0
  40. package/generated/clpp/modules.js +20 -9
  41. package/generated/clpp/runner.js +62 -31
  42. package/generated/compile.js +18 -1
  43. package/generated/compiler/ast.js +3 -0
  44. package/generated/compiler/checker/calls.js +999 -0
  45. package/generated/compiler/checker/checker.js +1215 -0
  46. package/generated/compiler/checker/exprs.js +791 -0
  47. package/generated/compiler/checker/flow.js +168 -0
  48. package/generated/compiler/checker/members.js +763 -0
  49. package/generated/compiler/checker/statements.js +792 -0
  50. package/generated/compiler/checker/symbols.js +90 -0
  51. package/generated/compiler/checker/typeops.js +348 -0
  52. package/generated/compiler/checker/yields.js +115 -0
  53. package/generated/compiler/diagnostics.js +138 -0
  54. package/generated/compiler/emit/emitter.js +2356 -0
  55. package/generated/compiler/emit/luau-types.js +137 -0
  56. package/generated/compiler/emit/names.js +130 -0
  57. package/generated/compiler/emit/runtime.js +432 -0
  58. package/generated/compiler/lexer.js +403 -0
  59. package/generated/compiler/parser.js +1881 -0
  60. package/generated/compiler/project.js +332 -0
  61. package/generated/compiler/roblox/api.js +388 -0
  62. package/generated/compiler/roblox/context.js +100 -0
  63. package/generated/compiler/source.js +59 -0
  64. package/generated/compiler/stdlib.js +232 -0
  65. package/generated/compiler/types.js +250 -0
  66. package/generated/intellisense.js +1 -15
  67. package/generated/lsp.js +1 -1
  68. package/generated/target/build-cache.js +4 -4
  69. package/generated/target/datamodel-generate.js +12 -0
  70. package/generated/target/gen-rojo-tree.js +15 -0
  71. package/generated/target/source-map.js +61 -8
  72. package/generated/target/studio-bridge.js +1 -0
  73. package/generated/utils/project.js +104 -31
  74. package/include/README.md +5 -0
  75. package/native/bin/.gitkeep +1 -0
  76. package/package.json +17 -6
  77. package/plugins/CluauppNav/Plugin.luau +28 -4
  78. package/rust/cluaupp/Cargo.lock +795 -0
  79. package/rust/cluaupp/Cargo.toml +17 -0
  80. package/rust/cluaupp/src/main.rs +717 -0
  81. package/scripts/postinstall-native.js +56 -0
  82. package/skills/README.md +3 -3
  83. package/skills/clpp-hive-flare/SKILL.md +30 -26
  84. package/skills/clpp-language/SKILL.md +67 -74
  85. package/skills/clpp-language/compiler.md +24 -36
  86. package/skills/clpp-style/SKILL.md +31 -44
  87. package/src/api/build-profile.ts +1 -1
  88. package/src/api/loader.ts +2 -2
  89. package/src/api/surface/build.ts +374 -0
  90. package/src/api/surface/luau-defs.ts +635 -0
  91. package/src/api/surface/model.ts +96 -0
  92. package/src/api-cli.ts +2 -2
  93. package/src/cli.ts +19 -18
  94. package/src/clpp/contract.ts +20 -3
  95. package/src/clpp/handshake.ts +25 -0
  96. package/src/clpp/modules.ts +17 -10
  97. package/src/clpp/runner.ts +52 -34
  98. package/src/compile.ts +20 -2
  99. package/src/compiler/ast.ts +336 -0
  100. package/src/compiler/checker/calls.ts +1025 -0
  101. package/src/compiler/checker/checker.ts +1352 -0
  102. package/src/compiler/checker/exprs.ts +805 -0
  103. package/src/compiler/checker/flow.ts +174 -0
  104. package/src/compiler/checker/members.ts +793 -0
  105. package/src/compiler/checker/statements.ts +802 -0
  106. package/src/compiler/checker/symbols.ts +219 -0
  107. package/src/compiler/checker/typeops.ts +355 -0
  108. package/src/compiler/checker/yields.ts +120 -0
  109. package/src/compiler/diagnostics.ts +163 -0
  110. package/src/compiler/emit/emitter.ts +2486 -0
  111. package/src/compiler/emit/luau-types.ts +143 -0
  112. package/src/compiler/emit/names.ts +130 -0
  113. package/src/compiler/emit/runtime.ts +438 -0
  114. package/src/compiler/lexer.ts +444 -0
  115. package/src/compiler/parser.ts +1943 -0
  116. package/src/compiler/project.ts +354 -0
  117. package/src/compiler/roblox/api.ts +408 -0
  118. package/src/compiler/roblox/context.ts +108 -0
  119. package/src/compiler/source.ts +67 -0
  120. package/src/compiler/stdlib.ts +256 -0
  121. package/src/compiler/types.ts +313 -0
  122. package/src/intellisense.ts +1 -13
  123. package/src/lsp.ts +1 -1
  124. package/src/target/build-cache.ts +5 -8
  125. package/src/target/datamodel-generate.ts +12 -1
  126. package/src/target/gen-rojo-tree.ts +15 -0
  127. package/src/target/source-map.ts +65 -27
  128. package/src/target/studio-bridge.ts +3 -0
  129. package/src/types.ts +3 -0
  130. package/src/utils/project.ts +99 -37
  131. package/templates/README.md +7 -0
  132. package/templates/{game → game-modern}/cluaupp.config.json +2 -2
  133. package/templates/{game → game-modern}/package.json +1 -2
  134. package/templates/game-modern/rokit.toml +2 -0
  135. package/templates/game-modern/src/ReplicatedStorage/Shared/GameInfo.clp +1 -0
  136. package/templates/game-modern/src/ServerScriptService/Handlers/Guard.server.clp +4 -0
  137. package/templates/game-modern/src/ServerScriptService/Main.server.clp +8 -0
  138. package/templates/game-modern/src/StarterPlayer/StarterPlayerScripts/Main.client.clp +4 -0
  139. package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau +0 -29
  140. package/examples/game/out/ReplicatedStorage/Shared/Constants/Datas/TemplateData.luau.map.json +0 -13
  141. package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau +0 -83
  142. package/examples/game/out/ReplicatedStorage/Shared/Net/Net.luau.map.json +0 -13
  143. package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau +0 -18
  144. package/examples/game/out/ServerScriptService/Boot/DataBoot.server.luau.map.json +0 -13
  145. package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau +0 -14
  146. package/examples/game/out/ServerScriptService/Configuration/ServerConfig.luau.map.json +0 -13
  147. package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau +0 -15
  148. package/examples/game/out/ServerScriptService/Handlers/Guard.server.luau.map.json +0 -13
  149. package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau +0 -50
  150. package/examples/game/out/ServerScriptService/Handlers/PlayerHandler.server.luau.map.json +0 -13
  151. package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau +0 -12
  152. package/examples/game/out/StarterPlayer/StarterCharacterScripts/Character/CharacterReady.client.luau.map.json +0 -13
  153. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau +0 -16
  154. package/examples/game/out/StarterPlayer/StarterPlayerScripts/Controllers/DataController.client.luau.map.json +0 -13
  155. package/examples/game/src/Client/Controllers/DataController.client.clpp +0 -7
  156. package/examples/game/src/Client/Main.client.clpp +0 -4
  157. package/examples/game/src/Declarations/Net.flare +0 -5
  158. package/examples/game/src/Include/GameTypes.clh +0 -40
  159. package/examples/game/src/Modules/Core/Guard.server.clpp +0 -7
  160. package/examples/game/src/Modules/Core/PlayerHandler.server.clpp +0 -23
  161. package/examples/game/src/Server/Boot/DataBoot.server.clpp +0 -13
  162. package/examples/game/src/Server/Main.server.clpp +0 -4
  163. package/templates/game/.clangd +0 -25
  164. package/templates/game/.vscode/extensions.json +0 -8
  165. package/templates/game/.vscode/settings.json +0 -17
  166. package/templates/game/Packages/.gitkeep +0 -0
  167. package/templates/game/compile_flags.txt +0 -6
  168. package/templates/game/default.project.json +0 -41
  169. package/templates/game/include/clpp/datatypes.clh +0 -575
  170. package/templates/game/include/clpp/generated/enums.clh +0 -4920
  171. package/templates/game/include/clpp/generated/instances.clh +0 -87182
  172. package/templates/game/include/clpp/libs/arrayindexer.clh +0 -12
  173. package/templates/game/include/clpp/libs/axiom.clh +0 -154
  174. package/templates/game/include/clpp/libs/bloom.clh +0 -58
  175. package/templates/game/include/clpp/libs/coil.clh +0 -37
  176. package/templates/game/include/clpp/libs/crest.clh +0 -22
  177. package/templates/game/include/clpp/libs/echo.clh +0 -52
  178. package/templates/game/include/clpp/libs/ember.clh +0 -17
  179. package/templates/game/include/clpp/libs/flare.clh +0 -65
  180. package/templates/game/include/clpp/libs/gleam.clh +0 -111
  181. package/templates/game/include/clpp/libs/guide.clh +0 -42
  182. package/templates/game/include/clpp/libs/helm.clh +0 -31
  183. package/templates/game/include/clpp/libs/hive.clh +0 -20
  184. package/templates/game/include/clpp/libs/keep.clh +0 -158
  185. package/templates/game/include/clpp/libs/lens.clh +0 -24
  186. package/templates/game/include/clpp/libs/mint.clh +0 -28
  187. package/templates/game/include/clpp/libs/net.clh +0 -31
  188. package/templates/game/include/clpp/libs/occlude.clh +0 -15
  189. package/templates/game/include/clpp/libs/pin.clh +0 -28
  190. package/templates/game/include/clpp/libs/promise.clh +0 -24
  191. package/templates/game/include/clpp/libs/roster.clh +0 -123
  192. package/templates/game/include/clpp/libs/shift.clh +0 -22
  193. package/templates/game/include/clpp/libs/spark.clh +0 -28
  194. package/templates/game/include/clpp/libs/stage.clh +0 -23
  195. package/templates/game/include/clpp/libs/sweep.clh +0 -23
  196. package/templates/game/include/clpp/libs/trace.clh +0 -18
  197. package/templates/game/include/clpp/libs/ward.clh +0 -19
  198. package/templates/game/include/clpp/libs.clh +0 -31
  199. package/templates/game/include/clpp/roblox.clh +0 -41
  200. package/templates/game/rokit.toml +0 -5
  201. package/templates/game/src/Client/Controllers/DataController.client.clpp +0 -7
  202. package/templates/game/src/Client/Main.client.clpp +0 -4
  203. package/templates/game/src/Declarations/Net.flare +0 -5
  204. package/templates/game/src/Include/GameTypes.clh +0 -40
  205. package/templates/game/src/Modules/Core/Guard.server.clpp +0 -7
  206. package/templates/game/src/Modules/Core/PlayerHandler.server.clpp +0 -24
  207. package/templates/game/src/Server/Boot/DataBoot.server.clpp +0 -13
  208. package/templates/game/src/Server/Main.server.clpp +0 -4
  209. package/templates/game/stylua.toml +0 -9
  210. package/templates/game/wally.toml +0 -12
@@ -0,0 +1,219 @@
1
+ /** Symbols, scopes and the per-module declaration registries shared by checker and emitter. */
2
+
3
+ import type { EnumDecl, Expr, FuncDecl, StructDecl, SymbolRef } from "../ast.js";
4
+ import type { RunContext } from "../roblox/context.js";
5
+ import type { Span } from "../source.js";
6
+ import type { StdModule } from "../stdlib.js";
7
+ import type { Type } from "../types.js";
8
+
9
+ export type SymKind = "var" | "param" | "func" | "const" | "signal" | "module" | "struct" | "enum" | "variant" | "alias" | "namespace" | "self" | "typeparam";
10
+
11
+ export interface Sym extends SymbolRef {
12
+ kind: SymKind;
13
+ type: Type;
14
+ mutable: boolean;
15
+ span: Span;
16
+ file: string;
17
+ topLevel: boolean;
18
+ /** Function nesting depth where it was declared (0 = file level). */
19
+ depth: number;
20
+ constValue?: number | string | boolean | null;
21
+ observable?: boolean;
22
+ atomic?: boolean;
23
+ /** Referenced from a function nested deeper than its declaration. */
24
+ captured?: boolean;
25
+ used?: boolean;
26
+ assigned?: boolean;
27
+ /** Struct/enum/variant id for type symbols. */
28
+ typeId?: string;
29
+ link?: LinkTarget;
30
+ decl?: FuncDecl;
31
+ /** CL++ signal: handlers connected with `~>`. */
32
+ handlers?: Expr[];
33
+ signalParams?: Type[] | null;
34
+ /** Mutated after declaration (copy elision needs to know). */
35
+ mutatedFields?: boolean;
36
+ /** Declared with `[[server]]` / `[[client]]`. */
37
+ contextOnly?: "server" | "client";
38
+ /** Doc comment. */
39
+ doc?: string;
40
+ }
41
+
42
+ export class Scope {
43
+ readonly values = new Map<string, Sym>();
44
+ readonly types = new Map<string, Sym>();
45
+ /** Narrowed types for paths (`sym#3`, `sym#3.Character`). */
46
+ readonly narrowed = new Map<string, Type>();
47
+
48
+ constructor(
49
+ readonly parent: Scope | null,
50
+ readonly kind: "file" | "function" | "block" | "loop",
51
+ readonly depth: number,
52
+ ) {}
53
+
54
+ lookup(name: string): Sym | undefined {
55
+ let cur: Scope | null = this;
56
+ while (cur) {
57
+ const s = cur.values.get(name);
58
+ if (s) {
59
+ return s;
60
+ }
61
+ cur = cur.parent;
62
+ }
63
+ return undefined;
64
+ }
65
+
66
+ lookupType(name: string): Sym | undefined {
67
+ let cur: Scope | null = this;
68
+ while (cur) {
69
+ const s = cur.types.get(name);
70
+ if (s) {
71
+ return s;
72
+ }
73
+ cur = cur.parent;
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ narrowedType(key: string): Type | undefined {
79
+ let cur: Scope | null = this;
80
+ while (cur) {
81
+ if (cur.narrowed.has(key)) {
82
+ return cur.narrowed.get(key);
83
+ }
84
+ cur = cur.parent;
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ /** Drop narrowings of a path and everything below it (after an assignment). */
90
+ invalidate(prefix: string): void {
91
+ let cur: Scope | null = this;
92
+ while (cur) {
93
+ for (const key of [...cur.narrowed.keys()]) {
94
+ if (key === prefix || key.startsWith(`${prefix}.`)) {
95
+ cur.narrowed.delete(key);
96
+ }
97
+ }
98
+ cur = cur.parent;
99
+ }
100
+ // Mark as explicitly unknown at this level so outer narrowings stop applying.
101
+ }
102
+
103
+ allValueNames(): string[] {
104
+ const out: string[] = [];
105
+ let cur: Scope | null = this;
106
+ while (cur) {
107
+ out.push(...cur.values.keys());
108
+ cur = cur.parent;
109
+ }
110
+ return out;
111
+ }
112
+ }
113
+
114
+ export interface FieldInfo {
115
+ name: string;
116
+ type: Type;
117
+ owner: string;
118
+ isPrivate: boolean;
119
+ defaultValue: Expr | null;
120
+ span: Span;
121
+ doc?: string;
122
+ }
123
+
124
+ export interface MethodInfo {
125
+ name: string;
126
+ owner: string;
127
+ /** Struct id that first declared this method (the root of overrides). */
128
+ root: string;
129
+ decl: FuncDecl;
130
+ type: Extract<Type, { k: "func" }>;
131
+ isStatic: boolean;
132
+ isAbstract: boolean;
133
+ isFinal: boolean;
134
+ isPrivate: boolean;
135
+ /** Does the method assign to `self` fields? */
136
+ mutatesSelf?: boolean;
137
+ }
138
+
139
+ export interface StructInfo {
140
+ id: string;
141
+ name: string;
142
+ /** Name used in emitted Luau (renamed on collisions with Roblox types). */
143
+ luauName: string;
144
+ moduleId: string;
145
+ decl: StructDecl;
146
+ typeParams: string[];
147
+ bases: string[];
148
+ /** Every field in constructor order (inherited first, diamond-deduplicated). */
149
+ fields: FieldInfo[];
150
+ methods: Map<string, MethodInfo>;
151
+ subclasses: string[];
152
+ isAbstract: boolean;
153
+ isFinal: boolean;
154
+ resolved: boolean;
155
+ }
156
+
157
+ export interface EnumInfo {
158
+ id: string;
159
+ name: string;
160
+ luauName: string;
161
+ moduleId: string;
162
+ decl: EnumDecl;
163
+ cases: Array<{ name: string; payload: Type | null }>;
164
+ hasPayload: boolean;
165
+ }
166
+
167
+ export interface VariantInfo {
168
+ id: string;
169
+ name: string;
170
+ alternatives: Type[];
171
+ }
172
+
173
+ export interface ExportedFunc {
174
+ name: string;
175
+ type: Extract<Type, { k: "func" }>;
176
+ contextOnly?: "server" | "client";
177
+ yields?: boolean;
178
+ doc?: string;
179
+ }
180
+
181
+ export interface ModuleExports {
182
+ moduleId: string;
183
+ file: string;
184
+ kind: "script" | "module";
185
+ context: RunContext;
186
+ funcs: Map<string, ExportedFunc>;
187
+ structs: Map<string, StructInfo>;
188
+ enums: Map<string, EnumInfo>;
189
+ variants: Map<string, VariantInfo>;
190
+ aliases: Map<string, Type>;
191
+ consts: Map<string, { type: Type; value: number | string | boolean | null }>;
192
+ namespaces: Map<string, Map<string, ExportedFunc>>;
193
+ /** DataModel path of the compiled ModuleScript, when known. */
194
+ dmPath: string[] | null;
195
+ }
196
+
197
+ export type LinkTarget =
198
+ | { kind: "roblox" }
199
+ | { kind: "std"; module: StdModule }
200
+ | { kind: "clp"; exports: ModuleExports; dmPath: string[] | null; file: string }
201
+ | { kind: "luau"; dmPath: string[]; exports?: ModuleExports; label: string }
202
+ | { kind: "error"; message: string; notes?: string[] };
203
+
204
+ export function emptyExports(moduleId: string, file: string, kind: "script" | "module", context: RunContext): ModuleExports {
205
+ return {
206
+ moduleId,
207
+ file,
208
+ kind,
209
+ context,
210
+ funcs: new Map(),
211
+ structs: new Map(),
212
+ enums: new Map(),
213
+ variants: new Map(),
214
+ aliases: new Map(),
215
+ consts: new Map(),
216
+ namespaces: new Map(),
217
+ dmPath: null,
218
+ };
219
+ }
@@ -0,0 +1,355 @@
1
+ /** Assignability, joins and member lookups that do not need checker state. */
2
+
3
+ import type { RobloxApi } from "../roblox/api.js";
4
+ import { isDynamic, prune, sameType, T, type Type } from "../types.js";
5
+ import type { StructInfo } from "./symbols.js";
6
+
7
+ export interface TypeEnv {
8
+ api: RobloxApi;
9
+ struct(id: string): StructInfo | undefined;
10
+ }
11
+
12
+ export type Assignability = "ok" | "nullable" | "narrowing" | "no";
13
+
14
+ /**
15
+ * Can a value of type `from` be stored where `to` is expected?
16
+ * "nullable": only differs by a possible null (`Part?` into `Part`) — error in strict, warning otherwise.
17
+ * "narrowing": float into int — always an error (would lose the fraction).
18
+ */
19
+ export function assignable(env: TypeEnv, to: Type, from: Type): Assignability {
20
+ const a = prune(to);
21
+ const b = prune(from);
22
+ if (a === b || isDynamic(a) || isDynamic(b) || b.k === "never") {
23
+ return "ok";
24
+ }
25
+ if (a.k === "infer") {
26
+ a.resolved = b;
27
+ return "ok";
28
+ }
29
+ if (a.k === "param" || b.k === "param") {
30
+ return "ok";
31
+ }
32
+ if (b.k === "union") {
33
+ let worst: Assignability = "ok";
34
+ for (const m of b.members) {
35
+ const r = assignable(env, a, m);
36
+ if (r === "no") {
37
+ return "no";
38
+ }
39
+ if (r !== "ok") {
40
+ worst = r;
41
+ }
42
+ }
43
+ return worst;
44
+ }
45
+ switch (a.k) {
46
+ case "void":
47
+ return b.k === "void" ? "ok" : "no";
48
+ case "null":
49
+ return b.k === "null" ? "ok" : "no";
50
+ case "option": {
51
+ if (b.k === "null") {
52
+ return "ok";
53
+ }
54
+ if (b.k === "option") {
55
+ return assignable(env, a.inner, b.inner) === "no" ? "no" : assignable(env, a.inner, b.inner);
56
+ }
57
+ return assignable(env, a.inner, b);
58
+ }
59
+ case "union": {
60
+ let best: Assignability = "no";
61
+ for (const m of a.members) {
62
+ const r = assignable(env, m, b);
63
+ if (r === "ok") {
64
+ return "ok";
65
+ }
66
+ if (r !== "no") {
67
+ best = r;
68
+ }
69
+ }
70
+ if (b.k === "null" && a.members.some((m) => prune(m).k === "null")) {
71
+ return "ok";
72
+ }
73
+ return best;
74
+ }
75
+ case "float":
76
+ return b.k === "float" || b.k === "int" || (b.k === "lit" && typeof b.value === "number") ? "ok" : nullableOr(env, a, b);
77
+ case "int":
78
+ if (b.k === "int" || (b.k === "lit" && typeof b.value === "number" && Number.isInteger(b.value))) {
79
+ return "ok";
80
+ }
81
+ if (b.k === "float" || (b.k === "lit" && typeof b.value === "number")) {
82
+ return "narrowing";
83
+ }
84
+ if (b.k === "enum") {
85
+ return "ok";
86
+ }
87
+ return nullableOr(env, a, b);
88
+ case "bool":
89
+ return b.k === "bool" || (b.k === "lit" && typeof b.value === "boolean") ? "ok" : nullableOr(env, a, b);
90
+ case "string":
91
+ return b.k === "string" || (b.k === "lit" && typeof b.value === "string") ? "ok" : nullableOr(env, a, b);
92
+ case "lit":
93
+ return b.k === "lit" && b.value === a.value ? "ok" : nullableOr(env, a, b);
94
+ case "list":
95
+ if (b.k === "list") {
96
+ if (!a.typed || !b.typed) {
97
+ return "ok";
98
+ }
99
+ const r = assignable(env, a.elem, b.elem);
100
+ return r === "ok" || r === "nullable" ? r : "no";
101
+ }
102
+ if (b.k === "tuple") {
103
+ return b.elems.every((e) => assignable(env, a.elem, e) === "ok") ? "ok" : "no";
104
+ }
105
+ return nullableOr(env, a, b);
106
+ case "dict":
107
+ if (b.k === "dict") {
108
+ return assignable(env, a.key, b.key) !== "no" && assignable(env, a.value, b.value) !== "no" ? "ok" : "no";
109
+ }
110
+ if (b.k === "table") {
111
+ return Object.values(b.fields).every((f) => assignable(env, a.value, f) !== "no") ? "ok" : "no";
112
+ }
113
+ return nullableOr(env, a, b);
114
+ case "table":
115
+ if (b.k === "table") {
116
+ for (const [k, v] of Object.entries(a.fields)) {
117
+ const bf = b.fields[k];
118
+ if (!bf) {
119
+ if (prune(v).k !== "option") {
120
+ return "no";
121
+ }
122
+ continue;
123
+ }
124
+ if (assignable(env, v, bf) === "no") {
125
+ return "no";
126
+ }
127
+ }
128
+ return "ok";
129
+ }
130
+ if (b.k === "dict") {
131
+ return "ok";
132
+ }
133
+ return nullableOr(env, a, b);
134
+ case "struct":
135
+ if (b.k === "struct") {
136
+ if (a.id === b.id) {
137
+ return a.args.length === 0 || b.args.length === 0 || a.args.every((x, i) => sameType(x, b.args[i] ?? T.any)) ? "ok" : "no";
138
+ }
139
+ return structExtends(env, b.id, a.id) ? "ok" : "no";
140
+ }
141
+ return nullableOr(env, a, b);
142
+ case "enum":
143
+ case "variant":
144
+ if (b.k === a.k && b.id === a.id) {
145
+ return "ok";
146
+ }
147
+ if (a.k === "variant") {
148
+ return a.alternatives.some((alt) => assignable(env, alt, b) === "ok") ? "ok" : nullableOr(env, a, b);
149
+ }
150
+ return nullableOr(env, a, b);
151
+ case "result":
152
+ if (b.k === "result") {
153
+ const ok = assignable(env, a.ok, b.ok);
154
+ const err = assignable(env, a.err, b.err);
155
+ return ok !== "no" && err !== "no" ? "ok" : "no";
156
+ }
157
+ return nullableOr(env, a, b);
158
+ case "task":
159
+ return b.k === "task" ? "ok" : nullableOr(env, a, b);
160
+ case "func":
161
+ if (b.k === "func") {
162
+ return "ok";
163
+ }
164
+ return nullableOr(env, a, b);
165
+ case "instance":
166
+ if (b.k === "instance") {
167
+ return b.cls === a.cls || env.api.isA(b.cls, a.cls) ? "ok" : "no";
168
+ }
169
+ return nullableOr(env, a, b);
170
+ case "datatype":
171
+ if (b.k === "datatype") {
172
+ return b.name === a.name ? "ok" : "no";
173
+ }
174
+ return nullableOr(env, a, b);
175
+ case "rbxEnumItem":
176
+ if (b.k === "rbxEnumItem") {
177
+ return a.enumName === "*" || b.enumName === a.enumName || b.enumName === "*" ? "ok" : "no";
178
+ }
179
+ return nullableOr(env, a, b);
180
+ case "rbxEnum":
181
+ return b.k === "rbxEnum" && (a.enumName === "*" || a.enumName === b.enumName) ? "ok" : "no";
182
+ case "rbxSignal":
183
+ return b.k === "rbxSignal" ? "ok" : "no";
184
+ case "tuple":
185
+ return b.k === "tuple" && b.elems.length >= a.elems.length ? "ok" : "no";
186
+ case "thread":
187
+ case "buffer":
188
+ return b.k === a.k ? "ok" : nullableOr(env, a, b);
189
+ default:
190
+ return sameType(a, b) ? "ok" : "no";
191
+ }
192
+ }
193
+
194
+ function nullableOr(env: TypeEnv, to: Type, from: Type): Assignability {
195
+ if (from.k === "option") {
196
+ return assignable(env, to, from.inner) === "ok" ? "nullable" : "no";
197
+ }
198
+ if (from.k === "null") {
199
+ return "nullable";
200
+ }
201
+ return "no";
202
+ }
203
+
204
+ export function structExtends(env: TypeEnv, childId: string, baseId: string): boolean {
205
+ const seen = new Set<string>();
206
+ const stack = [childId];
207
+ while (stack.length) {
208
+ const id = stack.pop()!;
209
+ if (id === baseId) {
210
+ return true;
211
+ }
212
+ if (seen.has(id)) {
213
+ continue;
214
+ }
215
+ seen.add(id);
216
+ const info = env.struct(id);
217
+ if (info) {
218
+ stack.push(...info.bases);
219
+ }
220
+ }
221
+ return false;
222
+ }
223
+
224
+ /** Least upper bound used for ternaries, list literals and inferred returns. */
225
+ export function join(env: TypeEnv, a: Type, b: Type): Type {
226
+ const x = prune(a);
227
+ const y = prune(b);
228
+ if (isDynamic(x)) {
229
+ return y;
230
+ }
231
+ if (isDynamic(y)) {
232
+ return x;
233
+ }
234
+ if (x.k === "never") {
235
+ return y;
236
+ }
237
+ if (y.k === "never") {
238
+ return x;
239
+ }
240
+ if (sameType(x, y)) {
241
+ return x;
242
+ }
243
+ if (x.k === "null") {
244
+ return T.option(y);
245
+ }
246
+ if (y.k === "null") {
247
+ return T.option(x);
248
+ }
249
+ if (x.k === "option" || y.k === "option") {
250
+ const inner = join(env, x.k === "option" ? x.inner : x, y.k === "option" ? y.inner : y);
251
+ return T.option(inner);
252
+ }
253
+ const numeric = (t: Type) => t.k === "int" || t.k === "float" || (t.k === "lit" && typeof t.value === "number");
254
+ if (numeric(x) && numeric(y)) {
255
+ const isInt = (t: Type) => t.k === "int" || (t.k === "lit" && Number.isInteger(t.value));
256
+ return isInt(x) && isInt(y) ? T.int : T.float;
257
+ }
258
+ if (x.k === "lit" && y.k === "lit" && typeof x.value === typeof y.value) {
259
+ return typeof x.value === "string" ? T.string : typeof x.value === "boolean" ? T.bool : T.float;
260
+ }
261
+ if ((x.k === "lit" && typeof x.value === "string" && y.k === "string") || (y.k === "lit" && typeof y.value === "string" && x.k === "string")) {
262
+ return T.string;
263
+ }
264
+ if (x.k === "instance" && y.k === "instance") {
265
+ return T.instance(commonClass(env, x.cls, y.cls));
266
+ }
267
+ if (x.k === "struct" && y.k === "struct") {
268
+ if (structExtends(env, x.id, y.id)) {
269
+ return y;
270
+ }
271
+ if (structExtends(env, y.id, x.id)) {
272
+ return x;
273
+ }
274
+ }
275
+ if (x.k === "list" && y.k === "list") {
276
+ return T.list(join(env, x.elem, y.elem), x.typed && y.typed);
277
+ }
278
+ return { k: "union", members: [x, y] };
279
+ }
280
+
281
+ function commonClass(env: TypeEnv, a: string, b: string): string {
282
+ const chain = new Set<string>();
283
+ let cur: string | null = a;
284
+ while (cur) {
285
+ chain.add(cur);
286
+ cur = env.api.classes[cur]?.superclass ?? null;
287
+ }
288
+ cur = b;
289
+ while (cur) {
290
+ if (chain.has(cur)) {
291
+ return cur;
292
+ }
293
+ cur = env.api.classes[cur]?.superclass ?? null;
294
+ }
295
+ return "Instance";
296
+ }
297
+
298
+ /** Replace generic parameters by their bindings. */
299
+ export function substitute(t: Type, bindings: Map<string, Type>): Type {
300
+ if (bindings.size === 0) {
301
+ return t;
302
+ }
303
+ const p = prune(t);
304
+ switch (p.k) {
305
+ case "param":
306
+ return bindings.get(p.name) ?? p;
307
+ case "list":
308
+ return T.list(substitute(p.elem, bindings), p.typed);
309
+ case "dict":
310
+ return T.dict(substitute(p.key, bindings), substitute(p.value, bindings));
311
+ case "option":
312
+ return T.option(substitute(p.inner, bindings));
313
+ case "result":
314
+ return { k: "result", ok: substitute(p.ok, bindings), err: substitute(p.err, bindings) };
315
+ case "task":
316
+ return { k: "task", result: substitute(p.result, bindings) };
317
+ case "struct":
318
+ return p.args.length ? { ...p, args: p.args.map((x) => substitute(x, bindings)) } : p;
319
+ case "func":
320
+ return {
321
+ ...p,
322
+ params: p.params.map((x) => ({ ...x, type: substitute(x.type, bindings) })),
323
+ ret: substitute(p.ret, bindings),
324
+ variadic: p.variadic ? substitute(p.variadic, bindings) : null,
325
+ };
326
+ case "union":
327
+ return { k: "union", members: p.members.map((m) => substitute(m, bindings)) };
328
+ case "tuple":
329
+ return { k: "tuple", elems: p.elems.map((m) => substitute(m, bindings)) };
330
+ default:
331
+ return p;
332
+ }
333
+ }
334
+
335
+ /** Bind generic parameters by matching a declared type against an argument type. */
336
+ export function bindGenerics(declared: Type, actual: Type, bindings: Map<string, Type>): void {
337
+ const d = prune(declared);
338
+ const a = prune(actual);
339
+ if (d.k === "param") {
340
+ if (!bindings.has(d.name) && !isDynamic(a)) {
341
+ bindings.set(d.name, a.k === "lit" ? (typeof a.value === "string" ? T.string : typeof a.value === "boolean" ? T.bool : Number.isInteger(a.value) ? T.int : T.float) : a);
342
+ }
343
+ return;
344
+ }
345
+ if (d.k === "list" && a.k === "list") {
346
+ bindGenerics(d.elem, a.elem, bindings);
347
+ } else if (d.k === "option") {
348
+ bindGenerics(d.inner, a.k === "option" ? a.inner : a, bindings);
349
+ } else if (d.k === "dict" && a.k === "dict") {
350
+ bindGenerics(d.key, a.key, bindings);
351
+ bindGenerics(d.value, a.value, bindings);
352
+ } else if (d.k === "struct" && a.k === "struct") {
353
+ d.args.forEach((x, i) => a.args[i] && bindGenerics(x, a.args[i], bindings));
354
+ }
355
+ }
@@ -0,0 +1,120 @@
1
+ /** Can executing these statements yield the current thread (task.wait, WaitForChild, *Async, :Wait())? */
2
+
3
+ import type { Expr, Stmt } from "../ast.js";
4
+ import { prune } from "../types.js";
5
+ import type { Checker } from "./checker.js";
6
+
7
+ const YIELDING_GLOBALS = new Set(["wait", "Wait"]);
8
+ const YIELDING_LIBRARY = new Set(["task.wait", "coroutine.yield"]);
9
+
10
+ export function mayYield(c: Checker, body: Stmt[]): boolean {
11
+ let found = false;
12
+ const visitExpr = (e: Expr | null | undefined): void => {
13
+ if (!e || found) {
14
+ return;
15
+ }
16
+ if (exprYields(c, e)) {
17
+ found = true;
18
+ return;
19
+ }
20
+ for (const [key, value] of Object.entries(e)) {
21
+ if (key === "ty" || key === "res" || key === "span" || key === "narrowedFrom") {
22
+ continue;
23
+ }
24
+ if (key === "body" && e.kind === "Function") {
25
+ continue; // a closure's body runs later
26
+ }
27
+ walk(value);
28
+ }
29
+ };
30
+ const walk = (node: unknown): void => {
31
+ if (found || !node || typeof node !== "object") {
32
+ return;
33
+ }
34
+ if (Array.isArray(node)) {
35
+ for (const n of node) {
36
+ walk(n);
37
+ }
38
+ return;
39
+ }
40
+ const rec = node as { kind?: string };
41
+ if (rec.kind && isExprKind(rec.kind)) {
42
+ visitExpr(node as Expr);
43
+ return;
44
+ }
45
+ for (const [key, value] of Object.entries(node)) {
46
+ if (key === "ty" || key === "res" || key === "span" || key === "sym") {
47
+ continue;
48
+ }
49
+ walk(value);
50
+ }
51
+ };
52
+ walk(body);
53
+ return found;
54
+ }
55
+
56
+ const EXPR_KINDS = new Set([
57
+ "Number", "String", "Template", "Bool", "Null", "Name", "SelfField", "Member", "Scope", "Index", "Call", "New", "Unary",
58
+ "Binary", "Ternary", "Function", "Spawn", "Await", "Thread", "Join", "Move", "PCall", "Parallel", "Update", "Cast", "Error",
59
+ ]);
60
+
61
+ function isExprKind(kind: string): boolean {
62
+ return EXPR_KINDS.has(kind);
63
+ }
64
+
65
+ function exprYields(c: Checker, e: Expr): boolean {
66
+ if (e.kind === "Await" || e.kind === "Join" || e.kind === "Parallel") {
67
+ return true;
68
+ }
69
+ if (e.kind !== "Call") {
70
+ return false;
71
+ }
72
+ const callee = e.callee;
73
+ const res = callee.res;
74
+ if (!res) {
75
+ return true;
76
+ }
77
+ switch (res.kind) {
78
+ case "rbxMethod": {
79
+ const name = callee.kind === "Member" || callee.kind === "Scope" ? callee.name : "";
80
+ const hit = c.api.member(res.owner, name);
81
+ return Boolean(hit?.member.yields) || name === "WaitForChild" || /Async$/.test(name);
82
+ }
83
+ case "datatypeMember":
84
+ return res.datatype === "RBXScriptSignal" && res.member === "Wait";
85
+ case "global":
86
+ return YIELDING_GLOBALS.has(res.luau);
87
+ case "library":
88
+ return YIELDING_LIBRARY.has(`${res.lib}.${res.member}`);
89
+ case "func":
90
+ case "local": {
91
+ const decl = (res.sym as { decl?: { fn?: { mayYield?: boolean } } }).decl;
92
+ if (decl) {
93
+ return decl.fn?.mayYield ?? true;
94
+ }
95
+ const t = prune(callee.ty ?? { k: "any" });
96
+ return t.k !== "func";
97
+ }
98
+ case "method": {
99
+ const info = c.structs.get(res.struct);
100
+ const m = info?.methods.get(res.name);
101
+ return m?.decl.fn?.mayYield ?? true;
102
+ }
103
+ case "moduleMember":
104
+ return true;
105
+ case "builtin":
106
+ case "stdlib":
107
+ case "datatypeStatic":
108
+ case "rbxProperty":
109
+ case "rbxEvent":
110
+ case "enumCase":
111
+ case "struct":
112
+ case "signal":
113
+ return false;
114
+ case "dynamic":
115
+ case "luauMethod":
116
+ return true;
117
+ default:
118
+ return false;
119
+ }
120
+ }