Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions Parsing/AST.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,11 @@ public record ArrowFunction(Token? Name, List<TypeParam>? TypeParams, string? Th
// through the function display class so the hoisted arrow reads them live, not a stale snapshot.
public bool IsLiftedForwarder { get; init; }
}
// Template literal
public record TemplateLiteral(List<string> Strings, List<Expr> Expressions) : Expr;
// Template literal. InvalidEscapeLines carries the source line of each part whose cooked value
// had an invalid escape sequence (`\xtraordinary`, `\u{hello}`, ...) — a real syntax error for an
// untagged template (TS1125), but recoverable: the parser substitutes an empty string for that
// part rather than aborting the whole file, and the checker reports it as a normal diagnostic.
public record TemplateLiteral(List<string> Strings, List<Expr> Expressions, List<int>? InvalidEscapeLines = null) : Expr;
// Tagged template literal: tag`template ${expr}`
public record TaggedTemplateLiteral(
Expr Tag, // The tag function expression
Expand Down Expand Up @@ -534,14 +537,16 @@ public record ImportSpecifier(Token Imported, Token? LocalName, bool IsTypeOnly
/// <param name="FromModulePath">Re-export source: export { x } from './file'</param>
/// <param name="IsDefaultExport">True for 'export default'</param>
/// <param name="ExportAssignment">CommonJS export assignment: export = expr</param>
/// <param name="NamespaceExportName">Namespace re-export alias: export * as ns from './file'</param>
public record Export(
Token Keyword,
Stmt? Declaration,
List<ExportSpecifier>? NamedExports,
Expr? DefaultExpr,
string? FromModulePath,
bool IsDefaultExport,
Expr? ExportAssignment = null
Expr? ExportAssignment = null,
Token? NamespaceExportName = null
) : Stmt;

/// <summary>
Expand Down
18 changes: 16 additions & 2 deletions Parsing/Parser.Declarations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ public partial class Parser
private Stmt Declaration()
{
// Module declarations - must be at top level
// Note: import followed by ( is dynamic import (expression), not static import (statement)
if (Check(TokenType.IMPORT) && PeekNext().Type != TokenType.LEFT_PAREN)
// Note: import followed by ( is dynamic import, and import followed by . is import.meta —
// both expressions, not a static import declaration.
if (Check(TokenType.IMPORT) && PeekNext().Type != TokenType.LEFT_PAREN && PeekNext().Type != TokenType.DOT)
{
Advance(); // consume IMPORT
// Detect import alias: import X = Namespace.Member
Expand Down Expand Up @@ -892,6 +893,19 @@ private Stmt ParseDeclareModuleMember()
{
Token exportKeyword = Previous();

// export { x, y as z } [from './module'] — e.g. `declare global { export { globalThis as global } }`
if (Match(TokenType.LEFT_BRACE))
{
var namedExports = ParseExportSpecifiers();
string? fromPath = null;
if (Match(TokenType.FROM))
{
fromPath = (string)Consume(TokenType.STRING, "Expect module path.").Literal!;
}
ConsumeSemicolon("Expect ';' after export.");
return new Stmt.Export(exportKeyword, null, namedExports, null, fromPath, IsDefaultExport: false);
}

// export interface Foo { }
if (Match(TokenType.INTERFACE))
{
Expand Down
57 changes: 37 additions & 20 deletions Parsing/Parser.Expressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ private Expr DispatchAssignmentTarget(
{
switch (target)
{
// Parens don't change whether an expression is a valid reference — `(a.b) = v`,
// `(a.b) &&= v`, etc. are all valid; unwrap (recursively, for `((a.b))`) before dispatch.
case Expr.Grouping grouping:
return DispatchAssignmentTarget(grouping.Expression, value, errorMessage, onVariable, onGet, onGetIndex, onGetPrivate);
case Expr.Variable variable:
if (_isStrictMode && (variable.Name.Lexeme == "eval" || variable.Name.Lexeme == "arguments"))
throw new Exception("SyntaxError: Unexpected eval or arguments in strict mode");
Expand Down Expand Up @@ -1015,8 +1019,14 @@ private Expr Primary()

// async function expression: async function [name]() {} or async function*() {}
// async arrow function: async () => {} or async (x) => x
if (Match(TokenType.ASYNC))
// `async` is also a valid plain identifier (e.g. a destructured binding named `async`,
// `if (async)`) when it isn't immediately followed by one of the tokens above — only
// commit to the async-function forms when one of those follows.
if (Check(TokenType.ASYNC) &&
(PeekNext().Type == TokenType.FUNCTION || PeekNext().Type == TokenType.LESS || PeekNext().Type == TokenType.LEFT_PAREN))
{
Advance(); // consume 'async'

// `async function ...` is an async function expression — defer to the shared
// FunctionExpression parser (which also handles the `*` for async generators),
// mirroring the statement-level `async function` declaration path.
Expand All @@ -1036,6 +1046,12 @@ private Expr Primary()
if (arrowFunc != null) return arrowFunc;
throw new Exception("Parse Error: Expected arrow function after 'async ('.");
}
if (Check(TokenType.ASYNC))
{
// Not followed by a function-introducing token — a bare reference to an identifier
// named `async`.
return new Expr.Variable(Advance());
}

if (Match(TokenType.LEFT_PAREN))
{
Expand All @@ -1053,10 +1069,12 @@ private Expr Primary()
if (Match(TokenType.TEMPLATE_FULL))
{
var value = (TemplateStringValue)Previous().Literal!;
// For untagged templates, cooked must not be null (invalid escapes are errors)
// For untagged templates, an invalid escape (`\xtraordinary`, `\u{hello}`, ...) is a real
// syntax error (TS1125) — but recoverable, not a reason to abort the whole file. Substitute
// an empty string and let the checker report it against this line.
if (value.Cooked == null)
{
throw new Exception("Parse Error: Invalid escape sequence in template literal.");
return new Expr.TemplateLiteral([""], [], [Previous().Line]);
}
return new Expr.TemplateLiteral([value.Cooked], []);
}
Expand Down Expand Up @@ -1203,12 +1221,21 @@ private Expr.ArrowFunction ParseObjectMethodShorthand(bool isAsync = false, bool
private Expr ParseTemplateLiteral()
{
var headValue = (TemplateStringValue)Previous().Literal!;
// For untagged templates, cooked must not be null
if (headValue.Cooked == null)
List<string> strings = [];
List<int>? invalidEscapeLines = null;
void AddPart(TemplateStringValue value, int line)
{
throw new Exception("Parse Error: Invalid escape sequence in template literal.");
if (value.Cooked == null)
{
strings.Add("");
(invalidEscapeLines ??= []).Add(line);
}
else
{
strings.Add(value.Cooked);
}
}
List<string> strings = [headValue.Cooked];
AddPart(headValue, Previous().Line);
List<Expr> expressions = [];

// Parse first expression
Expand All @@ -1217,25 +1244,15 @@ private Expr ParseTemplateLiteral()
// Parse middle parts
while (Match(TokenType.TEMPLATE_MIDDLE))
{
var midValue = (TemplateStringValue)Previous().Literal!;
if (midValue.Cooked == null)
{
throw new Exception("Parse Error: Invalid escape sequence in template literal.");
}
strings.Add(midValue.Cooked);
AddPart((TemplateStringValue)Previous().Literal!, Previous().Line);
expressions.Add(Expression());
}

// Expect tail
Consume(TokenType.TEMPLATE_TAIL, "Expect end of template literal.");
var tailValue = (TemplateStringValue)Previous().Literal!;
if (tailValue.Cooked == null)
{
throw new Exception("Parse Error: Invalid escape sequence in template literal.");
}
strings.Add(tailValue.Cooked);
AddPart((TemplateStringValue)Previous().Literal!, Previous().Line);

return new Expr.TemplateLiteral(strings, expressions);
return new Expr.TemplateLiteral(strings, expressions, invalidEscapeLines);
}

private Expr ParseTaggedTemplateLiteral(Expr tag)
Expand Down
11 changes: 9 additions & 2 deletions Parsing/Parser.Modules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,16 +293,23 @@ void RejectDecorators()
return new Stmt.Export(keyword, null, namedExports, null, fromPath, IsDefaultExport: false);
}

// export * from './module' (re-export all)
// export * from './module' (re-export all) or
// export * as ns from './module' (re-export the whole module as a named namespace)
if (Match(TokenType.STAR))
{
RejectDecorators();
Token? namespaceExportName = null;
if (Match(TokenType.AS))
{
namespaceExportName = ConsumeIdentifierName("Expect namespace name after 'as'.");
}
Consume(TokenType.FROM, "Expect 'from' after '*'.");
string fromPath = (string)Consume(TokenType.STRING, "Expect module path.").Literal!;
ConsumeSemicolon("Expect ';' after export.");

// Represent as export with null named exports and a fromPath (meaning all)
return new Stmt.Export(keyword, null, null, null, fromPath, IsDefaultExport: false);
return new Stmt.Export(keyword, null, null, null, fromPath, IsDefaultExport: false,
NamespaceExportName: namespaceExportName);
}

// export import X = Namespace.Member (re-export alias)
Expand Down
12 changes: 4 additions & 8 deletions Parsing/Parser.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1065,14 +1065,10 @@ private string ParseInlineObjectType()
members.Add(member);
}

// Handle separator - can be semicolon or comma, or nothing before closing brace
if (!Check(TokenType.RIGHT_BRACE))
{
if (!Match(TokenType.SEMICOLON) && !Match(TokenType.COMMA))
{
throw new Exception("Expect ';' or ',' between object type members.");
}
}
// Separator: ';', ',', or ASI (ok on a newline, before '}', or at EOF) — same rule as
// interface members (`bar(): { baz: T }` followed by `baz: T` on the next line needs no
// explicit separator between them).
ConsumeInterfaceMemberSeparator();
}

Consume(TokenType.RIGHT_BRACE, "Expect '}' after object type.");
Expand Down
10 changes: 5 additions & 5 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,16 +507,16 @@ The committed subset spans `types/typeRelationships/{assignmentCompatibility,sub

| Bucket | Count | Share |
|---|---:|---:|
| `Pass` | 203 | 68.6% |
| `Fail` | 68 | 23.0% |
| `ParseError` | 7 | 2.4% |
| `Pass` | 207 | 69.9% |
| `Fail` | 71 | 24.0% |
| `ParseError` | 0 | 0.0% |
| `Skipped` (multi-file 8 / lib-drift 6 / directive 3) | 17 | 5.7% |
| `TypeCheckError` | 1 | 0.3% |
| **Total** | **296** | |

The parser is no longer a significant bottleneck (an earlier sweep took the original subset's `ParseError` count from 57 → 24; a follow-up Track-1 round on the `symbolProperty*`/`symbolType*` cluster — computed well-known-symbol names in *value/class* position, which the [#99](https://github.com/nickna/SharpTS/issues/99) Phase-A parser work had only hardened for *type* position — took it 24 → 7): ambient `declare` of non-class declarations, `declare function`, generic/this/conditional/mapped/indexed-access/constructor/leading-operator types, `module Foo {}` namespaces, call/construct/index signatures, keyword & string/numeric property names, function-type and arrow optional/rest parameters, computed-key class fields/interface members/object-type members without an explicit type annotation (implicit `any`), and more. Negative-test matching itself was unlocked by propagating canonical `TSnnnn` codes through the type-checker's error-recovery path ([#125](https://github.com/nickna/SharpTS/issues/125)), which also added a `strictNullChecks` option (default on; the runner follows each test's `@strict`/`@strictNullChecks` directive). The residual 7 `ParseError`s are a scattered long tail (tagged-template escape sequences, `declare global` export forms, `import.meta`, `export * as ns`, logical-assignment edge cases, async-arrow-in-shared-memory) with no single shared root cause.
The parser is no longer a bottleneck at all for this subset: an earlier sweep took the original subset's `ParseError` count from 57 → 24; a Track-1 round on the `symbolProperty*`/`symbolType*` cluster — computed well-known-symbol names in *value/class* position, which the [#99](https://github.com/nickna/SharpTS/issues/99) Phase-A parser work had only hardened for *type* position — took it 24 → 7; a follow-up round cleared the remaining 7-test long tail (7 unrelated, independently-scoped gaps: object-type-member ASI, parenthesized logical-assignment targets, `async` used as a plain identifier, `import.meta` as a statement, `export {}` inside `declare global`, `export * as ns from`, and untagged-template invalid-escape sequences recovering as a `TS1125` diagnostic instead of aborting the parse) — **`ParseError` is now 0 for the committed subset.** Other parser work along the way: ambient `declare` of non-class declarations, `declare function`, generic/this/conditional/mapped/indexed-access/constructor/leading-operator types, `module Foo {}` namespaces, call/construct/index signatures, keyword & string/numeric property names, function-type and arrow optional/rest parameters, computed-key class fields/interface members/object-type members without an explicit type annotation (implicit `any`), and more. Negative-test matching itself was unlocked by propagating canonical `TSnnnn` codes through the type-checker's error-recovery path ([#125](https://github.com/nickna/SharpTS/issues/125)), which also added a `strictNullChecks` option (default on; the runner follows each test's `@strict`/`@strictNullChecks` directive).

The dominant bucket is `Fail` — tests that parse and reach the type checker but whose diagnostic set doesn't yet match `tsc`'s. A [#99](https://github.com/nickna/SharpTS/issues/99) measurement spike over the lib-sensitive folders decomposed these: of the original 66 lib-folder Fails, ~17 were **spurious** (SharpTS emitted an error `tsc` didn't; cleared by Track-1 — object literals/interfaces now model each computed well-known-symbol member as its own named member instead of collapsing them into one merged symbol index signature, closing the false positives on `Symbol.iterator`/`Symbol.toStringTag`/`Symbol.toPrimitive` reads+writes, string-index-signature compatibility, and `declare const x: unique symbol`), a long tail are individual checker gaps (assignability, symbol arithmetic, computed-name constraints), and only ~6 are explicit lib-version drift (`TS2550`/`TS2583`/`TS2585`). **The finding: loading `tsc`'s `lib.*.d.ts` is _not_ the current pass-rate lever — checker breadth and parser gaps dominate the divergence.** Loading `lib.*.d.ts` ([#99](https://github.com/nickna/SharpTS/issues/99)) remains valuable as an *enabler* (it removes the "`Pass` = coincidence" asterisk since globals resolve to `any` today, unblocks DOM/JSX wholesale, and is a prerequisite for per-node types/symbols in [#88](https://github.com/nickna/SharpTS/issues/88)), but the near-term levers are checker correctness (structural class-to-class assignability [#129](https://github.com/nickna/SharpTS/issues/129); cross-statement CFA narrowing for compound logical assignment) and clearing the remaining scattered `ParseError`/`Fail` long tail.
`Fail` is the largest non-`Pass` bucket — tests that parse and reach the type checker but whose diagnostic set doesn't yet match `tsc`'s. A [#99](https://github.com/nickna/SharpTS/issues/99) measurement spike over the lib-sensitive folders decomposed these: of the original 66 lib-folder Fails, ~17 were **spurious** (SharpTS emitted an error `tsc` didn't; cleared by Track-1 — object literals/interfaces now model each computed well-known-symbol member as its own named member instead of collapsing them into one merged symbol index signature, closing the false positives on `Symbol.iterator`/`Symbol.toStringTag`/`Symbol.toPrimitive` reads+writes, string-index-signature compatibility, and `declare const x: unique symbol`), a long tail are individual checker gaps (assignability, symbol arithmetic, computed-name constraints), and only ~6 are explicit lib-version drift (`TS2550`/`TS2583`/`TS2585`). **The finding: loading `tsc`'s `lib.*.d.ts` is _not_ the current pass-rate lever — checker breadth and parser gaps dominate the divergence.** Loading `lib.*.d.ts` ([#99](https://github.com/nickna/SharpTS/issues/99)) remains valuable as an *enabler* (it removes the "`Pass` = coincidence" asterisk since globals resolve to `any` today, unblocks DOM/JSX wholesale, and is a prerequisite for per-node types/symbols in [#88](https://github.com/nickna/SharpTS/issues/88)), but the near-term levers are checker correctness (structural class-to-class assignability [#129](https://github.com/nickna/SharpTS/issues/129); cross-statement CFA narrowing for compound logical assignment) and clearing the remaining `Fail`-bucket long tail.

`Skipped` includes:
- **Multi-file tests** (`Skipped:multi-file-deferred`) — cross-file resolution into the runner is follow-up work.
Expand Down
Loading
Loading