@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
@@ -12,6 +12,9 @@ export type TokenKind =
12
12
  | 'SLASH' // '/', always real division — yields a Float
13
13
  | 'KW_DIV' // the keyword div — Int-only floor division
14
14
  | 'KW_MOD' // the keyword mod — Int-only floored modulo
15
+ | 'KW_AND' // the keyword and — Bool-only logical and, short-circuits
16
+ | 'KW_OR' // the keyword or — Bool-only logical or, short-circuits
17
+ | 'KW_NOT' // the keyword not — Bool-only prefix negation
15
18
  | 'KW_FIX' // the keyword fix — declares a fixed slot
16
19
  | 'KW_MUT' // the keyword mut — declares a mutable slot
17
20
  | 'KW_IF' // the keyword if — starts a conditional expression
@@ -50,9 +53,24 @@ export interface Span {
50
53
  end: Position; // exclusive — points one past the last character
51
54
  }
52
55
 
56
+ // A supporting span a stage attaches to a marker — e.g. the earlier
57
+ // declaration a "can't reassign" error refers back to. `key` names the span's
58
+ // role; the matching label (prose) lives in the error's .yml row, keyed the
59
+ // same way, so no stage holds a user-facing sentence.
60
+ export interface RelatedMarker {
61
+ key: string;
62
+ span: Span;
63
+ }
64
+
53
65
  export interface Marker {
54
66
  code: string;
55
67
  span: Span;
68
+ related?: RelatedMarker[];
69
+ // Named values a stage knows but the source can't reconstruct — chiefly the
70
+ // type names in a type error ('Int', 'String'). Interpolated into the
71
+ // message/explanation as {key}. Kept as strings so the checker never holds a
72
+ // sentence, only the words that fill the blanks.
73
+ data?: Record<string, string>;
56
74
  }
57
75
 
58
76
  export interface Token {
package/src/parser/ast.ts CHANGED
@@ -2,9 +2,9 @@ import type { Span } from '../lexer/token.js';
2
2
 
3
3
  // TypeExpr is the AST node for a type written in source code.
4
4
  // It carries span information so the type checker can point at it in errors.
5
- export type TypeExpr =
6
- | { kind: 'TypeName'; name: 'Int' | 'Float' | 'Bool' | 'String'; span: Span }
7
- | { kind: 'ListType'; elem: TypeExpr; span: Span };
5
+ export type TypeName = { kind: 'TypeName'; name: 'Int' | 'Float' | 'Bool' | 'String'; span: Span };
6
+ export type ListType = { kind: 'ListType'; elem: TypeExpr; span: Span };
7
+ export type TypeExpr = TypeName | ListType;
8
8
 
9
9
  export type Literal = (
10
10
  | { kind: 'literal'; valueType: 'Int'; value: bigint; span: Span }
@@ -15,10 +15,11 @@ export type Literal = (
15
15
  | { kind: 'literal'; valueType: 'Done'; span: Span }
16
16
  );
17
17
 
18
- export type UnaryOp = '-';
18
+ export type UnaryOp = '-' | 'not';
19
19
  export type ArithmeticOp = '+' | '-' | '*' | '/' | 'div' | 'mod';
20
20
  export type ComparisonOp = '==' | '!=' | '<' | '<=' | '>' | '>=';
21
- export type BinaryOp = ArithmeticOp | ComparisonOp;
21
+ export type BooleanOp = 'and' | 'or';
22
+ export type BinaryOp = ArithmeticOp | ComparisonOp | BooleanOp;
22
23
 
23
24
  // A block is itself an expression — it yields the value of its last
24
25
  // statement, or Done when empty (the '{}' unit value).
@@ -53,7 +54,7 @@ export type Expr = (
53
54
  export type Statement = (
54
55
  | { kind: 'fix'; name: string; typeAnnotation: TypeExpr | null; init: Expr; span: Span }
55
56
  | { kind: 'mut'; name: string; typeAnnotation: TypeExpr | null; init: Expr; span: Span }
56
- | { kind: 'assign'; name: string; value: Expr; span: Span }
57
+ | { kind: 'assign'; name: string; nameSpan: Span; value: Expr; span: Span }
57
58
  | { kind: 'expr'; expr: Expr; span: Span }
58
59
  | { kind: 'while'; cond: Expr; body: Block; span: Span }
59
60
  );
@@ -22,14 +22,20 @@ import { parseBlock, parseIf } from './stmt.js';
22
22
  // This ladder is the single source of truth for what binds tighter
23
23
  // than what: postfix (`.method()`, `[index]`) binds tightest, then
24
24
  // unary '-', then '*'/'/'/'div'/'mod', then '+'/'-', then the
25
- // comparisons, loosest. Every table below is keyed off these numbers
25
+ // comparisons, then 'not', then 'and', then 'or', loosest
26
+ // the word operators sit below the comparisons (§5 of design.md), so
27
+ // `a == b and c == d` groups as `(a == b) and (c == d)`, never
28
+ // `a == (b and c) == d`. Every table below is keyed off these numbers
26
29
  // instead of inlining its own.
27
30
  const BP = {
28
- COMPARISON: 1,
29
- ADDITIVE: 2,
30
- MULTIPLICATIVE: 3,
31
- UNARY: 4,
32
- POSTFIX: 4,
31
+ OR: 1,
32
+ AND: 2,
33
+ NOT: 3,
34
+ COMPARISON: 4,
35
+ ADDITIVE: 5,
36
+ MULTIPLICATIVE: 6,
37
+ UNARY: 7,
38
+ POSTFIX: 7,
33
39
  } as const;
34
40
 
35
41
  // Every binary operator this parser knows about has one row in this
@@ -40,7 +46,12 @@ const BP = {
40
46
  // `(1 + 2) < (3 * 4)`. Comparisons are also marked `assoc: 'none'`:
41
47
  // unlike '+' or '*', two of them can never sit side by side
42
48
  // (`a < b < c` is rejected, not silently grouped one way or the other).
49
+ // 'or' belongs to a tier below 'and' — the same "same precedence,
50
+ // left-associative" shape as '+'/'-' — so `a or b or c` groups as
51
+ // `(a or b) or c`.
43
52
  const INFIX_OPS: Partial<Record<TokenKind, { op: BinaryOp; bp: number; assoc: 'left' | 'none' }>> = {
53
+ KW_OR: { op: 'or', bp: BP.OR, assoc: 'left' },
54
+ KW_AND: { op: 'and', bp: BP.AND, assoc: 'left' },
44
55
  EQ_EQ: { op: '==', bp: BP.COMPARISON, assoc: 'none' },
45
56
  BANG_EQ: { op: '!=', bp: BP.COMPARISON, assoc: 'none' },
46
57
  LT: { op: '<', bp: BP.COMPARISON, assoc: 'none' },
@@ -66,12 +77,14 @@ const POSTFIX_OPS: Partial<Record<TokenKind, { bp: number }>> = {
66
77
  LBRACKET: { bp: BP.POSTFIX },
67
78
  };
68
79
 
69
- // Prefix table — unary '-' is the Pratt parser's other operator kind
70
- // (a "nud" that still takes an operand, parsed in parseAtom below).
71
- // Only one entry today, but its binding power is declared here rather
72
- // than inlined at the call site.
80
+ // Prefix table — the Pratt parser's other operator kind (a "nud" that
81
+ // still takes an operand, parsed in parseAtom below). Unary '-' binds
82
+ // tight, at the same tier as postfix; 'not' binds much looser — tighter
83
+ // than 'and'/'or' but looser than the comparisons — which is what makes
84
+ // `not a == b` parse as `not (a == b)` rather than `(not a) == b`.
73
85
  const PREFIX_OPS: Partial<Record<TokenKind, { op: UnaryOp; bp: number }>> = {
74
86
  MINUS: { op: '-', bp: BP.UNARY },
87
+ KW_NOT: { op: 'not', bp: BP.NOT },
75
88
  };
76
89
 
77
90
  export function parseExpr(ts: TokenStream, minBp = 0): Expr | null {
@@ -152,12 +165,14 @@ function parseMethodCall(ts: TokenStream, receiver: Expr): Expr | null {
152
165
  ts.advance(); // consume method name
153
166
 
154
167
  if (ts.peek().kind !== 'LPAREN') {
155
- ts.report('S0001', ts.peek().span);
168
+ // A missing '(' here is not an unclosed group — the call's argument list
169
+ // never opened — so it's its own error, not S0001.
170
+ ts.report('S0014', ts.peek().span);
156
171
  return null;
157
172
  }
158
- ts.advance(); // consume '('
173
+ const openParen = ts.advance(); // consume '('
159
174
 
160
- const parsed = ts.parseSeparated(() => parseExpr(ts), 'COMMA', 'RPAREN', 'S0001');
175
+ const parsed = ts.parseSeparated(() => parseExpr(ts), 'COMMA', 'RPAREN', 'S0001', false, openParen.span);
161
176
  if (parsed === null) return null;
162
177
 
163
178
  return {
@@ -171,12 +186,12 @@ function parseMethodCall(ts: TokenStream, receiver: Expr): Expr | null {
171
186
 
172
187
  // 'list[index]' — LBRACKET already confirmed on lookahead by the Pratt loop.
173
188
  function parseIndex(ts: TokenStream, list: Expr): Expr | null {
174
- ts.advance(); // consume '['
189
+ const openBracket = ts.advance(); // consume '['
175
190
 
176
191
  const index = parseExpr(ts);
177
192
  if (index === null) return null;
178
193
 
179
- const rbracket = ts.expect('RBRACKET', 'S0013');
194
+ const rbracket = ts.expect('RBRACKET', 'S0013', [{ key: 'opener', span: openBracket.span }]);
180
195
  if (rbracket === null) return null;
181
196
 
182
197
  return {
@@ -255,7 +270,7 @@ function parseAtom(ts: TokenStream): Expr | null {
255
270
  }
256
271
  const closing = ts.peek();
257
272
  if (closing.kind !== 'RPAREN') {
258
- ts.report('S0001', closing.span);
273
+ ts.report('S0001', closing.span, [{ key: 'opener', span: tok.span }]);
259
274
  return null;
260
275
  }
261
276
  ts.advance(); // consume ')'
@@ -291,8 +306,8 @@ function parseAtom(ts: TokenStream): Expr | null {
291
306
 
292
307
  // 'name(arg, arg, …)' — callee token already consumed by parseAtom.
293
308
  function parseCall(ts: TokenStream, callee: Token): Expr | null {
294
- ts.advance(); // consume '('
295
- const parsed = ts.parseSeparated(() => parseExpr(ts), 'COMMA', 'RPAREN', 'S0001');
309
+ const openParen = ts.advance(); // consume '('
310
+ const parsed = ts.parseSeparated(() => parseExpr(ts), 'COMMA', 'RPAREN', 'S0001', false, openParen.span);
296
311
  if (parsed === null) return null;
297
312
 
298
313
  return {
@@ -306,7 +321,7 @@ function parseCall(ts: TokenStream, callee: Token): Expr | null {
306
321
  // '[' expr, expr, … ']' — list literal. Already peeked '[' in parseAtom.
307
322
  function parseList(ts: TokenStream): Expr | null {
308
323
  const openTok = ts.advance(); // consume '['
309
- const parsed = ts.parseSeparated(() => parseExpr(ts), 'COMMA', 'RBRACKET', 'S0013');
324
+ const parsed = ts.parseSeparated(() => parseExpr(ts), 'COMMA', 'RBRACKET', 'S0013', false, openTok.span);
310
325
  if (parsed === null) return null;
311
326
 
312
327
  return { kind: 'list', elements: parsed.items, span: { start: openTok.span.start, end: parsed.close.span.end } };
@@ -14,7 +14,7 @@ import { parseTypeExpr } from './type-expr.js';
14
14
  // this consumes its own.
15
15
  export function parseBlock(ts: TokenStream, openTok?: Token): Block | null {
16
16
  openTok ??= ts.advance(); // consume '{' unless already consumed
17
- const parsed = ts.parseSeparated(() => parseStmt(ts), 'SEMICOLON', 'RBRACE', 'S0005', true);
17
+ const parsed = ts.parseSeparated(() => parseStmt(ts), 'SEMICOLON', 'RBRACE', 'S0005', true, openTok.span);
18
18
  if (parsed === null) return null;
19
19
 
20
20
  return { kind: 'block', stmts: parsed.items, span: { start: openTok.span.start, end: parsed.close.span.end } };
@@ -24,14 +24,15 @@ export function parseBlock(ts: TokenStream, openTok?: Token): Block | null {
24
24
  // The body braces already delimit the construct, but the test stays
25
25
  // parenthesized to match the C-family/TS surface (§5).
26
26
  function parseCond(ts: TokenStream): Expr | null {
27
- if (ts.expect('LPAREN', 'S0006') === null) return null;
27
+ const open = ts.expect('LPAREN', 'S0006');
28
+ if (open === null) return null;
28
29
 
29
30
  const cond = parseExpr(ts);
30
31
  if (cond === null) {
31
32
  return null;
32
33
  }
33
34
 
34
- if (ts.expect('RPAREN', 'S0001') === null) return null;
35
+ if (ts.expect('RPAREN', 'S0001', [{ key: 'opener', span: open.span }]) === null) return null;
35
36
 
36
37
  return cond;
37
38
  }
@@ -149,6 +150,7 @@ function parseAssign(ts: TokenStream): Statement | null {
149
150
  return {
150
151
  kind: 'assign',
151
152
  name: nameTok.value,
153
+ nameSpan: nameTok.span,
152
154
  value,
153
155
  span: { start: nameTok.span.start, end: value.span.end },
154
156
  };
@@ -1,4 +1,4 @@
1
- import type { Token, TokenKind, Marker, Span } from '../lexer/token.js';
1
+ import type { Token, TokenKind, Marker, RelatedMarker, Span } from '../lexer/token.js';
2
2
 
3
3
  // The token stream is everything the grammar productions in expr.ts,
4
4
  // stmt.ts and type-expr.ts share but that isn't grammar itself: the
@@ -39,17 +39,20 @@ export class TokenStream {
39
39
  // Record a diagnostic at a given span. The one place productions push
40
40
  // to the error log when they need to report something expect() can't
41
41
  // express (e.g. "this token was fine but the *next* thing is wrong").
42
- public report(code: string, span: Span): void {
43
- this.errors.push({ code, span });
42
+ public report(code: string, span: Span, related?: RelatedMarker[]): void {
43
+ const marker: Marker = { code, span };
44
+ if (related !== undefined && related.length > 0) marker.related = related;
45
+ this.errors.push(marker);
44
46
  }
45
47
 
46
48
  // Consume-or-diagnose: the shape every "expect this exact token here"
47
49
  // check in the grammar shares. Returns the consumed token, or records
48
- // `code` at the offending token's span and returns null.
49
- public expect(kind: TokenKind, code: string): Token | null {
50
+ // `code` at the offending token's span and returns null. `related` carries
51
+ // any supporting spans (e.g. the '(' this missing ')' should have closed).
52
+ public expect(kind: TokenKind, code: string, related?: RelatedMarker[]): Token | null {
50
53
  const tok = this.peek();
51
54
  if (tok.kind !== kind) {
52
- this.report(code, tok.span);
55
+ this.report(code, tok.span, related);
53
56
  return null;
54
57
  }
55
58
  return this.advance();
@@ -87,12 +90,16 @@ export class TokenStream {
87
90
  // malformed statement doesn't take the rest of the file's diagnostics
88
91
  // down with it. The list can still come back null if synchronize()
89
92
  // runs all the way to EOF without ever finding `close`.
93
+ // `openSpan`, when given, is the span of the opening delimiter (the '(', '{'
94
+ // or '['); it rides along on the close-token error so an unclosed group can
95
+ // point back at where it opened.
90
96
  public parseSeparated<T>(
91
97
  parseItem: () => T | null,
92
98
  sep: TokenKind,
93
99
  close: TokenKind,
94
100
  closeCode: string,
95
101
  recover = false,
102
+ openSpan: Span | null = null,
96
103
  ): { items: T[]; close: Token } | null {
97
104
  const items: T[] = [];
98
105
  if (this.peek().kind !== close) {
@@ -111,10 +118,15 @@ export class TokenStream {
111
118
  items.push(item);
112
119
  if (this.peek().kind !== sep) break;
113
120
  this.advance(); // consume separator
114
- if (this.peek().kind === close) break; // trailing separator
121
+ // Break on the close OR on end-of-input: a trailing separator right
122
+ // before EOF means the group is simply unclosed, so fall straight to
123
+ // the close-token error below instead of trying to parse another item
124
+ // (which would spuriously demand an expression at end of file).
125
+ if (this.peek().kind === close || this.peek().kind === 'EOF') break;
115
126
  }
116
127
  }
117
- const closeTok = this.expect(close, closeCode);
128
+ const related: RelatedMarker[] = openSpan !== null ? [{ key: 'opener', span: openSpan }] : [];
129
+ const closeTok = this.expect(close, closeCode, related);
118
130
  if (closeTok === null) return null;
119
131
  return { items, close: closeTok };
120
132
  }
@@ -54,9 +54,10 @@ function parseArgDef(ts: TokenStream): ArgDef | null {
54
54
  export function parseArgs(ts: TokenStream): ArgDef[] | null {
55
55
  ts.advance(); // consume 'args'
56
56
 
57
- if (ts.expect('LPAREN', 'S0006') === null) return null;
57
+ const open = ts.expect('LPAREN', 'S0006');
58
+ if (open === null) return null;
58
59
 
59
- const parsed = ts.parseSeparated(() => parseArgDef(ts), 'COMMA', 'RPAREN', 'S0001');
60
+ const parsed = ts.parseSeparated(() => parseArgDef(ts), 'COMMA', 'RPAREN', 'S0001', false, open.span);
60
61
  if (parsed === null) return null;
61
62
 
62
63
  return parsed.items;