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,96 @@
1
+ /**
2
+ * Roblox surface: the typed Roblox API the CL++ checker validates against.
3
+ * Built from luau-lsp's Roblox definitions (types, nullability, overloads, operators) merged with
4
+ * the Roblox API dump profile (security, read-only, creatable, services, int vs float).
5
+ * Stored as `api/roblox/surface.json` and loaded once per process.
6
+ */
7
+
8
+ export type LType =
9
+ | { k: "name"; name: string; args?: LType[] }
10
+ | { k: "opt"; t: LType }
11
+ | { k: "union"; ts: LType[] }
12
+ | { k: "inter"; ts: LType[] }
13
+ | { k: "array"; t: LType }
14
+ | { k: "map"; key: LType; value: LType }
15
+ | { k: "table"; fields: Record<string, LType> }
16
+ | { k: "fn"; params: LParam[]; variadic?: LType; returns: LType[]; returnsVariadic?: LType; generics?: string[] }
17
+ | { k: "lit"; value: string | boolean }
18
+ | { k: "pack"; name: string };
19
+
20
+ export interface LParam {
21
+ name?: string;
22
+ t: LType;
23
+ }
24
+
25
+ export type LFn = Extract<LType, { k: "fn" }>;
26
+
27
+ export interface SurfaceMember {
28
+ kind: "property" | "method" | "event" | "callback";
29
+ /** Property type, or the function type of a callback. */
30
+ type?: LType;
31
+ /** Method overloads (without `self`). */
32
+ overloads?: LFn[];
33
+ /** Event argument types. */
34
+ args?: LType[];
35
+ readonly?: boolean;
36
+ /** `true`, or the replacement to suggest. */
37
+ deprecated?: string | true;
38
+ /** Read / call security when not `None`. */
39
+ security?: string;
40
+ writeSecurity?: string;
41
+ yields?: boolean;
42
+ notScriptable?: boolean;
43
+ /** Instance-valued property (may be nil even when the definitions say otherwise). */
44
+ notReplicated?: boolean;
45
+ }
46
+
47
+ export interface SurfaceClass {
48
+ name: string;
49
+ superclass: string | null;
50
+ members: Record<string, SurfaceMember>;
51
+ creatable: boolean;
52
+ service: boolean;
53
+ abstract?: boolean;
54
+ deprecated?: string | true;
55
+ notCreatable?: boolean;
56
+ tags?: string[];
57
+ }
58
+
59
+ export interface SurfaceStatic {
60
+ kind: "function" | "value";
61
+ overloads?: LFn[];
62
+ type?: LType;
63
+ deprecated?: string | true;
64
+ }
65
+
66
+ export interface SurfaceDatatype {
67
+ name: string;
68
+ members: Record<string, SurfaceMember>;
69
+ /** Metamethods: `__add`, `__sub`, `__mul`, `__div`, `__idiv`, `__mod`, `__unm`, `__eq`, `__lt`, `__le`. */
70
+ operators: Record<string, LFn[]>;
71
+ statics: Record<string, SurfaceStatic>;
72
+ }
73
+
74
+ export interface SurfaceEnum {
75
+ name: string;
76
+ items: string[];
77
+ deprecated?: string[];
78
+ }
79
+
80
+ export interface SurfaceGlobal {
81
+ type?: LType;
82
+ overloads?: LFn[];
83
+ deprecated?: string | true;
84
+ }
85
+
86
+ export interface RobloxSurface {
87
+ schema: 1;
88
+ generatedAt: string;
89
+ sources: Record<string, string>;
90
+ classes: Record<string, SurfaceClass>;
91
+ datatypes: Record<string, SurfaceDatatype>;
92
+ enums: Record<string, SurfaceEnum>;
93
+ globals: Record<string, SurfaceGlobal>;
94
+ libraries: Record<string, Record<string, SurfaceStatic>>;
95
+ aliases: Record<string, LType>;
96
+ }
package/src/api-cli.ts CHANGED
@@ -55,7 +55,7 @@ export function registerApiCommands(program: import("commander").Command): void
55
55
  .command("generate")
56
56
  .description("Build RobloxTargetProfile and regenerate CL++ / Luau projections")
57
57
  .option("--dump <path>", "Mini-API-Dump.json path")
58
- .option("--skip-headers", "Skip CL++ header generation")
58
+ .option("--skip-headers", "Skip legacy .clh API projections")
59
59
  .option("--skip-luau", "Skip .d.luau generation")
60
60
  .option("--docs", "No-op retained for CLI compat (Creator Docs are not mirrored)")
61
61
  .action((opts: { dump?: string; skipHeaders?: boolean; skipLuau?: boolean; docs?: boolean }) => {
@@ -235,7 +235,7 @@ export function registerTargetCommands(program: import("commander").Command): vo
235
235
 
236
236
  target
237
237
  .command("datamodel")
238
- .description("Build Typed DataModel profile + generated datamodel.clh from Rojo project")
238
+ .description("Build Typed DataModel profile (legacy projects also get .clh)")
239
239
  .argument("[folder]", "game root with default.project.json", ".")
240
240
  .option("-r, --rojo <path>", "Rojo project json")
241
241
  .action((folder: string, options: { rojo?: string }) => {
package/src/cli.ts CHANGED
@@ -4,8 +4,7 @@ import path from "node:path";
4
4
  import { pkg } from "./package-info.js";
5
5
  import { start as startLsp } from "./lsp.js";
6
6
  import { collectSources, toLuauPath } from "./clpp/paths.js";
7
- import { CLPP_INSTALL_HINT } from "./clpp/contract.js";
8
- import { clppInstalls, clppManifest, clppVersion, hasClpp, resolveClppBinary } from "./clpp/runner.js";
7
+ import { clppManifest, hasClpp } from "./clpp/runner.js";
9
8
  import { installEditorSupport, syncEditorSupport } from "./intellisense.js";
10
9
  import { RojoMapper } from "./utils/rojo-mapper.js";
11
10
  import { ProcessOrchestrator } from "./utils/process-orchestrator.js";
@@ -103,7 +102,8 @@ program
103
102
 
104
103
  program
105
104
  .command("lsp")
106
- .description("Language server stdio — Flare `.flare` IntelliSense. CL++ still uses `clpp setup`.")
105
+ .description("Schema language server over stdio")
106
+ .option("--stdio", "Use standard input/output (default)")
107
107
  .argument("[folder]", "project folder", ".")
108
108
  .action((folder: string) => {
109
109
  startLsp({ projectRoot: path.resolve(process.cwd(), folder) });
@@ -112,7 +112,7 @@ program
112
112
  program
113
113
  .command("intellisense")
114
114
  .alias("intelisense")
115
- .description("Install schema IntelliSense (.flare .hive .mint .bloom .helm .shift .axiom) + sync .vscode; CL++ via clpp install")
115
+ .description("Install schema editor support and sync .vscode")
116
116
  .argument("[folder]", "project folder", ".")
117
117
  .action(async (folder: string) => {
118
118
  try {
@@ -120,7 +120,6 @@ program
120
120
  syncEditorSupport(root, loadConfig(root));
121
121
  await installEditorSupport();
122
122
  console.log("cluaupp: editor associations written in", root);
123
- console.log("cluaupp:", CLPP_INSTALL_HINT);
124
123
  } catch (err) {
125
124
  console.error(err instanceof Error ? err.message : err);
126
125
  process.exit(1);
@@ -129,20 +128,22 @@ program
129
128
 
130
129
  program
131
130
  .command("language")
132
- .description("CL++ manifest (`clpp api manifest`)")
133
- .action(() => {
131
+ .description("Describe the Roblox .clp target; --legacy probes external clpp")
132
+ .option("--legacy", "Read the manifest of the compatible legacy compiler")
133
+ .action((options: { legacy?: boolean }) => {
134
134
  try {
135
- const bin = resolveClppBinary();
136
- const version = clppVersion();
137
- if (version) {
138
- console.log(version, `(${bin})`);
139
- }
140
- for (const install of clppInstalls()) {
141
- if (!install.selected) {
142
- console.log(`skipped ${install.bin} (${install.version || "unknown"})`);
143
- }
144
- }
145
- console.log(JSON.stringify(clppManifest(), null, "\t"));
135
+ const manifest = options.legacy ? clppManifest() : {
136
+ name: "Cluaupp Roblox target",
137
+ version: pkg.version,
138
+ status: "experimental",
139
+ sourceExtensions: [".clp"],
140
+ fileTags: { server: ".server.clp", client: ".client.clp", plugin: ".plugin.clp" },
141
+ importSyntax: "link",
142
+ headers: false,
143
+ builtinLinks: ["@clpp.roblox", "@clpp.libs.<Library>"],
144
+ legacy: { sourceExtensions: [".clpp", ".clh"], command: "cluaupp language --legacy" },
145
+ };
146
+ console.log(JSON.stringify(manifest, null, "\t"));
146
147
  } catch (err) {
147
148
  console.error(err instanceof Error ? err.message : err);
148
149
  process.exit(1);
@@ -1,4 +1,4 @@
1
- /** Stable CL++ ↔ Cluaupp contract (CL++ 0.8.x). Cluaupp invokes the `clpp` binary. */
1
+ /** Legacy adapter policy for CL++ 0.8.x. Requires an upstream compiler implementing the handshake. */
2
2
 
3
3
  export interface CompileRequest {
4
4
  source: string;
@@ -21,6 +21,10 @@ export interface CompileRequest {
21
21
 
22
22
  export interface CompileDiagnostic {
23
23
  message: string;
24
+ code?: string;
25
+ help?: string;
26
+ endLine?: number;
27
+ endColumn?: number;
24
28
  /** 1-based */
25
29
  line: number;
26
30
  /** 1-based */
@@ -36,6 +40,14 @@ export interface ModuleRequireEntry {
36
40
  toFile?: string;
37
41
  }
38
42
 
43
+ export interface SourceMapLine {
44
+ luauLine: number;
45
+ clppLine: number;
46
+ file: string;
47
+ luauColumn?: number;
48
+ clppColumn?: number;
49
+ }
50
+
39
51
  export interface CompileArtifact {
40
52
  ok: boolean;
41
53
  luau: string;
@@ -48,7 +60,7 @@ export interface CompileArtifact {
48
60
  libraries: string[];
49
61
  error?: string;
50
62
  diagnostics?: CompileDiagnostic[];
51
- sourceMap?: Array<{ luauLine: number; clppLine: number; file: string }>;
63
+ sourceMap?: SourceMapLine[];
52
64
  /** Named `import` / quoted include requires from CL++ (RFC 0003). */
53
65
  requires?: ModuleRequireEntry[];
54
66
  /** Selective @native candidates (RFC 0011). Not auto-applied as blanket. */
@@ -62,6 +74,8 @@ export interface CompileArtifact {
62
74
  }
63
75
 
64
76
  export interface LanguageManifest {
77
+ /** Explicit host protocol handshake; a VM manifest is not compatible. */
78
+ contractVersion: number;
65
79
  name: "CL++";
66
80
  id: "clpp";
67
81
  version: string;
@@ -77,6 +91,9 @@ export interface LanguageManifest {
77
91
  }
78
92
 
79
93
  export const MIN_CLPP_VERSION = "0.8.0";
94
+ export const MAX_CLPP_VERSION_EXCLUSIVE = "0.9.0";
95
+ export const CLPP_VERSION_RANGE = ">=0.8.0 <0.9.0";
96
+ export const CLPP_CONTRACT_VERSION = 1;
80
97
 
81
98
  export const CLPP_INSTALL_HINT =
82
- "Install CL++ 0.8.0+ (`clpp` from https://github.com/KartzRbx/CLPP/releases). Put it on PATH. Prefer `import { Name } from \"./x.clh\"` for language modules; angle `#include <clpp/…>` stays for host prelude. Override with CLPP or CLPP_PATH.";
99
+ "Install the legacy Luau compiler >=0.8.0 <0.9.0 with contractVersion 1 (the CL++ VM is incompatible) (`clpp` from https://github.com/KartzRbx/CLPP/releases). Put it on PATH. Prefer `import { Name } from \"./x.clh\"` for language modules; angle `#include <clpp/…>` stays for host prelude. Override with CLPP or CLPP_PATH.";
@@ -0,0 +1,25 @@
1
+ import { CLPP_CONTRACT_VERSION, CLPP_VERSION_RANGE, MIN_CLPP_VERSION, MAX_CLPP_VERSION_EXCLUSIVE, type LanguageManifest } from "./contract.js";
2
+
3
+ export function parseCompilerVersion(text: string): number[] | null {
4
+ const m = /^(?:clpp\s+|CL\+\+\s+)?(\d+)\.(\d+)\.(\d+)(?:-[\w.-]+)?(?:\+[\w.-]+)?$/i.exec(text.trim());
5
+ return m ? m.slice(1, 4).map(Number) : null;
6
+ }
7
+ function compare(a: number[], b: number[]): number {
8
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
9
+ }
10
+ export function acceptsCompilerVersion(text: string): boolean {
11
+ const v = parseCompilerVersion(text);
12
+ return !!v && compare(v, parseCompilerVersion(MIN_CLPP_VERSION)!) >= 0 && compare(v, parseCompilerVersion(MAX_CLPP_VERSION_EXCLUSIVE)!) < 0;
13
+ }
14
+ export function validateCompilerHandshake(version: string, value: unknown): LanguageManifest {
15
+ const fail = (reason: string): never => {throw new Error(`cluaupp: incompatible clpp (${version || "unknown"}): ${reason}. Require Luau compiler ${CLPP_VERSION_RANGE}, contractVersion ${CLPP_CONTRACT_VERSION}. The clpp game VM cannot emit Roblox Luau. Set CLPP or CLPP_PATH to the Luau compiler.`);};
16
+ if (!acceptsCompilerVersion(version)) fail("unsupported version");
17
+ if (!value || typeof value !== "object") fail("missing api manifest");
18
+ const m = value as LanguageManifest;
19
+ if (m.id !== "clpp" || m.name !== "CL++") fail("wrong compiler identity");
20
+ if (m.contractVersion !== CLPP_CONTRACT_VERSION) fail("missing or unsupported contractVersion");
21
+ if (!acceptsCompilerVersion(m.version) || compare(parseCompilerVersion(version)!, parseCompilerVersion(m.version)!) !== 0) fail("binary and manifest versions disagree");
22
+ if (!Array.isArray(m.extensions) || ![".clpp", ".clp", ".clh"].every(x => m.extensions.includes(x))) fail("missing source extensions");
23
+ if (!Array.isArray(m.tags) || ![["*.server.clpp", "server", "Script"], ["*.client.clpp", "client", "LocalScript"], ["*.clh", "header", "ModuleScript"]].every(([pattern, kind, cls]) => m.tags.some(t => t.pattern === pattern && t.scriptKind === kind && t.rojoClass === cls))) fail("missing Roblox file tags");
24
+ return m;
25
+ }
@@ -150,7 +150,7 @@ export function resolveLanguageDeps(source: string, fromFile: string, srcDir?: s
150
150
  }
151
151
 
152
152
  /**
153
- * Fingerprint source + one-hop language deps so import/include edits invalidate incremental cache.
153
+ * Fingerprint source + transitive language deps so import/include edits invalidate incremental cache.
154
154
  */
155
155
  export function fingerprintSourceWithDeps(
156
156
  source: string,
@@ -159,15 +159,22 @@ export function fingerprintSourceWithDeps(
159
159
  srcDir: string | undefined,
160
160
  hashText: (text: string) => string,
161
161
  ): string {
162
- const parts = [configKey, source];
163
- for (const dep of resolveLanguageDeps(source, fromFile, srcDir)) {
164
- try {
165
- parts.push(`\n//dep:${dep.replace(/\\/g, "/")}\n`);
166
- parts.push(fs.readFileSync(dep, "utf8"));
167
- } catch {
168
- parts.push(`\n//dep-missing:${dep.replace(/\\/g, "/")}\n`);
169
- }
170
- }
162
+ const parts = [configKey, source];
163
+ const seen = new Set<string>([path.resolve(fromFile)]);
164
+ const visit = (text: string, file: string) => {
165
+ for (const ref of collectModuleRefs(text)) {
166
+ if (ref.angled) continue;
167
+ const dep = resolveModuleFile(ref.modulePath, file, srcDir);
168
+ if (!dep) {parts.push("\n//missing:" + file + ":" + ref.modulePath); continue;}
169
+ const key = path.resolve(dep);
170
+ if (seen.has(key)) continue;
171
+ seen.add(key);
172
+ const contents = fs.readFileSync(key, "utf8");
173
+ parts.push("\n//dep:" + key.replace(/\\/g, "/") + "\n", contents);
174
+ visit(contents, key);
175
+ }
176
+ };
177
+ visit(source, fromFile);
171
178
  return hashText(parts.join(""));
172
179
  }
173
180
 
@@ -4,47 +4,66 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import {
6
6
  CLPP_INSTALL_HINT,
7
- MIN_CLPP_VERSION,
8
7
  type CompileArtifact,
9
8
  type CompileDiagnostic,
10
9
  type CompileRequest,
11
10
  type LanguageManifest,
12
11
  } from "./contract.js";
13
12
 
13
+ import { acceptsCompilerVersion, validateCompilerHandshake } from "./handshake.js";
14
+
14
15
  const MISSING_CLPP = `cluaupp: clpp not found. ${CLPP_INSTALL_HINT}`;
15
16
 
16
17
  type Semver = { major: number; minor: number; patch: number };
17
18
 
18
19
  let cachedBin: string | null | undefined;
20
+ let cachedSelectionKey = "";
21
+ const contractSupport = new Map<string, LanguageManifest>();
22
+
23
+ function binaryKey(bin: string): string {
24
+ const stat = fs.statSync(bin);
25
+ return JSON.stringify([bin, stat.size, stat.mtimeMs]);
26
+ }
19
27
 
20
28
  function parseSemver(text: string): Semver | null {
21
- const match = String(text).match(/(\d+)\.(\d+)\.(\d+)/);
29
+ const match = String(text).match(/(\d+)\.(\d+)(?:\.(\d+))?/);
22
30
  if (!match) {
23
31
  return null;
24
32
  }
25
- return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
33
+ return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3] || 0) };
26
34
  }
27
35
 
28
36
  function cmpSemver(a: Semver, b: Semver): number {
29
37
  return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
30
38
  }
31
39
 
32
- function minClpp(): Semver {
33
- return parseSemver(MIN_CLPP_VERSION) || { major: 0, minor: 8, patch: 0 };
34
- }
35
-
36
- function tooOldMessage(bin: string, version: string | null): string {
37
- return `cluaupp: clpp ${version || "unknown"} is too old (${bin}). Need CL++ ${MIN_CLPP_VERSION}+. Install https://github.com/KartzRbx/CLPP/releases or set CLPP / CLPP_PATH.`;
38
- }
39
-
40
40
  function probeVersion(bin: string): string | null {
41
- const result = spawnSync(bin, ["--version"], { encoding: "utf8", windowsHide: true });
41
+ const result = spawnSync(bin, ["--version"], { encoding: "utf8", windowsHide: true, timeout: 10_000 });
42
42
  if (result.status !== 0) {
43
43
  return null;
44
44
  }
45
45
  return String(result.stdout || result.stderr || "").trim() || null;
46
46
  }
47
47
 
48
+ function handshake(bin: string): LanguageManifest {
49
+ const key = binaryKey(bin);
50
+ const cached = contractSupport.get(key);
51
+ if (cached) return cached;
52
+ const version = probeVersion(bin) || "";
53
+ let manifest: unknown;
54
+ // Reject the VM before asking it to compile or interpret any input.
55
+ if (acceptsCompilerVersion(version)) {
56
+ const result = spawnSync(bin, ["api", "manifest"], { encoding: "utf8", windowsHide: true, timeout: 10_000 });
57
+ if (result.status === 0) {try {manifest = JSON.parse(String(result.stdout || ""));} catch {}}
58
+ }
59
+ const validated = validateCompilerHandshake(version, manifest);
60
+ contractSupport.set(key, validated);
61
+ return validated;
62
+ }
63
+ function supportsLuauContract(bin: string): boolean {
64
+ try {handshake(bin); return true;} catch {return false;}
65
+ }
66
+
48
67
  function pushCandidate(out: string[], seen: Set<string>, item: string | undefined): void {
49
68
  const value = String(item || "").trim();
50
69
  if (!value) {
@@ -71,9 +90,11 @@ function listClppCandidates(): string[] {
71
90
  const override: string[] = [];
72
91
  pushCandidate(override, seen, process.env.CLPP);
73
92
  pushCandidate(override, seen, process.env.CLPP_PATH);
74
- const overrideExisting = override.filter(existsBin);
75
- if (overrideExisting.length > 0) {
76
- return overrideExisting;
93
+ if (override.length > 0) {
94
+ for (const bin of override) {
95
+ if (!existsBin(bin)) throw new Error(`cluaupp: explicit CLPP/CLPP_PATH does not exist: ${bin}`);
96
+ }
97
+ return override;
77
98
  }
78
99
 
79
100
  const out: string[] = [];
@@ -106,7 +127,7 @@ function pickClppBinary(): string | null {
106
127
  if (!fallback) {
107
128
  fallback = bin;
108
129
  }
109
- if (!parsed) {
130
+ if (!parsed || !supportsLuauContract(bin)) {
110
131
  continue;
111
132
  }
112
133
  if (!best || cmpSemver(parsed, best.parsed) > 0) {
@@ -117,9 +138,11 @@ function pickClppBinary(): string | null {
117
138
  }
118
139
 
119
140
  function whichClpp(): string | null {
120
- if (cachedBin !== undefined) {
141
+ const selectionKey = JSON.stringify([process.env.CLPP, process.env.CLPP_PATH, process.env.PATH]);
142
+ if (cachedBin !== undefined && selectionKey === cachedSelectionKey) {
121
143
  return cachedBin;
122
144
  }
145
+ cachedSelectionKey = selectionKey;
123
146
  cachedBin = pickClppBinary();
124
147
  return cachedBin;
125
148
  }
@@ -133,11 +156,7 @@ export function resolveClppBinary(): string {
133
156
  if (!bin) {
134
157
  throw new Error(MISSING_CLPP);
135
158
  }
136
- const version = probeVersion(bin);
137
- const parsed = parseSemver(version || "");
138
- if (!parsed || cmpSemver(parsed, minClpp()) < 0) {
139
- throw new Error(tooOldMessage(bin, version));
140
- }
159
+ handshake(bin);
141
160
  return bin;
142
161
  }
143
162
 
@@ -145,7 +164,7 @@ function parseArtifact(stdout: string, fileName: string, fallbackError: string):
145
164
  const text = String(stdout || "").trim();
146
165
  try {
147
166
  const parsed = JSON.parse(text) as CompileArtifact;
148
- if (parsed && typeof parsed === "object") {
167
+ if (parsed && typeof parsed === "object" && typeof parsed.ok === "boolean" && typeof parsed.luau === "string" && typeof parsed.fileName === "string") {
149
168
  return parsed;
150
169
  }
151
170
  } catch {
@@ -171,7 +190,7 @@ export function formatClppFailure(artifact: CompileArtifact, fallback: string):
171
190
  return diags
172
191
  .map((d: CompileDiagnostic) => {
173
192
  const where = `${artifact.fileName || "clpp"}:${d.line}:${d.column}`;
174
- return `${where}: ${d.message}`;
193
+ return `${where}: ${d.severity}${d.code ? " [" + d.code + "]" : ""}: ${d.message}${d.help ? "\n " + d.help : ""}`;
175
194
  })
176
195
  .join("\n");
177
196
  }
@@ -200,7 +219,7 @@ export function compileViaClpp(request: CompileRequest): CompileArtifact {
200
219
  });
201
220
  const stderr = String(result.stderr || "").trim();
202
221
  const artifact = parseArtifact(result.stdout, request.fileName, stderr || result.error?.message || "clpp api compile failed");
203
- if (!artifact.ok) {
222
+ if (result.status !== 0 || result.error || !artifact.ok) {
204
223
  throw new Error(formatClppFailure(artifact, stderr || result.error?.message || `clpp failed (${result.status})`));
205
224
  }
206
225
  return artifact;
@@ -232,7 +251,7 @@ export function compileViaClppAsync(request: CompileRequest): Promise<CompileArt
232
251
  child.on("error", reject);
233
252
  child.on("close", (code) => {
234
253
  const artifact = parseArtifact(stdout, request.fileName, stderr.trim() || `clpp api compile failed (${code})`);
235
- if (!artifact.ok) {
254
+ if (code !== 0 || !artifact.ok) {
236
255
  reject(new Error(formatClppFailure(artifact, stderr.trim() || `clpp failed (${code})`)));
237
256
  return;
238
257
  }
@@ -256,14 +275,7 @@ export function compileViaClppAsync(request: CompileRequest): Promise<CompileArt
256
275
 
257
276
  export function clppManifest(): LanguageManifest {
258
277
  const bin = resolveClppBinary();
259
- const result = spawnSync(bin, ["api", "manifest"], {
260
- encoding: "utf8",
261
- windowsHide: true,
262
- });
263
- if (result.status !== 0) {
264
- throw new Error(String(result.stderr || result.error?.message || "clpp api manifest failed"));
265
- }
266
- return JSON.parse(result.stdout) as LanguageManifest;
278
+ return handshake(bin);
267
279
  }
268
280
 
269
281
  export function clppVersion(): string | null {
@@ -282,3 +294,9 @@ export function clppInstalls(): Array<{ bin: string; version: string | null; sel
282
294
  selected: bin === selected,
283
295
  }));
284
296
  }
297
+
298
+ /** Include the actual binary and handshake in incremental fingerprints. */
299
+ export function clppCacheIdentity(): string {
300
+ const bin = resolveClppBinary();
301
+ return JSON.stringify([binaryKey(bin), handshake(bin)]);
302
+ }
package/src/compile.ts CHANGED
@@ -5,7 +5,7 @@ import { compileViaClpp, compileViaClppAsync } from "./clpp/runner.js";
5
5
  import { clppLuauToGame, shouldSkipInit } from "./clpp/postprocess.js";
6
6
  import { emitKind, siblingHeader, toLuauPath } from "./clpp/paths.js";
7
7
  import type { CompileArtifact } from "./clpp/contract.js";
8
- import { projectRoot } from "./package-info.js";
8
+ import { pkg, projectRoot } from "./package-info.js";
9
9
  import { runContextFromFileName } from "./target/roblox.js";
10
10
  import { capabilityProfilePath, writeCapabilityProfile, loadCapabilityProfile } from "./target/capability-profile.js";
11
11
  import { assertContextSafe, listContextWarnings, formatContextViolations } from "./target/context-check.js";
@@ -15,6 +15,8 @@ import { writeDatamodelArtifacts } from "./target/datamodel-generate.js";
15
15
  import { assertParallelSafe } from "./target/parallel-check.js";
16
16
  import { assertAuthoritySafe, loadPlatformPolicy } from "./target/authority-check.js";
17
17
  import { assertGetServiceSafe } from "./target/get-service.js";
18
+ import { compileSource as compileRobloxClp } from "./compiler/project.js";
19
+ import { formatDiagnostic } from "./compiler/diagnostics.js";
18
20
 
19
21
  function defaultTargetProfilePath(): string | undefined {
20
22
  const cached = path.join(projectRoot, "api", "normalized", "roblox-target.profile.json");
@@ -130,7 +132,7 @@ function finishArtifact(
130
132
  return {
131
133
  kind: emitKind(relative),
132
134
  plan: { kind: emitKind(relative), strict: options.strict === true },
133
- files: [{ name: outName, contents: luau }],
135
+ files: [{ name: outName, contents: luau, sourceMap: artifact.sourceMap, generatedSource: artifact.luau }],
134
136
  stale: [],
135
137
  };
136
138
  }
@@ -140,6 +142,9 @@ export function compileSource(source: string, fileName?: string, options: Compil
140
142
  }
141
143
 
142
144
  export function compileService(source: string, fileName?: string, options: CompileOptions = {}): CompileServiceResult {
145
+ if (/\.clp$/i.test(options.relativeName || fileName || "")) {
146
+ return compileModernSource(source, fileName, options);
147
+ }
143
148
  const args = compileArgs(source, fileName, options);
144
149
  enforcePlatform(source, args.relative, args);
145
150
  const artifact = compileViaClpp(args.request);
@@ -147,8 +152,21 @@ export function compileService(source: string, fileName?: string, options: Compi
147
152
  }
148
153
 
149
154
  export async function compileServiceAsync(source: string, fileName?: string, options: CompileOptions = {}): Promise<CompileServiceResult> {
155
+ if (/\.clp$/i.test(options.relativeName || fileName || "")) {
156
+ return compileModernSource(source, fileName, options);
157
+ }
150
158
  const args = compileArgs(source, fileName, options);
151
159
  enforcePlatform(source, args.relative, args);
152
160
  const artifact = await compileViaClppAsync(args.request);
153
161
  return finishArtifact(artifact, args.relative, args.outName, args.options, args.skipInit, source);
154
162
  }
163
+
164
+ function compileModernSource(source: string, fileName: string | undefined, options: CompileOptions): CompileServiceResult {
165
+ const relative = (options.relativeName || fileName || "input.clp").replace(/\\/g, "/");
166
+ const srcDir = options.srcDir || (options.filePath ? path.dirname(path.resolve(options.filePath)) : process.cwd());
167
+ const result = compileRobloxClp(source, relative, { srcDir, strict: options.strict === true, version: pkg.version });
168
+ if (result.luau === null) {
169
+ throw new Error(result.diagnostics.filter((d) => d.severity === "error").map((d) => formatDiagnostic(d, result.source)).join("\n") || `${relative}: Luau emission failed`);
170
+ }
171
+ return { kind: emitKind(relative), files: [{ name: options.outName || toLuauPath(relative), contents: result.luau, sourceMap: result.sourceMap, generatedSource: result.luau }], stale: [] };
172
+ }