@ascent-lang/dev 0.1.0 → 0.3.0

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 (86) hide show
  1. package/dist/errors/elaborate.d.ts +24 -0
  2. package/dist/errors/elaborate.d.ts.map +1 -0
  3. package/dist/errors/elaborate.js +53 -0
  4. package/dist/errors/elaborate.js.map +1 -0
  5. package/dist/errors/index.d.ts.map +1 -1
  6. package/dist/errors/index.js +356 -30
  7. package/dist/errors/index.js.map +1 -1
  8. package/dist/errors/render.d.ts +3 -0
  9. package/dist/errors/render.d.ts.map +1 -0
  10. package/dist/errors/render.js +43 -0
  11. package/dist/errors/render.js.map +1 -0
  12. package/dist/errors/types.d.ts +29 -0
  13. package/dist/errors/types.d.ts.map +1 -1
  14. package/dist/index.js +21 -28
  15. package/dist/index.js.map +1 -1
  16. package/dist/interpreter.d.ts.map +1 -1
  17. package/dist/interpreter.js +28 -5
  18. package/dist/interpreter.js.map +1 -1
  19. package/dist/lexer/index.d.ts.map +1 -1
  20. package/dist/lexer/index.js +4 -3
  21. package/dist/lexer/index.js.map +1 -1
  22. package/dist/lexer/keywords.d.ts.map +1 -1
  23. package/dist/lexer/keywords.js +3 -0
  24. package/dist/lexer/keywords.js.map +1 -1
  25. package/dist/lexer/token.d.ts +7 -1
  26. package/dist/lexer/token.d.ts.map +1 -1
  27. package/dist/lib.d.ts +3 -3
  28. package/dist/lib.d.ts.map +1 -1
  29. package/dist/lib.js +11 -6
  30. package/dist/lib.js.map +1 -1
  31. package/dist/parser/ast.d.ts +8 -4
  32. package/dist/parser/ast.d.ts.map +1 -1
  33. package/dist/parser/expr.d.ts.map +1 -1
  34. package/dist/parser/expr.js +34 -19
  35. package/dist/parser/expr.js.map +1 -1
  36. package/dist/parser/index.d.ts +3 -5
  37. package/dist/parser/index.d.ts.map +1 -1
  38. package/dist/parser/index.js +26 -33
  39. package/dist/parser/index.js.map +1 -1
  40. package/dist/parser/printer.d.ts +1 -0
  41. package/dist/parser/printer.d.ts.map +1 -1
  42. package/dist/parser/printer.js +21 -0
  43. package/dist/parser/printer.js.map +1 -1
  44. package/dist/parser/stmt.d.ts.map +1 -1
  45. package/dist/parser/stmt.js +5 -3
  46. package/dist/parser/stmt.js.map +1 -1
  47. package/dist/parser/token-stream.d.ts +4 -4
  48. package/dist/parser/token-stream.d.ts.map +1 -1
  49. package/dist/parser/token-stream.js +21 -9
  50. package/dist/parser/token-stream.js.map +1 -1
  51. package/dist/parser/type-expr.d.ts +1 -1
  52. package/dist/parser/type-expr.d.ts.map +1 -1
  53. package/dist/parser/type-expr.js +11 -4
  54. package/dist/parser/type-expr.js.map +1 -1
  55. package/dist/parser/typechecker.d.ts +2 -2
  56. package/dist/parser/typechecker.d.ts.map +1 -1
  57. package/dist/parser/typechecker.js +109 -67
  58. package/dist/parser/typechecker.js.map +1 -1
  59. package/dist/types/types.d.ts +4 -0
  60. package/dist/types/types.d.ts.map +1 -1
  61. package/dist/types/types.js +27 -15
  62. package/dist/types/types.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/errors/elaborate.ts +88 -0
  65. package/src/errors/index.ts +356 -30
  66. package/src/errors/lexical.yml +48 -13
  67. package/src/errors/name.yml +45 -9
  68. package/src/errors/render.ts +59 -0
  69. package/src/errors/syntactic.yml +128 -49
  70. package/src/errors/typechecker.yml +147 -61
  71. package/src/errors/types.ts +55 -0
  72. package/src/index.ts +20 -30
  73. package/src/interpreter.ts +24 -6
  74. package/src/lexer/index.ts +4 -3
  75. package/src/lexer/keywords.ts +3 -0
  76. package/src/lexer/token.ts +18 -0
  77. package/src/lib.ts +12 -7
  78. package/src/parser/ast.ts +7 -6
  79. package/src/parser/expr.ts +34 -19
  80. package/src/parser/index.ts +32 -32
  81. package/src/parser/printer.ts +22 -0
  82. package/src/parser/stmt.ts +5 -3
  83. package/src/parser/token-stream.ts +20 -8
  84. package/src/parser/type-expr.ts +10 -4
  85. package/src/parser/typechecker.ts +142 -54
  86. package/src/types/types.ts +36 -16
@@ -0,0 +1,59 @@
1
+ import chalk from 'chalk';
2
+ import type { Span } from '../lexer/token.js';
3
+ import type { Diagnostic } from './elaborate.js';
4
+
5
+ // One span rendered as its source line plus an underline, with an optional
6
+ // trailing label. `mark` is the underline character and `colour` tints it, so
7
+ // the primary span (red '^') and a supporting span (cyan '-') look distinct.
8
+ function snippet(
9
+ sourceLines: string[],
10
+ span: Span,
11
+ mark: string,
12
+ colour: (s: string) => string,
13
+ label: string | null,
14
+ ): string[] {
15
+ const line = sourceLines[span.start.line - 1] ?? '';
16
+ // A span may run to the line end (an unterminated string), so clamp the
17
+ // underline to the visible text of its starting line.
18
+ const endColumn = span.end.line === span.start.line ? span.end.column : line.length + 1;
19
+ const underline = mark.repeat(Math.max(1, endColumn - span.start.column));
20
+ const gutter = String(span.start.line);
21
+ const blank = ' '.repeat(gutter.length);
22
+ const caret = `${' '.repeat(span.start.column - 1)}${colour(underline)}`;
23
+ return [
24
+ `${chalk.dim(`${gutter} |`)} ${line}`,
25
+ `${chalk.dim(`${blank} |`)} ${label !== null ? `${caret} ${colour(label)}` : caret}`,
26
+ ];
27
+ }
28
+
29
+ export function renderTerminal(d: Diagnostic, source: string, filePath: string | null): string {
30
+ const sourceLines = source.split('\n');
31
+ const lines: string[] = [];
32
+
33
+ lines.push(chalk.red.bold(`Error ${d.code}`) + chalk.bold(`: ${d.message}`));
34
+
35
+ const where = filePath !== null
36
+ ? `${filePath}:${d.span.start.line}:${d.span.start.column}`
37
+ : `line ${d.span.start.line}, column ${d.span.start.column}`;
38
+ lines.push(chalk.dim(` → ${where}`));
39
+
40
+ lines.push(...snippet(sourceLines, d.span, '^', chalk.red, null));
41
+
42
+ // Supporting spans (e.g. "'count' was created with 'fix' here") point back at
43
+ // context elsewhere in the source, each with its own snippet.
44
+ for (const rel of d.related) {
45
+ lines.push(...snippet(sourceLines, rel.span, '-', chalk.cyan, rel.label));
46
+ }
47
+
48
+ if (d.explanation !== null) {
49
+ lines.push('');
50
+ lines.push(d.explanation);
51
+ }
52
+
53
+ if (d.fix !== null) {
54
+ lines.push('');
55
+ lines.push(chalk.green(`fix: ${d.fix.title}`));
56
+ }
57
+
58
+ return lines.join('\n');
59
+ }
@@ -1,66 +1,145 @@
1
- # S — syntactic: the tokens don't form a valid expression
1
+ # S — syntactic: each word is fine on its own, but they don't fit together
2
2
 
3
- - code: 'S0001'
4
- name: 'unclosed-paren'
5
- category: 'syntactic'
3
+ - code: S0001
4
+ name: unclosed-paren
5
+ category: syntactic
6
6
  summary: "An opening '(' has no matching ')'."
7
+ message: "I expected a ')' here."
8
+ explanation: >-
9
+ Every '(' has to be closed with a matching ')'. One was opened earlier —
10
+ around a group like '(a + b)', a call's inputs, or an 'if' or 'while'
11
+ condition — and this is where its ')' should be.
12
+ related:
13
+ - key: opener
14
+ label: "this '(' was opened here"
7
15
 
8
- - code: 'S0002'
9
- name: 'expected-expression'
10
- category: 'syntactic'
11
- summary: 'An expression was required here but the input contained none.'
16
+ - code: S0002
17
+ name: expected-expression
18
+ category: syntactic
19
+ summary: A value was expected here, but there wasn't one.
20
+ message: "I expected a value here."
21
+ explanation: >-
22
+ This spot needs a value — a number, a String, a name, or something built
23
+ from them like 'a + b'. Places that need one include just after '=', just
24
+ after an operator like '+', and inside '( )'.
12
25
 
13
- - code: 'S0003'
14
- name: 'expected-slot-name'
15
- category: 'syntactic'
16
- summary: "A slot name (lowercase identifier) was expected after 'fix'."
26
+ - code: S0003
27
+ name: expected-slot-name
28
+ category: syntactic
29
+ summary: "A name was expected after 'fix' or 'mut', or as a program input."
30
+ message: "I expected a name here."
31
+ explanation: >-
32
+ A name is needed here — after 'fix' or 'mut' to create one
33
+ ('fix count = 0'), or as the name of a program input ('args (age: Int)'). A
34
+ name starts with a lowercase letter.
17
35
 
18
- - code: 'S0004'
19
- name: 'expected-equals'
20
- category: 'syntactic'
21
- summary: "An '=' was expected after the slot name in a 'fix' declaration."
36
+ - code: S0004
37
+ name: expected-equals
38
+ category: syntactic
39
+ summary: "An '=' was expected after the name in a 'fix' or 'mut' declaration."
40
+ message: "I expected an '=' here."
41
+ explanation: >-
42
+ 'fix' and 'mut' give a name its starting value, so the name is followed by
43
+ '=' and the value, like 'fix count = 0'. (A type can go in between, as in
44
+ 'fix count: Int = 0'.)
22
45
 
23
- - code: 'S0005'
24
- name: 'unclosed-brace'
25
- category: 'syntactic'
46
+ - code: S0005
47
+ name: unclosed-brace
48
+ category: syntactic
26
49
  summary: "An opening '{' has no matching '}'."
50
+ message: "I expected a '}' here."
51
+ explanation: >-
52
+ A block groups statements between '{' and '}'. One was opened earlier and
53
+ this is where its closing '}' should be.
54
+ related:
55
+ - key: opener
56
+ label: "this '{' was opened here"
27
57
 
28
- - code: 'S0006'
29
- name: 'expected-test-paren'
30
- category: 'syntactic'
31
- summary: "An '(' was expected here to start the condition."
58
+ - code: S0006
59
+ name: expected-test-paren
60
+ category: syntactic
61
+ summary: "A '(' was expected to open a condition or an 'args' list."
62
+ message: "I expected a '(' here."
63
+ explanation: >-
64
+ An 'if' or 'while' condition — and the inputs listed after 'args' — go
65
+ inside '( )', like 'if (age >= 18) { … }'. This is where that opening '('
66
+ should be.
32
67
 
33
- - code: 'S0007'
34
- name: 'expected-block'
35
- category: 'syntactic'
68
+ - code: S0007
69
+ name: expected-block
70
+ category: syntactic
36
71
  summary: "A block ('{ … }') was expected here."
72
+ message: "I expected a '{' here."
73
+ explanation: >-
74
+ The body of an 'if', 'else', or 'while' is always a block between '{' and
75
+ '}', even when it holds a single line, as in 'if (ok) { … }'.
37
76
 
38
- - code: 'S0008'
39
- name: 'chained-comparison'
40
- category: 'syntactic'
41
- summary: "Comparisons don't chain — 'a < b < c' isn't valid. Group with parentheses instead."
77
+ - code: S0008
78
+ name: chained-comparison
79
+ category: syntactic
80
+ summary: "Comparisons can't be chained — 'a < b < c' isn't allowed."
81
+ message: "I can't chain two comparisons like this."
82
+ explanation: >-
83
+ A comparison such as '<' or '==' looks at two values and gives back True or
84
+ False. Chaining a third one, as in 'a < b < c', would then compare that
85
+ True or False against another value, which has no clear meaning — so
86
+ compare two values at a time.
42
87
 
43
- - code: 'S0009'
44
- name: 'expected-colon'
45
- category: 'syntactic'
46
- summary: "A ':' was expected between the argument name and its type."
88
+ - code: S0009
89
+ name: expected-colon
90
+ category: syntactic
91
+ summary: "A ':' was expected between a program input's name and its type."
92
+ message: "I expected a ':' here."
93
+ explanation: >-
94
+ Each program input is written as a name, then ':', then its type, like
95
+ 'args (age: Int)'. This is where the ':' should be.
47
96
 
48
- - code: 'S0010'
49
- name: 'expected-type'
50
- category: 'syntactic'
51
- summary: "A type name was expected here. Valid types are Int, Float, Bool, String, and List<T>."
97
+ - code: S0010
98
+ name: expected-type
99
+ category: syntactic
100
+ summary: A type name was expected here.
101
+ message: "I expected a type name here."
102
+ explanation: >-
103
+ A type name says what kind of value this is. The built-in types are Int,
104
+ Float, Bool, and String, plus 'List<…>' for a list, as in 'List<Int>'.
52
105
 
53
- - code: 'S0011'
54
- name: 'expected-semicolon'
55
- category: 'syntactic'
56
- summary: "A ';' was expected here."
106
+ - code: S0011
107
+ name: expected-semicolon
108
+ category: syntactic
109
+ summary: "A ';' was expected to end the statement."
110
+ message: "I expected a ';' here."
111
+ explanation: >-
112
+ Every statement in Ascent ends with a ';'. It looks like the statement
113
+ before this point was never finished with one.
57
114
 
58
- - code: 'S0012'
59
- name: 'expected-method-name'
60
- category: 'syntactic'
61
- summary: "A method name (lowercase identifier) was expected after '.'."
115
+ - code: S0012
116
+ name: expected-method-name
117
+ category: syntactic
118
+ summary: "A method name was expected after '.'."
119
+ message: "I expected a method name here."
120
+ explanation: >-
121
+ A '.' calls a method on a value, so it's followed by the method's name and
122
+ '( )', like 'items.length()'. A method name starts with a lowercase letter.
62
123
 
63
- - code: 'S0013'
64
- name: 'unclosed-bracket'
65
- category: 'syntactic'
124
+ - code: S0013
125
+ name: unclosed-bracket
126
+ category: syntactic
66
127
  summary: "An opening '[' has no matching ']'."
128
+ message: "I expected a ']' here."
129
+ explanation: >-
130
+ Square brackets '[ ]' wrap a list like '[1, 2, 3]' or pick an item out of
131
+ one like 'items[0]'. One '[' was opened earlier and this is where its ']'
132
+ should be.
133
+ related:
134
+ - key: opener
135
+ label: "this '[' was opened here"
136
+
137
+ - code: S0014
138
+ name: expected-call-paren
139
+ category: syntactic
140
+ summary: "A method call needs '( )' after the method name."
141
+ message: "I expected a '(' here."
142
+ explanation: >-
143
+ A '.' calls a method, and a call always has '( )' after the name — even
144
+ when the method takes no inputs, as in 'items.length()'. This is where that
145
+ opening '(' should be.
@@ -1,61 +1,147 @@
1
- # T — type: a type rule is broken
2
-
3
- - code: 'T0001'
4
- name: 'annotation-mismatch'
5
- category: 'type'
6
- summary: "The declared type annotation doesn't match the inferred type of the initialiser."
7
-
8
- - code: 'T0002'
9
- name: 'incompatible-list-elements'
10
- category: 'type'
11
- summary: "List elements have incompatible types — a list must be homogeneous (all the same type, with Int widening to Float)."
12
-
13
- - code: 'T0003'
14
- name: 'empty-list-needs-annotation'
15
- category: 'type'
16
- summary: "An empty list '[]' has no element type. Annotate the variable: 'fix xs: List<Int> = []'."
17
-
18
- - code: 'T0004'
19
- name: 'condition-not-bool'
20
- category: 'type'
21
- summary: "The condition in 'if' or 'while' must be of type Bool."
22
-
23
- - code: 'T0005'
24
- name: 'if-branch-mismatch'
25
- category: 'type'
26
- summary: "The 'then' and 'else' branches of 'if' have incompatible types."
27
-
28
- - code: 'T0006'
29
- name: 'no-such-method'
30
- category: 'type'
31
- summary: "The type has no method with this name."
32
-
33
- - code: 'T0007'
34
- name: 'wrong-arg-count'
35
- category: 'type'
36
- summary: "Wrong number of arguments for this method or function call."
37
-
38
- - code: 'T0008'
39
- name: 'wrong-arg-type'
40
- category: 'type'
41
- summary: "An argument has the wrong type for this method or function call."
42
-
43
- - code: 'T0009'
44
- name: 'operator-type-error'
45
- category: 'type'
46
- summary: "An operator was applied to operands of incompatible types."
47
-
48
- - code: 'T0010'
49
- name: 'index-requires-list'
50
- category: 'type'
51
- summary: "The '[ ]' index operator requires a List, but the receiver has a different type."
52
-
53
- - code: 'T0011'
54
- name: 'index-not-int'
55
- category: 'type'
56
- summary: "List indices must be of type Int."
57
-
58
- - code: 'T0012'
59
- name: 'no-methods'
60
- category: 'type'
61
- summary: "This type has no methods."
1
+ # T — type: the code fits together, but the types don't line up
2
+ #
3
+ # Same shape as the other error files. Type errors need words the source can't
4
+ # supply — the actual type names ('Int', 'String'), an argument count — so the
5
+ # checker sends them along as data and the messages drop them in as {expected},
6
+ # {actual}, {type}, {op}, and so on. Related spans point at the other places
7
+ # that matter: the two branches of an 'if', the item that breaks a list, the
8
+ # annotation a value was measured against. See lexical.yml for the wording rules.
9
+
10
+ - code: T0001
11
+ name: annotation-mismatch
12
+ category: type
13
+ summary: A value's type doesn't match the type expected here (a written annotation, or the name's existing type).
14
+ message: "This value has type {actual}, but {expected} was expected here."
15
+ explanation: >-
16
+ A value's type has to fit the type asked for in this spot. Here the value is
17
+ {actual} and {expected} was expected, and those don't match. (An Int can go
18
+ where a Float is expected, but not the other way around.)
19
+ related:
20
+ - key: annotation
21
+ label: "the type was set to {expected} here"
22
+ - key: declaration
23
+ label: "this was created holding {expected}"
24
+
25
+ - code: T0002
26
+ name: incompatible-list-elements
27
+ category: type
28
+ summary: A list mixes items of types that have no common type.
29
+ message: "The items in this list don't all have the same type."
30
+ explanation: >-
31
+ Every item in a list shares one type. Some items here are {first}, but this
32
+ one is {other}, and those don't fit together. (A mix of Int and Float is
33
+ fine the Ints become Floats — but unrelated types like Int and String
34
+ can't share a list.)
35
+ related:
36
+ - key: element
37
+ label: "this item is {other}"
38
+
39
+ - code: T0003
40
+ name: empty-list-needs-annotation
41
+ category: type
42
+ summary: An empty list has no items to show its type, and none was written down.
43
+ message: "This empty list needs a type."
44
+ explanation: >-
45
+ An empty list '[]' has no items to show what it holds, so its type has to be
46
+ written down for example 'fix xs: List<Int> = []'.
47
+
48
+ - code: T0004
49
+ name: condition-not-bool
50
+ category: type
51
+ summary: "The condition of an 'if' or 'while' isn't a True/False value."
52
+ message: "This condition has type {actual}, but it has to be True or False."
53
+ explanation: >-
54
+ An 'if' or 'while' chooses what to do from a True/False value, so its
55
+ condition has to be one — a comparison like 'x > 0', or another Bool. This
56
+ one has type {actual}.
57
+
58
+ - code: T0005
59
+ name: if-branch-mismatch
60
+ category: type
61
+ summary: "The two branches of an 'if' produce different types."
62
+ message: "The two branches of this 'if' have different types."
63
+ explanation: >-
64
+ When an 'if' is used as a value, both branches have to produce the same
65
+ type, because the whole 'if' becomes one value. Here one branch gives
66
+ {then} and the other gives {else}.
67
+ related:
68
+ - key: then
69
+ label: "this branch gives {then}"
70
+ - key: else
71
+ label: "this branch gives {else}"
72
+
73
+ - code: T0006
74
+ name: no-such-method
75
+ category: type
76
+ summary: A method with this name doesn't exist on the value's type.
77
+ message: "{type} has no method called '{method}'."
78
+ explanation: >-
79
+ The methods you can call depend on the value's type. For example, an Int has
80
+ 'toStr()', 'toFloat()', and 'abs()'; a list has 'length()', 'append(…)',
81
+ 'reverse()', and more. Check the spelling of '{method}'.
82
+
83
+ - code: T0007
84
+ name: wrong-arg-count
85
+ category: type
86
+ summary: A method or function call was given the wrong number of inputs.
87
+ message: "This call has the wrong number of inputs."
88
+ explanation: >-
89
+ It needs {expected}, but was given {got}. Each input goes inside the '( )',
90
+ separated by commas.
91
+
92
+ - code: T0008
93
+ name: wrong-arg-type
94
+ category: type
95
+ summary: An input to a method or function has a type that isn't accepted here.
96
+ message: "This input has type {actual}, but {expected} was expected."
97
+ explanation: >-
98
+ Each method or function accepts inputs of certain types. Here {expected} was
99
+ expected, but the input given is {actual}.
100
+
101
+ - code: T0009
102
+ name: operator-type-error
103
+ category: type
104
+ summary: An operator was used on types it doesn't accept.
105
+ message: "I can't use '{op}' on {operands}."
106
+ explanation: >-
107
+ Operators only work on certain types: '+', '-', '*', and '/' need numbers
108
+ (Int or Float); 'div' and 'mod' need whole numbers (Int); 'and', 'or', and 'not' need True/False values (Bool); and a comparison needs two
109
+ values of the same kind. '{op}' doesn't work on {operands}.
110
+
111
+ - code: T0010
112
+ name: index-requires-list
113
+ category: type
114
+ summary: "The '[ ]' index was used on something that isn't a list."
115
+ message: "I can't use '[ ]' here — this has type {actual}, not a list."
116
+ explanation: >-
117
+ Reading an item with '[ ]', like 'items[0]', works only on a list. This
118
+ value has type {actual}, which isn't a list.
119
+
120
+ - code: T0011
121
+ name: index-not-int
122
+ category: type
123
+ summary: A list index isn't an Int.
124
+ message: "A list index has to be an Int, but this has type {actual}."
125
+ explanation: >-
126
+ Inside 'items[…]', the value in the brackets picks an item by its position,
127
+ counting from 0, so it has to be a whole number (Int). This one has type
128
+ {actual}.
129
+
130
+ - code: T0012
131
+ name: no-methods
132
+ category: type
133
+ summary: The value's type has no methods at all.
134
+ message: "Values of type {type} don't have any methods."
135
+ explanation: >-
136
+ Only some types have methods you can call with '.': Int, Float, and List. A
137
+ {type} has none, so 'value.something()' can't be used on it.
138
+
139
+ - code: T0013
140
+ name: unknown-function
141
+ category: type
142
+ summary: A call names a function that doesn't exist.
143
+ message: "There's no function called '{name}'."
144
+ explanation: >-
145
+ Ascent has just one built-in function right now — 'floor(x)', which rounds a
146
+ Float down to a whole number. Everything else is a method, called on a value
147
+ with '.', like 'x.toStr()'.
@@ -4,10 +4,65 @@
4
4
 
5
5
  export type Category = 'lexical' | 'syntactic' | 'name' | 'type' | 'runtime';
6
6
 
7
+ // A concrete, machine-applicable correction. Authored ONLY when the fix is
8
+ // certain and unambiguous (see the wording rules in the .yml files); an
9
+ // uncertain guess is left to the reader / the LLM engine, not baked in here.
10
+ export interface Fix {
11
+ title: string; // human label for the action, e.g. "Write '0.5'"
12
+ replacement: string; // text to put in place of the offending span
13
+ }
14
+
15
+ // A generic illustration of the rule — NOT "your code, fixed". `valid` shows a
16
+ // well-formed form, `invalid` a broken one, so the pair teaches the rule
17
+ // without guessing what the author actually meant.
18
+ export interface Example {
19
+ valid: string;
20
+ invalid: string;
21
+ }
22
+
23
+ // A label for a supporting span. `key` pairs it with the RelatedMarker the
24
+ // checker emits (by role); `label` is the prose shown beside that span and may
25
+ // contain {found}. If the checker doesn't supply a span for this key (e.g. the
26
+ // declaration has no source location), the label is simply dropped.
27
+ export interface RelatedLabel {
28
+ key: string;
29
+ label: string;
30
+ }
31
+
32
+ // A condition on the offending source text (`found`). The first variant whose
33
+ // `when` matches overrides the base fields it specifies. Kept deliberately tiny
34
+ // — these two forms cover every value-keyed case the lexer produces.
35
+ export interface When {
36
+ equals?: string;
37
+ startsWith?: string;
38
+ }
39
+
40
+ // A tailored override for a specific found text (e.g. a stray '!'). It overrides
41
+ // only the fields it sets; anything unset is inherited from the base entry.
42
+ export interface Variant {
43
+ when: When;
44
+ message?: string;
45
+ explanation?: string;
46
+ fix?: Fix;
47
+ example?: Example;
48
+ }
49
+
7
50
  export interface ErrorEntry {
8
51
  code: string;
9
52
  name: string;
10
53
  category: Category;
54
+ // One neutral line for the error catalogue / docs index.
11
55
  summary: string;
56
+ // The in-context headline shown to the reader. May contain {found}. When
57
+ // absent, the elaborator falls back to `summary`.
58
+ message?: string;
59
+ // The micro-lesson: FACTS about the language rule only, in beginner words.
60
+ // May contain {found}. Doubles as grounding for the LLM engine.
61
+ explanation?: string;
62
+ fix?: Fix;
63
+ example?: Example;
64
+ // Labels for supporting spans, keyed to the RelatedMarkers the checker emits.
65
+ related?: RelatedLabel[];
66
+ variants?: Variant[];
12
67
  retired?: boolean;
13
68
  }
package/src/index.ts CHANGED
@@ -3,11 +3,13 @@ import { createInterface } from 'node:readline/promises';
3
3
  import { readFile } from 'node:fs/promises';
4
4
  import chalk from 'chalk';
5
5
  import { Lexer } from './lexer/index.js';
6
- import { Parser } from './parser/index.js';
6
+ import { parse, parseTokens } from './parser/index.js';
7
7
  import { typecheck } from './parser/typechecker.js';
8
8
  import { formatValue } from './parser/printer.js';
9
9
  import { formatTypedStmt } from './parser/typed-printer.js';
10
10
  import { executeStmt, executeProgram, Environment, RuntimeValue } from './interpreter.js';
11
+ import { elaborate } from './errors/elaborate.js';
12
+ import { renderTerminal } from './errors/render.js';
11
13
  import type { ArgDef } from './parser/ast.js';
12
14
 
13
15
  // \x01 and \x02 bracket invisible bytes so readline counts the visible
@@ -94,34 +96,22 @@ const runFile = async (filePath: string): Promise<void> => {
94
96
  process.exit(1);
95
97
  }
96
98
 
97
- const lexResult = new Lexer(src).tokenize();
98
- const parseResult = new Parser(lexResult.tokens).parse();
99
-
100
- const errors = [...lexResult.errorMarkers, ...parseResult.errorMarkers];
101
- if (errors.length > 0) {
102
- for (const marker of errors) {
103
- process.stderr.write(chalk.red(`[${marker.code}]`) + '\n');
104
- }
105
- process.exit(1);
106
- }
107
-
108
- if (parseResult.program === null) return;
109
-
110
- const typeResult = typecheck(parseResult.program);
111
- if (typeResult.errorMarkers.length > 0) {
112
- for (const marker of typeResult.errorMarkers) {
113
- process.stderr.write(chalk.red(`[${marker.code}]`) + '\n');
99
+ const parseResult = parse(src);
100
+ if (parseResult.errorMarkers.length > 0) {
101
+ for (const marker of parseResult.errorMarkers) {
102
+ process.stderr.write(renderTerminal(elaborate(marker, src), src, filePath) + '\n\n');
114
103
  }
115
104
  process.exit(1);
116
105
  }
117
106
 
107
+ const typedProgram = parseResult.typedProgram!;
118
108
  const env = new Environment();
119
- if (parseResult.program.args.length > 0) {
120
- bindArgs(parseResult.program.args, parseCliFlags(process.argv.slice(3)), env);
109
+ if (typedProgram.args.length > 0) {
110
+ bindArgs(typedProgram.args, parseCliFlags(process.argv.slice(3)), env);
121
111
  }
122
112
 
123
113
  try {
124
- const result = executeProgram(typeResult.typedProgram!, env);
114
+ const result = executeProgram(typedProgram, env);
125
115
  if (result.type !== 'Done') {
126
116
  process.stdout.write(formatValue(result) + '\n');
127
117
  }
@@ -155,19 +145,19 @@ const runRepl = async (): Promise<void> => {
155
145
 
156
146
  process.stdout.write(tokenParts.join(` ${chalk.dim('·')} `) + '\n');
157
147
 
158
- const parseResult = new Parser(lexResult.tokens).parse();
148
+ const parseResult = parseTokens(lexResult.tokens);
159
149
 
160
150
  // A non-null program no longer means error-free: panic-mode
161
151
  // recovery can skip a malformed statement and still finish the
162
152
  // parse, so errorMarkers — not program nullness — is what decides
163
153
  // whether it's safe to typecheck/run.
164
- if (parseResult.errorMarkers.length > 0) {
165
- // Only show parser errors when the lexer succeeded — if the lexer
166
- // already flagged something, the parser error is a downstream echo.
167
- if (lexResult.errorMarkers.length === 0) {
168
- for (const marker of parseResult.errorMarkers) {
169
- process.stdout.write(chalk.red(`[${marker.code}]`) + '\n');
170
- }
154
+ if (lexResult.errorMarkers.length > 0) {
155
+ for (const marker of lexResult.errorMarkers) {
156
+ process.stdout.write(renderTerminal(elaborate(marker, line), line, null) + '\n');
157
+ }
158
+ } else if (parseResult.errorMarkers.length > 0) {
159
+ for (const marker of parseResult.errorMarkers) {
160
+ process.stdout.write(renderTerminal(elaborate(marker, line), line, null) + '\n');
171
161
  }
172
162
  } else if (parseResult.program !== null) {
173
163
  const typeResult = typecheck(parseResult.program);
@@ -175,7 +165,7 @@ const runRepl = async (): Promise<void> => {
175
165
 
176
166
  if (typeErrors.length > 0) {
177
167
  for (const marker of typeErrors) {
178
- process.stdout.write(chalk.red(`[${marker.code}]`) + '\n');
168
+ process.stdout.write(renderTerminal(elaborate(marker, line), line, null) + '\n');
179
169
  }
180
170
  } else {
181
171
  // Print the untyped parse tree; execute the typed AST.