From 236736a3be7d15f1e0c7b65481c7ef3c17229583 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Tue, 16 Jun 2026 10:06:40 +0100 Subject: [PATCH] fix(parser): accept parenthesised lvalue as assignment target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A statement beginning with '(' was rejected ("Expected statement"), so a parenthesised cast could not be an assignment TARGET: (a as TB).FX := 42; -> Parse error Reading the same expression worked, and the hard-cast target TB(a).FX := 42 worked, so only this statement-parser entry point was missing. ParseStmt now handles a leading '(' by parsing the parenthesised expression and requiring a '.Field := Expr' suffix, building a TFieldAssignment whose receiver is that expression (ObjExpr) — the same AST + semantic + codegen path already used for element-field writes (a[i].F := v). No AST/semantic/codegen change was needed; the gap was purely the parser. Found by the e2e test-hardening sweep of the inheritance cluster. Regression: TE2EMiscTests.TestRun_ParenCastAsAssignmentTarget, run on BOTH backends. docs/grammar.ebnf FieldAssignment rule updated. All three fixpoints + full suite (3223 tests) pass. --- compiler/src/main/pascal/uParser.pas | 39 +++++++++++++++++++ compiler/src/test/pascal/cp.test.e2e.misc.pas | 27 +++++++++++++ docs/grammar.ebnf | 1 + 3 files changed, 67 insertions(+) diff --git a/compiler/src/main/pascal/uParser.pas b/compiler/src/main/pascal/uParser.pas index c06ae48..4cf796b 100644 --- a/compiler/src/main/pascal/uParser.pas +++ b/compiler/src/main/pascal/uParser.pas @@ -2173,6 +2173,7 @@ var DerefNode: TDerefExpr; SecondIdent: string; FldNode: TFieldAccessExpr; + ParenExpr: TASTExpr; InnerFld: TFieldAccessExpr; CastRcv: TASTExpr; FCallNode: TFuncCallExpr; @@ -2267,6 +2268,44 @@ begin Exit(ParseCompoundStmt()); end; + { A statement that begins with '(' is an assignment whose lvalue is a + parenthesised expression, e.g. a typecast target: (a as TB).FX := V. + Parse the parenthesised expression, then require a '.Field := Expr' + suffix and build a TFieldAssignment whose receiver is that expression + (the same ObjExpr path used for element-field writes). } + if Check(tkLParen) then + begin + Line := FCurrent.Line; + Col := FCurrent.Col; + Advance(); { consume '(' } + ParenExpr := Self.ParseExpr(); + try + Expect(tkRParen); + if not Check(tkDot) then + raise EParseError.Create(Format( + 'Expected ''.'' after parenthesised assignment target at line %d col %d in %s', + [FCurrent.Line, FCurrent.Col, FLexer.Filename])); + Advance(); { consume '.' } + if not Check(tkIdent) then + raise EParseError.Create(Format( + 'Expected field name at line %d col %d in %s', + [FCurrent.Line, FCurrent.Col, FLexer.Filename])); + SecondIdent := FCurrent.Value; + Advance(); + Expect(tkAssign); + except + ParenExpr.Free(); + raise; + end; + FldAssign := TFieldAssignment.Create(); + FldAssign.Line := Line; + FldAssign.Col := Col; + FldAssign.FieldName := SecondIdent; + FldAssign.ObjExpr := ParenExpr; + FldAssign.Expr := Self.ParseExpr(); + Exit(FldAssign); + end; + if not Check(tkIdent) then raise EParseError.Create(Format( 'Expected statement at line %d col %d in %s', diff --git a/compiler/src/test/pascal/cp.test.e2e.misc.pas b/compiler/src/test/pascal/cp.test.e2e.misc.pas index 25dabb1..efc880e 100644 --- a/compiler/src/test/pascal/cp.test.e2e.misc.pas +++ b/compiler/src/test/pascal/cp.test.e2e.misc.pas @@ -104,6 +104,10 @@ type function and using its result (Result := inherited F() + ...). Was a parser gap (inherited only worked as a statement). } procedure TestRun_InheritedFunctionCall_InExpression; + + { (expr as T).Field := value — a parenthesised cast as an assignment + TARGET. Was a parser gap (statements could not start with '('). } + procedure TestRun_ParenCastAsAssignmentTarget; end; implementation @@ -1119,6 +1123,29 @@ begin AssertRunsOnAll(SrcInheritedExprCall, '105' + LE + '31' + LE, 0); end; +const + { Assign through a parenthesised cast: (a as TB).FX := 42. The statement + parser must accept a leading '(' as an assignment lvalue. } + SrcParenCastTarget = ''' + program Prg; + type + TBase = class end; + TDerived = class(TBase) FX: Integer; end; + var a: TBase; + begin + a := TDerived.Create(); + (a as TDerived).FX := 42; + WriteLn((a as TDerived).FX); + a.Free() + end. + '''; + +procedure TE2EMiscTests.TestRun_ParenCastAsAssignmentTarget; +begin + if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end; + AssertRunsOnAll(SrcParenCastTarget, '42' + LE, 0); +end; + initialization RegisterTest(TE2EMiscTests); diff --git a/docs/grammar.ebnf b/docs/grammar.ebnf index a565be9..9f9fd84 100644 --- a/docs/grammar.ebnf +++ b/docs/grammar.ebnf @@ -849,6 +849,7 @@ PointerWriteStmt FieldAssignment = IDENT DOT IDENT [ LBRACKET Expr RBRACKET ] ASSIGN Expr + | LPAREN Expr RPAREN DOT IDENT ASSIGN Expr (* parenthesised lvalue, e.g. (a as TB).FX := v *) ; MethodCall