@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.
- package/dist/errors/elaborate.d.ts +24 -0
- package/dist/errors/elaborate.d.ts.map +1 -0
- package/dist/errors/elaborate.js +53 -0
- package/dist/errors/elaborate.js.map +1 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +356 -30
- package/dist/errors/index.js.map +1 -1
- package/dist/errors/render.d.ts +3 -0
- package/dist/errors/render.d.ts.map +1 -0
- package/dist/errors/render.js +43 -0
- package/dist/errors/render.js.map +1 -0
- package/dist/errors/types.d.ts +29 -0
- package/dist/errors/types.d.ts.map +1 -1
- package/dist/index.js +21 -28
- package/dist/index.js.map +1 -1
- package/dist/interpreter.d.ts.map +1 -1
- package/dist/interpreter.js +28 -5
- package/dist/interpreter.js.map +1 -1
- package/dist/lexer/index.d.ts.map +1 -1
- package/dist/lexer/index.js +4 -3
- package/dist/lexer/index.js.map +1 -1
- package/dist/lexer/keywords.d.ts.map +1 -1
- package/dist/lexer/keywords.js +3 -0
- package/dist/lexer/keywords.js.map +1 -1
- package/dist/lexer/token.d.ts +7 -1
- package/dist/lexer/token.d.ts.map +1 -1
- package/dist/lib.d.ts +3 -3
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +11 -6
- package/dist/lib.js.map +1 -1
- package/dist/parser/ast.d.ts +8 -4
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/expr.d.ts.map +1 -1
- package/dist/parser/expr.js +34 -19
- package/dist/parser/expr.js.map +1 -1
- package/dist/parser/index.d.ts +3 -5
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +26 -33
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/printer.d.ts +1 -0
- package/dist/parser/printer.d.ts.map +1 -1
- package/dist/parser/printer.js +21 -0
- package/dist/parser/printer.js.map +1 -1
- package/dist/parser/stmt.d.ts.map +1 -1
- package/dist/parser/stmt.js +5 -3
- package/dist/parser/stmt.js.map +1 -1
- package/dist/parser/token-stream.d.ts +4 -4
- package/dist/parser/token-stream.d.ts.map +1 -1
- package/dist/parser/token-stream.js +21 -9
- package/dist/parser/token-stream.js.map +1 -1
- package/dist/parser/type-expr.d.ts +1 -1
- package/dist/parser/type-expr.d.ts.map +1 -1
- package/dist/parser/type-expr.js +11 -4
- package/dist/parser/type-expr.js.map +1 -1
- package/dist/parser/typechecker.d.ts +2 -2
- package/dist/parser/typechecker.d.ts.map +1 -1
- package/dist/parser/typechecker.js +109 -67
- package/dist/parser/typechecker.js.map +1 -1
- package/dist/types/types.d.ts +4 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +27 -15
- package/dist/types/types.js.map +1 -1
- package/package.json +1 -1
- package/src/errors/elaborate.ts +88 -0
- package/src/errors/index.ts +356 -30
- package/src/errors/lexical.yml +48 -13
- package/src/errors/name.yml +45 -9
- package/src/errors/render.ts +59 -0
- package/src/errors/syntactic.yml +128 -49
- package/src/errors/typechecker.yml +147 -61
- package/src/errors/types.ts +55 -0
- package/src/index.ts +20 -30
- package/src/interpreter.ts +24 -6
- package/src/lexer/index.ts +4 -3
- package/src/lexer/keywords.ts +3 -0
- package/src/lexer/token.ts +18 -0
- package/src/lib.ts +12 -7
- package/src/parser/ast.ts +7 -6
- package/src/parser/expr.ts +34 -19
- package/src/parser/index.ts +32 -32
- package/src/parser/printer.ts +22 -0
- package/src/parser/stmt.ts +5 -3
- package/src/parser/token-stream.ts +20 -8
- package/src/parser/type-expr.ts +10 -4
- package/src/parser/typechecker.ts +142 -54
- 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
|
+
}
|
package/src/errors/syntactic.yml
CHANGED
|
@@ -1,66 +1,145 @@
|
|
|
1
|
-
# S — syntactic:
|
|
1
|
+
# S — syntactic: each word is fine on its own, but they don't fit together
|
|
2
2
|
|
|
3
|
-
- code:
|
|
4
|
-
name:
|
|
5
|
-
category:
|
|
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:
|
|
9
|
-
name:
|
|
10
|
-
category:
|
|
11
|
-
summary:
|
|
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:
|
|
14
|
-
name:
|
|
15
|
-
category:
|
|
16
|
-
summary: "A
|
|
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:
|
|
19
|
-
name:
|
|
20
|
-
category:
|
|
21
|
-
summary: "An '=' was expected after the
|
|
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:
|
|
24
|
-
name:
|
|
25
|
-
category:
|
|
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:
|
|
29
|
-
name:
|
|
30
|
-
category:
|
|
31
|
-
summary: "
|
|
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:
|
|
34
|
-
name:
|
|
35
|
-
category:
|
|
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:
|
|
39
|
-
name:
|
|
40
|
-
category:
|
|
41
|
-
summary: "Comparisons
|
|
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:
|
|
44
|
-
name:
|
|
45
|
-
category:
|
|
46
|
-
summary: "A ':' was expected between
|
|
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:
|
|
49
|
-
name:
|
|
50
|
-
category:
|
|
51
|
-
summary:
|
|
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:
|
|
54
|
-
name:
|
|
55
|
-
category:
|
|
56
|
-
summary: "A ';' was expected
|
|
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:
|
|
59
|
-
name:
|
|
60
|
-
category:
|
|
61
|
-
summary: "A method name
|
|
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:
|
|
64
|
-
name:
|
|
65
|
-
category:
|
|
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:
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
- code:
|
|
49
|
-
name:
|
|
50
|
-
category:
|
|
51
|
-
summary: "The
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- code:
|
|
59
|
-
name:
|
|
60
|
-
category:
|
|
61
|
-
summary: "
|
|
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()'.
|
package/src/errors/types.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 (
|
|
120
|
-
bindArgs(
|
|
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(
|
|
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 =
|
|
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 (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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(
|
|
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.
|