From 39a53bdbe90ce7be6e2ec0dcb779d23a0e3de856 Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Wed, 19 Aug 2026 12:49:27 +0200 Subject: [PATCH 1/7] Give ntConstant an end position A constant declaration produced a node with no end position at all, so a consumer working in line ranges could not tell how far the declaration reached. That silently truncates any constant whose value spans lines: const Banner = 'first part ' + 'second part'; reported only the first line, and a tool slicing that range dropped the continuation. ntConstant was built with FStack.Push, so it was a plain TSyntaxNode with nowhere to record an end. Two changes are needed, because the node the caller finally sees is not the node that was parsed: ConstantDeclaration now pushes a compound node and records its end, the same way TypeDeclaration already does. This gives the intermediate ConstList an accurate extent. ConstSection rebuilds each constant from that ConstList, so it also has to push a compound node and inherit the end from it. The start still comes from the name, which is what a caller looking for the constant expects; only the end is new. TCompoundSyntaxNode.AssignEndPositionFrom is the counterpart to the existing AssignPositionFrom, for exactly this rebuild case. Single-line constants keep ending on their own line - the new test asserts both directions, since an end that ran on to the next declaration would be no more useful than one that stopped short. Verified against the existing suite: 42 passing, with the one pre-existing Serialization.BinaryRoundTrip failure unchanged (line_seq holds a pointer value that does not survive a round trip, unrelated to this change). Co-Authored-By: Claude Opus 5 (1M context) --- Source/DelphiAST.Classes.pas | 8 +++++++ Source/DelphiAST.pas | 9 ++++++-- Test/UnitTests/DelphiAST.Tests.pas | 34 ++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/Source/DelphiAST.Classes.pas b/Source/DelphiAST.Classes.pas index 10b785b..8752255 100644 --- a/Source/DelphiAST.Classes.pas +++ b/Source/DelphiAST.Classes.pas @@ -104,6 +104,8 @@ TCompoundSyntaxNode = class(TSyntaxNode) FEndLine: Integer; public function Clone: TSyntaxNode; override; + //counterpart to AssignPositionFrom, for a node rebuilt from a parsed one + procedure AssignEndPositionFrom(const Node: TCompoundSyntaxNode); property EndCol: Integer read FEndCol write FEndCol; property EndLine: Integer read FEndLine write FEndLine; @@ -629,6 +631,12 @@ procedure TSyntaxNode.AssignPositionFrom(const Node: TSyntaxNode); { TCompoundSyntaxNode } +procedure TCompoundSyntaxNode.AssignEndPositionFrom(const Node: TCompoundSyntaxNode); +begin + FEndCol := Node.EndCol; + FEndLine := Node.EndLine; +end; + function TCompoundSyntaxNode.Clone: TSyntaxNode; begin Result := inherited; diff --git a/Source/DelphiAST.pas b/Source/DelphiAST.pas index 7a1dcb5..d25d70f 100644 --- a/Source/DelphiAST.pas +++ b/Source/DelphiAST.pas @@ -1195,9 +1195,11 @@ procedure TPasSyntaxTreeBuilder.CompoundStatement; procedure TPasSyntaxTreeBuilder.ConstantDeclaration; begin - FStack.Push(ntConstant); + //compound: a constant's value can span lines, so record where it ends. as TypeDeclaration. + FStack.PushCompoundSyntaxNode(ntConstant); try inherited; + SetCurrentCompoundNodesEndPosition; finally FStack.Pop; end; @@ -1329,9 +1331,12 @@ procedure TPasSyntaxTreeBuilder.ConstSection; if Constant.Typ <> ntName then Continue; - Temp := FStack.Push(ConstList.Typ); + //compound: start from the name, end from the ConstList that measured the value. + Temp := FStack.PushCompoundSyntaxNode(ConstList.Typ); try Temp.AssignPositionFrom(Constant); + if ConstList is TCompoundSyntaxNode then + TCompoundSyntaxNode(Temp).AssignEndPositionFrom(TCompoundSyntaxNode(ConstList)); FStack.AddChild(Constant.Clone); if Assigned(TypeInfo) then diff --git a/Test/UnitTests/DelphiAST.Tests.pas b/Test/UnitTests/DelphiAST.Tests.pas index 6d15948..96cf3c6 100644 --- a/Test/UnitTests/DelphiAST.Tests.pas +++ b/Test/UnitTests/DelphiAST.Tests.pas @@ -177,6 +177,39 @@ procedure TestSourcePositions; end; end; +procedure TestConstantEndPosition; +var + Root, ConstsNode, Node, Spanning, Single: TSyntaxNode; +begin + Root := ParseSource('unit Consts;' + sLineBreak + 'interface' + sLineBreak + 'const' + + sLineBreak + ' Spanning = ''first part '' +' + sLineBreak + ' ''second part'';' + + sLineBreak + ' Single = 42;' + sLineBreak + 'implementation' + sLineBreak + 'end.'); + try + ConstsNode := FindDescendant(Root, ntConstants); + AssertNotNil(ConstsNode, 'No const section was produced'); + Spanning := nil; + Single := nil; + for Node in ConstsNode.ChildNodes do + if Node.Typ = ntConstant then + if Node.Line = 4 then + Spanning := Node + else if Node.Line = 6 then + Single := Node; + + AssertNotNil(Spanning, 'No constant starting on line 4'); + AssertTrue(Spanning is TCompoundSyntaxNode, + 'Constant node must be compound so it can carry an end position'); + AssertEquals(5, TCompoundSyntaxNode(Spanning).EndLine, + 'A multi-line constant must reach the last line of its value'); + + AssertNotNil(Single, 'No constant starting on line 6'); + AssertEquals(6, TCompoundSyntaxNode(Single).EndLine, + 'A single-line constant must end on its own line, not run on to what follows'); + finally + Root.Free; + end; +end; + procedure TestInvalidSyntax; var Root: TSyntaxNode; @@ -229,6 +262,7 @@ procedure RunAllTests; RunTest('AST.GenericRecordAndProperty', TestGenericRecordAndProperty); RunTest('Writer.LiteralsUnicodeAndXmlEscaping', TestLiteralsAndUnicode); RunTest('AST.SourcePositions', TestSourcePositions); + RunTest('AST.ConstantEndPosition', TestConstantEndPosition); RunTest('Parser.InvalidSyntax', TestInvalidSyntax); {$IFNDEF FPC} RunTest('Serialization.BinaryRoundTrip', TestBinarySerializationRoundTrip); From 5acca9061bfe207e493589df61ec4f785a9bd860 Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Wed, 19 Aug 2026 14:11:44 +0200 Subject: [PATCH 2/7] Give ntVariable an end position Same gap as the previous commit, same shape of fix. A variable declaration produced a node positioned at its name with no end at all, so a declaration whose type or initialiser spans lines reported only its first line: var Grid: array[0..1] of Integer; VarDeclaration pushed ntVariables with FStack.Push, so the VarList that RearrangeVarSection rebuilds each variable from had no extent to pass on. Both now push compound nodes, and the rebuilt ntVariable inherits the end via AssignEndPositionFrom - the second caller for the helper added in the previous commit, which is the pattern it exists for. The compound ntVariables nodes are the throwaway VarSect children that VarSection frees, so the output footprint matches the constant change exactly: VARIABLE gains begin/end, and the VARIABLES section node that reaches the tree is untouched. Names sharing one line (`A, B: Integer;`) each get the declaration's extent, which is the whole declaration they share. Co-Authored-By: Claude Opus 5 (1M context) --- Source/DelphiAST.pas | 9 +++++++-- Test/UnitTests/DelphiAST.Tests.pas | 32 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Source/DelphiAST.pas b/Source/DelphiAST.pas index d25d70f..5ff7b5c 100644 --- a/Source/DelphiAST.pas +++ b/Source/DelphiAST.pas @@ -3210,9 +3210,11 @@ procedure TPasSyntaxTreeBuilder.VarAbsolute; procedure TPasSyntaxTreeBuilder.VarDeclaration; begin - FStack.Push(ntVariables); + //compound: a variable's type or initialiser can span lines, so record where it ends. + FStack.PushCompoundSyntaxNode(ntVariables); try inherited; + SetCurrentCompoundNodesEndPosition; finally FStack.Pop; end; @@ -3303,9 +3305,12 @@ procedure TPasSyntaxTreeBuilder.RearrangeVarSection(const VarSect: TSyntaxNode); begin if Variable.Typ <> ntName then Continue; - Temp := FStack.Push(ntVariable); + //compound: start from the name, end from the VarList that measured the declaration. + Temp := FStack.PushCompoundSyntaxNode(ntVariable); try Temp.AssignPositionFrom(Variable); + if VarList is TCompoundSyntaxNode then + TCompoundSyntaxNode(Temp).AssignEndPositionFrom(TCompoundSyntaxNode(VarList)); FStack.AddChild(Variable.Clone); if Assigned(TypeInfo) then FStack.AddChild(TypeInfo.Clone); diff --git a/Test/UnitTests/DelphiAST.Tests.pas b/Test/UnitTests/DelphiAST.Tests.pas index 96cf3c6..c46a2bb 100644 --- a/Test/UnitTests/DelphiAST.Tests.pas +++ b/Test/UnitTests/DelphiAST.Tests.pas @@ -210,6 +210,37 @@ procedure TestConstantEndPosition; end; end; +procedure TestVariableEndPosition; +var + Root, Node, Spanning, Single: TSyntaxNode; +begin + Root := ParseSource('unit Vars;' + sLineBreak + 'interface' + sLineBreak + 'var' + + sLineBreak + ' Spanning: array[0..1] of' + sLineBreak + ' Integer;' + sLineBreak + + ' Single: Integer;' + sLineBreak + 'implementation' + sLineBreak + 'end.'); + try + Spanning := nil; + Single := nil; + for Node in FindDescendant(Root, ntVariables).ChildNodes do + if Node.Typ = ntVariable then + if Node.Line = 4 then + Spanning := Node + else if Node.Line = 6 then + Single := Node; + + AssertNotNil(Spanning, 'No variable starting on line 4'); + AssertTrue(Spanning is TCompoundSyntaxNode, + 'Variable node must be compound so it can carry an end position'); + AssertEquals(5, TCompoundSyntaxNode(Spanning).EndLine, + 'A variable whose declaration spans lines must reach its last line'); + + AssertNotNil(Single, 'No variable starting on line 6'); + AssertEquals(6, TCompoundSyntaxNode(Single).EndLine, + 'A single-line variable must end on its own line, not run on to what follows'); + finally + Root.Free; + end; +end; + procedure TestInvalidSyntax; var Root: TSyntaxNode; @@ -263,6 +294,7 @@ procedure RunAllTests; RunTest('Writer.LiteralsUnicodeAndXmlEscaping', TestLiteralsAndUnicode); RunTest('AST.SourcePositions', TestSourcePositions); RunTest('AST.ConstantEndPosition', TestConstantEndPosition); + RunTest('AST.VariableEndPosition', TestVariableEndPosition); RunTest('Parser.InvalidSyntax', TestInvalidSyntax); {$IFNDEF FPC} RunTest('Serialization.BinaryRoundTrip', TestBinarySerializationRoundTrip); From 9bb8fbfceb041979362cb3205d1ce5adb61217c4 Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Mon, 31 Aug 2026 17:17:12 +0200 Subject: [PATCH 3/7] Accept a constant expression as an array bound An array bound may be any constant expression, but OrdinalType only ever accepted a constant or a type name: const mlab = 4; mlog = 12; type TRanges = record iu: array[mlab + 1..mlog] of Integer; end; failed with 'SquareClose' expected found '+'. OrdinalType decided on a single token of lookahead: an identifier followed by anything but '(' or '..' was a type name, so it read the bound as the type `mlab`, returned, and left ArrayBounds looking for the ']' it found a '+' at. The upper bound already worked, which is what makes the gap easy to miss. `array[mlab..mlog + 1]` sends the first bound to ConstantExpression on the '..' lookahead, and OrdinalType's own trailing '..' branch parses the rest as an expression. Only the first bound of a subrange went down the type-name path. The lookahead now also routes the operators SimpleExpression and Term accept to ConstantExpression. Every one of those tokens is a parse error in this position today, so no input that parses now takes a different path: an identifier in an OrdinalType is followed by ']', ',', '..', 'of' or ';', never by an operator. Set types and variant record tag types go through the same procedure and gain the same forms. A single token is enough here and a full ahead-parse would be worse. Coming from the identifier, SimpleType's `AheadParse.NextToken; AheadParse.Simple- Expression` idiom would meet the ']' of the common `array[TIndex] of Byte` and hand it to Factor as a set constructor. Parenthesized bounds are deliberately left alone. `array[(mlab + 1)..mlog]` goes to EnumeratedType, and dcc32 reads it the same way - it reports "Identifier redeclared: 'mlab'" - so the parser already agrees with the compiler. Test/Snippets/arrayboundexpression.pas covers the first bound, both bounds, two dimensions, `*`, `-` and `shl`, and a set of a computed subrange; dcc32 compiles it clean. Without the parser change it fails with the original 'SquareClose' expected found '+', so it guards the actual bug. Suite: 43 tests, 42 passing, with the pre-existing Serialization.BinaryRoundTrip failure unchanged (line_seq holds a pointer value that does not survive a round trip). Co-Authored-By: Claude Opus 5 (1M context) --- Source/SimpleParser/SimpleParser.pas | 10 ++++++++++ Test/Snippets/arrayboundexpression.pas | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 Test/Snippets/arrayboundexpression.pas diff --git a/Source/SimpleParser/SimpleParser.pas b/Source/SimpleParser/SimpleParser.pas index 06caf50..f632596 100644 --- a/Source/SimpleParser/SimpleParser.pas +++ b/Source/SimpleParser/SimpleParser.pas @@ -3648,6 +3648,16 @@ procedure TmwSimplePasPar.OrdinalType; begin ConstantExpression; end; + { An operator after the identifier means the bound is a constant + expression, not a type name: array[mlab + 1..mlog]. Reading it as a + type name consumes the identifier alone and leaves the caller at the + operator, where it can only report an error. These are the operators + SimpleExpression and Term accept. } + ptAnd, ptDiv, ptMinus, ptMod, ptOr, ptPlus, ptShl, ptShr, ptSlash, + ptStar, ptXor: + begin + ConstantExpression; + end; else begin TypeID; diff --git a/Test/Snippets/arrayboundexpression.pas b/Test/Snippets/arrayboundexpression.pas new file mode 100644 index 0000000..1d695d1 --- /dev/null +++ b/Test/Snippets/arrayboundexpression.pas @@ -0,0 +1,22 @@ +unit arrayboundexpression; + +interface + +const + mlab = 4; + mlog = 12; + +type + { An array bound is a constant EXPRESSION, not just a constant or a type name. + OrdinalType decided on one token of lookahead and read `mlab` as a type name, + so the whole unit failed to parse at the `+`. } + TRanges = record + iu: array[mlab + 1..mlog] of Integer; + du: array[mlab * 2..mlog - 1, 0..mlab shl 1] of Byte; + end; + + TSetOfExpression = set of mlab + 1..mlog; + +implementation + +end. From 6964589dc9e219f9a2261e24d7d95be41844f3e6 Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Thu, 8 Oct 2026 17:42:25 +0200 Subject: [PATCH 4/7] Overwrite an attribute the node already carries TSyntaxNode.SetAttribute pointed its entry pointer at a slot only when it added the key. For a key the node already carried the pointer was left uninitialised and the value was written through it anyway; dcc32 says so with W1036 on AttributeEntry. Any directive stored twice reaches it: procedure x(a: Integer); stdcall; stdcall; external 'a.dll' index 93; is legal Delphi, stores anCallingConvention twice and ends in an access violation, or in a silent write to whatever address the stack held. SetAttribute now looks the existing entry up with TryGetAttributeEntry and overwrites its value, and adds an entry only for a new key. An empty value removes the attribute and returns, instead of falling through to the same uninitialised pointer after RemoveAttribute. The last value stored wins, so `stdcall; cdecl;` records cdecl. --- Source/DelphiAST.Classes.pas | 9 ++++--- Test/UnitTests/DelphiAST.Tests.pas | 39 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/Source/DelphiAST.Classes.pas b/Source/DelphiAST.Classes.pas index 8752255..4d4dad3 100644 --- a/Source/DelphiAST.Classes.pas +++ b/Source/DelphiAST.Classes.pas @@ -421,16 +421,19 @@ procedure TSyntaxNode.SetAttribute(const Key: TAttributeName; const Value: strin AttributeEntry: PAttributeEntry; len: Integer; begin - if not HasAttribute(Key) then + if (Value = '') then + begin + RemoveAttribute(Key); + Exit; + end; + if not TryGetAttributeEntry(Key, AttributeEntry) then begin - if (Value = '') then Exit; //no action needed len := Length(FAttributes); SetLength(FAttributes, len + 1); AttributeEntry := @FAttributes[len]; AttributeEntry^.Key := Key; Include(FAttributesInUse, Key); end; - if (Value = '') then RemoveAttribute(Key); AttributeEntry^.Value := Value; end; diff --git a/Test/UnitTests/DelphiAST.Tests.pas b/Test/UnitTests/DelphiAST.Tests.pas index c46a2bb..f4cd81b 100644 --- a/Test/UnitTests/DelphiAST.Tests.pas +++ b/Test/UnitTests/DelphiAST.Tests.pas @@ -241,6 +241,43 @@ procedure TestVariableEndPosition; end; end; +procedure TestAttributeOverwrite; +var + Node: TSyntaxNode; +begin + Node := TSyntaxNode.Create(ntMethod); + try + Node.SetAttribute(anName, 'First'); + Node.SetAttribute(anType, 'Kept'); + Node.SetAttribute(anName, 'Second'); + AssertEquals('Second', Node.GetAttribute(anName), 'Overwritten attribute.'); + AssertEquals('Kept', Node.GetAttribute(anType), 'Other attribute.'); + AssertEquals(2, Length(Node.Attributes), 'Overwriting must not add an entry.'); + finally + Node.Free; + end; +end; + +procedure TestRepeatedCallingConvention; +var + Root, IntfNode: TSyntaxNode; +begin + Root := ParseSource('unit Example; interface ' + + 'procedure Same(A: Integer); stdcall; stdcall; external ''a.dll'' index 93; ' + + 'procedure Other; stdcall; cdecl; external ''a.dll''; implementation end.'); + try + IntfNode := FindDescendant(Root, ntInterface); + AssertNotNil(IntfNode, 'Missing interface'); + AssertEquals(2, CountDescendants(IntfNode, ntMethod), 'Method count.'); + AssertEquals('stdcall', IntfNode.ChildNodes[0].GetAttribute(anCallingConvention), + 'A repeated calling convention.'); + AssertEquals('cdecl', IntfNode.ChildNodes[1].GetAttribute(anCallingConvention), + 'The last of two calling conventions.'); + finally + Root.Free; + end; +end; + procedure TestInvalidSyntax; var Root: TSyntaxNode; @@ -295,6 +332,8 @@ procedure RunAllTests; RunTest('AST.SourcePositions', TestSourcePositions); RunTest('AST.ConstantEndPosition', TestConstantEndPosition); RunTest('AST.VariableEndPosition', TestVariableEndPosition); + RunTest('Node.AttributeOverwrite', TestAttributeOverwrite); + RunTest('AST.RepeatedCallingConvention', TestRepeatedCallingConvention); RunTest('Parser.InvalidSyntax', TestInvalidSyntax); {$IFNDEF FPC} RunTest('Serialization.BinaryRoundTrip', TestBinarySerializationRoundTrip); From 6b0d0bac7fbea7762aa03aefb233a7aaacdfa9dc Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Thu, 8 Oct 2026 17:42:53 +0200 Subject: [PATCH 5/7] Remove an attribute by dropping its entry SetAttribute with an empty value goes through RemoveAttribute, which did not remove anything. It computed the byte offset of the entry past the one being removed and then moved the entries one slot further along rather than back, so the removed entry stayed, the one before the last was overwritten, and the array kept its length. Only the key's bit in FAttributesInUse was cleared, which hid the stale entry from HasAttribute but not from Attributes, so the writers and the binary serializer still saw it. The Move also copied the string values bit for bit, leaving two entries owning one reference. RemoveAttribute now shifts the entries after the removed one back by assignment, which keeps the string reference counts right, and shortens the array by one. --- Source/DelphiAST.Classes.pas | 25 ++++++++++++++----------- Test/UnitTests/DelphiAST.Tests.pas | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Source/DelphiAST.Classes.pas b/Source/DelphiAST.Classes.pas index 4d4dad3..869d614 100644 --- a/Source/DelphiAST.Classes.pas +++ b/Source/DelphiAST.Classes.pas @@ -438,18 +438,21 @@ procedure TSyntaxNode.SetAttribute(const Key: TAttributeName; const Value: strin end; procedure TSyntaxNode.RemoveAttribute(const Key: TAttributeName); -const - Size = SizeOf(TAttributeEntry); var - Entry: PAttributeEntry; - Index: integer; -begin - if HasAttribute(Key) then begin - TryGetAttributeEntry(Key, Entry); - Index:= (NativeUInt(Entry) - NativeUInt(@FAttributes[0])) + Size; - Move(Entry^, Pointer(NativeUInt(Entry)+Size)^, (High(FAttributes) * Size) - Index); - Exclude(FAttributesInUse, Key); - end; + i, Kept: Integer; +begin + if not HasAttribute(Key) then + Exit; + Kept := 0; + for i := 0 to High(FAttributes) do + if FAttributes[i].Key <> Key then + begin + if Kept <> i then + FAttributes[Kept] := FAttributes[i]; + Inc(Kept); + end; + SetLength(FAttributes, Kept); + Exclude(FAttributesInUse, Key); end; function SameText(const Needle: string; const HayStack: array of string): boolean; overload; diff --git a/Test/UnitTests/DelphiAST.Tests.pas b/Test/UnitTests/DelphiAST.Tests.pas index f4cd81b..ffdf85b 100644 --- a/Test/UnitTests/DelphiAST.Tests.pas +++ b/Test/UnitTests/DelphiAST.Tests.pas @@ -258,6 +258,28 @@ procedure TestAttributeOverwrite; end; end; +procedure TestAttributeRemove; +var + Node: TSyntaxNode; +begin + Node := TSyntaxNode.Create(ntMethod); + try + Node.SetAttribute(anName, 'Name'); + Node.SetAttribute(anType, 'Type'); + Node.SetAttribute(anKind, 'Kind'); + Node.SetAttribute(anName, ''); + AssertFalse(Node.HasAttribute(anName), 'An empty value removes the attribute.'); + AssertEquals(2, Length(Node.Attributes), 'Removing must drop the entry.'); + AssertEquals('Type', Node.GetAttribute(anType), 'First remaining attribute.'); + AssertEquals('Kind', Node.GetAttribute(anKind), 'Second remaining attribute.'); + Node.SetAttribute(anName, 'Again'); + AssertEquals('Again', Node.GetAttribute(anName), 'A removed attribute can be set again.'); + AssertEquals(3, Length(Node.Attributes), 'Setting it again adds one entry.'); + finally + Node.Free; + end; +end; + procedure TestRepeatedCallingConvention; var Root, IntfNode: TSyntaxNode; @@ -333,6 +355,7 @@ procedure RunAllTests; RunTest('AST.ConstantEndPosition', TestConstantEndPosition); RunTest('AST.VariableEndPosition', TestVariableEndPosition); RunTest('Node.AttributeOverwrite', TestAttributeOverwrite); + RunTest('Node.AttributeRemove', TestAttributeRemove); RunTest('AST.RepeatedCallingConvention', TestRepeatedCallingConvention); RunTest('Parser.InvalidSyntax', TestInvalidSyntax); {$IFNDEF FPC} From 1ddce7c53e8bd8ed476bb54a9be92b5775ebc8c5 Mon Sep 17 00:00:00 2001 From: Patrick Quist Date: Thu, 8 Oct 2026 17:43:55 +0200 Subject: [PATCH 6/7] Report a syntax error at its line, not its column TPasSyntaxTreeBuilder.Run turns an ESyntaxError into an ESyntaxTreeException, whose constructor takes the line first and the column second. It passed E.PosXY.X, E.PosXY.Y, but TmwBasePasLex.GetPosXY puts the column in X and the line in Y, so the two came out swapped. The EParserException path next to it was right; ParserMessage already passes Y, X. ESyntaxError is what ExpectedFatal raises, which is how an unexpected end of file is reported. A unit that stops after the `begin` of a routine on line 5 unit Broken; interface implementation procedure Run; begin was reported at line 6, col 5 instead of line 5, col 6. --- Source/DelphiAST.pas | 2 +- Test/UnitTests/DelphiAST.Tests.pas | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Source/DelphiAST.pas b/Source/DelphiAST.pas index 5ff7b5c..0b79f42 100644 --- a/Source/DelphiAST.pas +++ b/Source/DelphiAST.pas @@ -2672,7 +2672,7 @@ function TPasSyntaxTreeBuilder.Run(SourceStream: TStream): TSyntaxNode; on E: EParserException do raise ESyntaxTreeException.Create(E.Line, E.Col, Lexer.FileName, E.Message, Result); on E: ESyntaxError do - raise ESyntaxTreeException.Create(E.PosXY.X, E.PosXY.Y, Lexer.FileName, E.Message, Result); + raise ESyntaxTreeException.Create(E.PosXY.Y, E.PosXY.X, Lexer.FileName, E.Message, Result); else FreeAndNil(Result); raise; diff --git a/Test/UnitTests/DelphiAST.Tests.pas b/Test/UnitTests/DelphiAST.Tests.pas index c46a2bb..da6354a 100644 --- a/Test/UnitTests/DelphiAST.Tests.pas +++ b/Test/UnitTests/DelphiAST.Tests.pas @@ -258,6 +258,28 @@ procedure TestInvalidSyntax; AssertTrue(Raised, 'Invalid syntax did not raise ESyntaxTreeException'); end; +procedure TestUnexpectedEndOfFilePosition; +var + Root: TSyntaxNode; + Raised: Boolean; +begin + Root := nil; + Raised := False; + try + Root := ParseSource('unit Broken;' + sLineBreak + 'interface' + sLineBreak + + 'implementation' + sLineBreak + 'procedure Run;' + sLineBreak + 'begin'); + except + on E: ESyntaxTreeException do + begin + Raised := True; + AssertEquals(5, E.Line, 'The end of file is on line 5.'); + AssertEquals(6, E.Col, 'The end of file is after the 5 characters of "begin".'); + end; + end; + Root.Free; + AssertTrue(Raised, 'An unexpected end of file did not raise ESyntaxTreeException'); +end; + {$IFNDEF FPC} procedure TestBinarySerializationRoundTrip; var @@ -296,6 +318,7 @@ procedure RunAllTests; RunTest('AST.ConstantEndPosition', TestConstantEndPosition); RunTest('AST.VariableEndPosition', TestVariableEndPosition); RunTest('Parser.InvalidSyntax', TestInvalidSyntax); + RunTest('Parser.UnexpectedEndOfFilePosition', TestUnexpectedEndOfFilePosition); {$IFNDEF FPC} RunTest('Serialization.BinaryRoundTrip', TestBinarySerializationRoundTrip); {$ENDIF} From d58721b15ce6b25a558eb3a18d898b7d995faf32 Mon Sep 17 00:00:00 2001 From: Partouf Date: Thu, 8 Oct 2026 18:11:20 +0200 Subject: [PATCH 7/7] Use System.Generics.* when FPC's RTL has dotted unit names An FPC built with dotted unit names (FPC_DOTTEDUNITS, as in FPC trunk's namespaced RTL) ships System.Generics.Collections and no unit called Generics.Collections, so DelphiAST does not compile against it as-is. Reaching the undotted name through a unit scope (-FNSystem) is not enough either: a generic specialised in two units that each found Generics.Collections that way comes out as two incompatible types, e.g. DelphiAST.pas(726,67) Error: Incompatible type for arg no. 2: Got "TList", expected "TLIST" Follow the convention the dotted RTL itself uses and name the dotted unit under {$IFDEF FPC_DOTTEDUNITS}. Nothing defines that for user code, so a dotted build passes -dFPC_DOTTEDUNITS; Delphi and an undotted FPC see exactly the uses clauses they saw before. Co-Authored-By: Claude Opus 5.5 --- Source/DelphiAST.Classes.pas | 2 +- Source/DelphiAST.ProjectIndexer.pas | 7 ++++++- Source/DelphiAST.Serialize.Binary.pas | 2 +- Source/DelphiAST.SimpleParserEx.pas | 2 +- Source/DelphiAST.Writer.pas | 2 +- Source/DelphiAST.pas | 2 +- Source/SimpleParser/SimpleParser.Lexer.pas | 2 +- 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Source/DelphiAST.Classes.pas b/Source/DelphiAST.Classes.pas index 869d614..a03af08 100644 --- a/Source/DelphiAST.Classes.pas +++ b/Source/DelphiAST.Classes.pas @@ -5,7 +5,7 @@ interface uses - SysUtils, Generics.Collections, SimpleParser.Lexer.Types, DelphiAST.Consts; + SysUtils, {$IFDEF FPC_DOTTEDUNITS}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}, SimpleParser.Lexer.Types, DelphiAST.Consts; type EParserException = class(Exception) diff --git a/Source/DelphiAST.ProjectIndexer.pas b/Source/DelphiAST.ProjectIndexer.pas index 77e217a..4655908 100644 --- a/Source/DelphiAST.ProjectIndexer.pas +++ b/Source/DelphiAST.ProjectIndexer.pas @@ -5,7 +5,12 @@ interface uses - Classes, Generics.Defaults, Generics.Collections, + Classes, + {$IFDEF FPC_DOTTEDUNITS} + System.Generics.Defaults, System.Generics.Collections, + {$ELSE} + Generics.Defaults, Generics.Collections, + {$ENDIF} SimpleParser.Lexer.Types, DelphiAST, DelphiAST.Classes, DelphiAST.Consts; diff --git a/Source/DelphiAST.Serialize.Binary.pas b/Source/DelphiAST.Serialize.Binary.pas index 04ac283..f4a820e 100644 --- a/Source/DelphiAST.Serialize.Binary.pas +++ b/Source/DelphiAST.Serialize.Binary.pas @@ -6,7 +6,7 @@ interface uses Classes, - Generics.Collections, + {$IFDEF FPC_DOTTEDUNITS}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}, DelphiAST.Consts, DelphiAST.Classes; diff --git a/Source/DelphiAST.SimpleParserEx.pas b/Source/DelphiAST.SimpleParserEx.pas index 6afd710..6f015b2 100644 --- a/Source/DelphiAST.SimpleParserEx.pas +++ b/Source/DelphiAST.SimpleParserEx.pas @@ -3,7 +3,7 @@ interface uses - SysUtils, Generics.Collections, SimpleParser, SimpleParser.Lexer.Types, + SysUtils, {$IFDEF FPC_DOTTEDUNITS}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}, SimpleParser, SimpleParser.Lexer.Types, SimpleParser.Lexer, Classes; type diff --git a/Source/DelphiAST.Writer.pas b/Source/DelphiAST.Writer.pas index e85b06e..d98fda2 100644 --- a/Source/DelphiAST.Writer.pas +++ b/Source/DelphiAST.Writer.pas @@ -26,7 +26,7 @@ TSyntaxTreeWriter = class implementation uses - Generics.Collections, + {$IFDEF FPC_DOTTEDUNITS}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}, {$IFNDEF FPC} DelphiAST.Serialize.Binary, {$ENDIF} diff --git a/Source/DelphiAST.pas b/Source/DelphiAST.pas index 0b79f42..b812935 100644 --- a/Source/DelphiAST.pas +++ b/Source/DelphiAST.pas @@ -5,7 +5,7 @@ interface uses - SysUtils, Classes, Generics.Collections, SimpleParser, SimpleParser.Lexer, + SysUtils, Classes, {$IFDEF FPC_DOTTEDUNITS}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}, SimpleParser, SimpleParser.Lexer, SimpleParser.Lexer.Types, DelphiAST.Classes, DelphiAST.Consts, DelphiAST.SimpleParserEx; type diff --git a/Source/SimpleParser/SimpleParser.Lexer.pas b/Source/SimpleParser/SimpleParser.Lexer.pas index ef8428f..2257785 100644 --- a/Source/SimpleParser/SimpleParser.Lexer.pas +++ b/Source/SimpleParser/SimpleParser.Lexer.pas @@ -56,7 +56,7 @@ interface uses SysUtils, Classes, Character, {$IFDEF FPC} - Generics.Collections, + {$IFDEF FPC_DOTTEDUNITS}System.Generics.Collections{$ELSE}Generics.Collections{$ENDIF}, {$ENDIF} SimpleParser.Lexer.Types;