rip-lang 3.13.91 → 3.13.93

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.
package/src/typecheck.js CHANGED
@@ -33,14 +33,55 @@ export function countLines(str) {
33
33
  export function toVirtual(p) { return p + '.ts'; }
34
34
  export function fromVirtual(p) { return p.endsWith('.rip.ts') ? p.slice(0, -3) : p; }
35
35
 
36
+ // Patch uninitialized, untyped variables with inferred types from their
37
+ // first assignment. This makes `let total; total = count + ratio;` behave
38
+ // like `let total: number;` — so a later `total = "string"` is caught.
39
+ // Called by both the LSP and the CLI type-checker to keep them aligned.
40
+ export function patchUninitializedTypes(ts, service, compiledEntries) {
41
+ const program = service.getProgram();
42
+ if (!program) return;
43
+ const checker = program.getTypeChecker();
44
+ for (const [filePath] of compiledEntries) {
45
+ const sf = program.getSourceFile(toVirtual(filePath));
46
+ if (!sf) continue;
47
+ const uninitialized = new Map();
48
+ for (const stmt of sf.statements) {
49
+ if (ts.isVariableStatement(stmt)) {
50
+ for (const decl of stmt.declarationList.declarations) {
51
+ if (!decl.initializer && !decl.type && ts.isIdentifier(decl.name)) {
52
+ const sym = checker.getSymbolAtLocation(decl.name);
53
+ if (sym) uninitialized.set(decl.name.text, sym);
54
+ }
55
+ }
56
+ }
57
+ if (ts.isExpressionStatement(stmt) && ts.isBinaryExpression(stmt.expression) &&
58
+ stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
59
+ ts.isIdentifier(stmt.expression.left)) {
60
+ const name = stmt.expression.left.text;
61
+ const sym = uninitialized.get(name);
62
+ if (sym) {
63
+ const rhsType = checker.getTypeAtLocation(stmt.expression.right);
64
+ sym.flags |= ts.SymbolFlags.Transient;
65
+ sym.links = { type: rhsType };
66
+ uninitialized.delete(name);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+
36
73
  // TS error codes to skip — Rip resolves modules differently and
37
74
  // treats async return types transparently.
38
75
  export const SKIP_CODES = new Set([
39
76
  2300, // Duplicate identifier (DTS declarations coexist with compiled class bodies)
40
77
  2304, // Cannot find name
41
78
  2307, // Cannot find module
79
+ 2389, // Function implementation name must match overload (DTS + compiled body)
80
+ 2391, // Function implementation is missing (DTS overload sigs separated from implementations)
42
81
  2393, // Duplicate function implementation
82
+ 2394, // Overload signature not compatible with implementation (untyped compiled params)
43
83
  2451, // Cannot redeclare block-scoped variable
84
+ 2567, // Enum declarations can only merge with namespace or other enum (DTS + compiled body)
44
85
  1064, // Return type of async function must be Promise
45
86
  2582, // Cannot find name 'test' (test runner globals)
46
87
  2593, // Cannot find name 'describe' (test runner globals)
@@ -94,26 +135,120 @@ export function compileForCheck(filePath, source, compiler) {
94
135
  // Ensure every file is treated as a module (not a global script)
95
136
  if (!/\bexport\b/.test(code) && !/\bimport\b/.test(code)) code += '\nexport {};\n';
96
137
 
97
- const tsContent = (hasTypes ? dts + '\n' : '') + code;
98
- const headerLines = hasTypes ? countLines(dts + '\n') : 1;
138
+ // Interleave function overload signatures from DTS header into the code
139
+ // section, immediately before their implementations. TypeScript requires
140
+ // overload signatures adjacent to the implementation — without this, TS
141
+ // reports error 2391 ("Function implementation is missing or not immediately
142
+ // following the declaration"). Moving signatures into the code also enables
143
+ // proper call-site type checking of function parameters.
144
+ let headerDts = dts;
145
+ if (hasTypes && dts && code) {
146
+ const dl = dts.split('\n');
147
+ const cl = code.split('\n');
148
+ const fnSigs = [];
149
+ for (let i = 0; i < dl.length; i++) {
150
+ const m = dl[i].match(/^(?:export\s+)?(?:declare\s+)?function\s+(\w+)/);
151
+ if (m) fnSigs.push({ name: m[1], sig: dl[i], idx: i });
152
+ }
153
+ if (fnSigs.length > 0) {
154
+ const injections = [];
155
+ const moved = new Set();
156
+ for (const fn of fnSigs) {
157
+ const pat = new RegExp(`^(?:export\\s+)?(?:async\\s+)?function\\s+${fn.name}\\s*[(<]`);
158
+ for (let j = 0; j < cl.length; j++) {
159
+ if (pat.test(cl[j])) {
160
+ injections.push({ codeLine: j, sig: fn.sig });
161
+ moved.add(fn.idx);
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ if (injections.length > 0) {
167
+ injections.sort((a, b) => a.codeLine - b.codeLine);
168
+ // Adjust reverseMap: each injection shifts subsequent code lines down by 1
169
+ if (result.reverseMap) {
170
+ for (const [, entry] of result.reverseMap) {
171
+ let offset = 0;
172
+ for (const inj of injections) {
173
+ if (inj.codeLine <= entry.genLine + offset) offset++;
174
+ }
175
+ entry.genLine += offset;
176
+ }
177
+ }
178
+ // Insert signatures bottom-up to preserve indices.
179
+ // Strip 'declare ' — signatures must be non-ambient to match implementations.
180
+ for (let k = injections.length - 1; k >= 0; k--) {
181
+ cl.splice(injections[k].codeLine, 0, injections[k].sig.replace(/^declare /, ''));
182
+ }
183
+ code = cl.join('\n');
184
+ // Rebuild header DTS without the moved function signatures
185
+ headerDts = dl.filter((_, i) => !moved.has(i)).join('\n').trimEnd() + '\n';
186
+ }
187
+ }
188
+ }
189
+
190
+ // Annotate reactive/readonly/computed const assignments with their declared
191
+ // types from the DTS header, and remove the corresponding `declare const`
192
+ // from the header. This enables TypeScript to check initializer values
193
+ // against type annotations: `const x: Signal<number> = __state("oops")`
194
+ // produces a real type error, whereas two separate declarations
195
+ // (`declare const x: Signal<number>` + `const x = __state("oops")`)
196
+ // only produce a duplicate-identifier error (2451), which is suppressed.
197
+ if (hasTypes && headerDts && code) {
198
+ const dl = headerDts.split('\n');
199
+ const cl = code.split('\n');
200
+ const constTypes = new Map();
201
+
202
+ for (let i = 0; i < dl.length; i++) {
203
+ const m = dl[i].match(/^(?:export\s+)?declare\s+const\s+(\w+):\s+(.+);$/);
204
+ if (m) constTypes.set(m[1], { type: m[2], idx: i });
205
+ }
206
+
207
+ if (constTypes.size > 0) {
208
+ const movedDts = new Set();
209
+
210
+ for (let j = 0; j < cl.length; j++) {
211
+ const cm = cl[j].match(/^((?:export\s+)?const\s+)(\w+)(\s*=\s*)/);
212
+ if (cm && constTypes.has(cm[2])) {
213
+ const entry = constTypes.get(cm[2]);
214
+ cl[j] = cm[1] + cm[2] + ': ' + entry.type + cm[3] + cl[j].slice(cm[0].length);
215
+ movedDts.add(entry.idx);
216
+ }
217
+ }
218
+
219
+ if (movedDts.size > 0) {
220
+ code = cl.join('\n');
221
+ headerDts = dl.filter((_, i) => !movedDts.has(i)).join('\n').trimEnd() + '\n';
222
+ }
223
+ }
224
+ }
225
+
226
+ let tsContent = (hasTypes ? headerDts + '\n' : '') + code;
227
+ const headerLines = hasTypes ? countLines(headerDts + '\n') : 1;
99
228
 
100
229
  // Build bidirectional line maps
101
230
  const { srcToGen, genToSrc } = buildLineMap(result.reverseMap, result.map, headerLines);
102
231
 
232
+ // Snapshot code-section mappings before DTS mapping can overwrite them.
233
+ // Needed by @ts-expect-error injection which must target code lines, not DTS.
234
+ const codeSrcToGen = new Map(srcToGen);
235
+
103
236
  // Map DTS declaration lines back to source lines (bidirectional).
104
- // Covers: let/var declarations, type aliases, interfaces, enums, classes.
237
+ // Covers: imports, let/var declarations, type aliases, interfaces, enums, classes.
105
238
  // This enables hover, go-to-definition, and diagnostics for type-only code.
106
- if (hasTypes && dts) {
107
- const dtsLines = dts.split('\n');
239
+ if (hasTypes && headerDts) {
240
+ const dtsLines = headerDts.split('\n');
108
241
  const srcLines = source.split('\n');
109
242
  for (let i = 0; i < dtsLines.length; i++) {
110
243
  const line = dtsLines[i];
244
+
111
245
  const m = line.match(/^(?:export\s+)?(?:declare\s+)?(?:let|var|type|interface|enum|class)\s+(\w+)/);
112
246
  if (!m) continue;
113
247
  const name = m[1];
114
248
  for (let s = 0; s < srcLines.length; s++) {
115
249
  const src = srcLines[s];
116
- if (new RegExp('\\b' + name + '\\s*(?:::=|::)').test(src) ||
250
+ if (new RegExp('\\b' + name + '\\s*::').test(src) ||
251
+ new RegExp('^(?:export\\s+)?type\\s+' + name + '\\b').test(src) ||
117
252
  new RegExp('^(?:export\\s+)?interface\\s+' + name + '\\b').test(src) ||
118
253
  new RegExp('^(?:export\\s+)?enum\\s+' + name + '\\b').test(src) ||
119
254
  new RegExp('^(?:export\\s+)?' + name + '\\s*=\\s*component\\b').test(src)) {
@@ -144,6 +279,50 @@ export function compileForCheck(filePath, source, compiler) {
144
279
  }
145
280
  }
146
281
 
282
+ // Inject @ts-expect-error directives from Rip source into the generated
283
+ // TypeScript. This lets TypeScript natively suppress expected errors and
284
+ // report TS2578 for unused directives — works in both CLI and LSP.
285
+ if (hasTypes) {
286
+ const srcLines = source.split('\n');
287
+ const injects = [];
288
+ for (let s = 0; s < srcLines.length; s++) {
289
+ const m = srcLines[s].match(/^\s*#\s*(@ts-expect-error\b.*)/);
290
+ if (m) {
291
+ const nextSrc = s + 1;
292
+ // Prefer code-section line (where the assignment lives and TS reports
293
+ // the error) over the DTS declaration line.
294
+ let genLine = codeSrcToGen.get(nextSrc);
295
+ if (genLine === undefined) {
296
+ genLine = srcToGen.get(nextSrc);
297
+ if (genLine !== undefined && genLine < headerLines) genLine = undefined;
298
+ }
299
+ if (genLine !== undefined) {
300
+ injects.push({ genLine, srcLine: s, comment: `// ${m[1]}` });
301
+ }
302
+ }
303
+ }
304
+ if (injects.length > 0) {
305
+ // Sort descending so bottom-up insertion doesn't shift earlier positions
306
+ injects.sort((a, b) => b.genLine - a.genLine);
307
+ const tsLines = tsContent.split('\n');
308
+ for (const { genLine, srcLine, comment } of injects) {
309
+ tsLines.splice(genLine, 0, comment);
310
+ // Shift existing gen→src mappings at or after the insertion point
311
+ const shifted = new Map();
312
+ for (const [g, s] of genToSrc) shifted.set(g >= genLine ? g + 1 : g, s);
313
+ shifted.set(genLine, srcLine);
314
+ genToSrc.clear();
315
+ for (const [g, s] of shifted) genToSrc.set(g, s);
316
+ // Shift existing src→gen mappings that pointed at or past the insertion
317
+ for (const [s, g] of srcToGen) {
318
+ if (g >= genLine) srcToGen.set(s, g + 1);
319
+ }
320
+ srcToGen.set(srcLine, genLine);
321
+ }
322
+ tsContent = tsLines.join('\n');
323
+ }
324
+ }
325
+
147
326
  return { tsContent, headerLines, hasTypes, srcToGen, genToSrc, source, dts };
148
327
  }
149
328
 
@@ -288,6 +467,9 @@ export async function runCheck(targetDir, opts = {}) {
288
467
 
289
468
  const service = ts.createLanguageService(host, ts.createDocumentRegistry());
290
469
 
470
+ // Patch uninitialized variables with inferred types (same as LSP)
471
+ patchUninitializedTypes(ts, service, compiled);
472
+
291
473
  // Collect diagnostics
292
474
  let totalErrors = 0;
293
475
  let totalWarnings = 0;
package/src/types.js CHANGED
@@ -26,7 +26,7 @@ export function installTypeSupport(Lexer) {
26
26
  //
27
27
  // Scans the token stream for:
28
28
  // :: (TYPE_ANNOTATION) — collects type string, stores on surviving token
29
- // ::= (TYPE_ALIAS) — collects type body, replaces with TYPE_DECL marker
29
+ // type Name = (contextual keyword) — collects type body, replaces with TYPE_DECL marker
30
30
  // INTERFACE — collects body, replaces with TYPE_DECL marker
31
31
  // DEF IDENTIFIER<...> — collects generic params via .spaced detection
32
32
  //
@@ -50,29 +50,27 @@ export function installTypeSupport(Lexer) {
50
50
  this.scanTokens((token, i, tokens) => {
51
51
  let tag = token[0];
52
52
 
53
- // ── Generic type parameters: DEF name<T>(...) or Name<T> ::= ───────
53
+ // ── Generic type parameters: DEF name<T>(...) ──────────────────────
54
+ // (Generic params on type aliases are handled by the `type` keyword handler below)
54
55
  if (tag === 'IDENTIFIER') {
55
56
  let next = tokens[i + 1];
56
57
  if (next && next[0] === 'COMPARE' && next[1] === '<' && !next.spaced) {
57
58
  let isDef = tokens[i - 1]?.[0] === 'DEF';
58
59
  let genTokens = collectBalancedAngles(tokens, i + 1);
59
- if (genTokens) {
60
- let isAlias = !isDef && tokens[i + 1 + genTokens.length]?.[0] === 'TYPE_ALIAS';
61
- if (isDef || isAlias) {
62
- if (!token.data) token.data = {};
63
- token.data.typeParams = buildTypeString(genTokens);
64
- tokens.splice(i + 1, genTokens.length);
65
- // After removing <T>, retag ( as CALL_START if it follows DEF IDENTIFIER
66
- if (isDef && tokens[i + 1]?.[0] === '(') {
67
- tokens[i + 1][0] = 'CALL_START';
68
- // Find matching ) and retag as CALL_END
69
- let d = 1, m = i + 2;
70
- while (m < tokens.length && d > 0) {
71
- if (tokens[m][0] === '(' || tokens[m][0] === 'CALL_START') d++;
72
- if (tokens[m][0] === ')' || tokens[m][0] === 'CALL_END') d--;
73
- if (d === 0) tokens[m][0] = 'CALL_END';
74
- m++;
75
- }
60
+ if (genTokens && isDef) {
61
+ if (!token.data) token.data = {};
62
+ token.data.typeParams = buildTypeString(genTokens);
63
+ tokens.splice(i + 1, genTokens.length);
64
+ // After removing <T>, retag ( as CALL_START if it follows DEF IDENTIFIER
65
+ if (tokens[i + 1]?.[0] === '(') {
66
+ tokens[i + 1][0] = 'CALL_START';
67
+ // Find matching ) and retag as CALL_END
68
+ let d = 1, m = i + 2;
69
+ while (m < tokens.length && d > 0) {
70
+ if (tokens[m][0] === '(' || tokens[m][0] === 'CALL_START') d++;
71
+ if (tokens[m][0] === ')' || tokens[m][0] === 'CALL_END') d--;
72
+ if (d === 0) tokens[m][0] = 'CALL_END';
73
+ m++;
76
74
  }
77
75
  }
78
76
  }
@@ -125,14 +123,33 @@ export function installTypeSupport(Lexer) {
125
123
  return 0;
126
124
  }
127
125
 
128
- // ── TYPE_ALIAS (::=)collect type body, create TYPE_DECL marker ───
129
- if (tag === 'TYPE_ALIAS') {
130
- let nameToken = tokens[i - 1];
131
- if (!nameToken) return 1;
126
+ // ── type Name = ... contextual type keyword ──────────────────────
127
+ if (tag === 'IDENTIFIER' && token[1] === 'type') {
128
+ let prevTag = tokens[i - 1]?.[0];
129
+ let atStatement = !prevTag || prevTag === 'TERMINATOR' || prevTag === 'INDENT' || prevTag === 'EXPORT';
130
+ if (!atStatement) return 1;
131
+
132
+ let nameIdx = i + 1;
133
+ let nameToken = tokens[nameIdx];
134
+ if (!nameToken || nameToken[0] !== 'IDENTIFIER') return 1;
132
135
  let name = nameToken[1];
133
- let exported = i >= 2 && tokens[i - 2]?.[0] === 'EXPORT';
134
- let removeFrom = exported ? i - 2 : i - 1;
135
- let next = tokens[i + 1];
136
+
137
+ let exported = prevTag === 'EXPORT';
138
+ let removeFrom = exported ? i - 1 : i;
139
+
140
+ // Handle generic type parameters: type Name<T> = ...
141
+ let eqIdx = nameIdx + 1;
142
+ if (tokens[eqIdx]?.[0] === 'COMPARE' && tokens[eqIdx]?.[1] === '<' && !tokens[eqIdx].spaced) {
143
+ let genTokens = collectBalancedAngles(tokens, eqIdx);
144
+ if (genTokens) {
145
+ if (!nameToken.data) nameToken.data = {};
146
+ nameToken.data.typeParams = buildTypeString(genTokens);
147
+ tokens.splice(eqIdx, genTokens.length);
148
+ }
149
+ }
150
+
151
+ // Must have = after name (or after stripped generics)
152
+ if (tokens[eqIdx]?.[0] !== '=') return 1;
136
153
 
137
154
  let makeDecl = (typeText) => {
138
155
  let dt = gen('TYPE_DECL', name, nameToken);
@@ -141,26 +158,29 @@ export function installTypeSupport(Lexer) {
141
158
  return dt;
142
159
  };
143
160
 
144
- // Structural type: Name ::= type INDENT ... OUTDENT
145
- if (next && next[0] === 'IDENTIFIER' && next[1] === 'type' &&
146
- tokens[i + 2]?.[0] === 'INDENT') {
147
- let endIdx = findMatchingOutdent(tokens, i + 2);
148
- tokens.splice(removeFrom, endIdx - removeFrom + 1, makeDecl(collectStructuralType(tokens, i + 2)));
149
- return 0;
150
- }
161
+ let afterEq = eqIdx + 1;
162
+ let next = tokens[afterEq];
151
163
 
152
- // Block union: Name ::= TERMINATOR INDENT | "a" | "b" ... OUTDENT
164
+ // Block union: type Name = (TERMINATOR?) INDENT | "a" | "b" ... OUTDENT
165
+ // Must check before structural — `=` suppresses TERMINATOR so INDENT follows directly
153
166
  if (next && (next[0] === 'TERMINATOR' || next[0] === 'INDENT')) {
154
- let result = collectBlockUnion(tokens, i + 1);
167
+ let result = collectBlockUnion(tokens, afterEq);
155
168
  if (result) {
156
169
  tokens.splice(removeFrom, result.endIdx - removeFrom + 1, makeDecl(result.typeText));
157
170
  return 0;
158
171
  }
159
172
  }
160
173
 
161
- // Simple alias: Name ::= type-expression
162
- let typeTokens = collectTypeExpression(tokens, i + 1);
163
- tokens.splice(removeFrom, i + 1 + typeTokens.length - removeFrom, makeDecl(buildTypeString(typeTokens)));
174
+ // Structural type: type Name = INDENT ... OUTDENT
175
+ if (next && next[0] === 'INDENT') {
176
+ let endIdx = findMatchingOutdent(tokens, afterEq);
177
+ tokens.splice(removeFrom, endIdx - removeFrom + 1, makeDecl(collectStructuralType(tokens, afterEq)));
178
+ return 0;
179
+ }
180
+
181
+ // Simple alias: type Name = type-expression
182
+ let typeTokens = collectTypeExpression(tokens, afterEq);
183
+ tokens.splice(removeFrom, afterEq + typeTokens.length - removeFrom, makeDecl(buildTypeString(typeTokens)));
164
184
  return 0;
165
185
  }
166
186
 
@@ -932,9 +952,14 @@ export function emitTypes(tokens, sexpr = null) {
932
952
  let preamble = [];
933
953
  if (usesSignal) {
934
954
  preamble.push('interface Signal<T> { value: T; read(): T; lock(): Signal<T>; free(): Signal<T>; kill(): T; }');
955
+ preamble.push('declare function __state<T>(value: T): Signal<T>;');
935
956
  }
936
957
  if (usesComputed) {
937
958
  preamble.push('interface Computed<T> { readonly value: T; read(): T; lock(): Computed<T>; free(): Computed<T>; kill(): T; }');
959
+ preamble.push('declare function __computed<T>(fn: () => T): Computed<T>;');
960
+ }
961
+ if (usesSignal || usesComputed) {
962
+ preamble.push('declare function __effect(fn: () => void | (() => void)): () => void;');
938
963
  }
939
964
  if (preamble.length > 0) {
940
965
  preamble.push('');