@ascent-lang/dev 0.3.0 → 0.5.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 (47) hide show
  1. package/README.md +17 -6
  2. package/dist/errors/index.js +1 -1
  3. package/dist/errors/index.js.map +1 -1
  4. package/dist/index.js +14 -14
  5. package/dist/index.js.map +1 -1
  6. package/dist/interpreter.d.ts +12 -3
  7. package/dist/interpreter.d.ts.map +1 -1
  8. package/dist/interpreter.js +47 -1
  9. package/dist/interpreter.js.map +1 -1
  10. package/dist/lexer/index.d.ts.map +1 -1
  11. package/dist/lexer/index.js +4 -1
  12. package/dist/lexer/index.js.map +1 -1
  13. package/dist/lexer/token.d.ts +1 -1
  14. package/dist/lexer/token.d.ts.map +1 -1
  15. package/dist/lib.d.ts +4 -2
  16. package/dist/lib.d.ts.map +1 -1
  17. package/dist/lib.js +5 -4
  18. package/dist/lib.js.map +1 -1
  19. package/dist/parser/ast.d.ts +3 -3
  20. package/dist/parser/ast.d.ts.map +1 -1
  21. package/dist/parser/expr.d.ts.map +1 -1
  22. package/dist/parser/expr.js +21 -4
  23. package/dist/parser/expr.js.map +1 -1
  24. package/dist/parser/index.js +2 -2
  25. package/dist/parser/index.js.map +1 -1
  26. package/dist/parser/type-expr.d.ts +2 -2
  27. package/dist/parser/type-expr.d.ts.map +1 -1
  28. package/dist/parser/typechecker.d.ts +19 -3
  29. package/dist/parser/typechecker.d.ts.map +1 -1
  30. package/dist/parser/typechecker.js +23 -6
  31. package/dist/parser/typechecker.js.map +1 -1
  32. package/dist/parser/typed-ast.d.ts +2 -2
  33. package/dist/parser/typed-ast.d.ts.map +1 -1
  34. package/package.json +5 -3
  35. package/src/errors/index.ts +1 -1
  36. package/src/errors/typechecker.yml +3 -2
  37. package/src/index.ts +15 -15
  38. package/src/interpreter.ts +61 -3
  39. package/src/lexer/index.ts +3 -1
  40. package/src/lexer/token.ts +1 -0
  41. package/src/lib.ts +8 -4
  42. package/src/parser/ast.ts +3 -3
  43. package/src/parser/expr.ts +22 -5
  44. package/src/parser/index.ts +2 -2
  45. package/src/parser/type-expr.ts +3 -3
  46. package/src/parser/typechecker.ts +26 -7
  47. package/src/parser/typed-ast.ts +2 -2
@@ -9,6 +9,7 @@ export type TokenKind =
9
9
  | 'PLUS' // '+'
10
10
  | 'MINUS' // '-'
11
11
  | 'STAR' // '*'
12
+ | 'STAR_STAR' // '**' — raises the left operand to the right operand's power
12
13
  | 'SLASH' // '/', always real division — yields a Float
13
14
  | 'KW_DIV' // the keyword div — Int-only floor division
14
15
  | 'KW_MOD' // the keyword mod — Int-only floored modulo
package/src/lib.ts CHANGED
@@ -2,32 +2,36 @@
2
2
  // The `ascent` CLI (see index.ts) is the primary entry point. Embedders
3
3
  // wanting source-to-typed-AST in one call should use parse():
4
4
  //
5
- // const { typedProgram, errorMarkers } = parse(src);
6
- // const result = executeProgram(typedProgram!, new Environment());
5
+ // const { program, errorMarkers } = parse(src);
6
+ // const inputs = new ProgramInputs(program!.args).set('name', { type: 'String', value: 'Ada' });
7
+ // const result = executeProgram(program!, inputs);
7
8
  //
8
9
  // The individual stages are also re-exported for tools that need
9
10
  // intermediate results (e.g. tokens, or the untyped AST):
10
11
  //
11
12
  // const { tokens, errorMarkers } = new Lexer(src).tokenize();
12
13
  // const { program } = parseTokens(tokens);
13
- // const { typedProgram } = typecheck(program!);
14
+ // const { program: typedProgram } = typecheck(program!);
14
15
 
15
16
  export { Lexer } from './lexer/index.js';
16
17
  export type { LexResult } from './lexer/index.js';
17
18
 
18
19
  export { parse, parseTokens } from './parser/index.js';
19
20
  export type { ParseResult } from './parser/index.js';
21
+ export type { Program, ProgramArg } from './parser/ast.js';
20
22
 
21
23
  export { typecheck } from './parser/typechecker.js';
22
24
  export type { TypedResult } from './parser/typechecker.js';
25
+ export type { TypedProgram } from './parser/typed-ast.js';
23
26
 
24
27
  export {
25
28
  Environment,
26
29
  evaluateExpr,
27
30
  executeStmt,
28
31
  executeProgram,
32
+ ProgramInputs,
29
33
  } from './interpreter.js';
30
- export type { RuntimeValue, AssignResult } from './interpreter.js';
34
+ export type { RuntimeValue, AssignResult, PrimitiveValue } from './interpreter.js';
31
35
 
32
36
  export { formatExpr, formatStmt, formatValue, valueToString } from './parser/printer.js';
33
37
  export { formatTypedStmt } from './parser/typed-printer.js';
package/src/parser/ast.ts CHANGED
@@ -16,7 +16,7 @@ export type Literal = (
16
16
  );
17
17
 
18
18
  export type UnaryOp = '-' | 'not';
19
- export type ArithmeticOp = '+' | '-' | '*' | '/' | 'div' | 'mod';
19
+ export type ArithmeticOp = '+' | '-' | '*' | '/' | 'div' | 'mod' | '**';
20
20
  export type ComparisonOp = '==' | '!=' | '<' | '<=' | '>' | '>=';
21
21
  export type BooleanOp = 'and' | 'or';
22
22
  export type BinaryOp = ArithmeticOp | ComparisonOp | BooleanOp;
@@ -60,6 +60,6 @@ export type Statement = (
60
60
  );
61
61
 
62
62
  export type ArgType = 'Int' | 'Float' | 'Bool' | 'String';
63
- export type ArgDef = { name: string; type: ArgType };
63
+ export type ProgramArg = { name: string; type: ArgType };
64
64
 
65
- export type Program = { args: ArgDef[]; stmts: Statement[] };
65
+ export type Program = { args: ProgramArg[]; stmts: Statement[] };
@@ -20,13 +20,23 @@ import { parseBlock, parseIf } from './stmt.js';
20
20
  // threshold passed down on each recursive call.
21
21
  //
22
22
  // This ladder is the single source of truth for what binds tighter
23
- // than what: postfix (`.method()`, `[index]`) binds tightest, then
24
- // unary '-', then '*'/'/'/'div'/'mod', then '+'/'-', then the
25
- // comparisons, then 'not', then 'and', then 'or', loosest —
23
+ // than what: postfix (`.method()`, `[index]`) and '**' bind tightest
24
+ // (tighter even than unary '-', per §5 of design.md `-2 ** 2` is
25
+ // `-(2 ** 2)`), then unary '-', then '*'/'/'/'div'/'mod', then '+'/'-',
26
+ // then the comparisons, then 'not', then 'and', then 'or', loosest —
26
27
  // the word operators sit below the comparisons (§5 of design.md), so
27
28
  // `a == b and c == d` groups as `(a == b) and (c == d)`, never
28
29
  // `a == (b and c) == d`. Every table below is keyed off these numbers
29
30
  // instead of inlining its own.
31
+ //
32
+ // EXPONENT shares POSTFIX's tier rather than sitting strictly above it.
33
+ // '**' is right-associative, so its right operand is parsed with
34
+ // `minBp = infix.bp` (not `+ 1`, see the loop below) — if EXPONENT
35
+ // outbid POSTFIX, that recursive parse would stop *before* absorbing a
36
+ // trailing postfix chain (`2 ** a.b()` would parse `a` alone, leaving
37
+ // `.b()` to wrongly attach to the whole `2 ** a` once control returned
38
+ // to the caller). Equal tiers let the right-hand parse swallow both a
39
+ // further '**' and any postfix chain in one pass.
30
40
  const BP = {
31
41
  OR: 1,
32
42
  AND: 2,
@@ -36,6 +46,7 @@ const BP = {
36
46
  MULTIPLICATIVE: 6,
37
47
  UNARY: 7,
38
48
  POSTFIX: 7,
49
+ EXPONENT: 7,
39
50
  } as const;
40
51
 
41
52
  // Every binary operator this parser knows about has one row in this
@@ -49,7 +60,7 @@ const BP = {
49
60
  // 'or' belongs to a tier below 'and' — the same "same precedence,
50
61
  // left-associative" shape as '+'/'-' — so `a or b or c` groups as
51
62
  // `(a or b) or c`.
52
- const INFIX_OPS: Partial<Record<TokenKind, { op: BinaryOp; bp: number; assoc: 'left' | 'none' }>> = {
63
+ const INFIX_OPS: Partial<Record<TokenKind, { op: BinaryOp; bp: number; assoc: 'left' | 'right' | 'none' }>> = {
53
64
  KW_OR: { op: 'or', bp: BP.OR, assoc: 'left' },
54
65
  KW_AND: { op: 'and', bp: BP.AND, assoc: 'left' },
55
66
  EQ_EQ: { op: '==', bp: BP.COMPARISON, assoc: 'none' },
@@ -64,6 +75,8 @@ const INFIX_OPS: Partial<Record<TokenKind, { op: BinaryOp; bp: number; assoc: 'l
64
75
  SLASH: { op: '/', bp: BP.MULTIPLICATIVE, assoc: 'left' },
65
76
  KW_DIV: { op: 'div', bp: BP.MULTIPLICATIVE, assoc: 'left' },
66
77
  KW_MOD: { op: 'mod', bp: BP.MULTIPLICATIVE, assoc: 'left' },
78
+ // Right-associative — `2 ** 3 ** 2` groups as `2 ** (3 ** 2)` (§5).
79
+ STAR_STAR: { op: '**', bp: BP.EXPONENT, assoc: 'right' },
67
80
  };
68
81
 
69
82
  // Postfix table — dot-calls and indexing are "led" operators exactly
@@ -135,7 +148,11 @@ export function parseExpr(ts: TokenStream, minBp = 0): Expr | null {
135
148
  // operators reuse the same `bp + 1` call — it still keeps looser
136
149
  // operators out of the right operand, and the `chained` check
137
150
  // above is what stops them from reappearing at this level.
138
- const right = parseExpr(ts, infix.bp + 1);
151
+ // Right-associative operators ('**') do the opposite: reusing the
152
+ // same `bp` (not `+ 1`) lets a second '**' at this level be
153
+ // absorbed into the right operand instead of returned to this loop,
154
+ // which is what makes `2 ** 3 ** 2` group as `2 ** (3 ** 2)`.
155
+ const right = parseExpr(ts, infix.assoc === 'right' ? infix.bp : infix.bp + 1);
139
156
  if (right === null) {
140
157
  return null;
141
158
  }
@@ -38,12 +38,12 @@ export const parseTokens = (tokens: Token[]): ParseResult => {
38
38
  export const parse = (src: string): TypedResult => {
39
39
  const lexResult = new Lexer(src).tokenize();
40
40
  if (lexResult.errorMarkers.length > 0) {
41
- return { typedProgram: null, errorMarkers: lexResult.errorMarkers };
41
+ return { program: null, errorMarkers: lexResult.errorMarkers };
42
42
  }
43
43
 
44
44
  const parseResult = parseTokens(lexResult.tokens);
45
45
  if (parseResult.program === null || parseResult.errorMarkers.length > 0) {
46
- return { typedProgram: null, errorMarkers: parseResult.errorMarkers };
46
+ return { program: null, errorMarkers: parseResult.errorMarkers };
47
47
  }
48
48
 
49
49
  return typecheck(parseResult.program);
@@ -1,4 +1,4 @@
1
- import type { TypeExpr, ArgDef, ArgType } from './ast.js';
1
+ import type { TypeExpr, ProgramArg, ArgType } from './ast.js';
2
2
  import type { TokenStream } from './token-stream.js';
3
3
 
4
4
  // 'Int', 'Float', 'Bool', 'String', or 'List<Type>' — used in type annotations.
@@ -30,7 +30,7 @@ export function parseTypeExpr(ts: TokenStream): TypeExpr | null {
30
30
  // annotation this only allows a bare type name (no 'List<…>'), so it
31
31
  // reads the TYPE_NAME token directly rather than going through
32
32
  // parseTypeExpr.
33
- function parseArgDef(ts: TokenStream): ArgDef | null {
33
+ function parseArgDef(ts: TokenStream): ProgramArg | null {
34
34
  const nameTok = ts.peek();
35
35
  if (nameTok.kind !== 'SLOT') {
36
36
  ts.report('S0003', nameTok.span);
@@ -53,7 +53,7 @@ function parseArgDef(ts: TokenStream): ArgDef | null {
53
53
  // 'args (name: Type, …) ;' — the program's typed input declaration, if
54
54
  // present. Returns [] (not null) when there's no 'args' keyword at all;
55
55
  // null is reserved for an actual parse error.
56
- export function parseArgsSection(ts: TokenStream): ArgDef[] | null {
56
+ export function parseArgsSection(ts: TokenStream): ProgramArg[] | null {
57
57
  if (ts.peek().kind !== 'KW_ARGS') return [];
58
58
  ts.advance(); // consume 'args'
59
59
 
@@ -7,7 +7,7 @@ import {
7
7
  } from '../types/types.js';
8
8
 
9
9
  export interface TypedResult {
10
- typedProgram: TypedProgram | null;
10
+ program: TypedProgram | null;
11
11
  errorMarkers: Marker[];
12
12
  }
13
13
 
@@ -22,7 +22,7 @@ interface Binding {
22
22
  }
23
23
 
24
24
  // A chain of scopes mirroring Environment in the interpreter.
25
- class TypeEnv {
25
+ export class TypeEnv {
26
26
  private vars = new Map<string, Binding>();
27
27
  public constructor(private readonly parent: TypeEnv | null = null) { }
28
28
 
@@ -37,6 +37,13 @@ class TypeEnv {
37
37
  public child(): TypeEnv {
38
38
  return new TypeEnv(this);
39
39
  }
40
+
41
+ // The bindings declared directly in this scope (not inherited from a
42
+ // parent) — used to promote a successful trial scope's new names into
43
+ // a persistent parent, e.g. across REPL lines.
44
+ public ownEntries(): IterableIterator<[string, Binding]> {
45
+ return this.vars.entries();
46
+ }
40
47
  }
41
48
 
42
49
  // ---- Type formation: ⊢ T type --------------------------------------
@@ -191,7 +198,7 @@ const inferExpr = (
191
198
 
192
199
  let type: AscentType;
193
200
  switch (expr.op) {
194
- case '+': case '-': case '*': {
201
+ case '+': case '-': case '*': case '**': {
195
202
  if ((lt.kind !== 'Int' && lt.kind !== 'Float') || (rt.kind !== 'Int' && rt.kind !== 'Float')) {
196
203
  return operandError(markers, expr.op, expr.span, lt, rt);
197
204
  }
@@ -472,9 +479,14 @@ const inferStmt = (stmt: Statement, env: TypeEnv, markers: Marker[]): TypedState
472
479
  }
473
480
  };
474
481
 
475
- export const typecheck = (program: Program): TypedResult => {
482
+ // parentEnv lets a caller (the REPL) carry name bindings across separate
483
+ // typecheck() calls: each call type-checks into a child scope, and only
484
+ // promotes its new bindings into parentEnv once the whole program
485
+ // succeeds, so a line that fails typechecking never leaks a partial
486
+ // declaration into later lines.
487
+ export const typecheck = (program: Program, parentEnv?: TypeEnv): TypedResult => {
476
488
  const markers: Marker[] = [];
477
- const env = new TypeEnv();
489
+ const env = parentEnv !== undefined ? parentEnv.child() : new TypeEnv();
478
490
 
479
491
  for (const arg of program.args) {
480
492
  env.set(arg.name, typeFromName(arg.type), 'arg');
@@ -488,7 +500,14 @@ export const typecheck = (program: Program): TypedResult => {
488
500
  }
489
501
 
490
502
  if (failed || markers.length > 0) {
491
- return { typedProgram: null, errorMarkers: markers };
503
+ return { program: null, errorMarkers: markers };
504
+ }
505
+
506
+ if (parentEnv !== undefined) {
507
+ for (const [name, binding] of env.ownEntries()) {
508
+ parentEnv.set(name, binding.ty, binding.origin, binding.declSpan);
509
+ }
492
510
  }
493
- return { typedProgram: { args: program.args, stmts: typedStmts }, errorMarkers: [] };
511
+
512
+ return { program: { args: program.args, stmts: typedStmts }, errorMarkers: [] };
494
513
  };
@@ -1,5 +1,5 @@
1
1
  import type { Span } from '../lexer/token.js';
2
- import type { UnaryOp, BinaryOp, ArgDef, TypeExpr } from './ast.js';
2
+ import type { UnaryOp, BinaryOp, ProgramArg, TypeExpr } from './ast.js';
3
3
  import type { AscentType } from '../types/types.js';
4
4
 
5
5
  // Every typed expression carries `type`: the Type inferred by the type
@@ -58,6 +58,6 @@ export type TypedStatement = (
58
58
  );
59
59
 
60
60
  export type TypedProgram = {
61
- args: ArgDef[];
61
+ args: ProgramArg[];
62
62
  stmts: TypedStatement[];
63
63
  };