@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.
Files changed (69) 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 +18 -11
  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/parser/ast.d.ts +8 -4
  28. package/dist/parser/ast.d.ts.map +1 -1
  29. package/dist/parser/expr.d.ts.map +1 -1
  30. package/dist/parser/expr.js +34 -19
  31. package/dist/parser/expr.js.map +1 -1
  32. package/dist/parser/stmt.d.ts.map +1 -1
  33. package/dist/parser/stmt.js +5 -3
  34. package/dist/parser/stmt.js.map +1 -1
  35. package/dist/parser/token-stream.d.ts +4 -4
  36. package/dist/parser/token-stream.d.ts.map +1 -1
  37. package/dist/parser/token-stream.js +21 -9
  38. package/dist/parser/token-stream.js.map +1 -1
  39. package/dist/parser/type-expr.d.ts.map +1 -1
  40. package/dist/parser/type-expr.js +3 -2
  41. package/dist/parser/type-expr.js.map +1 -1
  42. package/dist/parser/typechecker.d.ts.map +1 -1
  43. package/dist/parser/typechecker.js +109 -67
  44. package/dist/parser/typechecker.js.map +1 -1
  45. package/dist/types/types.d.ts +4 -0
  46. package/dist/types/types.d.ts.map +1 -1
  47. package/dist/types/types.js +27 -15
  48. package/dist/types/types.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/errors/elaborate.ts +88 -0
  51. package/src/errors/index.ts +356 -30
  52. package/src/errors/lexical.yml +48 -13
  53. package/src/errors/name.yml +45 -9
  54. package/src/errors/render.ts +59 -0
  55. package/src/errors/syntactic.yml +128 -49
  56. package/src/errors/typechecker.yml +147 -61
  57. package/src/errors/types.ts +55 -0
  58. package/src/index.ts +17 -11
  59. package/src/interpreter.ts +24 -6
  60. package/src/lexer/index.ts +4 -3
  61. package/src/lexer/keywords.ts +3 -0
  62. package/src/lexer/token.ts +18 -0
  63. package/src/parser/ast.ts +7 -6
  64. package/src/parser/expr.ts +34 -19
  65. package/src/parser/stmt.ts +5 -3
  66. package/src/parser/token-stream.ts +20 -8
  67. package/src/parser/type-expr.ts +3 -2
  68. package/src/parser/typechecker.ts +140 -52
  69. package/src/types/types.ts +36 -16
@@ -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
@@ -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
- const errors = [...lexResult.errorMarkers, ...parseResult.errorMarkers];
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(chalk.red(`[${marker.code}]`) + '\n');
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(chalk.red(`[${marker.code}]`) + '\n');
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 (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
- }
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(chalk.red(`[${marker.code}]`) + '\n');
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.
@@ -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 { AscentType } from './types/types.js';
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 when the target is Float
58
- // and the value is Int the only implicit widening the language allows.
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 (targetType.kind === 'Float' && v.type === 'Int') {
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
- // N0001 / N0002 are caught at type-check time; this is an internal guard.
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
  }
@@ -114,13 +114,14 @@ export class Lexer {
114
114
  return this.readWord();
115
115
  }
116
116
 
117
- // A leading-dot float like .5 looks like a number attempt, so L0002 is
118
- // more helpful than L0001 ("unexpected character").
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('L0002', this.c.spanFrom(start));
124
+ return this.error('L0004', this.c.spanFrom(start));
124
125
  }
125
126
 
126
127
  const start = this.c.mark();
@@ -3,6 +3,9 @@ import type { TokenKind } from './token.js';
3
3
  export const KEYWORDS: Record<string, TokenKind> = {
4
4
  div: 'KW_DIV',
5
5
  mod: 'KW_MOD',
6
+ and: 'KW_AND',
7
+ or: 'KW_OR',
8
+ not: 'KW_NOT',
6
9
  fix: 'KW_FIX',
7
10
  mut: 'KW_MUT',
8
11
  if: 'KW_IF',