@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.
- package/README.md +17 -6
- package/dist/errors/index.js +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/index.js +14 -14
- package/dist/index.js.map +1 -1
- package/dist/interpreter.d.ts +12 -3
- package/dist/interpreter.d.ts.map +1 -1
- package/dist/interpreter.js +47 -1
- package/dist/interpreter.js.map +1 -1
- package/dist/lexer/index.d.ts.map +1 -1
- package/dist/lexer/index.js +4 -1
- package/dist/lexer/index.js.map +1 -1
- package/dist/lexer/token.d.ts +1 -1
- package/dist/lexer/token.d.ts.map +1 -1
- package/dist/lib.d.ts +4 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +5 -4
- package/dist/lib.js.map +1 -1
- package/dist/parser/ast.d.ts +3 -3
- package/dist/parser/ast.d.ts.map +1 -1
- package/dist/parser/expr.d.ts.map +1 -1
- package/dist/parser/expr.js +21 -4
- package/dist/parser/expr.js.map +1 -1
- package/dist/parser/index.js +2 -2
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/type-expr.d.ts +2 -2
- package/dist/parser/type-expr.d.ts.map +1 -1
- package/dist/parser/typechecker.d.ts +19 -3
- package/dist/parser/typechecker.d.ts.map +1 -1
- package/dist/parser/typechecker.js +23 -6
- package/dist/parser/typechecker.js.map +1 -1
- package/dist/parser/typed-ast.d.ts +2 -2
- package/dist/parser/typed-ast.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/typechecker.yml +3 -2
- package/src/index.ts +15 -15
- package/src/interpreter.ts +61 -3
- package/src/lexer/index.ts +3 -1
- package/src/lexer/token.ts +1 -0
- package/src/lib.ts +8 -4
- package/src/parser/ast.ts +3 -3
- package/src/parser/expr.ts +22 -5
- package/src/parser/index.ts +2 -2
- package/src/parser/type-expr.ts +3 -3
- package/src/parser/typechecker.ts +26 -7
- package/src/parser/typed-ast.ts +2 -2
package/src/lexer/token.ts
CHANGED
|
@@ -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 {
|
|
6
|
-
// const
|
|
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
|
|
63
|
+
export type ProgramArg = { name: string; type: ArgType };
|
|
64
64
|
|
|
65
|
-
export type Program = { args:
|
|
65
|
+
export type Program = { args: ProgramArg[]; stmts: Statement[] };
|
package/src/parser/expr.ts
CHANGED
|
@@ -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]`)
|
|
24
|
-
// unary '-',
|
|
25
|
-
//
|
|
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
|
-
|
|
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
|
}
|
package/src/parser/index.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
46
|
+
return { program: null, errorMarkers: parseResult.errorMarkers };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
return typecheck(parseResult.program);
|
package/src/parser/type-expr.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { TypeExpr,
|
|
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):
|
|
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):
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
511
|
+
|
|
512
|
+
return { program: { args: program.args, stmts: typedStmts }, errorMarkers: [] };
|
|
494
513
|
};
|
package/src/parser/typed-ast.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Span } from '../lexer/token.js';
|
|
2
|
-
import type { UnaryOp, BinaryOp,
|
|
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:
|
|
61
|
+
args: ProgramArg[];
|
|
62
62
|
stmts: TypedStatement[];
|
|
63
63
|
};
|