@kernlang/core 3.4.2 → 3.4.3-canary.11.1.8a8b6d69

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 (37) hide show
  1. package/dist/codegen/body-ts.d.ts +5 -2
  2. package/dist/codegen/body-ts.js +54 -7
  3. package/dist/codegen/body-ts.js.map +1 -1
  4. package/dist/codegen/kern-stdlib.d.ts +1 -1
  5. package/dist/codegen/kern-stdlib.js +12 -1
  6. package/dist/codegen/kern-stdlib.js.map +1 -1
  7. package/dist/codegen-expression.js +27 -0
  8. package/dist/codegen-expression.js.map +1 -1
  9. package/dist/concepts.d.ts +53 -0
  10. package/dist/concepts.js.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/native-eligibility-ast.d.ts +52 -0
  15. package/dist/native-eligibility-ast.js +189 -0
  16. package/dist/native-eligibility-ast.js.map +1 -0
  17. package/dist/native-eligibility.d.ts +29 -18
  18. package/dist/native-eligibility.js +24 -26
  19. package/dist/native-eligibility.js.map +1 -1
  20. package/dist/parser-expression.d.ts +4 -2
  21. package/dist/parser-expression.js +72 -11
  22. package/dist/parser-expression.js.map +1 -1
  23. package/dist/parser-validate-body-statements.d.ts +6 -6
  24. package/dist/parser-validate-body-statements.js +7 -6
  25. package/dist/parser-validate-body-statements.js.map +1 -1
  26. package/dist/parser-validate-native-eligible.d.ts +11 -6
  27. package/dist/parser-validate-native-eligible.js +12 -7
  28. package/dist/parser-validate-native-eligible.js.map +1 -1
  29. package/dist/schema.js +11 -4
  30. package/dist/schema.js.map +1 -1
  31. package/dist/spec.d.ts +1 -1
  32. package/dist/spec.js +1 -1
  33. package/dist/spec.js.map +1 -1
  34. package/dist/value-ir.d.ts +6 -0
  35. package/dist/value-ir.js +2 -1
  36. package/dist/value-ir.js.map +1 -1
  37. package/package.json +1 -1
@@ -0,0 +1,189 @@
1
+ /** Native KERN handler-body AST eligibility — slice α-3.
2
+ *
3
+ * Replaces slice 5a's regex pre-screen (in `native-eligibility.ts`) with a
4
+ * TS-AST walk that mirrors the migrator's `mapStatement` rules in
5
+ * `packages/cli/src/commands/migrate-native-handlers.ts`. After this slice,
6
+ * the diagnostic and the migrator agree by construction:
7
+ *
8
+ * classifyHandlerBodyAst(body).eligible === true
9
+ * ⟺ kern migrate native-handlers will emit a `lang="kern"` rewrite for it.
10
+ *
11
+ * Why this matters: the slice 5a regex disagreed with the migrator's deeper
12
+ * TS-AST shape check on ~34% of "eligible" bodies (agon scan, 2026-05-04).
13
+ * Promoting the diagnostic from `info` to `warn` at that disagreement rate
14
+ * would surface fix-or-suppress noise on bodies the migrator silently bails
15
+ * on — exactly the no-unused-vars trust-collapse pattern. AST agreement is
16
+ * the prerequisite for the future warn promotion.
17
+ *
18
+ * The reason strings here are deliberately specific (e.g. `var-destructure`,
19
+ * `try-finally`, `expr-stmt-mutation`) so users running
20
+ * `kern migrate native-handlers` and `kern review` see actionable hints
21
+ * instead of a generic "ineligible". */
22
+ import ts from 'typescript';
23
+ import { parseExpression } from './parser-expression.js';
24
+ /** True when `exprText` parses cleanly under KERN's parser-expression. The
25
+ * multi-line guard catches body-statement attributes (`value="…"`) where
26
+ * raw newlines would break the line shape.
27
+ *
28
+ * Exported so the migrator (`migrate-native-handlers.ts`) shares the
29
+ * same predicate the classifier uses — slice α-3 gemini review pulled
30
+ * the formerly duplicated helper into core to prevent the migrator's
31
+ * bail conditions from drifting away from the classifier's pass
32
+ * conditions. */
33
+ export function isValidKernExpression(exprText) {
34
+ if (/\n/.test(exprText))
35
+ return false;
36
+ try {
37
+ parseExpression(exprText);
38
+ return true;
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /** True when `bodyText` contains any line or block comment. The migrator
45
+ * drops comments silently on rewrite, so a body containing them is
46
+ * ineligible — preserving the comment is the user's responsibility.
47
+ * Exported (slice α-3 gemini review) so the migrator imports the same
48
+ * scanner predicate and comment-detection cannot diverge between the
49
+ * two sides. */
50
+ export function hasComments(bodyText) {
51
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, /*skipTrivia*/ false);
52
+ scanner.setText(bodyText);
53
+ while (true) {
54
+ const kind = scanner.scan();
55
+ if (kind === ts.SyntaxKind.EndOfFileToken)
56
+ return false;
57
+ if (kind === ts.SyntaxKind.SingleLineCommentTrivia || kind === ts.SyntaxKind.MultiLineCommentTrivia)
58
+ return true;
59
+ }
60
+ }
61
+ /** Classify a single statement. Returns null if the migrator can emit it,
62
+ * otherwise a kebab-case reason. Recurses through if/try branches. */
63
+ function classifyStmt(stmt, sf) {
64
+ if (ts.isVariableStatement(stmt)) {
65
+ const flags = stmt.declarationList.flags;
66
+ if (!(flags & ts.NodeFlags.Const))
67
+ return 'var-non-const';
68
+ const decls = stmt.declarationList.declarations;
69
+ if (decls.length !== 1)
70
+ return 'var-multi-decl';
71
+ const decl = decls[0];
72
+ if (!ts.isIdentifier(decl.name))
73
+ return 'var-destructure';
74
+ if (!decl.initializer)
75
+ return 'var-no-init';
76
+ if (decl.type)
77
+ return 'var-typed';
78
+ if (!isValidKernExpression(decl.initializer.getText(sf)))
79
+ return 'var-bad-expr';
80
+ return null;
81
+ }
82
+ if (ts.isReturnStatement(stmt)) {
83
+ if (!stmt.expression)
84
+ return null;
85
+ if (!isValidKernExpression(stmt.expression.getText(sf)))
86
+ return 'return-bad-expr';
87
+ return null;
88
+ }
89
+ if (ts.isThrowStatement(stmt)) {
90
+ if (!stmt.expression)
91
+ return 'throw-no-expr';
92
+ if (!isValidKernExpression(stmt.expression.getText(sf)))
93
+ return 'throw-bad-expr';
94
+ return null;
95
+ }
96
+ if (ts.isIfStatement(stmt)) {
97
+ if (!isValidKernExpression(stmt.expression.getText(sf)))
98
+ return 'if-bad-cond';
99
+ const thenReason = classifyBranch(stmt.thenStatement, sf);
100
+ if (thenReason !== null)
101
+ return thenReason;
102
+ if (stmt.elseStatement) {
103
+ // `else if (…)` is a nested IfStatement here. The migrator's mapIf
104
+ // recurses for it, and the body emitters (TS + Python) collapse the
105
+ // resulting `else > if` shape back to `else if` / `elif` (commit
106
+ // 88c06dcc on dev). classifyBranch handles the nested IfStatement
107
+ // by re-entering classifyStmt, so the recursion is automatic.
108
+ const elseReason = classifyBranch(stmt.elseStatement, sf);
109
+ if (elseReason !== null)
110
+ return elseReason;
111
+ }
112
+ return null;
113
+ }
114
+ if (ts.isTryStatement(stmt)) {
115
+ if (!stmt.catchClause)
116
+ return 'try-no-catch';
117
+ if (stmt.finallyBlock)
118
+ return 'try-finally';
119
+ const cc = stmt.catchClause;
120
+ if (cc.variableDeclaration && !ts.isIdentifier(cc.variableDeclaration.name))
121
+ return 'try-destruct-catch';
122
+ const tryReason = classifyBranch(stmt.tryBlock, sf);
123
+ if (tryReason !== null)
124
+ return tryReason;
125
+ return classifyBranch(cc.block, sf);
126
+ }
127
+ if (ts.isExpressionStatement(stmt)) {
128
+ // Slice α-1: ExpressionStatement → `do value="…"`. Reject mutation
129
+ // (assignments, ++, --) here so the classifier matches what the migrator
130
+ // emits — the migrator has the same defensive guards.
131
+ if (ts.isBinaryExpression(stmt.expression)) {
132
+ const op = stmt.expression.operatorToken.kind;
133
+ if (op >= ts.SyntaxKind.FirstAssignment && op <= ts.SyntaxKind.LastAssignment)
134
+ return 'expr-stmt-assignment';
135
+ }
136
+ if (ts.isPostfixUnaryExpression(stmt.expression) || ts.isPrefixUnaryExpression(stmt.expression)) {
137
+ const op = stmt.expression.operator;
138
+ if (op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken)
139
+ return 'expr-stmt-mutation';
140
+ }
141
+ if (!isValidKernExpression(stmt.expression.getText(sf)))
142
+ return 'expr-stmt-bad-expr';
143
+ return null;
144
+ }
145
+ if (ts.isForStatement(stmt) || ts.isForOfStatement(stmt) || ts.isForInStatement(stmt))
146
+ return 'for-stmt';
147
+ if (ts.isWhileStatement(stmt) || ts.isDoStatement(stmt))
148
+ return 'while-do-stmt';
149
+ if (ts.isSwitchStatement(stmt))
150
+ return 'switch-stmt';
151
+ if (ts.isBlock(stmt))
152
+ return 'bare-block';
153
+ // Fallback — the TS SyntaxKind name surfaces in diagnostics so users have
154
+ // a starting point when they hit something exotic (label, with, debugger).
155
+ return `unsupported-stmt-${ts.SyntaxKind[stmt.kind]}`;
156
+ }
157
+ function classifyBranch(node, sf) {
158
+ const stmts = ts.isBlock(node) ? Array.from(node.statements) : [node];
159
+ for (const s of stmts) {
160
+ const r = classifyStmt(s, sf);
161
+ if (r !== null)
162
+ return r;
163
+ }
164
+ return null;
165
+ }
166
+ /** Classify a raw `<<<…>>>` handler body — the AST-aware replacement for
167
+ * the slice 5a regex pass. Returns `eligible: true` only if every top-level
168
+ * statement (and every nested if/try branch) maps to a body-statement form
169
+ * the migrator can emit. */
170
+ export function classifyHandlerBodyAst(rawBody) {
171
+ const trimmed = rawBody.trim();
172
+ if (trimmed === '')
173
+ return { eligible: true, reason: 'empty' };
174
+ if (hasComments(rawBody))
175
+ return { eligible: false, reason: 'comments-present' };
176
+ const sf = ts.createSourceFile('__handler.ts', rawBody, ts.ScriptTarget.Latest, true);
177
+ // ts.SourceFile carries `parseDiagnostics` despite not exposing it on the
178
+ // public type — the migrator reads it the same way.
179
+ const diags = sf.parseDiagnostics;
180
+ if (diags && diags.length > 0)
181
+ return { eligible: false, reason: 'ts-parse-error' };
182
+ for (const stmt of sf.statements) {
183
+ const r = classifyStmt(stmt, sf);
184
+ if (r !== null)
185
+ return { eligible: false, reason: r };
186
+ }
187
+ return { eligible: true, reason: 'ok' };
188
+ }
189
+ //# sourceMappingURL=native-eligibility-ast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-eligibility-ast.js","sourceRoot":"","sources":["../src/native-eligibility-ast.ts"],"names":[],"mappings":"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"}
@@ -1,33 +1,37 @@
1
- /** Native KERN handler body eligibility classifier — slice 5a foundation.
1
+ /** Native KERN handler body eligibility classifier — slice 5a foundation
2
+ * (slice α-3 update: delegates to the AST walker in
3
+ * `native-eligibility-ast.ts`; slice α-4: diagnostic surfaces at `warning`).
2
4
  *
3
5
  * Given a raw `<<<...>>>` handler body, determines whether it could compile
4
6
  * under `lang="kern"` opt-in WITHOUT manual rewrite. Used by:
5
7
  *
6
- * 1. The compiler diagnostic layer (parser-validate-propagation siblings)
7
- * to surface `info`-level hints suggesting opt-in.
8
- * 2. The future `kern migrate native-handlers` CLI (slice 5b) to bulk-convert.
8
+ * 1. The compiler diagnostic layer (`parser-validate-native-eligible.ts`)
9
+ * to surface `warning`-level `NATIVE_KERN_ELIGIBLE` hints suggesting opt-in.
10
+ * 2. The `kern migrate native-handlers` CLI (slice 5b) to bulk-convert.
9
11
  * 3. Empirical scans of real-world repos (e.g. Agon-AI) to measure the
10
12
  * practical adoption ceiling for native bodies.
11
13
  *
12
- * The classifier is INTENTIONALLY HEURISTIC full eligibility requires the
13
- * type-aware AST walk that slice 5b's rewriter performs. False positives here
14
- * surface as compile errors when the user opts in; false negatives just
15
- * deflate the suggestion rate. We err toward false negatives so the
16
- * diagnostic stays trustworthy ("if I see this hint, opting in WILL work").
14
+ * Slice α-3: replaced the regex pre-screen with a TS-AST walk that mirrors
15
+ * the migrator's `mapStatement` rules. Eligibility now equals migrate-success
16
+ * by construction the prerequisite for slice α-4's promotion of
17
+ * `NATIVE_KERN_ELIGIBLE` from `info` to `warning` without producing
18
+ * fix-or-suppress noise on bodies the migrator silently bails on.
17
19
  *
18
- * Slice 4d update: removed `try` / `catch` / `throw` / `finally`, `new`
19
- * keyword, and `??` walrus from the disqualifier list those landed in
20
- * slice 4c+4d ship. Object spread is allowed; argument spread (`f(...x)`)
21
- * is still gated since slice 4d only lowered the object-literal form.
20
+ * The legacy regex disqualifier set lives at `LEGACY_NEG_PATTERNS` for
21
+ * consumers that need a fast pre-filter (no TS parse). The canonical
22
+ * classifier (`classifyHandlerBody`) uses the AST walker.
22
23
  */
23
24
  /** Result of classifying a single handler body. */
24
25
  export interface EligibilityResult {
25
26
  /** True iff the body uses ONLY syntactic patterns that lang=kern supports. */
26
27
  eligible: boolean;
27
- /** When eligible: 'empty' (whitespace-only body) or 'no-disqualifier'.
28
- * When ineligible: the source of the matching disqualifier regex (e.g.
29
- * '\\bfor\\s*\\('). Surfaces in diagnostics + migrate reports so users
30
- * can see what's blocking. */
28
+ /** When eligible: `'empty'` (whitespace-only body) or `'ok'` (passed AST walk).
29
+ * When ineligible: a kebab-case slug naming the first blocking shape
30
+ * e.g. `'var-destructure'`, `'try-finally'`, `'expr-stmt-mutation'`,
31
+ * `'comments-present'`, `'ts-parse-error'`. See
32
+ * `native-eligibility-ast.ts` for the full set. The legacy regex source
33
+ * (e.g. `'\\bfor\\s*\\('`) is no longer surfaced — older callers that
34
+ * switched on the regex string need to migrate to the new slugs. */
31
35
  reason: string;
32
36
  }
33
37
  /** A raw `<<<…>>>` handler body extracted from a `.kern` source file,
@@ -48,7 +52,14 @@ export interface FileEligibilityReport {
48
52
  * as the bodies returned by `extractRawBodies(content)`. */
49
53
  bodies: Array<RawBody & EligibilityResult>;
50
54
  }
51
- /** Classify a single raw body. */
55
+ /** Slice α-3: legacy regex disqualifier set. Kept exported for fast
56
+ * pre-filtering (no TS parse) in tools that don't need precise reasons —
57
+ * e.g. histogram scanners that only want a coarse "ineligible" signal.
58
+ * The canonical classifier (`classifyHandlerBody`) no longer uses this set;
59
+ * it delegates to the AST walker in `native-eligibility-ast.ts`. */
60
+ export declare const LEGACY_NEG_PATTERNS: ReadonlyArray<RegExp>;
61
+ /** Classify a single raw body. Slice α-3: delegates to the AST walker so
62
+ * eligibility ≡ migrate-success by construction. */
52
63
  export declare function classifyHandlerBody(rawBody: string): EligibilityResult;
53
64
  /** Walk a `.kern` source file's text and pull out every `<<< … >>>` body,
54
65
  * preserving line positions. Mirrors the behaviour of `parser-core.ts`
@@ -1,29 +1,33 @@
1
- /** Native KERN handler body eligibility classifier — slice 5a foundation.
1
+ /** Native KERN handler body eligibility classifier — slice 5a foundation
2
+ * (slice α-3 update: delegates to the AST walker in
3
+ * `native-eligibility-ast.ts`; slice α-4: diagnostic surfaces at `warning`).
2
4
  *
3
5
  * Given a raw `<<<...>>>` handler body, determines whether it could compile
4
6
  * under `lang="kern"` opt-in WITHOUT manual rewrite. Used by:
5
7
  *
6
- * 1. The compiler diagnostic layer (parser-validate-propagation siblings)
7
- * to surface `info`-level hints suggesting opt-in.
8
- * 2. The future `kern migrate native-handlers` CLI (slice 5b) to bulk-convert.
8
+ * 1. The compiler diagnostic layer (`parser-validate-native-eligible.ts`)
9
+ * to surface `warning`-level `NATIVE_KERN_ELIGIBLE` hints suggesting opt-in.
10
+ * 2. The `kern migrate native-handlers` CLI (slice 5b) to bulk-convert.
9
11
  * 3. Empirical scans of real-world repos (e.g. Agon-AI) to measure the
10
12
  * practical adoption ceiling for native bodies.
11
13
  *
12
- * The classifier is INTENTIONALLY HEURISTIC full eligibility requires the
13
- * type-aware AST walk that slice 5b's rewriter performs. False positives here
14
- * surface as compile errors when the user opts in; false negatives just
15
- * deflate the suggestion rate. We err toward false negatives so the
16
- * diagnostic stays trustworthy ("if I see this hint, opting in WILL work").
14
+ * Slice α-3: replaced the regex pre-screen with a TS-AST walk that mirrors
15
+ * the migrator's `mapStatement` rules. Eligibility now equals migrate-success
16
+ * by construction the prerequisite for slice α-4's promotion of
17
+ * `NATIVE_KERN_ELIGIBLE` from `info` to `warning` without producing
18
+ * fix-or-suppress noise on bodies the migrator silently bails on.
17
19
  *
18
- * Slice 4d update: removed `try` / `catch` / `throw` / `finally`, `new`
19
- * keyword, and `??` walrus from the disqualifier list those landed in
20
- * slice 4c+4d ship. Object spread is allowed; argument spread (`f(...x)`)
21
- * is still gated since slice 4d only lowered the object-literal form.
20
+ * The legacy regex disqualifier set lives at `LEGACY_NEG_PATTERNS` for
21
+ * consumers that need a fast pre-filter (no TS parse). The canonical
22
+ * classifier (`classifyHandlerBody`) uses the AST walker.
22
23
  */
23
- /** Patterns that disqualify a raw JS-like body from compiling under
24
- * `lang="kern"` without manual rewrite. Order doesn't affect correctness
25
- * but the first match wins and is reported as the `reason`. */
26
- const NEG_PATTERNS = [
24
+ import { classifyHandlerBodyAst } from './native-eligibility-ast.js';
25
+ /** Slice α-3: legacy regex disqualifier set. Kept exported for fast
26
+ * pre-filtering (no TS parse) in tools that don't need precise reasons
27
+ * e.g. histogram scanners that only want a coarse "ineligible" signal.
28
+ * The canonical classifier (`classifyHandlerBody`) no longer uses this set;
29
+ * it delegates to the AST walker in `native-eligibility-ast.ts`. */
30
+ export const LEGACY_NEG_PATTERNS = [
27
31
  /=>/,
28
32
  /\bfunction\b/,
29
33
  /\bclass\s+\w/,
@@ -73,16 +77,10 @@ const NEG_PATTERNS = [
73
77
  /\(\s*\.{3}/,
74
78
  /\/\w+\/[gimsy]*/,
75
79
  ];
76
- /** Classify a single raw body. */
80
+ /** Classify a single raw body. Slice α-3: delegates to the AST walker so
81
+ * eligibility ≡ migrate-success by construction. */
77
82
  export function classifyHandlerBody(rawBody) {
78
- const trimmed = rawBody.trim();
79
- if (trimmed === '')
80
- return { eligible: true, reason: 'empty' };
81
- for (const pat of NEG_PATTERNS) {
82
- if (pat.test(rawBody))
83
- return { eligible: false, reason: pat.source };
84
- }
85
- return { eligible: true, reason: 'no-disqualifier' };
83
+ return classifyHandlerBodyAst(rawBody);
86
84
  }
87
85
  /** Walk a `.kern` source file's text and pull out every `<<< … >>>` body,
88
86
  * preserving line positions. Mirrors the behaviour of `parser-core.ts`
@@ -1 +1 @@
1
- {"version":3,"file":"native-eligibility.js","sourceRoot":"","sources":["../src/native-eligibility.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"native-eligibility.js","sourceRoot":"","sources":["../src/native-eligibility.ts"],"names":[],"mappings":"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"}
@@ -8,8 +8,10 @@
8
8
  * spec, `none` is the canonical empty-value form in `lang=kern` bodies; `null` is
9
9
  * retained for legacy/round-trip compatibility.
10
10
  *
11
- * Intentionally NOT yet supported: arithmetic, comparisons, ternary, indexing,
12
- * bitwise, assignment. Those land in a later slice. */
11
+ * Slice 2c added arithmetic and comparisons; slice α-2 added ternary
12
+ * `a ? b : c`. Still NOT supported: indexing (`xs[0]`), bitwise ops,
13
+ * assignment — these would require shape changes the body emitter
14
+ * doesn't have, so the parser deliberately rejects them. */
13
15
  import type { ValueIR } from './value-ir.js';
14
16
  export type ExprTokenKind = 'ident' | 'num' | 'str' | 'tmplStart' | 'dot' | 'optDot' | 'nullish' | 'or' | 'and' | 'lparen' | 'rparen' | 'lbrace' | 'rbrace' | 'lbracket' | 'rbracket' | 'colon' | 'comma' | 'spread' | 'qmark' | 'eq' | 'neq' | 'strictEq' | 'strictNeq' | 'bang' | 'lt' | 'lte' | 'gt' | 'gte' | 'plus' | 'minus' | 'star' | 'slash' | 'percent' | 'kwNull' | 'kwUndef' | 'kwTrue' | 'kwFalse' | 'kwAwait' | 'kwNew' | 'eof';
15
17
  export interface ExprToken {
@@ -8,8 +8,39 @@
8
8
  * spec, `none` is the canonical empty-value form in `lang=kern` bodies; `null` is
9
9
  * retained for legacy/round-trip compatibility.
10
10
  *
11
- * Intentionally NOT yet supported: arithmetic, comparisons, ternary, indexing,
12
- * bitwise, assignment. Those land in a later slice. */
11
+ * Slice 2c added arithmetic and comparisons; slice α-2 added ternary
12
+ * `a ? b : c`. Still NOT supported: indexing (`xs[0]`), bitwise ops,
13
+ * assignment — these would require shape changes the body emitter
14
+ * doesn't have, so the parser deliberately rejects them. */
15
+ /** Slice α-2: token kinds that can start an expression. Used by parsePostfix
16
+ * and the await branch of parseUnary to disambiguate postfix `?`
17
+ * (propagation) from ternary `?` (which is always followed by an expression).
18
+ * If the token AFTER `?` is in this set, the `?` belongs to the outer
19
+ * ternary (parseConditional); otherwise, it's propagation.
20
+ *
21
+ * `plus` is not in the set — KERN's parseUnary doesn't accept unary `+`.
22
+ * `kwNew` isn't in the set — `new` is matched by parseUnary as an `ident`
23
+ * token with value `'new'`. */
24
+ const EXPR_START_KINDS = new Set([
25
+ 'ident',
26
+ 'num',
27
+ 'str',
28
+ 'tmplStart',
29
+ 'kwTrue',
30
+ 'kwFalse',
31
+ 'kwNull',
32
+ 'kwUndef',
33
+ 'kwAwait',
34
+ 'lparen',
35
+ 'lbrace',
36
+ 'lbracket',
37
+ 'spread',
38
+ 'bang',
39
+ 'minus',
40
+ ]);
41
+ function isExprStartKind(kind) {
42
+ return EXPR_START_KINDS.has(kind);
43
+ }
13
44
  const KEYWORDS = {
14
45
  null: 'kwNull',
15
46
  none: 'kwNull',
@@ -356,13 +387,37 @@ class Parser {
356
387
  return this.advance();
357
388
  }
358
389
  parse() {
359
- const result = this.parseNullish();
390
+ const result = this.parseConditional();
360
391
  if (this.peek().kind !== 'eof') {
361
392
  const t = this.peek();
362
393
  throw new Error(`Unexpected token ${t.kind} ('${t.value}') at column ${t.pos + 1}`);
363
394
  }
364
395
  return result;
365
396
  }
397
+ // Slice α-2: ternary `test ? consequent : alternate`. Right-associative —
398
+ // `a ? b : c ? d : e` parses as `a ? b : (c ? d : e)`. Lower precedence
399
+ // than `??`/`||`/`&&`/binary ops, so it wraps `parseNullish`.
400
+ //
401
+ // Disambiguation with propagation `?`: parsePostfix and the await branch
402
+ // of parseUnary both consume a postfix `?` when the next token is NOT an
403
+ // expression-start. So when this method sees `?` at the top, it's
404
+ // unambiguously a ternary `?` (the next token MUST be an expression-start
405
+ // — otherwise parsePostfix would have consumed the `?` as propagation).
406
+ parseConditional() {
407
+ const test = this.parseNullish();
408
+ if (this.peek().kind !== 'qmark')
409
+ return test;
410
+ if (!isExprStartKind(this.peek(1).kind)) {
411
+ // Defensive: shouldn't happen given the parsePostfix lookahead rule.
412
+ const t = this.peek();
413
+ throw new Error(`Unexpected '?' at column ${t.pos + 1}. Postfix '?' (propagation) is recognized inside expressions; ternary '?' must be followed by an expression then ':'.`);
414
+ }
415
+ this.advance(); // consume `?`
416
+ const consequent = this.parseConditional();
417
+ this.expect('colon');
418
+ const alternate = this.parseConditional();
419
+ return { kind: 'conditional', test, consequent, alternate };
420
+ }
366
421
  parseNullish() {
367
422
  let left = this.parseOr();
368
423
  while (this.peek().kind === 'nullish') {
@@ -463,7 +518,10 @@ class Parser {
463
518
  // instead of the semantically-correct `propagate(await(call()))`.
464
519
  const argument = this.parseCall();
465
520
  const awaited = { kind: 'await', argument };
466
- if (this.peek().kind === 'qmark') {
521
+ // Slice α-2: only consume postfix `?` as propagation if the token
522
+ // after it is NOT an expression-start. Otherwise leave it for the
523
+ // outer parseConditional (ternary).
524
+ if (this.peek().kind === 'qmark' && !isExprStartKind(this.peek(1).kind)) {
467
525
  this.advance();
468
526
  return { kind: 'propagate', argument: awaited, op: '?' };
469
527
  }
@@ -481,7 +539,10 @@ class Parser {
481
539
  }
482
540
  parsePostfix() {
483
541
  const node = this.parseCall();
484
- if (this.peek().kind === 'qmark') {
542
+ // Slice α-2: only consume postfix `?` as propagation if the token after
543
+ // it is NOT an expression-start. Otherwise leave it for the outer
544
+ // parseConditional (ternary).
545
+ if (this.peek().kind === 'qmark' && !isExprStartKind(this.peek(1).kind)) {
485
546
  this.advance();
486
547
  return { kind: 'propagate', argument: node, op: '?' };
487
548
  }
@@ -526,10 +587,10 @@ class Parser {
526
587
  const args = [];
527
588
  if (this.peek().kind === 'rparen')
528
589
  return args;
529
- args.push(this.parseNullish());
590
+ args.push(this.parseConditional());
530
591
  while (this.peek().kind === 'comma') {
531
592
  this.advance();
532
- args.push(this.parseNullish());
593
+ args.push(this.parseConditional());
533
594
  }
534
595
  return args;
535
596
  }
@@ -566,7 +627,7 @@ class Parser {
566
627
  return { kind: 'undefLit' };
567
628
  case 'lparen': {
568
629
  this.advance();
569
- const inner = this.parseNullish();
630
+ const inner = this.parseConditional();
570
631
  this.expect('rparen');
571
632
  return inner;
572
633
  }
@@ -595,7 +656,7 @@ class Parser {
595
656
  const keyTok = this.peek();
596
657
  if (keyTok.kind === 'spread') {
597
658
  this.advance();
598
- const argument = this.parseNullish();
659
+ const argument = this.parseConditional();
599
660
  entries.push({ kind: 'spread', argument });
600
661
  }
601
662
  else {
@@ -623,7 +684,7 @@ class Parser {
623
684
  }
624
685
  else {
625
686
  this.expect('colon');
626
- const value = this.parseNullish();
687
+ const value = this.parseConditional();
627
688
  entries.push({ key, value });
628
689
  }
629
690
  }
@@ -647,7 +708,7 @@ class Parser {
647
708
  return { kind: 'arrayLit', items };
648
709
  }
649
710
  while (true) {
650
- items.push(this.parseNullish());
711
+ items.push(this.parseConditional());
651
712
  if (this.peek().kind === 'comma') {
652
713
  this.advance();
653
714
  if (this.peek().kind === 'rbracket')