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:
parent
62165dac3d
commit
703e44dfa1
|
|
@ -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]));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue