@ascent-lang/dev 0.1.0 → 0.2.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 +18 -11
- 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/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/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.map +1 -1
- package/dist/parser/type-expr.js +3 -2
- package/dist/parser/type-expr.js.map +1 -1
- 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 +17 -11
- 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/parser/ast.ts +7 -6
- package/src/parser/expr.ts +34 -19
- package/src/parser/stmt.ts +5 -3
- package/src/parser/token-stream.ts +20 -8
- package/src/parser/type-expr.ts +3 -2
- package/src/parser/typechecker.ts +140 -52
- package/src/types/types.ts +36 -16
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
|
@@ -8,6 +8,8 @@ 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
|
|
@@ -97,10 +99,14 @@ const runFile = async (filePath: string): Promise<void> => {
|
|
|
97
99
|
const lexResult = new Lexer(src).tokenize();
|
|
98
100
|
const parseResult = new Parser(lexResult.tokens).parse();
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
// Lexer errors mask downstream parser noise: if the characters didn't form
|
|
103
|
+
// valid tokens, the parser's complaints are just echoes of that.
|
|
104
|
+
const errors = lexResult.errorMarkers.length > 0
|
|
105
|
+
? lexResult.errorMarkers
|
|
106
|
+
: parseResult.errorMarkers;
|
|
101
107
|
if (errors.length > 0) {
|
|
102
108
|
for (const marker of errors) {
|
|
103
|
-
process.stderr.write(
|
|
109
|
+
process.stderr.write(renderTerminal(elaborate(marker, src), src, filePath) + '\n\n');
|
|
104
110
|
}
|
|
105
111
|
process.exit(1);
|
|
106
112
|
}
|
|
@@ -110,7 +116,7 @@ const runFile = async (filePath: string): Promise<void> => {
|
|
|
110
116
|
const typeResult = typecheck(parseResult.program);
|
|
111
117
|
if (typeResult.errorMarkers.length > 0) {
|
|
112
118
|
for (const marker of typeResult.errorMarkers) {
|
|
113
|
-
process.stderr.write(
|
|
119
|
+
process.stderr.write(renderTerminal(elaborate(marker, src), src, filePath) + '\n\n');
|
|
114
120
|
}
|
|
115
121
|
process.exit(1);
|
|
116
122
|
}
|
|
@@ -161,13 +167,13 @@ const runRepl = async (): Promise<void> => {
|
|
|
161
167
|
// recovery can skip a malformed statement and still finish the
|
|
162
168
|
// parse, so errorMarkers — not program nullness — is what decides
|
|
163
169
|
// whether it's safe to typecheck/run.
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
if (lexResult.errorMarkers.length > 0) {
|
|
171
|
+
for (const marker of lexResult.errorMarkers) {
|
|
172
|
+
process.stdout.write(renderTerminal(elaborate(marker, line), line, null) + '\n');
|
|
173
|
+
}
|
|
174
|
+
} else if (parseResult.errorMarkers.length > 0) {
|
|
175
|
+
for (const marker of parseResult.errorMarkers) {
|
|
176
|
+
process.stdout.write(renderTerminal(elaborate(marker, line), line, null) + '\n');
|
|
171
177
|
}
|
|
172
178
|
} else if (parseResult.program !== null) {
|
|
173
179
|
const typeResult = typecheck(parseResult.program);
|
|
@@ -175,7 +181,7 @@ const runRepl = async (): Promise<void> => {
|
|
|
175
181
|
|
|
176
182
|
if (typeErrors.length > 0) {
|
|
177
183
|
for (const marker of typeErrors) {
|
|
178
|
-
process.stdout.write(
|
|
184
|
+
process.stdout.write(renderTerminal(elaborate(marker, line), line, null) + '\n');
|
|
179
185
|
}
|
|
180
186
|
} else {
|
|
181
187
|
// Print the untyped parse tree; execute the typed AST.
|
package/src/interpreter.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BinaryOp } from './parser/ast.js';
|
|
2
2
|
import type { TypedExpr, TypedBlock, TypedStatement, TypedProgram } from './parser/typed-ast.js';
|
|
3
|
-
import type
|
|
3
|
+
import { INT_TYPE, subtype, type AscentType } from './types/types.js';
|
|
4
4
|
|
|
5
5
|
export type RuntimeValue = (
|
|
6
6
|
| { type: 'Int'; value: bigint }
|
|
@@ -54,11 +54,11 @@ export class Environment {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
// Coerce a runtime value to match a target type
|
|
58
|
-
//
|
|
57
|
+
// Coerce a runtime value to match a target type, per the witness `subtype`
|
|
58
|
+
// produces — currently only Int <: Float, so only an Int value ever moves.
|
|
59
59
|
// All other type conversions are explicit (methods like toFloat/toInt).
|
|
60
60
|
const coerce = (v: RuntimeValue, targetType: AscentType): RuntimeValue => {
|
|
61
|
-
if (
|
|
61
|
+
if (v.type === 'Int' && subtype(INT_TYPE, targetType) === 'intToFloat') {
|
|
62
62
|
return { type: 'Float', value: Number(v.value) };
|
|
63
63
|
}
|
|
64
64
|
return v;
|
|
@@ -77,7 +77,8 @@ export const evaluateExpr = (expr: TypedExpr, env: Environment): RuntimeValue =>
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
case 'slot': {
|
|
80
|
-
//
|
|
80
|
+
// Name-binding errors (N0001–N0003) are caught at type-check time; this
|
|
81
|
+
// is an internal guard.
|
|
81
82
|
const value = env.get(expr.name);
|
|
82
83
|
if (value === undefined) throw new Error(`internal: unbound slot '${expr.name}'`);
|
|
83
84
|
return value;
|
|
@@ -119,12 +120,29 @@ export const evaluateExpr = (expr: TypedExpr, env: Environment): RuntimeValue =>
|
|
|
119
120
|
}
|
|
120
121
|
case 'unary': {
|
|
121
122
|
const operand = evaluateExpr(expr.operand, env);
|
|
123
|
+
if (expr.op === 'not') {
|
|
124
|
+
if (operand.type !== 'Bool') throw new Error(`internal: 'not' on ${operand.type}`);
|
|
125
|
+
return { type: 'Bool', value: !operand.value };
|
|
126
|
+
}
|
|
122
127
|
if (operand.type === 'Int') return { type: 'Int', value: -operand.value };
|
|
123
128
|
if (operand.type === 'Float') return { type: 'Float', value: -operand.value };
|
|
124
129
|
throw new Error(`internal: unary '-' on ${operand.type}`);
|
|
125
130
|
}
|
|
126
|
-
case 'binary':
|
|
131
|
+
case 'binary': {
|
|
132
|
+
// 'and'/'or' short-circuit: the left operand alone can decide the
|
|
133
|
+
// result ('False and e' / 'True or e'), so 'e' is only evaluated
|
|
134
|
+
// when it's still needed — the same laziness every mainstream
|
|
135
|
+
// language gives its logical operators.
|
|
136
|
+
if (expr.op === 'and' || expr.op === 'or') {
|
|
137
|
+
const left = evaluateExpr(expr.left, env);
|
|
138
|
+
if (left.type !== 'Bool') throw new Error(`internal: '${expr.op}' on non-Bool`);
|
|
139
|
+
if (expr.op === 'and' ? !left.value : left.value) return left;
|
|
140
|
+
const right = evaluateExpr(expr.right, env);
|
|
141
|
+
if (right.type !== 'Bool') throw new Error(`internal: '${expr.op}' on non-Bool`);
|
|
142
|
+
return right;
|
|
143
|
+
}
|
|
127
144
|
return evaluateBinary(expr.op, evaluateExpr(expr.left, env), evaluateExpr(expr.right, env));
|
|
145
|
+
}
|
|
128
146
|
case 'block': {
|
|
129
147
|
return evaluateBlock(expr, env);
|
|
130
148
|
}
|
package/src/lexer/index.ts
CHANGED
|
@@ -114,13 +114,14 @@ export class Lexer {
|
|
|
114
114
|
return this.readWord();
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
// A leading-dot float like .5
|
|
118
|
-
//
|
|
117
|
+
// A leading-dot float like .5 is a number missing its integer part, so
|
|
118
|
+
// L0004 (its own error, with a certain '0.5' fix) is more helpful than
|
|
119
|
+
// L0001 ("unexpected character") or L0002 (a number run into letters).
|
|
119
120
|
if (ch === '.' && isDigit(this.c.peek(1))) {
|
|
120
121
|
const start = this.c.mark();
|
|
121
122
|
this.c.advance(); // '.'
|
|
122
123
|
this.consumeWhile(isDigit);
|
|
123
|
-
return this.error('
|
|
124
|
+
return this.error('L0004', this.c.spanFrom(start));
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
const start = this.c.mark();
|
package/src/lexer/keywords.ts
CHANGED