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,793 @@
1
+ /** Member access: CL++ structs/modules/enums and the Roblox API (properties, methods, events, children). */
2
+
3
+ import type { Expr } from "../ast.js";
4
+ import { suggest } from "../diagnostics.js";
5
+ import type { SurfaceMember } from "../../api/surface/model.js";
6
+ import { MEMBER_RULES, RISKY_MEMBERS } from "../roblox/context.js";
7
+ import { stdFunctionType, stdModule } from "../stdlib.js";
8
+ import { isDynamic, prune, T, typeToString, type DataModelNode, type Namespace, type Type } from "../types.js";
9
+ import type { Checker } from "./checker.js";
10
+ import { checkExpr, exprPathKey, rootSymbol } from "./exprs.js";
11
+ import type { ModuleExports, StructInfo, Sym } from "./symbols.js";
12
+ import { substitute } from "./typeops.js";
13
+
14
+ const VECTOR_COMPONENTS: Record<string, string[]> = {
15
+ Vector3: ["x", "y", "z"],
16
+ Vector2: ["x", "y"],
17
+ Vector3int16: ["x", "y", "z"],
18
+ Vector2int16: ["x", "y"],
19
+ };
20
+
21
+ const SIGNAL_METHODS = new Set(["Connect", "Once", "ConnectParallel", "Wait"]);
22
+
23
+ /** `string` library members usable with method syntax: `name.upper()` → `name:upper()`. */
24
+ const STRING_METHODS = new Set(["byte", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", "reverse", "split", "sub", "upper"]);
25
+
26
+ export interface MemberContext {
27
+ /** The member is being called (methods are fine). */
28
+ forCall: boolean;
29
+ }
30
+
31
+ export function checkMember(c: Checker, e: Extract<Expr, { kind: "Member" }>, expected?: Type, ctx: MemberContext = { forCall: false }): Type {
32
+ // observable: `coins.OnChange(f)`
33
+ if (e.name === "OnChange" && e.object.kind === "Name") {
34
+ const sym = c.scope.lookup(e.object.name);
35
+ if (sym && sym.observable) {
36
+ sym.used = true;
37
+ e.object.res = { kind: "local", sym };
38
+ e.object.ty = sym.type;
39
+ e.res = { kind: "builtin", name: "OnChange" };
40
+ return T.func([{ name: "handler", type: T.func([{ name: "value", type: sym.type, optional: false }], T.void), optional: false }], T.void);
41
+ }
42
+ }
43
+ const objType = prune(checkExpr(c, e.object));
44
+ return memberOf(c, e, objType, expected, ctx);
45
+ }
46
+
47
+ export function memberOf(c: Checker, e: Extract<Expr, { kind: "Member" }>, objType: Type, expected: Type | undefined, ctx: MemberContext): Type {
48
+ const name = e.name;
49
+ switch (objType.k) {
50
+ case "namespace":
51
+ return namespaceMember(c, e, objType.ns, ctx);
52
+ case "rbxEnums": {
53
+ if (name === "GetEnums") {
54
+ e.res = { kind: "rbxMethod", owner: "Enums" };
55
+ return T.func([], T.list({ k: "rbxEnum", enumName: "*" }));
56
+ }
57
+ if (!c.api.hasEnum(name)) {
58
+ const s = suggest(name, Object.keys(c.api.enums));
59
+ c.error("RBX5001", e.nameSpan, `Enum.${name} is not a Roblox enum`, s ? [`did you mean Enum.${s}?`] : undefined);
60
+ return T.unknown;
61
+ }
62
+ e.res = { kind: "rbxEnum", enumName: name };
63
+ return { k: "rbxEnum", enumName: name };
64
+ }
65
+ case "rbxEnum": {
66
+ if (name === "GetEnumItems" || name === "FromName" || name === "FromValue") {
67
+ requireCall(c, e, ctx);
68
+ e.res = { kind: "rbxMethod", owner: "Enum" };
69
+ const item: Type = { k: "rbxEnumItem", enumName: objType.enumName };
70
+ if (name === "GetEnumItems") {
71
+ return T.func([], T.list(item));
72
+ }
73
+ return T.func([{ name: name === "FromName" ? "name" : "value", type: name === "FromName" ? T.string : T.int, optional: false }], T.option(item));
74
+ }
75
+ const en = c.api.enums[objType.enumName];
76
+ if (!en || !en.items.includes(name)) {
77
+ const s = en ? suggest(name, en.items) : null;
78
+ c.error("RBX5003", e.nameSpan, `Enum.${objType.enumName} has no item '${name}'`, s ? [`did you mean Enum.${objType.enumName}.${s}?`] : en ? [`items: ${en.items.slice(0, 12).join(", ")}${en.items.length > 12 ? ", …" : ""}`] : undefined);
79
+ return T.unknown;
80
+ }
81
+ if (en.deprecated?.includes(name)) {
82
+ c.warn("RBX5010", e.nameSpan, `Enum.${objType.enumName}.${name} is deprecated`);
83
+ }
84
+ e.res = { kind: "rbxEnumItem", enumName: objType.enumName, item: name };
85
+ return { k: "rbxEnumItem", enumName: objType.enumName };
86
+ }
87
+ case "rbxEnumItem":
88
+ switch (name) {
89
+ case "Name":
90
+ e.res = { kind: "rbxProperty", owner: "EnumItem" };
91
+ return T.string;
92
+ case "Value":
93
+ e.res = { kind: "rbxProperty", owner: "EnumItem" };
94
+ return T.int;
95
+ case "EnumType":
96
+ e.res = { kind: "rbxProperty", owner: "EnumItem" };
97
+ return { k: "rbxEnum", enumName: objType.enumName };
98
+ case "IsA":
99
+ requireCall(c, e, ctx);
100
+ e.res = { kind: "rbxMethod", owner: "EnumItem" };
101
+ return T.func([{ name: "enumName", type: T.string, optional: false }], T.bool);
102
+ default:
103
+ c.error("RBX5004", e.nameSpan, `an EnumItem has Name, Value and EnumType, not '${name}'`);
104
+ return T.unknown;
105
+ }
106
+ case "instance":
107
+ return instanceMember(c, e, objType, ctx);
108
+ case "datatype":
109
+ return datatypeMember(c, e, objType.name, ctx);
110
+ case "rbxSignal": {
111
+ if (!SIGNAL_METHODS.has(name)) {
112
+ const s = suggest(name, [...SIGNAL_METHODS]);
113
+ c.error("RBX5005", e.nameSpan, `an event has Connect, Once, ConnectParallel and Wait, not '${name}'`, s ? [`did you mean '${s}'?`] : undefined);
114
+ return T.unknown;
115
+ }
116
+ requireCall(c, e, ctx);
117
+ e.res = { kind: "datatypeMember", datatype: "RBXScriptSignal", member: name, isMethod: true };
118
+ const handler = T.func(objType.args.map((a, i) => ({ name: `arg${i + 1}`, type: a, optional: false })), T.void);
119
+ if (name === "Wait") {
120
+ c.noteYield();
121
+ return T.func([], objType.args.length === 0 ? T.void : objType.args.length === 1 ? objType.args[0] : { k: "tuple", elems: objType.args });
122
+ }
123
+ if (name === "ConnectParallel") {
124
+ c.warn("PAR7001", e.nameSpan, "ConnectParallel handlers run in parallel: they cannot write Instance properties", ["only works in a script parented under an Actor"]);
125
+ }
126
+ return T.func([{ name: "handler", type: handler, optional: false }], T.datatype("RBXScriptConnection"));
127
+ }
128
+ case "struct":
129
+ return structMember(c, e, objType, ctx);
130
+ case "string":
131
+ case "lit": {
132
+ if (objType.k === "lit" && typeof objType.value !== "string") {
133
+ break;
134
+ }
135
+ if (STRING_METHODS.has(name)) {
136
+ requireCall(c, e, ctx);
137
+ c.requireRoblox(e.nameSpan, `string.${name}`);
138
+ const fn = prune(c.api.libraryFunction("string", name) ?? T.any);
139
+ e.res = { kind: "library", lib: "string", member: name, stringMethod: true };
140
+ if (fn.k === "func") {
141
+ return { ...fn, params: fn.params.slice(1) };
142
+ }
143
+ return T.any;
144
+ }
145
+ c.error("CL3090", e.nameSpan, `text has no member '${name}'`, name === "length" || name === "size" ? ["use len(text)"] : ["link @clpp.text and use Text." + name + "(…)"]);
146
+ return T.unknown;
147
+ }
148
+ case "list":
149
+ c.error("CL3091", e.nameSpan, `lists have no member '${name}'`, name === "length" || name === "size" || name === "Length" ? ["use len(list)"] : ["use push/pop/insert/remove/find/sort"]);
150
+ return T.unknown;
151
+ case "dict":
152
+ c.error("CL3092", e.nameSpan, `dictionaries are read with [key], not .${name}`, [`write d["${name}"]`]);
153
+ return T.unknown;
154
+ case "table": {
155
+ const f = objType.fields[name];
156
+ if (f) {
157
+ e.res = { kind: "dynamic" };
158
+ return f;
159
+ }
160
+ c.error("CL3093", e.nameSpan, `this table has no field '${name}'`, [`fields: ${Object.keys(objType.fields).join(", ")}`]);
161
+ return T.unknown;
162
+ }
163
+ case "option": {
164
+ c.strictError("CL3011", e.object.span, `${typeToString(objType)} may be null here`, [
165
+ `check it first: \`if (${describe(e.object)} != null) { … }\``,
166
+ ]);
167
+ const inner = memberOf(c, e, prune(objType.inner), expected, ctx);
168
+ return inner;
169
+ }
170
+ case "null":
171
+ c.error("CL3011", e.object.span, "this value is always null");
172
+ return T.unknown;
173
+ case "any":
174
+ case "unknown":
175
+ case "infer":
176
+ e.res = { kind: "dynamic" };
177
+ return T.any;
178
+ case "task":
179
+ c.error("CL3094", e.nameSpan, "a task has no members; get its result with `await`");
180
+ return T.unknown;
181
+ default:
182
+ break;
183
+ }
184
+ c.error("CL3095", e.nameSpan, `${typeToString(objType)} has no member '${name}'`);
185
+ return T.unknown;
186
+ }
187
+
188
+ function describe(e: Expr): string {
189
+ switch (e.kind) {
190
+ case "Name":
191
+ return e.name;
192
+ case "Member":
193
+ return `${describe(e.object)}.${e.name}`;
194
+ case "SelfField":
195
+ return `@${e.name}`;
196
+ default:
197
+ return "value";
198
+ }
199
+ }
200
+
201
+ function requireCall(c: Checker, e: Extract<Expr, { kind: "Member" }>, ctx: MemberContext): void {
202
+ if (!ctx.forCall) {
203
+ c.error("CL3096", e.nameSpan, `'${e.name}' is a method; call it: ${describe(e)}(…)`, ["to pass it around, wrap it: `func () { " + describe(e) + "(); }`"]);
204
+ }
205
+ }
206
+
207
+ // ---------- namespaces ----------
208
+
209
+ function namespaceMember(c: Checker, e: Extract<Expr, { kind: "Member" }>, ns: Namespace, ctx: MemberContext): Type {
210
+ const name = e.name;
211
+ switch (ns.kind) {
212
+ case "module":
213
+ return moduleMember(c, e, ns.alias, ns.moduleId);
214
+ case "luauModule":
215
+ e.res = { kind: "moduleMember", alias: ns.alias, moduleId: ns.alias, member: name, memberKind: "value" };
216
+ return T.any;
217
+ case "library": {
218
+ const lib = c.api.library(ns.name);
219
+ const member = lib?.[name];
220
+ if (!member) {
221
+ if (ns.name === "task" && name === "defer") {
222
+ // CL++ core task.defer is the Roblox one
223
+ }
224
+ const s = lib ? suggest(name, Object.keys(lib)) : null;
225
+ c.error("RBX5006", e.nameSpan, `${ns.name}.${name} does not exist`, s ? [`did you mean ${ns.name}.${s}?`] : undefined);
226
+ return T.unknown;
227
+ }
228
+ if (member.deprecated) {
229
+ c.warn("RBX5010", e.nameSpan, `${ns.name}.${name} is deprecated`);
230
+ }
231
+ e.res = { kind: "library", lib: ns.name, member: name };
232
+ if (ns.name === "task" && name === "wait") {
233
+ c.noteYield();
234
+ }
235
+ if (ns.name === "coroutine" && name === "yield") {
236
+ c.noteYield();
237
+ }
238
+ return c.api.libraryFunction(ns.name, name) ?? T.any;
239
+ }
240
+ case "datatype": {
241
+ const st = c.api.datatypeStatic(ns.name, name);
242
+ if (!st) {
243
+ const statics = c.api.datatypes[ns.name]?.statics ?? {};
244
+ const s = suggest(name, Object.keys(statics));
245
+ c.error("RBX5007", e.nameSpan, `${ns.name}.${name} does not exist`, s ? [`did you mean ${ns.name}.${s}?`] : [`available: ${Object.keys(statics).join(", ")}`]);
246
+ return T.unknown;
247
+ }
248
+ if (st.deprecated) {
249
+ c.warn("RBX5010", e.nameSpan, `${ns.name}.${name} is deprecated`);
250
+ }
251
+ if (ns.name !== "Vector3" && ns.name !== "Vector2") {
252
+ c.requireRoblox(e.span, ns.name);
253
+ }
254
+ e.res = { kind: "datatypeStatic", datatype: ns.name, member: name };
255
+ return c.api.staticFunctionType(st);
256
+ }
257
+ case "instanceStatic":
258
+ if (name === "new" || name === "fromExisting") {
259
+ e.res = { kind: "datatypeStatic", datatype: "Instance", member: name };
260
+ return name === "new"
261
+ ? T.func([{ name: "className", type: T.string, optional: false }, { name: "parent", type: T.instance("Instance"), optional: true }], T.instance("Instance"))
262
+ : T.func([{ name: "existing", type: T.instance("Instance"), optional: false }], T.instance("Instance"));
263
+ }
264
+ c.error("RBX5007", e.nameSpan, `Instance.${name} does not exist`, ["Instance.new(className) or Instance.fromExisting(instance)"]);
265
+ return T.unknown;
266
+ case "struct": {
267
+ const info = c.structs.get(ns.id);
268
+ const m = info?.methods.get(name);
269
+ if (m && m.isStatic) {
270
+ e.res = { kind: "method", struct: ns.id, name, isStatic: true };
271
+ return m.type;
272
+ }
273
+ if (m) {
274
+ c.error("CL3097", e.nameSpan, `'${name}' is an instance method; call it on a ${ns.name} value`);
275
+ return m.type;
276
+ }
277
+ const s = info ? suggest(name, [...info.methods.keys()]) : null;
278
+ c.error("CL3098", e.nameSpan, `struct ${ns.name} has no static member '${name}'`, s ? [`did you mean '${s}'?`] : undefined);
279
+ return T.unknown;
280
+ }
281
+ case "enum": {
282
+ const info = c.enums.get(ns.id);
283
+ if (!info) {
284
+ return T.unknown;
285
+ }
286
+ const index = info.cases.findIndex((k) => k.name === name);
287
+ if (index < 0) {
288
+ const s = suggest(name, info.cases.map((k) => k.name));
289
+ c.error("CL3099", e.nameSpan, `enum ${info.name} has no case '${name}'`, s ? [`did you mean ${info.name}.${s}?`] : [`cases: ${info.cases.map((k) => k.name).join(", ")}`]);
290
+ return T.unknown;
291
+ }
292
+ const kase = info.cases[index];
293
+ e.res = { kind: "enumCase", enumName: info.id, index, hasPayload: Boolean(kase.payload), anyPayload: info.hasPayload };
294
+ const enumType: Type = { k: "enum", id: info.id, name: info.name };
295
+ if (kase.payload) {
296
+ if (!ctx.forCall) {
297
+ c.error("CL3100", e.nameSpan, `${info.name}.${name} carries a ${typeToString(kase.payload)}; build it with ${info.name}.${name}(value)`);
298
+ }
299
+ return T.func([{ name: "value", type: kase.payload, optional: false }], enumType);
300
+ }
301
+ return enumType;
302
+ }
303
+ case "clNamespace": {
304
+ const funcs = ns.moduleId === c.input.moduleId ? c.exports.namespaces.get(ns.name) : undefined;
305
+ const f = funcs?.get(name);
306
+ if (!f) {
307
+ const s = funcs ? suggest(name, [...funcs.keys()]) : null;
308
+ c.error("CL2022", e.nameSpan, `namespace ${ns.name} has no function '${name}'`, s ? [`did you mean '${s}'?`] : undefined);
309
+ return T.unknown;
310
+ }
311
+ e.res = { kind: "moduleMember", alias: ns.name, moduleId: "namespace", member: name, memberKind: "func" };
312
+ return f.type;
313
+ }
314
+ case "stdlib": {
315
+ const fn = stdModule(ns.module).get(name);
316
+ if (!fn) {
317
+ const s = suggest(name, [...stdModule(ns.module).keys()]);
318
+ c.error("CL2023", e.nameSpan, `${ns.alias}.${name} does not exist`, s ? [`did you mean ${ns.alias}.${s}?`] : undefined);
319
+ return T.unknown;
320
+ }
321
+ e.res = { kind: "stdlib", module: ns.module, member: name };
322
+ return stdFunctionType(fn);
323
+ }
324
+ default:
325
+ return T.unknown;
326
+ }
327
+ }
328
+
329
+ function moduleMember(c: Checker, e: Extract<Expr, { kind: "Member" }>, alias: string, moduleId: string): Type {
330
+ const sym = c.scope.lookup(alias);
331
+ const target = sym?.link;
332
+ const exports: ModuleExports | undefined = target && (target.kind === "clp" ? target.exports : target.kind === "luau" ? target.exports : undefined);
333
+ if (!exports) {
334
+ e.res = { kind: "moduleMember", alias, moduleId, member: e.name, memberKind: "value" };
335
+ return T.any;
336
+ }
337
+ const name = e.name;
338
+ const f = exports.funcs.get(name);
339
+ if (f) {
340
+ e.res = { kind: "moduleMember", alias, moduleId, member: name, memberKind: "func" };
341
+ if (f.contextOnly) {
342
+ c.checkContext(f.contextOnly, e.nameSpan, `${alias}.${name}`, `it is marked [[${f.contextOnly}]]`);
343
+ }
344
+ return f.type;
345
+ }
346
+ const k = exports.consts.get(name);
347
+ if (k) {
348
+ e.res = { kind: "const", value: k.value };
349
+ return k.type;
350
+ }
351
+ const s = exports.structs.get(name);
352
+ if (s) {
353
+ e.res = { kind: "moduleMember", alias, moduleId, member: name, memberKind: "struct" };
354
+ return { k: "namespace", ns: { kind: "struct", id: s.id, name: s.name } };
355
+ }
356
+ const en = exports.enums.get(name);
357
+ if (en) {
358
+ e.res = { kind: "moduleMember", alias, moduleId, member: name, memberKind: "enum" };
359
+ return { k: "namespace", ns: { kind: "enum", id: en.id, name: en.name } };
360
+ }
361
+ const nsFuncs = exports.namespaces.get(name);
362
+ if (nsFuncs) {
363
+ e.res = { kind: "moduleMember", alias, moduleId, member: name, memberKind: "value" };
364
+ return { k: "namespace", ns: { kind: "clNamespace", name, moduleId: exports.moduleId } };
365
+ }
366
+ const v = exports.variants.get(name);
367
+ if (v) {
368
+ e.res = { kind: "moduleMember", alias, moduleId, member: name, memberKind: "func" };
369
+ return T.func([{ name: "value", type: { k: "union", members: v.alternatives }, optional: false }], { k: "variant", id: v.id, name: v.name, alternatives: v.alternatives });
370
+ }
371
+ const names = [...exports.funcs.keys(), ...exports.consts.keys(), ...exports.structs.keys(), ...exports.enums.keys()];
372
+ const hint = suggest(name, names);
373
+ c.error("CL2024", e.nameSpan, `module ${alias} has no member '${name}'`, hint ? [`did you mean ${alias}.${hint}?`] : names.length ? [`it exports: ${names.slice(0, 10).join(", ")}${names.length > 10 ? ", …" : ""}`] : ["it exports nothing"]);
374
+ return T.unknown;
375
+ }
376
+
377
+ // ---------- Roblox instances ----------
378
+
379
+ function memberRuleFor(c: Checker, cls: string, member: string): (typeof MEMBER_RULES)[string] | undefined {
380
+ let cur: string | null = cls;
381
+ let guard = 0;
382
+ while (cur && guard++ < 64) {
383
+ const rule = MEMBER_RULES[`${cur}.${member}`];
384
+ if (rule) {
385
+ return rule;
386
+ }
387
+ cur = c.api.classes[cur]?.superclass ?? null;
388
+ }
389
+ return undefined;
390
+ }
391
+
392
+ function checkSecurity(c: Checker, e: Extract<Expr, { kind: "Member" }>, owner: string, member: SurfaceMember, write: boolean): void {
393
+ const level = write ? member.writeSecurity : member.security;
394
+ if (member.notScriptable) {
395
+ c.error("RBX5040", e.nameSpan, `${owner}.${e.name} is not scriptable`);
396
+ return;
397
+ }
398
+ if (!level) {
399
+ return;
400
+ }
401
+ if (level === "PluginSecurity") {
402
+ if (c.input.context !== "plugin") {
403
+ c.error("RBX5041", e.nameSpan, `${owner}.${e.name} needs plugin security; game scripts cannot ${write ? "set" : "use"} it`);
404
+ }
405
+ return;
406
+ }
407
+ c.error("RBX5042", e.nameSpan, `${owner}.${e.name} is restricted (${level}); game scripts cannot ${write ? "set" : "use"} it`);
408
+ }
409
+
410
+ function deprecation(c: Checker, e: Extract<Expr, { kind: "Member" }>, owner: string, member: SurfaceMember): void {
411
+ if (!member.deprecated) {
412
+ return;
413
+ }
414
+ const use = typeof member.deprecated === "string" ? member.deprecated.replace(/^.*?:/, "") : null;
415
+ c.warn("RBX5010", e.nameSpan, `${owner}.${e.name} is deprecated${use ? `; use ${use}` : ""}`);
416
+ }
417
+
418
+ function instanceMember(c: Checker, e: Extract<Expr, { kind: "Member" }>, obj: Extract<Type, { k: "instance" }>, ctx: MemberContext): Type {
419
+ const name = e.name;
420
+ const api = c.api;
421
+ // game.Players → GetService("Players")
422
+ if (obj.cls === "DataModel" && api.isService(name) && !api.member("DataModel", name)) {
423
+ c.useService(name, e.nameSpan);
424
+ e.res = { kind: "service", service: name };
425
+ const dm = obj.dm?.children.get(name);
426
+ return { k: "instance", cls: name, ...(dm ? { dm } : {}) };
427
+ }
428
+ const hit = api.member(obj.cls, name);
429
+ if (hit) {
430
+ const m = hit.member;
431
+ deprecation(c, e, hit.owner, m);
432
+ checkSecurity(c, e, hit.owner, m, false);
433
+ const rule = memberRuleFor(c, obj.cls, name);
434
+ if (rule) {
435
+ c.checkContext(rule.only, e.nameSpan, `${obj.cls}.${name}`, rule.why, rule.severity);
436
+ }
437
+ const risky = RISKY_MEMBERS[`${hit.owner}.${name}`];
438
+ if (risky) {
439
+ c.warn("RBX5043", e.nameSpan, `${hit.owner}.${name}: ${risky}`);
440
+ }
441
+ switch (m.kind) {
442
+ case "property": {
443
+ e.res = { kind: "rbxProperty", owner: hit.owner };
444
+ const t = m.type ? api.convert(m.type) : T.any;
445
+ if (c.fn?.isParallel) {
446
+ // reading is fine in parallel; writes are checked in checkAssignTarget
447
+ }
448
+ return t;
449
+ }
450
+ case "method":
451
+ requireCall(c, e, ctx);
452
+ e.res = { kind: "rbxMethod", owner: hit.owner };
453
+ if (m.yields || name === "WaitForChild" || /Async$/.test(name)) {
454
+ c.noteYield();
455
+ }
456
+ return api.methodType(m);
457
+ case "event":
458
+ e.res = { kind: "rbxEvent", owner: hit.owner };
459
+ return { k: "rbxSignal", args: (m.args || []).map((a) => api.convert(a)) };
460
+ case "callback":
461
+ e.res = { kind: "rbxProperty", owner: hit.owner };
462
+ return m.type ? api.convert(m.type) : T.any;
463
+ default:
464
+ return T.any;
465
+ }
466
+ }
467
+ // Typed DataModel: children known from the project layout.
468
+ const child = obj.dm?.children.get(name);
469
+ if (child) {
470
+ e.res = { kind: "rbxChild", owner: obj.cls, childName: name, safe: true };
471
+ return dmType(child);
472
+ }
473
+ const members = [...api.members(obj.cls).keys()];
474
+ const typo = suggest(name, members);
475
+ if (typo) {
476
+ c.error("RBX5020", e.nameSpan, `${obj.cls} has no member '${name}'`, [`did you mean '${typo}'?`]);
477
+ return T.unknown;
478
+ }
479
+ if (/^[a-z]/.test(name) && !obj.dm) {
480
+ c.error("RBX5020", e.nameSpan, `${obj.cls} has no member '${name}'`, ["Roblox members are PascalCase"]);
481
+ return T.unknown;
482
+ }
483
+ const known = obj.dm ? [...obj.dm.children.keys()] : [];
484
+ const notes = [
485
+ `if '${name}' is a child Instance, prefer ${describe(e.object)}.WaitForChild("${name}") (it may not exist yet) or FindFirstChild`,
486
+ ];
487
+ if (known.length) {
488
+ notes.unshift(`known children: ${known.slice(0, 8).join(", ")}${known.length > 8 ? ", …" : ""}`);
489
+ }
490
+ c.strictError("RBX5021", e.nameSpan, `'${name}' is not a property of ${obj.cls}; it is read as a child Instance`, notes);
491
+ e.res = { kind: "rbxChild", owner: obj.cls, childName: name, safe: false };
492
+ return T.instance("Instance");
493
+ }
494
+
495
+ export function dmType(node: DataModelNode): Type {
496
+ return { k: "instance", cls: node.className, dm: node };
497
+ }
498
+
499
+ function datatypeMember(c: Checker, e: Extract<Expr, { kind: "Member" }>, name: string, ctx: MemberContext): Type {
500
+ const member = e.name;
501
+ const components = VECTOR_COMPONENTS[name];
502
+ if (components && components.includes(member)) {
503
+ e.res = { kind: "vectorComponent", component: member.toUpperCase() };
504
+ return T.float;
505
+ }
506
+ if (name === "RBXScriptConnection") {
507
+ if (member === "Disconnect") {
508
+ requireCall(c, e, ctx);
509
+ e.res = { kind: "datatypeMember", datatype: name, member, isMethod: true };
510
+ return T.func([], T.void);
511
+ }
512
+ if (member === "Connected") {
513
+ e.res = { kind: "datatypeMember", datatype: name, member, isMethod: false };
514
+ return T.bool;
515
+ }
516
+ }
517
+ const m = c.api.datatypeMember(name, member);
518
+ if (!m) {
519
+ const dt = c.api.datatypes[name];
520
+ const s = dt ? suggest(member, Object.keys(dt.members)) : null;
521
+ c.error("RBX5022", e.nameSpan, `${name} has no member '${member}'`, s ? [`did you mean '${s}'?`] : components ? [`components: ${components.join(", ")} (or ${components.map((x) => x.toUpperCase()).join(", ")})`] : undefined);
522
+ return T.unknown;
523
+ }
524
+ if (m.deprecated) {
525
+ c.warn("RBX5010", e.nameSpan, `${name}.${member} is deprecated`);
526
+ }
527
+ if (m.kind === "method") {
528
+ requireCall(c, e, ctx);
529
+ e.res = { kind: "datatypeMember", datatype: name, member, isMethod: true };
530
+ return c.api.methodType(m);
531
+ }
532
+ if (components && /^[XYZ]$/.test(member)) {
533
+ e.res = { kind: "vectorComponent", component: member };
534
+ return T.float;
535
+ }
536
+ e.res = { kind: "datatypeMember", datatype: name, member, isMethod: false };
537
+ return m.type ? c.api.convert(m.type) : T.any;
538
+ }
539
+
540
+ // ---------- CL++ structs ----------
541
+
542
+ function structMember(c: Checker, e: Extract<Expr, { kind: "Member" }>, obj: Extract<Type, { k: "struct" }>, ctx: MemberContext): Type {
543
+ const info = c.structs.get(obj.id);
544
+ if (!info) {
545
+ return T.unknown;
546
+ }
547
+ const bindings = new Map<string, Type>();
548
+ info.typeParams.forEach((p, i) => {
549
+ const arg = obj.args[i];
550
+ if (arg && !(prune(arg).k === "param" && (prune(arg) as { name: string }).name === p)) {
551
+ bindings.set(p, arg);
552
+ }
553
+ });
554
+ const field = info.fields.find((f) => f.name === e.name);
555
+ if (field) {
556
+ if (field.isPrivate && !canSeePrivate(c, info)) {
557
+ c.error("CL2041", e.nameSpan, `'${e.name}' is a private member of ${field.owner}`, ["only methods of the struct can use it"]);
558
+ }
559
+ e.res = { kind: "field", struct: info.id };
560
+ return substitute(field.type, bindings);
561
+ }
562
+ const method = info.methods.get(e.name);
563
+ if (method) {
564
+ if (method.isPrivate && !canSeePrivate(c, info)) {
565
+ c.error("CL2041", e.nameSpan, `'${e.name}' is a private method of ${method.owner}`);
566
+ }
567
+ requireCall(c, e, ctx);
568
+ e.res = { kind: "method", struct: info.id, name: e.name, isStatic: method.isStatic };
569
+ return substitute(method.type, bindings);
570
+ }
571
+ const s = suggest(e.name, [...info.fields.map((f) => f.name), ...info.methods.keys()]);
572
+ c.error("CL2006", e.nameSpan, `${info.name} has no field or method '${e.name}'`, s ? [`did you mean '${s}'?`] : [`fields: ${info.fields.map((f) => f.name).join(", ") || "none"}`]);
573
+ return T.unknown;
574
+ }
575
+
576
+ function canSeePrivate(c: Checker, info: StructInfo): boolean {
577
+ const here = c.fn?.structId;
578
+ if (!here) {
579
+ return false;
580
+ }
581
+ if (here === info.id) {
582
+ return true;
583
+ }
584
+ return info.fields.some((f) => f.owner === c.structs.get(here)?.name);
585
+ }
586
+
587
+ // ---------- a::b ----------
588
+
589
+ const NATIVE_RENAMES: Record<string, Record<string, string>> = {
590
+ buffer: { size: "len", write_string: "writestring", read_string: "readstring", from_string: "fromstring", to_string: "tostring" },
591
+ string: { trim: "__trim", upper: "upper", lower: "lower", starts_with: "__startsWith", ends_with: "__endsWith" },
592
+ };
593
+
594
+ export function checkScopeAccess(c: Checker, e: Extract<Expr, { kind: "Scope" }>): Type {
595
+ const objType = prune(checkExpr(c, e.object));
596
+ if (objType.k === "namespace") {
597
+ let name = e.name;
598
+ if (objType.ns.kind === "library") {
599
+ const renamed = NATIVE_RENAMES[objType.ns.name]?.[name] ?? name.replace(/_/g, "");
600
+ if (renamed.startsWith("__")) {
601
+ const text = stdModule("text");
602
+ const fn = text.get(renamed.slice(2));
603
+ if (fn) {
604
+ e.res = { kind: "stdlib", module: "text", member: fn.name };
605
+ return stdFunctionType(fn);
606
+ }
607
+ }
608
+ name = renamed;
609
+ }
610
+ const fake: Extract<Expr, { kind: "Member" }> = { kind: "Member", object: e.object, name, nameSpan: e.nameSpan, typeArgs: null, span: e.span };
611
+ const t = namespaceMember(c, fake, objType.ns, { forCall: true });
612
+ e.res = fake.res;
613
+ return t;
614
+ }
615
+ // explicit Luau method call on a value: obj::Method(args) → obj:Method(args)
616
+ e.res = { kind: "luauMethod" };
617
+ if (objType.k === "instance") {
618
+ const hit = c.api.member(objType.cls, e.name);
619
+ if (hit && hit.member.kind === "method") {
620
+ return c.api.methodType(hit.member);
621
+ }
622
+ }
623
+ if (objType.k === "datatype") {
624
+ const m = c.api.datatypeMember(objType.name, e.name);
625
+ if (m && m.kind === "method") {
626
+ return c.api.methodType(m);
627
+ }
628
+ }
629
+ return T.any;
630
+ }
631
+
632
+ // ---------- assignment targets ----------
633
+
634
+ export interface AssignTarget {
635
+ type: Type;
636
+ }
637
+
638
+ export function checkAssignTarget(c: Checker, target: Expr, compound: boolean): AssignTarget {
639
+ switch (target.kind) {
640
+ case "Name": {
641
+ const sym = c.scope.lookup(target.name);
642
+ if (!sym) {
643
+ const t = checkExpr(c, target);
644
+ return { type: t };
645
+ }
646
+ sym.used = sym.used || compound;
647
+ if (c.fn && sym.depth < c.fn.depth) {
648
+ sym.captured = true;
649
+ }
650
+ target.res = { kind: "local", sym };
651
+ target.ty = sym.type;
652
+ switch (sym.kind) {
653
+ case "var":
654
+ case "param":
655
+ if (!sym.mutable) {
656
+ c.diags.error("CL2040", target.span, `cannot assign to '${sym.name}': it is declared with let`, ["declare it with `let mut` to change it"], {
657
+ span: sym.span,
658
+ text: `mut ${sym.name}`,
659
+ title: `Make '${sym.name}' mutable (let mut)`,
660
+ });
661
+ }
662
+ if (sym.atomic) {
663
+ c.error("CL2042", target.span, `'${sym.name}' is atomic; change it with fetch_add(${sym.name}, n)`);
664
+ }
665
+ sym.assigned = true;
666
+ return { type: sym.type };
667
+ case "self":
668
+ c.error("CL2043", target.span, "cannot replace self; assign its fields instead (`@hp = …`)");
669
+ return { type: sym.type };
670
+ case "const":
671
+ c.error("CL2044", target.span, `'${sym.name}' is a constant`);
672
+ return { type: sym.type };
673
+ default:
674
+ c.error("CL2045", target.span, `cannot assign to ${sym.kind} '${sym.name}'`);
675
+ return { type: T.unknown };
676
+ }
677
+ }
678
+ case "SelfField": {
679
+ const t = checkExpr(c, target);
680
+ if (target.res?.kind === "field" && c.fn) {
681
+ c.fn.mutatesSelf = true;
682
+ } else if (target.res?.kind === "method") {
683
+ c.error("CL2045", target.span, `cannot assign to method '${target.name}'`);
684
+ } else if (target.res?.kind === "dataModel") {
685
+ c.error("CL2045", target.span, "cannot replace @game");
686
+ }
687
+ return { type: t };
688
+ }
689
+ case "Member": {
690
+ const objType = prune(checkExpr(c, target.object));
691
+ const t = memberOf(c, target, objType, undefined, { forCall: false });
692
+ target.ty = t;
693
+ const res = target.res;
694
+ if (!res) {
695
+ return { type: t };
696
+ }
697
+ switch (res.kind) {
698
+ case "field": {
699
+ requireMutableRoot(c, target);
700
+ return { type: t };
701
+ }
702
+ case "rbxProperty": {
703
+ const owner = res.owner;
704
+ if (objType.k === "instance") {
705
+ const hit = c.api.member(objType.cls, target.name);
706
+ if (hit) {
707
+ if (hit.member.readonly) {
708
+ c.error("RBX5030", target.nameSpan, `${hit.owner}.${target.name} is read-only`);
709
+ }
710
+ checkSecurity(c, target, hit.owner, hit.member, true);
711
+ if (c.fn?.isParallel) {
712
+ c.error("PAR7002", target.span, `[[parallel]] code cannot write ${hit.owner}.${target.name}`, ["write Instance properties after task.synchronize()"]);
713
+ }
714
+ }
715
+ } else {
716
+ c.error("RBX5030", target.nameSpan, `${owner}.${target.name} is read-only`);
717
+ }
718
+ return { type: t };
719
+ }
720
+ case "vectorComponent": {
721
+ const inner = checkAssignTarget(c, target.object, true);
722
+ void inner;
723
+ return { type: T.float };
724
+ }
725
+ case "datatypeMember":
726
+ c.error("RBX5031", target.nameSpan, `${typeToString(objType)} values are immutable; build a new one`, objType.k === "datatype" && objType.name === "CFrame" ? ["e.g. part.CFrame = CFrame.new(position) * rotation"] : undefined);
727
+ return { type: t };
728
+ case "rbxChild":
729
+ c.error("RBX5032", target.nameSpan, `cannot assign to the child '${target.name}'; set its Parent instead`);
730
+ return { type: t };
731
+ case "service":
732
+ c.error("RBX5032", target.nameSpan, "services cannot be replaced");
733
+ return { type: t };
734
+ case "rbxEvent":
735
+ case "rbxMethod":
736
+ c.error("RBX5033", target.nameSpan, `cannot assign to ${target.name}; connect to an event with \`${describe(target)} ~> handler;\``);
737
+ return { type: t };
738
+ case "moduleMember":
739
+ case "const":
740
+ case "enumCase":
741
+ case "rbxEnumItem":
742
+ case "library":
743
+ case "datatypeStatic":
744
+ case "stdlib":
745
+ c.error("CL2045", target.nameSpan, `cannot assign to ${describe(target)}`);
746
+ return { type: t };
747
+ default:
748
+ return { type: t };
749
+ }
750
+ }
751
+ case "Index": {
752
+ const objType = prune(checkExpr(c, target.object));
753
+ if (objType.k === "string" || objType.k === "lit") {
754
+ c.error("CL2046", target.span, "text is immutable; build a new string");
755
+ } else if (objType.k === "list" || objType.k === "dict") {
756
+ requireMutableRoot(c, target);
757
+ }
758
+ const t = checkExpr(c, target);
759
+ return { type: t };
760
+ }
761
+ default:
762
+ c.error("CL1108", target.span, "cannot assign to this expression");
763
+ return { type: T.unknown };
764
+ }
765
+ }
766
+
767
+ /** Fields/elements of a `let` binding are frozen; `self` fields mark the method as mutating. */
768
+ function requireMutableRoot(c: Checker, target: Expr): void {
769
+ const root = rootSymbol(target);
770
+ if (!root) {
771
+ return;
772
+ }
773
+ if (root.kind === "self") {
774
+ if (c.fn) {
775
+ c.fn.mutatesSelf = true;
776
+ }
777
+ return;
778
+ }
779
+ if ((root.kind === "var" || root.kind === "param") && !root.mutable) {
780
+ c.diags.error("CL2040", target.span, `cannot change '${describe(target)}': '${root.name}' is declared with let`, ["declare it with `let mut`"], {
781
+ span: root.span,
782
+ text: `mut ${root.name}`,
783
+ title: `Make '${root.name}' mutable (let mut)`,
784
+ });
785
+ }
786
+ (root as Sym).mutatedFields = true;
787
+ const key = exprPathKey(target);
788
+ if (key) {
789
+ c.scope.invalidate(key);
790
+ }
791
+ }
792
+
793
+ export { isDynamic };