tova 0.2.9 → 0.3.1

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.
@@ -0,0 +1,255 @@
1
+ // Tova Diagnostic Error Codes Registry
2
+ // Every diagnostic has a unique code: E### for errors, W### for warnings
3
+
4
+ // ─── Error Codes ─────────────────────────────────────────────
5
+
6
+ export const ErrorCode = {
7
+ // === Syntax / Parse Errors (E001–E099) ===
8
+ E001: { code: 'E001', title: 'Unexpected token', category: 'syntax' },
9
+ E002: { code: 'E002', title: 'Unterminated string', category: 'syntax' },
10
+ E003: { code: 'E003', title: 'Expected closing delimiter', category: 'syntax' },
11
+ E004: { code: 'E004', title: 'Invalid number literal', category: 'syntax' },
12
+ E005: { code: 'E005', title: 'Unexpected character', category: 'syntax' },
13
+ E006: { code: 'E006', title: 'Unterminated comment', category: 'syntax' },
14
+ E007: { code: 'E007', title: 'Expected expression', category: 'syntax' },
15
+ E008: { code: 'E008', title: 'Mismatched JSX tag', category: 'syntax' },
16
+ E009: { code: 'E009', title: 'Invalid operator', category: 'syntax' },
17
+ E010: { code: 'E010', title: 'Max nesting depth exceeded', category: 'syntax' },
18
+
19
+ // === Type Errors (E100–E199) ===
20
+ E100: { code: 'E100', title: 'Type mismatch', category: 'type' },
21
+ E101: { code: 'E101', title: 'Return type mismatch', category: 'type' },
22
+ E102: { code: 'E102', title: 'Cannot assign to type', category: 'type' },
23
+ E103: { code: 'E103', title: 'Invalid argument type', category: 'type' },
24
+ E104: { code: 'E104', title: 'Incompatible operand types', category: 'type' },
25
+ E105: { code: 'E105', title: 'Cannot apply operator to type', category: 'type' },
26
+
27
+ // === Scope / Definition Errors (E200–E299) ===
28
+ E200: { code: 'E200', title: 'Undefined variable', category: 'scope' },
29
+ E201: { code: 'E201', title: 'Duplicate definition', category: 'scope' },
30
+ E202: { code: 'E202', title: 'Cannot reassign immutable', category: 'scope' },
31
+ E203: { code: 'E203', title: 'Invalid redeclaration', category: 'scope' },
32
+
33
+ // === Context Errors (E300–E399) ===
34
+ E300: { code: 'E300', title: 'Invalid context: await', category: 'context' },
35
+ E301: { code: 'E301', title: 'Invalid context: return', category: 'context' },
36
+ E302: { code: 'E302', title: 'Invalid context: client-only', category: 'context' },
37
+ E303: { code: 'E303', title: 'Invalid context: server-only', category: 'context' },
38
+ E304: { code: 'E304', title: 'Invalid context: function-only', category: 'context' },
39
+
40
+ // === Import Errors (E400–E499) ===
41
+ E400: { code: 'E400', title: 'Invalid import', category: 'import' },
42
+ E401: { code: 'E401', title: 'Circular import', category: 'import' },
43
+
44
+ // === Pattern Match Errors (E500–E599) ===
45
+ E500: { code: 'E500', title: 'Invalid pattern', category: 'match' },
46
+
47
+ // === Trait / Impl Errors (E600–E699) ===
48
+ E600: { code: 'E600', title: 'Missing trait method', category: 'trait' },
49
+ E601: { code: 'E601', title: 'Trait method signature mismatch', category: 'trait' },
50
+ E602: { code: 'E602', title: 'Unknown trait', category: 'trait' },
51
+ };
52
+
53
+ // ─── Warning Codes ───────────────────────────────────────────
54
+
55
+ export const WarningCode = {
56
+ // === Unused (W001–W099) ===
57
+ W001: { code: 'W001', title: 'Unused variable', category: 'unused' },
58
+ W002: { code: 'W002', title: 'Unused function', category: 'unused' },
59
+ W003: { code: 'W003', title: 'Unused import', category: 'unused' },
60
+
61
+ // === Style (W100–W199) ===
62
+ W100: { code: 'W100', title: 'Naming convention violation', category: 'style' },
63
+ W101: { code: 'W101', title: 'Variable shadows outer', category: 'style' },
64
+
65
+ // === Potential Bugs (W200–W299) ===
66
+ W200: { code: 'W200', title: 'Non-exhaustive match', category: 'match' },
67
+ W201: { code: 'W201', title: 'Unreachable code', category: 'logic' },
68
+ W202: { code: 'W202', title: 'Condition always true', category: 'logic' },
69
+ W203: { code: 'W203', title: 'Condition always false', category: 'logic' },
70
+ W204: { code: 'W204', title: 'Potential data loss', category: 'type' },
71
+ W205: { code: 'W205', title: 'Missing return on some paths', category: 'logic' },
72
+ W206: { code: 'W206', title: 'Non-Tova keyword used', category: 'style' },
73
+ W207: { code: 'W207', title: 'Unreachable match arm', category: 'match' },
74
+ W208: { code: 'W208', title: 'Defer outside function', category: 'context' },
75
+
76
+ // === Trait Conformance (W300–W399) ===
77
+ W300: { code: 'W300', title: 'Missing trait method', category: 'trait' },
78
+ W301: { code: 'W301', title: 'Trait parameter mismatch', category: 'trait' },
79
+ W302: { code: 'W302', title: 'Trait return type mismatch', category: 'trait' },
80
+ W303: { code: 'W303', title: 'Unknown derive trait', category: 'trait' },
81
+ };
82
+
83
+ // ─── Lookup maps ─────────────────────────────────────────────
84
+
85
+ const _allCodes = new Map();
86
+ for (const entry of Object.values(ErrorCode)) _allCodes.set(entry.code, entry);
87
+ for (const entry of Object.values(WarningCode)) _allCodes.set(entry.code, entry);
88
+
89
+ export function lookupCode(code) {
90
+ return _allCodes.get(code) || null;
91
+ }
92
+
93
+ export function isErrorCode(code) {
94
+ return code.startsWith('E');
95
+ }
96
+
97
+ export function isWarningCode(code) {
98
+ return code.startsWith('W');
99
+ }
100
+
101
+ // ─── tova-ignore comment parsing ─────────────────────────────
102
+
103
+ const IGNORE_PATTERN = /\/\/\s*tova-ignore\s+((?:[EW]\d{3}(?:\s*,\s*)?)+)/;
104
+
105
+ export function parseIgnoreComment(line) {
106
+ const match = line.match(IGNORE_PATTERN);
107
+ if (!match) return null;
108
+ return match[1].split(',').map(c => c.trim()).filter(c => c);
109
+ }
110
+
111
+ // ─── Explanation text for --explain flag ─────────────────────
112
+
113
+ const EXPLANATIONS = {
114
+ E001: `
115
+ Unexpected token in source code.
116
+
117
+ This error occurs when the parser encounters a token it doesn't expect
118
+ in the current context.
119
+
120
+ Example:
121
+ fn foo() {
122
+ x = 1 +
123
+ } // error: unexpected '}', expected expression after '+'
124
+
125
+ Fix: complete the expression or remove the trailing operator.
126
+ `,
127
+ E100: `
128
+ Type mismatch between expected and actual types.
129
+
130
+ This error occurs when a value of one type is used where a different
131
+ type is expected.
132
+
133
+ Example:
134
+ fn add(a: Int, b: Int) -> Int {
135
+ return a + b
136
+ }
137
+ add("hello", 5) // error: expected Int, got String
138
+
139
+ Fix: ensure the value matches the expected type, or use a conversion
140
+ function like to_int(), to_string(), etc.
141
+ `,
142
+ E200: `
143
+ Reference to an undefined variable or function.
144
+
145
+ This error occurs when you use a name that hasn't been defined in the
146
+ current scope or any parent scope.
147
+
148
+ Example:
149
+ print(foo) // error: 'foo' is not defined
150
+
151
+ Fix: define the variable before using it, or check for typos.
152
+ `,
153
+ E202: `
154
+ Attempt to reassign an immutable variable.
155
+
156
+ In Tova, variables bound with '=' are immutable by default. Use 'var'
157
+ to create a mutable variable.
158
+
159
+ Example:
160
+ x = 5
161
+ x = 10 // error: cannot reassign immutable variable 'x'
162
+
163
+ var y = 5
164
+ y = 10 // ok: 'var' makes it mutable
165
+
166
+ Fix: change the declaration to 'var x = 5' if you need mutability.
167
+ `,
168
+ E300: `
169
+ 'await' used outside an async function.
170
+
171
+ The 'await' keyword can only be used inside functions declared with
172
+ the 'async' keyword.
173
+
174
+ Example:
175
+ fn fetch_data() {
176
+ data = await fetch("/api") // error: await outside async
177
+
178
+ }
179
+
180
+ async fn fetch_data() {
181
+ data = await fetch("/api") // ok
182
+ }
183
+
184
+ Fix: add 'async' to the enclosing function declaration.
185
+ `,
186
+ E302: `
187
+ Client-only feature used outside a client block.
188
+
189
+ Features like 'state', 'computed', 'effect', 'component', and 'store'
190
+ can only be used inside a client { } block.
191
+
192
+ Example:
193
+ state count = 0 // error: 'state' outside client block
194
+
195
+ client {
196
+ state count = 0 // ok
197
+ }
198
+
199
+ Fix: move the code inside a client { } block.
200
+ `,
201
+ E303: `
202
+ Server-only feature used outside a server block.
203
+
204
+ Features like 'route', 'middleware', 'ws', 'db', 'auth', etc.
205
+ can only be used inside a server { } block.
206
+
207
+ Example:
208
+ route GET "/api/users" => get_users // error: outside server block
209
+
210
+ server {
211
+ route GET "/api/users" => get_users // ok
212
+ }
213
+
214
+ Fix: move the code inside a server { } block.
215
+ `,
216
+ W001: `
217
+ A variable is declared but never used.
218
+
219
+ This warning helps catch typos and dead code.
220
+
221
+ Example:
222
+ fn foo() {
223
+ x = 5 // warning: 'x' declared but never used
224
+ return 10
225
+ }
226
+
227
+ Fix: remove the variable, or prefix with _ to suppress: _x = 5
228
+ `,
229
+ W200: `
230
+ A match expression doesn't cover all possible variants.
231
+
232
+ This can lead to runtime errors if an unmatched variant is encountered.
233
+
234
+ Example:
235
+ type Color = Red | Green | Blue
236
+ match color {
237
+ Red => "red"
238
+ Green => "green"
239
+ } // warning: missing 'Blue' variant
240
+
241
+ Fix: add the missing variants, or add a wildcard: _ => "other"
242
+ `,
243
+ W204: `
244
+ Implicit narrowing conversion that may lose data.
245
+
246
+ Example:
247
+ x: Int = 3.14 // warning: assigning Float to Int loses decimal
248
+
249
+ Fix: use an explicit conversion like floor(), round(), or to_int().
250
+ `,
251
+ };
252
+
253
+ export function getExplanation(code) {
254
+ return EXPLANATIONS[code] || null;
255
+ }
@@ -1,5 +1,23 @@
1
1
  // Rich error message formatter for the Tova language
2
- // Produces Rust/Elm-style error messages with source context and carets
2
+ // Produces Rust/Elm-style error messages with source context, carets, and fix suggestions
3
+
4
+ import { parseIgnoreComment } from './error-codes.js';
5
+
6
+ // ─── ANSI color helpers (terminal only) ──────────────────────
7
+
8
+ const _isTTY = typeof process !== 'undefined' && process.stderr && process.stderr.isTTY;
9
+
10
+ const _c = {
11
+ red: (s) => _isTTY ? `\x1b[31m${s}\x1b[0m` : s,
12
+ yellow: (s) => _isTTY ? `\x1b[33m${s}\x1b[0m` : s,
13
+ cyan: (s) => _isTTY ? `\x1b[36m${s}\x1b[0m` : s,
14
+ green: (s) => _isTTY ? `\x1b[32m${s}\x1b[0m` : s,
15
+ blue: (s) => _isTTY ? `\x1b[34m${s}\x1b[0m` : s,
16
+ dim: (s) => _isTTY ? `\x1b[2m${s}\x1b[0m` : s,
17
+ bold: (s) => _isTTY ? `\x1b[1m${s}\x1b[0m` : s,
18
+ };
19
+
20
+ // ─── DiagnosticFormatter ─────────────────────────────────────
3
21
 
4
22
  export class DiagnosticFormatter {
5
23
  constructor(source, filename = '<stdin>') {
@@ -8,24 +26,46 @@ export class DiagnosticFormatter {
8
26
  this.lines = source.split('\n');
9
27
  }
10
28
 
11
- format(level, message, loc, hint = null) {
12
- const line = loc.line || 1;
29
+ /**
30
+ * Format a diagnostic with full source context.
31
+ * @param {string} level - 'error' | 'warning' | 'info' | 'hint'
32
+ * @param {string} message - Human-readable message
33
+ * @param {object} loc - { line, column }
34
+ * @param {object} [opts] - { hint, code, length, fix }
35
+ * hint: string — suggestion text
36
+ * code: string — error code like 'E100'
37
+ * length: number — underline length (default 1)
38
+ * fix: { description, replacement } — auto-fix suggestion
39
+ */
40
+ format(level, message, loc, opts = {}) {
41
+ // Backwards compat: opts can be a string (hint)
42
+ if (typeof opts === 'string') opts = { hint: opts };
43
+
44
+ const hint = opts.hint || null;
45
+ const code = opts.code || null;
46
+ const length = opts.length || 0;
47
+ const fix = opts.fix || null;
48
+
49
+ const line = loc.line || 1;
13
50
  const column = loc.column || 1;
14
51
  const lineNum = Math.max(1, Math.min(line, this.lines.length));
15
- const gutterWidth = String(lineNum + 1).length;
52
+ const gutterWidth = Math.max(String(lineNum + 1).length, 3);
16
53
 
17
54
  let output = '';
18
55
 
19
- // Header
20
- const levelStr = level === 'error' ? 'error' : 'warning';
21
- output += `${levelStr}: ${message}\n`;
22
- output += `${' '.repeat(gutterWidth)} --> ${this.filename}:${lineNum}:${column}\n`;
56
+ // ── Header ──
57
+ const levelStr = level === 'error' ? _c.red(_c.bold('error'))
58
+ : level === 'warning' ? _c.yellow(_c.bold('warning'))
59
+ : _c.blue(_c.bold(level));
60
+ const codeStr = code ? _c.dim(`[${code}]`) + ' ' : '';
61
+ output += `${levelStr}: ${codeStr}${_c.bold(message)}\n`;
62
+ output += `${' '.repeat(gutterWidth)}${_c.blue(' -->')} ${this.filename}:${lineNum}:${column}\n`;
23
63
 
24
- // Context lines
64
+ // ── Context lines ──
25
65
  const startLine = Math.max(1, lineNum - 2);
26
66
  const endLine = Math.min(this.lines.length, lineNum + 1);
27
67
 
28
- output += `${' '.repeat(gutterWidth)} |\n`;
68
+ output += `${' '.repeat(gutterWidth)} ${_c.blue('|')}\n`;
29
69
 
30
70
  for (let i = startLine; i <= endLine; i++) {
31
71
  const lineContent = this.lines[i - 1] || '';
@@ -33,53 +73,79 @@ export class DiagnosticFormatter {
33
73
 
34
74
  if (i === lineNum) {
35
75
  // The error line
36
- output += `${lineStr} | ${lineContent}\n`;
37
- // Caret pointer
76
+ output += `${_c.blue(lineStr)} ${_c.blue('|')} ${lineContent}\n`;
77
+ // Underline with carets
38
78
  const caretPad = ' '.repeat(Math.max(0, column - 1));
39
- output += `${' '.repeat(gutterWidth)} | ${caretPad}^\n`;
79
+ const underlineLen = Math.max(1, length || _guessLength(lineContent, column));
80
+ const underline = '^'.repeat(underlineLen);
81
+ const caretColor = level === 'error' ? _c.red : _c.yellow;
82
+ output += `${' '.repeat(gutterWidth)} ${_c.blue('|')} ${caretPad}${caretColor(underline)}\n`;
40
83
  } else {
41
- output += `${lineStr} | ${lineContent}\n`;
84
+ output += `${_c.dim(lineStr)} ${_c.blue('|')} ${lineContent}\n`;
42
85
  }
43
86
  }
44
87
 
45
- output += `${' '.repeat(gutterWidth)} |\n`;
88
+ output += `${' '.repeat(gutterWidth)} ${_c.blue('|')}\n`;
46
89
 
47
- // Hint
90
+ // ── Hint ──
48
91
  if (hint) {
49
- output += `${' '.repeat(gutterWidth)} = hint: ${hint}\n`;
92
+ output += `${' '.repeat(gutterWidth)} ${_c.blue('=')} ${_c.cyan('hint')}: ${hint}\n`;
93
+ }
94
+
95
+ // ── Fix suggestion ──
96
+ if (fix) {
97
+ output += `${' '.repeat(gutterWidth)} ${_c.blue('=')} ${_c.green('fix')}: ${fix.description}\n`;
98
+ if (fix.replacement !== undefined) {
99
+ output += `${' '.repeat(gutterWidth)} ${_c.green('|')} ${_c.green(fix.replacement)}\n`;
100
+ }
50
101
  }
51
102
 
52
103
  return output;
53
104
  }
54
105
 
55
- formatError(message, loc, hint = null) {
56
- return this.format('error', message, loc, hint);
106
+ formatError(message, loc, hintOrOpts = null) {
107
+ const opts = typeof hintOrOpts === 'string' ? { hint: hintOrOpts } : (hintOrOpts || {});
108
+ return this.format('error', message, loc, opts);
57
109
  }
58
110
 
59
- formatWarning(message, loc, hint = null) {
60
- return this.format('warning', message, loc, hint);
111
+ formatWarning(message, loc, hintOrOpts = null) {
112
+ const opts = typeof hintOrOpts === 'string' ? { hint: hintOrOpts } : (hintOrOpts || {});
113
+ return this.format('warning', message, loc, opts);
61
114
  }
62
115
  }
63
116
 
64
- // Format errors from the Lexer, Parser, or Analyzer
117
+ // Guess a reasonable underline length from the token at the given column
118
+ function _guessLength(lineContent, column) {
119
+ const rest = lineContent.slice(column - 1);
120
+ // Try to find a word boundary
121
+ const wordMatch = rest.match(/^(\w+|[^\s]+)/);
122
+ if (wordMatch) return wordMatch[1].length;
123
+ return 1;
124
+ }
125
+
126
+ // ─── Format arrays of diagnostics ────────────────────────────
127
+
65
128
  export function formatDiagnostics(source, filename, errors, warnings = []) {
66
129
  const formatter = new DiagnosticFormatter(source, filename);
130
+ const ignoredCodes = _collectIgnoredCodes(source);
67
131
  let output = '';
68
132
 
69
133
  for (const err of errors) {
134
+ if (err.code && ignoredCodes.has(err.code)) continue;
70
135
  output += formatter.formatError(
71
136
  err.message,
72
137
  { line: err.line, column: err.column },
73
- err.hint
138
+ { hint: err.hint, code: err.code, length: err.length, fix: err.fix }
74
139
  );
75
140
  output += '\n';
76
141
  }
77
142
 
78
143
  for (const warn of warnings) {
144
+ if (warn.code && ignoredCodes.has(warn.code)) continue;
79
145
  output += formatter.formatWarning(
80
146
  warn.message,
81
147
  { line: warn.line, column: warn.column },
82
- warn.hint
148
+ { hint: warn.hint, code: warn.code, length: warn.length, fix: warn.fix }
83
149
  );
84
150
  output += '\n';
85
151
  }
@@ -87,7 +153,20 @@ export function formatDiagnostics(source, filename, errors, warnings = []) {
87
153
  return output;
88
154
  }
89
155
 
90
- // Helper: extract location from typical Tova error messages
156
+ // Collect all tova-ignore codes from source comments
157
+ function _collectIgnoredCodes(source) {
158
+ const codes = new Set();
159
+ for (const line of source.split('\n')) {
160
+ const parsed = parseIgnoreComment(line);
161
+ if (parsed) {
162
+ for (const c of parsed) codes.add(c);
163
+ }
164
+ }
165
+ return codes;
166
+ }
167
+
168
+ // ─── Helper: extract location from typical Tova error messages ──
169
+
91
170
  export function parseErrorLocation(errorMessage) {
92
171
  const match = errorMessage.match(/^(.+?):(\d+):(\d+)\s*[—-]\s*(.+)/);
93
172
  if (match) {
@@ -101,14 +180,47 @@ export function parseErrorLocation(errorMessage) {
101
180
  return null;
102
181
  }
103
182
 
104
- // Create a rich error from a Tova parse/analysis error
183
+ // ─── Create a rich error from a Tova parse/analysis error ────
184
+
105
185
  export function richError(source, error, filename = '<stdin>') {
106
186
  const formatter = new DiagnosticFormatter(source, filename);
107
187
 
188
+ // If error carries structured errors, format them all
189
+ if (error.errors && Array.isArray(error.errors)) {
190
+ let output = '';
191
+ for (const e of error.errors) {
192
+ if (e.line && e.column) {
193
+ output += formatter.formatError(
194
+ e.message,
195
+ { line: e.line, column: e.column },
196
+ { hint: e.hint, code: e.code, length: e.length, fix: e.fix }
197
+ );
198
+ output += '\n';
199
+ } else if (e.loc) {
200
+ output += formatter.formatError(
201
+ e.message.replace(/^.+?:\d+:\d+\s*[—-]\s*/, ''),
202
+ { line: e.loc.line, column: e.loc.column },
203
+ { hint: e.hint, code: e.code }
204
+ );
205
+ output += '\n';
206
+ } else {
207
+ // Try to parse location from message
208
+ const loc = parseErrorLocation(e.message);
209
+ if (loc) {
210
+ output += formatter.formatError(loc.message, { line: loc.line, column: loc.column });
211
+ output += '\n';
212
+ } else {
213
+ output += formatter.formatError(e.message, { line: 1, column: 1 });
214
+ output += '\n';
215
+ }
216
+ }
217
+ }
218
+ return output || error.message;
219
+ }
220
+
108
221
  // Try to extract location from error message
109
222
  const loc = parseErrorLocation(error.message);
110
223
  if (loc) {
111
- // Try to detect hint
112
224
  let hint = null;
113
225
  if (loc.message.includes("Expected '}'")) {
114
226
  hint = "check for a matching opening '{' above";
@@ -118,7 +230,7 @@ export function richError(source, error, filename = '<stdin>') {
118
230
  hint = "check for a matching opening '[' above";
119
231
  }
120
232
 
121
- return formatter.formatError(loc.message, { line: loc.line, column: loc.column }, hint);
233
+ return formatter.formatError(loc.message, { line: loc.line, column: loc.column }, { hint });
122
234
  }
123
235
 
124
236
  // Fallback: try to parse the error for analysis errors
@@ -137,3 +249,13 @@ export function richError(source, error, filename = '<stdin>') {
137
249
 
138
250
  return error.message;
139
251
  }
252
+
253
+ // ─── Summary line for CLI output ─────────────────────────────
254
+
255
+ export function formatSummary(errorCount, warningCount) {
256
+ const parts = [];
257
+ if (errorCount > 0) parts.push(_c.red(`${errorCount} error${errorCount === 1 ? '' : 's'}`));
258
+ if (warningCount > 0) parts.push(_c.yellow(`${warningCount} warning${warningCount === 1 ? '' : 's'}`));
259
+ if (parts.length === 0) return _c.green('no errors');
260
+ return parts.join(', ') + ' emitted';
261
+ }