feat(semantic): accept set literals as 'set of' arguments

A bracket literal can now be passed directly where a `set of <enum>`
parameter is expected, e.g. Configure([optA, optC]) or Report([]) — no
named intermediate variable needed.

Previously such a call failed overload resolution: a bare [a, b] is
analysed (lacking set context) as an open-array of the enum, which does
not match the set parameter. Now:

- ArgMatchScore matches a TArrayLiteralExpr argument against a set
  parameter — non-empty when the members share the param's base enum,
  and the empty [] against any set — checked before the nil-arg bail so
  [] (which has no resolved type) is handled.
- AnalyseArrayLiteralExpr defers an empty [] to a nil type instead of
  erroring outright; it is only meaningful with a set target.
- After overload resolution, RetypeSetLiteralArgs re-points each matched
  set-literal argument's ResolvedType at the parameter's set type, so
  codegen emits the bitmask (EmitArrayLiteralExpr dispatches on tySet).
- CheckTypesMatch gained a nil-actual guard (a clean "no value type"
  error instead of a segfault on a stray []), and the assignment path
  rejects an empty [] assigned to a non-set LHS.

Fixes a second, latent bug exposed by passing sets by value: a `set of`
value parameter of <=32 members (QBE type w) was spilled in the prologue
with storel, which QBE rejects. Added a tySet case to both param-spill
sites (storew for w sets, storel for l sets).

Tests: 6 IR/semantic cases in cp.test.sets (arg OK / empty / wrong-enum
/ empty-non-set-assign fail, bitmask fold, w-width param spill) and an
e2e in cp.test.e2e.misc. language-rationale updated. Full suite 2335
tests pass; fixpoint clean.
This commit is contained in:
Graeme Geldenhuys 2026-06-02 20:04:17 +01:00
parent 62165dac3d
commit 703e44dfa1
5 changed files with 294 additions and 9 deletions

View file

@ -5076,6 +5076,20 @@ begin
EmitLine(Format(' storel %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end;
tySet:
{ A set is a w (≤32 members) or l (≤64) bitmask — spill at its width. }
if TSetTypeDesc(Par.ResolvedType).BitCount <= 32 then
begin
EmitLine(Format(' %%_var_%s =l alloc4 1', [Par.ParamName]));
EmitLine(Format(' storew %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end
else
begin
EmitLine(Format(' %%_var_%s =l alloc8 1', [Par.ParamName]));
EmitLine(Format(' storel %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end;
tyDouble:
begin
EmitLine(Format(' %%_var_%s =l alloc8 1', [Par.ParamName]));
@ -5907,6 +5921,20 @@ begin
EmitLine(Format(' storew %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end;
tySet:
{ w (≤32 members) or l (≤64) bitmask — spill at its width. }
if TSetTypeDesc(Par.ResolvedType).BitCount <= 32 then
begin
EmitLine(Format(' %%_var_%s =l alloc4 1', [Par.ParamName]));
EmitLine(Format(' storew %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end
else
begin
EmitLine(Format(' %%_var_%s =l alloc8 1', [Par.ParamName]));
EmitLine(Format(' storel %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end;
tyDouble:
begin
EmitLine(Format(' %%_var_%s =l alloc8 1', [Par.ParamName]));

View file

@ -127,6 +127,10 @@ type
{ Per-arg compatibility: 2 = exact, 1 = widening, 0 = no match. }
function ArgMatchScore(AParam: TTypeDesc; AArg: TTypeDesc;
AArgExpr: TASTExpr = nil): Integer;
{ Shared enum of a bracket literal's elements, or nil (see impl). }
function SetLiteralBaseEnum(AExpr: TArrayLiteralExpr): TTypeDesc;
{ Re-type set-literal args to their `set of` param type post-overload. }
procedure RetypeSetLiteralArgs(AArgs: TObjectList; AMDecl: TMethodDecl);
procedure AnalyseMethodCall(ACall: TMethodCallStmt);
procedure AnalyseInheritedCall(ACall: TInheritedCallStmt);
procedure AnalyseCaseStmt(AStmt: TCaseStmt);
@ -435,6 +439,14 @@ var
begin
if AExpected = AActual then
Exit;
{ A nil actual type means the right-hand expression could not be typed —
e.g. a bare empty set literal '[]' used outside a set context. Report it
rather than dereferencing nil (segfault). }
if AActual = nil then
SemanticError(Format('Expression has no value type in %s', [AContext]),
ALine, ACol);
if AExpected = nil then
Exit;
{ nil is compatible with any class, interface, pointer, PChar, or string type }
if (AActual.Kind = tyNil) and (AExpected.Kind in [tyClass, tyInterface, tyPointer, tyPChar, tyString, tyProcedural]) then
Exit;
@ -4724,6 +4736,14 @@ begin
TSetTypeDesc(VarSym.TypeDesc));
Exit;
end;
{ An empty bracket literal [] assigned to a non-set LHS has no inferable
type (it deferred to a nil ResolvedType); only a set target gives it
meaning. Reject cleanly rather than passing nil to CheckTypesMatch. }
if (AAssign.Expr is TArrayLiteralExpr) and
(TArrayLiteralExpr(AAssign.Expr).Elements.Count = 0) then
SemanticError(Format(
'Empty set literal ''[]'' cannot be assigned to non-set variable ''%s'' of type ''%s''',
[AAssign.Name, VarSym.TypeDesc.Name]), AAssign.Line, AAssign.Col);
ExprType := AnalyseExpr(AAssign.Expr);
CheckTypesMatch(VarSym.TypeDesc, ExprType, 'assignment', AAssign.Line, AAssign.Col);
@ -5197,11 +5217,89 @@ begin
end;
end;
{ After overload resolution: any bracket-literal argument matched against a
`set of` parameter is a set constructor, but it was analysed without set
context (so its ResolvedType is an open-array, not the set). Re-point each
such argument's ResolvedType at the parameter's set type so codegen emits a
bitmask (EmitArrayLiteralExpr dispatches on ResolvedType.Kind = tySet). }
procedure TSemanticAnalyser.RetypeSetLiteralArgs(AArgs: TObjectList;
AMDecl: TMethodDecl);
var
I: Integer;
Par: TMethodParam;
Arg: TASTExpr;
N: Integer;
begin
N := AArgs.Count;
if AMDecl.Params.Count < N then
N := AMDecl.Params.Count;
for I := 0 to N - 1 do
begin
Par := TMethodParam(AMDecl.Params.Items[I]);
Arg := TASTExpr(AArgs.Items[I]);
if (Par.ResolvedType <> nil) and (Par.ResolvedType.Kind = tySet) and
(Arg is TArrayLiteralExpr) then
Arg.ResolvedType := Par.ResolvedType;
end;
end;
{ If every element of the bracket literal is an enum constant of one shared
enum, return that enum descriptor; otherwise nil. Used to recognise a set
constructor [a, b] passed where a `set of <enum>` is expected. An empty
literal returns nil (the caller treats [] as matching any set). }
function TSemanticAnalyser.SetLiteralBaseEnum(AExpr: TArrayLiteralExpr): TTypeDesc;
var
I: Integer;
Elem: TASTExpr;
Sym: TSymbol;
begin
Result := nil;
if AExpr.Elements.Count = 0 then Exit;
for I := 0 to AExpr.Elements.Count - 1 do
begin
Elem := TASTExpr(AExpr.Elements.Items[I]);
if not (Elem is TIdentExpr) then
begin
Result := nil;
Exit;
end;
Sym := FTable.Lookup(TIdentExpr(Elem).Name);
if (Sym = nil) or (Sym.Kind <> skConstant) or (Sym.TypeDesc = nil) or
(Sym.TypeDesc.Kind <> tyEnum) then
begin
Result := nil;
Exit;
end;
if Result = nil then
Result := Sym.TypeDesc
else if Sym.TypeDesc <> Result then
begin
Result := nil; { mixed enums — not a clean set constructor }
Exit;
end;
end;
end;
function TSemanticAnalyser.ArgMatchScore(AParam: TTypeDesc;
AArg: TTypeDesc; AArgExpr: TASTExpr): Integer;
begin
Result := 0;
if (AParam = nil) or (AArg = nil) then Exit;
if AParam = nil then Exit;
{ A bracket literal [a, b] against a `set of <enum>` parameter is a set
constructor, even though (lacking set context) it was analysed as an
open-array — or, for the empty literal [], left untyped. Match it here,
before the nil-arg bail, so [] also matches. AnalyseProcCall re-types the
argument to the set type before codegen so the bitmask is emitted. Checked
first because an empty-literal arg has no ResolvedType. }
if (AParam.Kind = tySet) and (AArgExpr is TArrayLiteralExpr) then
begin
if (TArrayLiteralExpr(AArgExpr).Elements.Count = 0) or
(TSetTypeDesc(AParam).BaseType =
SetLiteralBaseEnum(TArrayLiteralExpr(AArgExpr))) then
Result := 2;
Exit;
end;
if AArg = nil then Exit;
{ Integer literal (untyped constant) matches any integer type exactly —
mirrors Pascal's treatment of untyped integer constants. Floating-point
params score 1 (widening) so an Integer overload beats a Double overload
@ -5568,6 +5666,7 @@ begin
{ Non-var argument compatibility was verified by overload scoring;
no second CheckTypesMatch needed here. }
end;
RetypeSetLiteralArgs(ACall.Args, MDecl);
AppendDefaultArgs(ACall.Args, MDecl, ACall.Name, ACall.Line, ACall.Col);
ACall.ResolvedDecl := MDecl;
end
@ -8234,9 +8333,18 @@ var
ActType: TTypeDesc;
I: Integer;
begin
{ An empty literal [] has no element type to infer. It is only valid in a
context that supplies the target type — a set assignment (handled before
this is reached) or a set-typed argument (resolved by RetypeSetLiteralArgs
after overload resolution). Defer here (nil type, no error) rather than
rejecting outright; an [] that never gets a context surfaces later as an
unresolved-type use. }
if AExpr.Elements.Count = 0 then
SemanticError('Array literal must contain at least one element',
AExpr.Line, AExpr.Col);
begin
AExpr.ResolvedType := nil;
Result := nil;
Exit;
end;
ElemType := AnalyseExpr(TASTExpr(AExpr.Elements.Items[0]));
for I := 1 to AExpr.Elements.Count - 1 do
begin

View file

@ -59,6 +59,7 @@ type
procedure TestRun_Set_InOperator;
procedure TestRun_Set_UnionIntersect;
procedure TestRun_Set_ValuedConstant;
procedure TestRun_Set_LiteralArgument;
{ for..in }
procedure TestRun_ForIn_String_ByteVar_PrintsBytes;
@ -383,6 +384,23 @@ const
end.
''';
{ A set literal passed directly as a `set of` argument (both non-empty and
empty), exercising the set-param ABI (w-width spill) too. }
SrcSetLiteralArg = '''
program P;
type TDir = (North, South, East, West);
TDirs = set of TDir;
procedure Report(D: TDirs);
begin
if North in D then WriteLn('N') else WriteLn('no-N');
if East in D then WriteLn('E') else WriteLn('no-E')
end;
begin
Report([East, West]);
Report([])
end.
''';
SrcForInStringByte = '''
program P;
var
@ -701,6 +719,17 @@ begin
'no-N' + LE + 'E' + LE + 'W' + LE + 'cleared' + LE, Output);
end;
procedure TE2EMiscTests.TestRun_Set_LiteralArgument;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcSetLiteralArg, Output, RCode));
AssertEquals('exit code 0', 0, RCode);
{ Report([East,West]): no-N, E. Report([]): no-N, no-E. }
AssertEquals('set literal arg',
'no-N' + LE + 'E' + LE + 'no-N' + LE + 'no-E' + LE, Output);
end;
procedure TE2EMiscTests.TestRun_ForIn_String_ByteVar_PrintsBytes;
var Output: string; RCode: Integer;
begin

View file

@ -81,6 +81,16 @@ type
procedure TestSemantic_SetConst_NonEnumMember_Fails;
procedure TestCodegen_SetConst_FoldsToBitmask;
procedure TestCodegen_SetConst_AssignableToNamedSetType;
{ ------------------------------------------------------------------ }
{ set literal as a call argument }
{ ------------------------------------------------------------------ }
procedure TestSemantic_SetLiteralArg_OK;
procedure TestSemantic_SetLiteralArg_Empty_OK;
procedure TestSemantic_SetLiteralArg_WrongEnum_Fails;
procedure TestSemantic_EmptyLiteral_NonSetAssign_Fails;
procedure TestCodegen_SetLiteralArg_FoldsToBitmask;
procedure TestCodegen_SetParam_SpillsAtWordWidth;
end;
implementation
@ -188,6 +198,58 @@ const
end.
''';
{ A set literal passed directly as a `set of` argument: mask dNorth(0)|
dEast(2) = 5. }
SrcSetLiteralArg =
'program P;' + #10 +
DirEnum +
'''
procedure Take(S: TDirSet);
begin
if dNorth in S then Halt(0)
end;
begin
Take([dNorth, dEast])
end.
''';
SrcSetLiteralArgEmpty =
'program P;' + #10 +
DirEnum +
'''
procedure Take(S: TDirSet);
begin
if dNorth in S then Halt(0)
end;
begin
Take([])
end.
''';
SrcSetLiteralArgWrongEnum =
'''
program P;
type
TDir = (dNorth, dEast);
TDirSet = set of TDir;
TColor = (cRed, cBlue);
procedure Take(S: TDirSet);
begin
end;
begin
Take([cRed])
end.
''';
SrcEmptyLiteralNonSetAssign =
'''
program P;
var x: Integer;
begin
x := []
end.
''';
SrcSetInOperator =
'program P;' + #10 +
DirEnum +
@ -698,6 +760,56 @@ begin
SemanticOK(SrcSetConstInferred);
end;
{ ------------------------------------------------------------------ }
{ set literal as a call argument }
{ ------------------------------------------------------------------ }
procedure TSetTests.TestSemantic_SetLiteralArg_OK;
begin
{ Take([dNorth, dEast]) resolves the set-literal argument against the
`set of TDir` parameter. }
SemanticOK(SrcSetLiteralArg);
end;
procedure TSetTests.TestSemantic_SetLiteralArg_Empty_OK;
begin
{ An empty literal [] matches any set parameter. }
SemanticOK(SrcSetLiteralArgEmpty);
end;
procedure TSetTests.TestSemantic_SetLiteralArg_WrongEnum_Fails;
begin
{ [cRed] (a TColor set constructor) does not match a `set of TDir`. }
SemanticFail(SrcSetLiteralArgWrongEnum);
end;
procedure TSetTests.TestSemantic_EmptyLiteral_NonSetAssign_Fails;
begin
{ x := [] where x is Integer: empty literal has no set context — a clean
error, not a crash. }
SemanticFail(SrcEmptyLiteralNonSetAssign);
end;
procedure TSetTests.TestCodegen_SetLiteralArg_FoldsToBitmask;
var
IR: string;
begin
{ [dNorth, dEast] at the call site folds to mask 5. }
IR := GenIR(SrcSetLiteralArg);
AssertTrue('set literal arg folds to mask 5', Pos('copy 5', IR) > 0);
end;
procedure TSetTests.TestCodegen_SetParam_SpillsAtWordWidth;
var
IR: string;
begin
{ A ≤32-member set parameter is a w; its prologue spill must use storew, not
storel (which QBE rejects for a w operand). }
IR := GenIR(SrcSetLiteralArg);
AssertTrue('set param spilled with storew', Pos('storew %_par_S', IR) > 0);
AssertFalse('set param not spilled with storel', Pos('storel %_par_S', IR) > 0);
end;
initialization
RegisterTest(TSetTests);

View file

@ -1216,13 +1216,21 @@ Sets are value types (copied on assignment), not heap-allocated.
==== Set literals and disambiguation
Set literals use the same `[elem, ...]` bracket notation as array literals.
The semantic pass resolves the ambiguity by examining the assignment target
type: when the LHS is a set type, the bracket expression is analysed as a set
literal; otherwise it is treated as an array literal.
The semantic pass resolves the ambiguity from the target type:
An empty literal `[]` is valid only in a set context (the empty set, bitmask 0).
In an array context, `[]` remains a semantic error (element type cannot be
inferred without type-context propagation).
- *Assignment* — when the LHS is a set type, the bracket expression is analysed
as a set literal; otherwise it is an array literal.
- *Call argument* — a bracket literal passed where a `set of <enum>` parameter
is expected is treated as a set constructor. Overload resolution matches it
when the members share the parameter's base enum (or the literal is empty);
the chosen call then re-types the argument to the parameter's set type so the
bitmask is emitted. This means a set value can be passed inline, e.g.
`Configure([optA, optC])`, without a named intermediate variable.
An empty literal `[]` carries no element type of its own, so it is only valid
where a set target supplies the type — a set assignment or a `set of`
argument. Used anywhere else (e.g. assigned to a non-set variable) it is a
semantic error, not a silent no-op.
==== Set operators