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
202 changes: 135 additions & 67 deletions source/units/Goccia.Parser.pas
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ TGocciaParser = class
procedure SkipExpression;
procedure SkipExpressionWithLexicalGoal(
const ALexicalGoal: TGocciaLexicalGoal);
function TokenRequiresFollowingOperand(
const ATokenType: TGocciaTokenType): Boolean;
// TC39 Pattern Matching
function ParseMatchExpression: TGocciaMatchExpression;
function ParseMatchPattern: TGocciaMatchPattern;
Expand Down Expand Up @@ -2866,6 +2868,20 @@ function TGocciaParser.IsMatchExpressionAhead: Boolean;
EnsureToken(ScanIndex, ALexicalGoal);
while ScanIndex < FTokens.Count do
begin
// Skip a template literal whole so its '${ ... }' substitution
// closer (a gttRightBrace under this goal) and any parentheses in
// the substitution are not miscounted as discriminant structure.
// SkipTemplateLiteral works off the live cursor, so drive it from
// ScanIndex and resume the index scan past the template; the outer
// finally restores FCurrent (see SkipTemplateLiteral).
if FTokens[ScanIndex].TokenType in [gttTemplateHead, gttTemplate] then
begin
FCurrent := ScanIndex;
SkipTemplateLiteral;
ScanIndex := FCurrent;
EnsureToken(ScanIndex, ALexicalGoal);
Continue;
end;
case FTokens[ScanIndex].TokenType of
gttLeftParen:
Inc(Depth);
Expand Down Expand Up @@ -4732,15 +4748,34 @@ function TGocciaParser.IfStatement: TGocciaStatement;
// Skips a complete template literal whose leading token (gttTemplate or
// gttTemplateHead) is at the cursor, leaving the cursor just past the closing
// backtick. The error-recovery skips below (SkipBlock, SkipBalancedParens,
// SkipUntilSemicolon) must route templates through here: the '}' that closes a
// '${ ... }' substitution is lexed as a plain gttRightBrace under the default
// goal, so naive brace/paren counting would miscount it as a structural brace,
// stop early, and then re-scan the trailing span as a fresh, unterminated
// template literal. After consuming that '}', the continuation span must be
// re-lexed with the template-tail goal, exactly as ParseTemplateLiteral does.
// SkipUntilSemicolon) and the speculative parenthesized-group probes
// (IsArrowFunction's SkipExpressionWithLexicalGoal / SkipDestructuringPattern,
// IsMatchExpressionAhead, LooksLikeTraditionalForHeader) must route templates
// through here: the '}' that closes a '${ ... }' substitution is lexed as a
// plain gttRightBrace under any non-template goal, so naive brace/paren counting
// would miscount it as a structural brace, stop early, and then re-scan the
// trailing span as a fresh, unterminated template literal. After consuming that
// '}', the continuation span must be re-lexed with the template-tail goal,
// exactly as ParseTemplateLiteral does.
//
// The substitution body is scanned operand-aware (RegExp goal where an operand
// is expected, Div otherwise) so a '/' inside the substitution is classified as
// a regex literal vs a division operator just as the real parse would — a
// mis-lexed '/' would otherwise run a regex literal past the substitution's '}'.
procedure TGocciaParser.SkipTemplateLiteral;
var
Depth: Integer;
NeedsOperand: Boolean;
CurrentType: TGocciaTokenType;

function SubstitutionGoal: TGocciaLexicalGoal;
begin
if NeedsOperand then
Result := glgInputElementRegExp
else
Result := glgInputElementDiv;
end;

begin
// A template with no substitutions is a single token.
if Check(gttTemplate) then
Expand All @@ -4754,27 +4789,40 @@ procedure TGocciaParser.SkipTemplateLiteral;
// Skip the substitution expression up to the '}' that closes it, treating
// nested templates as units and balancing any inner object/block braces.
Depth := 0;
while not IsAtEnd do
NeedsOperand := True;
while True do
begin
if Check(gttTemplateHead) or Check(gttTemplate) then
SkipTemplateLiteral
else if Check(gttLeftBrace) then
begin
Inc(Depth);
Advance;
end
else if Check(gttRightBrace) then
begin
if Depth = 0 then
Break;
Dec(Depth);
Advance;
end
// Peek under the operand-aware goal *before* testing for EOF: a plain
// IsAtEnd/Peek would lex the next token under the default goal and a '/'
// would become a runaway regex literal that swallows the closing '}'.
CurrentType := PeekWithLexicalGoal(SubstitutionGoal).TokenType;
if CurrentType = gttEOF then
Exit; // unterminated substitution; leave the cursor at EOF
case CurrentType of
gttTemplateHead, gttTemplate:
begin
SkipTemplateLiteral;
NeedsOperand := False;
end;
gttLeftBrace:
begin
Inc(Depth);
Advance;
NeedsOperand := True;
end;
gttRightBrace:
begin
if Depth = 0 then
Break;
Dec(Depth);
Advance;
NeedsOperand := False;
end;
else
Advance;
NeedsOperand := TokenRequiresFollowingOperand(CurrentType);
end;
end;
if IsAtEnd then
Exit;
Advance; // gttRightBrace closing the substitution
// Re-lex the following span with the template-tail goal so it becomes a
// TemplateMiddle/TemplateTail token instead of a stray template start.
Expand Down Expand Up @@ -5457,6 +5505,21 @@ function TGocciaParser.LooksLikeTraditionalForHeader: Boolean;
EnsureToken(Idx, ALexicalGoal);
while (Idx < FTokens.Count) and (Depth > 0) do
begin
// Skip a template literal whole so its '${ ... }' substitution
// closer (a gttRightBrace under this goal) is not miscounted as a
// structural brace, which would drop Depth to 0 and misclassify a
// traditional `for(init; ...)` whose init holds a template as a
// for-of/in head. SkipTemplateLiteral works off the live cursor, so
// drive it from Idx and resume past the template; the outer finally
// restores FCurrent (see SkipTemplateLiteral).
if FTokens[Idx].TokenType in [gttTemplateHead, gttTemplate] then
begin
FCurrent := Idx;
SkipTemplateLiteral;
Idx := FCurrent;
EnsureToken(Idx, ALexicalGoal);
Continue;
end;
Tok := FTokens[Idx];
case Tok.TokenType of
gttLeftParen, gttLeftBracket, gttLeftBrace:
Expand Down Expand Up @@ -8448,6 +8511,34 @@ function TGocciaParser.ContinueStatement: TGocciaStatement;
Result := TGocciaContinueStatement.Create(Line, Column, TargetLabel);
end;

// Whether a '/' immediately following a token of the given type begins a regex
// literal (an operand is expected) rather than a division operator. Shared by
// the speculative skips (SkipExpressionWithLexicalGoal, SkipDestructuringPattern)
// and the template-substitution scan in SkipTemplateLiteral so they classify a
// following '/' under the correct lexical goal.
function TGocciaParser.TokenRequiresFollowingOperand(
const ATokenType: TGocciaTokenType): Boolean;
begin
case ATokenType of
gttAssign, gttPlusAssign, gttMinusAssign, gttStarAssign,
gttSlashAssign, gttPercentAssign, gttPowerAssign,
gttNullishCoalescingAssign, gttLogicalAndAssign,
gttLogicalOrAssign,
gttQuestion, gttColon, gttComma,
gttOr, gttAnd, gttNullishCoalescing,
gttBitwiseOr, gttBitwiseXor, gttBitwiseAnd,
gttEqual, gttNotEqual, gttLooseEqual, gttLooseNotEqual,
gttGreater, gttGreaterEqual, gttLess, gttLessEqual,
gttInstanceof, gttIn,
gttLeftShift, gttRightShift, gttUnsignedRightShift,
gttPlus, gttMinus, gttStar, gttSlash, gttPercent, gttPower,
gttNot, gttBitwiseNot, gttTypeof, gttVoid, gttDelete,
gttNew, gttArrow:
Exit(True);
end;
Result := False;
end;

procedure TGocciaParser.SkipDestructuringPattern;
var
BracketCount, BraceCount: Integer;
Expand All @@ -8461,28 +8552,6 @@ procedure TGocciaParser.SkipDestructuringPattern;
Result := glgInputElementDiv;
end;

function TokenRequiresFollowingOperand(
const ATokenType: TGocciaTokenType): Boolean;
begin
case ATokenType of
gttAssign, gttPlusAssign, gttMinusAssign, gttStarAssign,
gttSlashAssign, gttPercentAssign, gttPowerAssign,
gttNullishCoalescingAssign, gttLogicalAndAssign,
gttLogicalOrAssign,
gttQuestion, gttColon, gttComma,
gttOr, gttAnd, gttNullishCoalescing,
gttBitwiseOr, gttBitwiseXor, gttBitwiseAnd,
gttEqual, gttNotEqual, gttLooseEqual, gttLooseNotEqual,
gttGreater, gttGreaterEqual, gttLess, gttLessEqual,
gttInstanceof, gttIn,
gttLeftShift, gttRightShift, gttUnsignedRightShift,
gttPlus, gttMinus, gttStar, gttSlash, gttPercent, gttPower,
gttNot, gttBitwiseNot, gttTypeof, gttVoid, gttDelete,
gttNew, gttArrow:
Exit(True);
end;
Result := False;
end;
begin
BracketCount := 0;
BraceCount := 0;
Expand All @@ -8503,6 +8572,16 @@ procedure TGocciaParser.SkipDestructuringPattern;
if CurrentType = gttEOF then
Break;

// Skip a template literal whole: its '${ ... }' substitution closer is a
// gttRightBrace under this goal and would otherwise be miscounted as a
// destructuring brace (see SkipTemplateLiteral).
if CurrentType in [gttTemplateHead, gttTemplate] then
begin
SkipTemplateLiteral;
NeedsOperand := False;
Continue;
end;

case CurrentType of
gttLeftBracket:
begin
Expand Down Expand Up @@ -8554,28 +8633,6 @@ procedure TGocciaParser.SkipExpressionWithLexicalGoal(
Result := ALexicalGoal;
end;

function TokenRequiresFollowingOperand(
const ATokenType: TGocciaTokenType): Boolean;
begin
case ATokenType of
gttAssign, gttPlusAssign, gttMinusAssign, gttStarAssign,
gttSlashAssign, gttPercentAssign, gttPowerAssign,
gttNullishCoalescingAssign, gttLogicalAndAssign,
gttLogicalOrAssign,
gttQuestion, gttColon, gttComma,
gttOr, gttAnd, gttNullishCoalescing,
gttBitwiseOr, gttBitwiseXor, gttBitwiseAnd,
gttEqual, gttNotEqual, gttLooseEqual, gttLooseNotEqual,
gttGreater, gttGreaterEqual, gttLess, gttLessEqual,
gttInstanceof, gttIn,
gttLeftShift, gttRightShift, gttUnsignedRightShift,
gttPlus, gttMinus, gttStar, gttSlash, gttPercent, gttPower,
gttNot, gttBitwiseNot, gttTypeof, gttVoid, gttDelete,
gttNew, gttArrow:
Exit(True);
end;
Result := False;
end;
begin
ParenCount := 0;
BracketCount := 0;
Expand All @@ -8594,6 +8651,17 @@ procedure TGocciaParser.SkipExpressionWithLexicalGoal(
(CurrentType in [gttComma, gttRightParen]) then
Break;

// Skip a template literal whole: its '${ ... }' substitution closer is a
// gttRightBrace under this goal and would otherwise be miscounted as a
// structural brace, stopping the skip mid-default-value (see
// SkipTemplateLiteral).
if CurrentType in [gttTemplateHead, gttTemplate] then
begin
SkipTemplateLiteral;
NeedsOperand := False;
Continue;
end;

case CurrentType of
gttLeftParen:
begin
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*---
description: Template literals with substitutions survive speculative parenthesized-group probes
features: [template-literals, template-interpolation, pattern-matching]
---*/

describe("template interpolation inside parenthesized expressions", () => {
test("parenthesized template with division in substitution", () => {
const x = (`d=${6 / 2}`);
expect(x).toBe("d=3");
});

test("parenthesized template with regex literal in substitution", () => {
const x = (`m=${/a.c/.test("abc")}`);
expect(x).toBe("m=true");
});

test("parenthesized template is not mistaken for an arrow head", () => {
const x = (`v${1 + 1}`) + "!";
expect(x).toBe("v2!");
});
});

describe("template interpolation as a match discriminant", () => {
test("match discriminant with division in substitution", () => {
const r = match (`n=${4 / 2}`) {
"n=2": "two";
default: "other";
};
expect(r).toBe("two");
});

test("match discriminant with a call and brackets in substitution", () => {
const r = match (`${[10, 20][1] / 2}`) {
"10": "ten";
default: "no";
};
expect(r).toBe("ten");
});

test("match discriminant with regex literal in substitution", () => {
const r = match (`${/x/.test("x")}`) {
"true": "hit";
default: "miss";
};
expect(r).toBe("hit");
});

test("match discriminant with a no-substitution template", () => {
const r = match (`plain`) {
"plain": "ok";
default: "no";
};
expect(r).toBe("ok");
});
});
30 changes: 30 additions & 0 deletions tests/language/for-loop/traditional-for-template-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*---
description: Traditional for-loop headers whose parts contain template literals with substitutions
features: [compat-traditional-for-loop, template-literals, template-interpolation]
---*/

describe("traditional for-loop header with template literals", () => {
test("template literal in the init is recognized as a traditional for-loop", () => {
let out = "";
for (let s = `v${1}w`; s.length > 0; s = s.slice(1)) {
out = out + s[0];
}
expect(out).toBe("v1w");
});

test("template literal with division in the init", () => {
const seen = [];
for (let s = `c=${6 / 2}`; seen.length < 1; seen.push(s)) {
// body runs once, then update pushes the substituted value
}
expect(seen).toEqual(["c=3"]);
});

test("for-of over template literals is not misread as a traditional header", () => {
const out = [];
for (const ch of [`a${1}`, `b${2}`]) {
out.push(ch);
}
expect(out).toEqual(["a1", "b2"]);
});
});
Loading
Loading