fix(semantic): set equality S=[] and S=[lit] no longer crash

AnalyseBinaryExpr crashed with a nil-deref when one operand was a set
type (tySet) and the other was an empty array literal [] — because
AnalyseArrayLiteralExpr returns nil for an empty literal (no element
type to infer), and the set-dispatch guard read .Kind off that nil
pointer.

Fix: before the set-dispatch guard, coerce any array literal on either
side (empty or non-empty) to the set type of the opposite operand,
mirroring the existing pattern for `x in [A, B, C]`.  This also
enables `S = [fA, fB]` without requiring an explicit cast.

The native-backend set-variable crash documented in bugs.txt was
already fixed in HEAD (the release binary produced a codegen error, but
the current compiler handles it correctly).

Tests added:
- TSetTests.TestSemantic_Set_EqualityEmptyLiteral_OK — S=[] must not crash
- TSetTests.TestSemantic_Set_EqualityLiteral_OK — S=[fA,fB] accepted
- TSetTests.TestCodegen_Set_EqualityEmptyLiteralEmitsCeqw — IR check
- TSetTests.TestCodegen_Set_EqualityLiteralEmitsCeqw — IR check
- TE2EMiscTests.TestRun_Set_EqualityWithLiteral — compile+run assertion
- TE2ENativeTests.TestRun_ZeroInit_SetLocal now runs on both backends
This commit is contained in:
Graeme Geldenhuys 2026-06-12 16:21:25 +01:00
parent a2d263e0e8
commit 75db9584dc
4 changed files with 106 additions and 22 deletions

View file

@ -8938,18 +8938,31 @@ begin
Exit;
end;
{ Set arithmetic and equality when both operands are set types }
if (LType.Kind = tySet) or (RType.Kind = tySet) then
{ Set arithmetic and equality: coerce array literals [...] or empty []
to the set type of the other operand. AnalyseArrayLiteralExpr returns nil
for [] (no element type to infer) and tyOpenArray for [a, b, c]. }
if (LType <> nil) and (LType.Kind = tySet) and
(ABin.Right is TArrayLiteralExpr) and
((RType = nil) or (RType.Kind = tyOpenArray)) then
RType := AnalyseSetLiteralExpr(TArrayLiteralExpr(ABin.Right),
TSetTypeDesc(LType));
if (RType <> nil) and (RType.Kind = tySet) and
(ABin.Left is TArrayLiteralExpr) and
((LType = nil) or (LType.Kind = tyOpenArray)) then
LType := AnalyseSetLiteralExpr(TArrayLiteralExpr(ABin.Left),
TSetTypeDesc(RType));
if ((LType <> nil) and (LType.Kind = tySet)) or
((RType <> nil) and (RType.Kind = tySet)) then
begin
if LType.Kind <> tySet then
if (LType = nil) or (LType.Kind <> tySet) then
SemanticError(
Format('Left operand of ''%s'' must be a set type, got ''%s''',
[BinaryOpName(ABin.Op), LType.Name]),
Format('Left operand of ''%s'' must be a set type',
[BinaryOpName(ABin.Op)]),
ABin.Line, ABin.Col);
if RType.Kind <> tySet then
if (RType = nil) or (RType.Kind <> tySet) then
SemanticError(
Format('Right operand of ''%s'' must be a set type, got ''%s''',
[BinaryOpName(ABin.Op), RType.Name]),
Format('Right operand of ''%s'' must be a set type',
[BinaryOpName(ABin.Op)]),
ABin.Line, ABin.Col);
if LType <> RType then
SemanticError(

View file

@ -62,6 +62,7 @@ type
procedure TestRun_Set_UnionIntersect;
procedure TestRun_Set_ValuedConstant;
procedure TestRun_Set_LiteralArgument;
procedure TestRun_Set_EqualityWithLiteral;
{ 64-bit sets (>32 members) }
procedure TestRun_Set64_InOperator_HighBit;
@ -452,6 +453,22 @@ const
end.
''';
{ Set equality: S = [] and S = [literal] comparisons. }
SrcSetEquality = '''
program Prg;
type TDir = (North, South, East, West);
TDirs = set of TDir;
var S: TDirs;
begin
S := [];
if S = [] then WriteLn('empty') else WriteLn('not-empty');
S := [East, West];
if S = [] then WriteLn('bad-empty') else WriteLn('not-empty2');
if S = [East, West] then WriteLn('match') else WriteLn('no-match');
if S <> [East, West] then WriteLn('bad-ne') else WriteLn('equal')
end.
''';
BigEnum64 =
'''
type
@ -865,6 +882,16 @@ begin
'no-N' + LE + 'E' + LE + 'no-N' + LE + 'no-E' + LE, Output);
end;
procedure TE2EMiscTests.TestRun_Set_EqualityWithLiteral;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcSetEquality, Output, RCode));
AssertEquals('exit code 0', 0, RCode);
AssertEquals('set equality with literals',
'empty' + LE + 'not-empty2' + LE + 'match' + LE + 'equal' + LE, Output);
end;
{ ------------------------------------------------------------------ }
{ 64-bit sets (>32 members) }
{ ------------------------------------------------------------------ }

View file

@ -385,9 +385,7 @@ type
starts at its zero value even on a dirty stack. Each test category uses
a Dirty() helper that fills the stack with 0xDEADBEEF garbage before the
real procedure runs, proving that the zero-init comes from the prologue
and not from lucky stack layout.
Note: TestRun_ZeroInit_SetLocal runs QBE-only because the native backend
currently crashes the compiler on set-typed variables (bugs.txt). }
and not from lucky stack layout. }
procedure TestRun_ZeroInit_ScalarIntegers;
procedure TestRun_ZeroInit_FloatLocals;
procedure TestRun_ZeroInit_BooleanAndChar;
@ -6067,19 +6065,9 @@ begin
end;
procedure TE2ENativeTests.TestRun_ZeroInit_SetLocal;
var
Stdout: string;
ExitCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
{ Native backend crashes the compiler on set-typed variables — QBE only
until that pre-existing bug is fixed (see bugs.txt).
Note: set equality (S = []) also crashes the semantic pass (separate bug),
so we probe zero-init via the 'in' operator instead. }
if not CompileAndRun(SrcZeroInit_SetLocal, Stdout, ExitCode) then
Fail('QBE compile/run failed');
AssertEquals('0' + LE, Stdout);
AssertEquals(0, ExitCode);
AssertRunsOnAll(SrcZeroInit_SetLocal, '0' + LE, 0);
end;
procedure TE2ENativeTests.TestRun_ZeroInit_RecordWithMixedFields;

View file

@ -50,6 +50,8 @@ type
procedure TestSemantic_Set_Difference_OK;
procedure TestSemantic_Set_Intersection_OK;
procedure TestSemantic_Set_Equality_OK;
procedure TestSemantic_Set_EqualityEmptyLiteral_OK;
procedure TestSemantic_Set_EqualityLiteral_OK;
procedure TestSemantic_Set_BaseTypeMustBeEnum;
procedure TestSemantic_Set_LiteralElementMustMatchBase;
@ -68,6 +70,8 @@ type
procedure TestCodegen_Set_IntersectionEmitsAnd;
procedure TestCodegen_Set_EqualityEmitsCeqw;
procedure TestCodegen_Set_InequalityEmitsCnew;
procedure TestCodegen_Set_EqualityEmptyLiteralEmitsCeqw;
procedure TestCodegen_Set_EqualityLiteralEmitsCeqw;
{ ------------------------------------------------------------------ }
{ set-valued constants (const X = [a, b]) }
@ -361,6 +365,28 @@ const
end.
''';
SrcSetEqualityEmptyLiteral =
'program P;' + #10 +
DirEnum +
'''
var S: TDirSet; B: Boolean;
begin
S := [];
B := S = []
end.
''';
SrcSetEqualityLiteral =
'program P;' + #10 +
DirEnum +
'''
var S: TDirSet; B: Boolean;
begin
S := [dNorth, dEast];
B := S = [dNorth, dEast]
end.
''';
BigEnum =
'''
type
@ -767,6 +793,18 @@ begin
SemanticOK(SrcSetInequality);
end;
procedure TSetTests.TestSemantic_Set_EqualityEmptyLiteral_OK;
begin
{ S = [] must not crash the semantic pass (was nil-deref before fix). }
SemanticOK(SrcSetEqualityEmptyLiteral);
end;
procedure TSetTests.TestSemantic_Set_EqualityLiteral_OK;
begin
{ S = [dNorth, dEast] — literal RHS coerced to the set type of LHS. }
SemanticOK(SrcSetEqualityLiteral);
end;
procedure TSetTests.TestSemantic_Set_BaseTypeMustBeEnum;
begin
SemanticFail(SrcSetBadBaseType);
@ -898,6 +936,24 @@ begin
AssertTrue('set inequality emits cnew', Pos('cnew', IR) > 0);
end;
procedure TSetTests.TestCodegen_Set_EqualityEmptyLiteralEmitsCeqw;
var
IR: string;
begin
{ S = [] coerces [] to the set type of S; bitmask 0 vs S — still ceqw. }
IR := GenIR(SrcSetEqualityEmptyLiteral);
AssertTrue('S = [] emits ceqw', Pos('ceqw', IR) > 0);
end;
procedure TSetTests.TestCodegen_Set_EqualityLiteralEmitsCeqw;
var
IR: string;
begin
{ S = [dNorth, dEast] coerces the literal to mask 5; comparison is ceqw. }
IR := GenIR(SrcSetEqualityLiteral);
AssertTrue('S = [dNorth, dEast] emits ceqw', Pos('ceqw', IR) > 0);
end;
{ ------------------------------------------------------------------ }
{ set-valued constants }
{ ------------------------------------------------------------------ }