From 08beb336899b8f108889a8ec2b3d28debaf7603a Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Sun, 17 May 2026 00:02:18 +0100 Subject: [PATCH] fix(semantic): reject duplicate identifiers across const and var sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A var declaration shadowing a same-block const (or a const declared twice in the same block) was silently accepted instead of raising a Duplicate identifier error. Root cause: AnalyseBlock registers consts in the outer scope then pushes a new inner scope before calling AnalyseVarDecls, so FTable.Define (which only checks the current scope) could not detect the clash. Fix: AnalyseVarDecls now scans the block's own ConstDecls list before defining each var name. AnalyseConstDecls now treats a duplicate as an error when the collision originates from an earlier const in the same block; cross-unit const shadowing (e.g. a unit redefining a system.pas constant) continues to be silently accepted. Also removes the redundant LineEnding const from blaise.testing.pas — it is already exported by system.pas. Fixes: https://github.com/graemeg/blaise/issues/30 --- README.adoc | 2 +- compiler/src/main/pascal/blaise.testing.pas | 3 -- compiler/src/main/pascal/uSemantic.pas | 29 +++++++++++++++++-- compiler/src/test/pascal/cp.test.semantic.pas | 16 ++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/README.adoc b/README.adoc index bfb2f52..82f9d52 100644 --- a/README.adoc +++ b/README.adoc @@ -38,7 +38,7 @@ https://github.com/graemeg/opdebugger[OPDF] debug format support. == 🚀 Project Status * **Self-Hosting:** Yes. Blaise bootstraps and recompiles itself with byte-for-byte fixpoint. FPC is no longer required — the entire toolchain runs on Blaise alone. -* **Testing:** 1846 tests and growing (Test-Driven Development from day one). The test suite itself compiles under Blaise. +* **Testing:** 1934 tests and growing (Test-Driven Development from day one). The test suite itself compiles under Blaise. * **Backends:** Currently utilising a QBE backend, with an LLVM backend in active development. diff --git a/compiler/src/main/pascal/blaise.testing.pas b/compiler/src/main/pascal/blaise.testing.pas index 3b37786..402a531 100644 --- a/compiler/src/main/pascal/blaise.testing.pas +++ b/compiler/src/main/pascal/blaise.testing.pas @@ -36,9 +36,6 @@ interface uses Classes, SysUtils; -const - LineEnding = #10; - type { TRunMethod — type of a parameter-less method on any TObject descendant. Used as the cast target for the published-method dispatch trampoline: diff --git a/compiler/src/main/pascal/uSemantic.pas b/compiler/src/main/pascal/uSemantic.pas index 6570ca9..ab28243 100644 --- a/compiler/src/main/pascal/uSemantic.pas +++ b/compiler/src/main/pascal/uSemantic.pas @@ -1881,6 +1881,7 @@ var RefSym: TSymbol; TD: TTypeDesc; Resolved: string; + IsSameBlockDup: Boolean; begin for I := 0 to ABlock.ConstDecls.Count - 1 do begin @@ -1927,7 +1928,21 @@ begin Sym.ConstValue := CD.IntVal; Sym.ConstString := CD.StrVal; if not FTable.Define(Sym) then - Sym.Free; { duplicate const — silently ignore } + begin + Sym.Free; + { Only error for same-block duplicates. Cross-unit const shadowing + (e.g. a unit redefining a system.pas constant) is silently accepted, + matching FPC behaviour and preserving the existing test coverage. } + IsSameBlockDup := False; + for J := 0 to I - 1 do + if SameText(TConstDecl(ABlock.ConstDecls.Items[J]).Name, CD.Name) then + begin + IsSameBlockDup := True; + Break; + end; + if IsSameBlockDup then + SemanticError(Format('Duplicate identifier ''%s''', [CD.Name]), CD.Line, CD.Col); + end; end; end; @@ -3370,7 +3385,7 @@ end; procedure TSemanticAnalyser.AnalyseVarDecls(ABlock: TBlock); var - I, J: Integer; + I, J, K: Integer; Decl: TVarDecl; Typ: TTypeDesc; VarName: string; @@ -3407,6 +3422,16 @@ begin for J := 0 to Decl.Names.Count - 1 do begin VarName := Decl.Names.Strings[J]; + { Consts from the same block live in the immediately enclosing scope + (AnalyseBlock pushes a new scope before calling AnalyseVarDecls, so + FTable.Define cannot see same-block consts). Scan the block's own + ConstDecls list so we catch only same-block clashes, not legitimate + shadowing of outer-scope or unit-imported consts. } + for K := 0 to ABlock.ConstDecls.Count - 1 do + if SameText(TConstDecl(ABlock.ConstDecls.Items[K]).Name, VarName) then + SemanticError( + Format('Duplicate identifier ''%s''', [VarName]), + Decl.Line, Decl.Col); Sym := TSymbol.Create(VarName, skVariable, Typ); Sym.IsWeak := Decl.IsWeak; Sym.IsGlobal := Decl.IsGlobal; diff --git a/compiler/src/test/pascal/cp.test.semantic.pas b/compiler/src/test/pascal/cp.test.semantic.pas index b0b028b..0bc1892 100644 --- a/compiler/src/test/pascal/cp.test.semantic.pas +++ b/compiler/src/test/pascal/cp.test.semantic.pas @@ -29,6 +29,10 @@ type procedure TestVarDecl_MultiName_BothRegistered; procedure TestVarDecl_UnknownType_RaisesError; procedure TestVarDecl_Duplicate_RaisesError; + { Const then var with same name in same block is a duplicate } + procedure TestVarDecl_DuplicatesConst_RaisesError; + { Const redeclared with same name in same block is a duplicate } + procedure TestConst_Duplicate_RaisesError; { Expression type inference } procedure TestExpr_IntLiteral_TypeIsInteger; @@ -169,6 +173,18 @@ begin 'program P; var x: Integer; x: string; begin end.'); end; +procedure TSemanticTests.TestVarDecl_DuplicatesConst_RaisesError; +begin + AnalyseExpectError( + 'program P; const b = 42; var b: Integer; begin end.'); +end; + +procedure TSemanticTests.TestConst_Duplicate_RaisesError; +begin + AnalyseExpectError( + 'program P; const b = 42; b = 99; begin end.'); +end; + { ------------------------------------------------------------------ } { Expression type inference } { ------------------------------------------------------------------ }