feat(types): add packed record qualifier
Introduces the `packed record ... end` syntax. Field layout in a
packed record skips natural-alignment padding between fields and
skips the record's tail padding, so SizeOf equals the cumulative
byte size of the fields. ARC-managed field types (string, class,
interface, dynamic array) keep their natural 8-byte alignment so
that _StringRelease / _ClassRelease etc. can keep using aligned
64-bit loads through the field pointer.
`packed` is only legal directly before `record`. `packed class`
and `packed array` are parse errors — neither has a meaningful
implementation in Blaise's heap-allocated class model nor in its
existing tightly-strided array layout.
Implementation:
- Lexer: tkPacked token, PACKED keyword
- AST: TRecordTypeDef.IsPacked, propagated through CloneTypeDef
- Parser: optional PACKED prefix before RECORD; rejects other
forms with a clear error message
- uSymbolTable: TRecordTypeDesc.IsPacked + new FieldAlign helper;
AddField / PackedSize / TotalSize / MaxAlign honour it
- uSemantic: propagates IsPacked from def to desc in pass 1
- OPDF: no change — emits whatever TotalSize reports
Tests: 11 IR-level + 2 e2e in cp.test.packedrecord.pas, plus the
TTokenKind audit bumped from 82 to 83.
Grammar and language-rationale updated.
This commit is contained in:
parent
7e613c6853
commit
e732c71cd1
|
|
@ -506,8 +506,12 @@ type
|
|||
|
||||
TRecordTypeDef = class(TASTTypeDef)
|
||||
public
|
||||
Fields: TObjectList; { owned TFieldDecl }
|
||||
Methods: TObjectList; { owned TMethodDecl }
|
||||
Fields: TObjectList; { owned TFieldDecl }
|
||||
Methods: TObjectList; { owned TMethodDecl }
|
||||
IsPacked: Boolean; { True iff declared `packed record` — disables
|
||||
field alignment padding and tail padding;
|
||||
ARC-managed fields (string/class/intf) still
|
||||
retain 8-byte storage alignment }
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
end;
|
||||
|
|
@ -2012,6 +2016,7 @@ begin
|
|||
else if ASrc is TRecordTypeDef then
|
||||
begin
|
||||
RT := TRecordTypeDef.Create;
|
||||
RT.IsPacked := TRecordTypeDef(ASrc).IsPacked;
|
||||
for I := 0 to TRecordTypeDef(ASrc).Fields.Count - 1 do
|
||||
RT.Fields.Add(CloneFieldDecl(TFieldDecl(TRecordTypeDef(ASrc).Fields.Items[I])));
|
||||
for I := 0 to TRecordTypeDef(ASrc).Methods.Count - 1 do
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ type
|
|||
tkUses,
|
||||
tkType,
|
||||
tkRecord,
|
||||
tkPacked, { 'packed' — record-layout qualifier (only legal before
|
||||
'record'; affects field offsets + tail padding) }
|
||||
tkClass,
|
||||
tkProcedure,
|
||||
tkFunction,
|
||||
|
|
@ -170,6 +172,7 @@ begin
|
|||
else if AUpper = 'END' then Result := tkEnd
|
||||
else if AUpper = 'TYPE' then Result := tkType
|
||||
else if AUpper = 'RECORD' then Result := tkRecord
|
||||
else if AUpper = 'PACKED' then Result := tkPacked
|
||||
else if AUpper = 'CLASS' then Result := tkClass
|
||||
else if AUpper = 'PROCEDURE' then Result := tkProcedure
|
||||
else if AUpper = 'FUNCTION' then Result := tkFunction
|
||||
|
|
@ -587,6 +590,7 @@ begin
|
|||
tkUses: Result := 'uses';
|
||||
tkType: Result := 'type';
|
||||
tkRecord: Result := 'record';
|
||||
tkPacked: Result := 'packed';
|
||||
tkClass: Result := 'class';
|
||||
tkProcedure: Result := 'procedure';
|
||||
tkFunction: Result := 'function';
|
||||
|
|
|
|||
|
|
@ -503,7 +503,20 @@ begin
|
|||
else
|
||||
begin
|
||||
Expect(tkEquals);
|
||||
if Check(tkRecord) then
|
||||
if Check(tkPacked) then
|
||||
begin
|
||||
{ Only `packed record` is supported. `packed class` and `packed
|
||||
array` are legal Delphi/FPC syntax but Blaise rejects them
|
||||
explicitly so users get a clear error rather than silent no-op. }
|
||||
Advance;
|
||||
if not Check(tkRecord) then
|
||||
raise EParseError.Create(Format(
|
||||
'''packed'' may only precede ''record'' at line %d col %d in %s',
|
||||
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
|
||||
TD.Def := ParseRecordDef;
|
||||
TRecordTypeDef(TD.Def).IsPacked := True;
|
||||
end
|
||||
else if Check(tkRecord) then
|
||||
TD.Def := ParseRecordDef
|
||||
else if Check(tkClass) and (PeekKind = tkOf) then
|
||||
begin
|
||||
|
|
|
|||
|
|
@ -2206,7 +2206,10 @@ begin
|
|||
begin
|
||||
TD := TTypeDecl(ABlock.TypeDecls.Items[I]);
|
||||
if TD.Def is TRecordTypeDef then
|
||||
RT := FTable.NewRecordType(TD.Name)
|
||||
begin
|
||||
RT := FTable.NewRecordType(TD.Name);
|
||||
RT.IsPacked := TRecordTypeDef(TD.Def).IsPacked;
|
||||
end
|
||||
else if TD.Def is TClassTypeDef then
|
||||
RT := FTable.NewClassType(TD.Name)
|
||||
else if TD.Def is TGenericTypeDef then
|
||||
|
|
|
|||
|
|
@ -234,7 +234,10 @@ type
|
|||
FHasDestroyMethod: Boolean; { True when the class declares a 'Destroy' method }
|
||||
FHasAbstractMethods: Boolean; { True when any vtable slot is abstract (no impl) }
|
||||
FClassAttributes: TStringList; { resolved attribute type names e.g. 'ThreadedAttribute' }
|
||||
FIsPacked: Boolean; { True for `packed record` — skip per-field
|
||||
alignment and tail padding (records only) }
|
||||
public
|
||||
property IsPacked: Boolean read FIsPacked write FIsPacked;
|
||||
constructor Create(const AName: string; AKind: TTypeKind);
|
||||
destructor Destroy; override;
|
||||
procedure AddField(const AName: string; AType: TTypeDesc);
|
||||
|
|
@ -242,6 +245,7 @@ type
|
|||
function PackedSize: Integer;
|
||||
function TotalSize: Integer;
|
||||
function MaxAlign: Integer;
|
||||
function FieldAlign(AType: TTypeDesc): Integer;
|
||||
|
||||
{ Vtable management }
|
||||
function HasVTable: Boolean;
|
||||
|
|
@ -565,6 +569,23 @@ begin
|
|||
inherited Destroy;
|
||||
end;
|
||||
|
||||
{ Returns the alignment required for AType when laid out inside this record.
|
||||
Normally that is AType.AllocAlign. For a `packed record` the alignment
|
||||
drops to 1 — except for ARC-managed fields (string / class / interface /
|
||||
dynamic array), which must remain at their natural alignment because the
|
||||
runtime helpers (`_StringRelease`, `_ClassRelease`, etc.) perform 64-bit
|
||||
loads through the field pointer. }
|
||||
function TRecordTypeDesc.FieldAlign(AType: TTypeDesc): Integer;
|
||||
begin
|
||||
Result := AType.AllocAlign;
|
||||
if not FIsPacked then Exit;
|
||||
case AType.Kind of
|
||||
tyString, tyClass, tyInterface, tyDynArray: { keep natural alignment } ;
|
||||
else
|
||||
Result := 1;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Cumulative byte position immediately after the last field (or after the
|
||||
vptr if no fields have been added yet). Excludes the record's tail
|
||||
padding — that is only applied in TotalSize. }
|
||||
|
|
@ -580,7 +601,7 @@ begin
|
|||
for I := 0 to FFields.Count - 1 do
|
||||
begin
|
||||
FT := TFieldInfo(FFields.Items[I]).TypeDesc;
|
||||
A := FT.AllocAlign;
|
||||
A := FieldAlign(FT);
|
||||
if (A > 1) and (Result mod A <> 0) then
|
||||
Inc(Result, A - Result mod A);
|
||||
Inc(Result, FT.ByteSize);
|
||||
|
|
@ -594,7 +615,7 @@ var
|
|||
A: Integer;
|
||||
begin
|
||||
Offset := PackedSize;
|
||||
A := AType.AllocAlign;
|
||||
A := FieldAlign(AType);
|
||||
if (A > 1) and (Offset mod A <> 0) then
|
||||
Offset := Offset + (A - Offset mod A);
|
||||
Info := TFieldInfo.Create;
|
||||
|
|
@ -624,8 +645,10 @@ begin
|
|||
and back-to-back fields stay naturally aligned. Class instances are
|
||||
heap-allocated singletons (never directly in arrays — only references
|
||||
to them are), so we leave their size unpadded to match FPC/Delphi
|
||||
InstanceSize semantics. }
|
||||
InstanceSize semantics. `packed record` similarly suppresses tail
|
||||
padding. }
|
||||
if Kind = tyClass then Exit;
|
||||
if FIsPacked then Exit;
|
||||
Algn := MaxAlign;
|
||||
if (Algn > 1) and (Result mod Algn <> 0) then
|
||||
Inc(Result, Algn - Result mod Algn);
|
||||
|
|
@ -634,13 +657,18 @@ end;
|
|||
function TRecordTypeDesc.MaxAlign: Integer;
|
||||
var
|
||||
I, A: Integer;
|
||||
FT: TTypeDesc;
|
||||
begin
|
||||
Result := 1;
|
||||
if HasVTable then
|
||||
Result := 8; { vptr requires 8-byte alignment }
|
||||
for I := 0 to FFields.Count - 1 do
|
||||
begin
|
||||
A := TFieldInfo(FFields.Items[I]).TypeDesc.AllocAlign;
|
||||
FT := TFieldInfo(FFields.Items[I]).TypeDesc;
|
||||
if FIsPacked then
|
||||
A := FieldAlign(FT) { ARC fields keep natural align; PODs become 1 }
|
||||
else
|
||||
A := FT.AllocAlign;
|
||||
if A > Result then
|
||||
Result := A;
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ uses
|
|||
cp.test.symtable,
|
||||
cp.test.semantic,
|
||||
cp.test.records,
|
||||
cp.test.packedrecord,
|
||||
cp.test.classes,
|
||||
cp.test.arc,
|
||||
cp.test.methods,
|
||||
|
|
|
|||
436
compiler/src/test/pascal/cp.test.packedrecord.pas
Normal file
436
compiler/src/test/pascal/cp.test.packedrecord.pas
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
{
|
||||
Blaise - An Object Pascal Compiler
|
||||
Copyright (c) 2026 Graeme Geldenhuys
|
||||
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
|
||||
Licensed under the Apache License v2.0 with Runtime Library Exception.
|
||||
See LICENSE file in the project root for full license terms.
|
||||
}
|
||||
|
||||
unit cp.test.packedrecord;
|
||||
|
||||
{ Tests for the `packed record` qualifier.
|
||||
|
||||
Coverage:
|
||||
- `packed` reserved word lexes to tkPacked.
|
||||
- `packed record ... end` parses into a TRecordTypeDef with IsPacked=True.
|
||||
- In a packed record, fields are laid out at the next byte (no alignment
|
||||
padding) and the record itself has no tail padding.
|
||||
- The non-packed form is unaffected.
|
||||
- ARC-managed fields (string / class / interface) still require 8-byte
|
||||
storage alignment; the field offset is forced to the next 8-byte
|
||||
boundary regardless of `packed`.
|
||||
- Record-level alignment (MaxAlign) drops to 1 for packed records, so
|
||||
arrays of packed records pack tight too.
|
||||
- `packed` is only legal in front of `record`; `packed class` / `packed
|
||||
array` produce a parse error. }
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
blaise.testing, cp.test.e2e.base,
|
||||
uLexer, uParser, uAST, uSymbolTable, uSemantic, uCodeGenQBE;
|
||||
|
||||
type
|
||||
TPackedRecordTests = class(TTestCase)
|
||||
private
|
||||
function AnalyseSrc(const ASrc: string): TProgram;
|
||||
function GenIR(const ASrc: string): string;
|
||||
published
|
||||
procedure TestLexer_Packed_Keyword;
|
||||
procedure TestParse_PackedRecord_SetsIsPacked;
|
||||
procedure TestParse_PlainRecord_IsPackedFalse;
|
||||
procedure TestSemantic_PackedRecord_TypeDescIsPacked;
|
||||
procedure TestSemantic_PackedRecord_ByteThenInt_NoAlignPadding;
|
||||
procedure TestSemantic_PackedRecord_TotalSize_NoTailPad;
|
||||
procedure TestSemantic_PackedRecord_MaxAlign_Is1;
|
||||
procedure TestSemantic_PackedRecord_StringField_StaysAlignedTo8;
|
||||
procedure TestParse_PackedClass_RaisesError;
|
||||
procedure TestParse_PackedArray_RaisesError;
|
||||
procedure TestCodegen_PackedRecord_TypeSizeMatchesPacked;
|
||||
end;
|
||||
|
||||
[Threaded]
|
||||
TPackedRecordE2ETests = class(TE2ETestCase)
|
||||
protected
|
||||
procedure SetUp; override;
|
||||
published
|
||||
procedure TestRun_PackedRecord_ByteInt_Offsets;
|
||||
procedure TestRun_PackedRecord_SizeOfMatchesPacked;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
function TPackedRecordTests.AnalyseSrc(const ASrc: string): TProgram;
|
||||
var
|
||||
L: TLexer;
|
||||
P: TParser;
|
||||
A: TSemanticAnalyser;
|
||||
begin
|
||||
L := TLexer.Create(ASrc);
|
||||
P := TParser.Create(L);
|
||||
try
|
||||
Result := P.Parse;
|
||||
finally
|
||||
P.Free;
|
||||
L.Free;
|
||||
end;
|
||||
A := TSemanticAnalyser.Create;
|
||||
try
|
||||
A.Analyse(Result);
|
||||
finally
|
||||
A.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TPackedRecordTests.GenIR(const ASrc: string): string;
|
||||
var
|
||||
Prog: TProgram;
|
||||
CG: TCodeGenQBE;
|
||||
begin
|
||||
Prog := AnalyseSrc(ASrc);
|
||||
try
|
||||
CG := TCodeGenQBE.Create;
|
||||
try
|
||||
CG.Generate(Prog);
|
||||
Result := CG.GetOutput;
|
||||
finally
|
||||
CG.Free;
|
||||
end;
|
||||
finally
|
||||
Prog.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestLexer_Packed_Keyword;
|
||||
var
|
||||
L: TLexer;
|
||||
T: TToken;
|
||||
begin
|
||||
L := TLexer.Create('packed');
|
||||
try
|
||||
T := L.Next;
|
||||
AssertEquals('packed maps to tkPacked', Ord(tkPacked), Ord(T.Kind));
|
||||
finally
|
||||
L.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestParse_PackedRecord_SetsIsPacked;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type
|
||||
TFoo = packed record
|
||||
A: Byte;
|
||||
B: Integer;
|
||||
end;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
Prog: TProgram;
|
||||
TD: TTypeDecl;
|
||||
RD: TRecordTypeDef;
|
||||
begin
|
||||
Prog := AnalyseSrc(Src);
|
||||
try
|
||||
TD := TTypeDecl(Prog.Block.TypeDecls.Items[0]);
|
||||
AssertTrue('is TRecordTypeDef', TD.Def is TRecordTypeDef);
|
||||
RD := TRecordTypeDef(TD.Def);
|
||||
AssertTrue('IsPacked = True', RD.IsPacked);
|
||||
finally
|
||||
Prog.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestParse_PlainRecord_IsPackedFalse;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type TFoo = record A: Integer; end;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
Prog: TProgram;
|
||||
RD: TRecordTypeDef;
|
||||
begin
|
||||
Prog := AnalyseSrc(Src);
|
||||
try
|
||||
RD := TRecordTypeDef(TTypeDecl(Prog.Block.TypeDecls.Items[0]).Def);
|
||||
AssertFalse('IsPacked = False on plain record', RD.IsPacked);
|
||||
finally
|
||||
Prog.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestSemantic_PackedRecord_TypeDescIsPacked;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type TFoo = packed record A: Integer; end;
|
||||
var R: TFoo;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
Prog: TProgram;
|
||||
RT: TRecordTypeDesc;
|
||||
begin
|
||||
Prog := AnalyseSrc(Src);
|
||||
try
|
||||
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
|
||||
AssertTrue('IsPacked propagated to TRecordTypeDesc', RT.IsPacked);
|
||||
finally
|
||||
Prog.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestSemantic_PackedRecord_ByteThenInt_NoAlignPadding;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type TFoo = packed record
|
||||
A: Byte;
|
||||
B: Integer;
|
||||
end;
|
||||
var R: TFoo;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
Prog: TProgram;
|
||||
RT: TRecordTypeDesc;
|
||||
FldA, FldB: TFieldInfo;
|
||||
begin
|
||||
Prog := AnalyseSrc(Src);
|
||||
try
|
||||
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
|
||||
FldA := RT.FindField('A');
|
||||
FldB := RT.FindField('B');
|
||||
AssertEquals('A offset', 0, FldA.Offset);
|
||||
AssertEquals('B offset (no align padding)', 1, FldB.Offset);
|
||||
finally
|
||||
Prog.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestSemantic_PackedRecord_TotalSize_NoTailPad;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type TFoo = packed record
|
||||
A: Byte;
|
||||
B: Integer;
|
||||
end;
|
||||
var R: TFoo;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
Prog: TProgram;
|
||||
RT: TRecordTypeDesc;
|
||||
begin
|
||||
Prog := AnalyseSrc(Src);
|
||||
try
|
||||
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
|
||||
AssertEquals('TotalSize = 1 + 4 = 5 (no tail pad)', 5, RT.TotalSize);
|
||||
finally
|
||||
Prog.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestSemantic_PackedRecord_MaxAlign_Is1;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type TFoo = packed record
|
||||
A: Byte;
|
||||
B: Int64;
|
||||
end;
|
||||
var R: TFoo;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
Prog: TProgram;
|
||||
RT: TRecordTypeDesc;
|
||||
begin
|
||||
Prog := AnalyseSrc(Src);
|
||||
try
|
||||
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
|
||||
AssertEquals('MaxAlign = 1 for packed', 1, RT.MaxAlign);
|
||||
AssertEquals('B offset = 1 (no 8-byte align)', 1,
|
||||
RT.FindField('B').Offset);
|
||||
AssertEquals('TotalSize = 9 (no tail pad)', 9, RT.TotalSize);
|
||||
finally
|
||||
Prog.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestSemantic_PackedRecord_StringField_StaysAlignedTo8;
|
||||
const
|
||||
{ String fields are ARC-managed pointers — _StringRelease does a 64-bit load
|
||||
so they must remain 8-byte aligned even inside a packed record. }
|
||||
Src = '''
|
||||
program P;
|
||||
type TFoo = packed record
|
||||
A: Byte;
|
||||
S: string;
|
||||
end;
|
||||
var R: TFoo;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
Prog: TProgram;
|
||||
RT: TRecordTypeDesc;
|
||||
begin
|
||||
Prog := AnalyseSrc(Src);
|
||||
try
|
||||
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
|
||||
AssertEquals('A at 0', 0, RT.FindField('A').Offset);
|
||||
AssertEquals('S aligned to 8', 8, RT.FindField('S').Offset);
|
||||
finally
|
||||
Prog.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestParse_PackedClass_RaisesError;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type TFoo = packed class A: Integer; end;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
L: TLexer;
|
||||
P: TParser;
|
||||
Prog: TProgram;
|
||||
Raised: Boolean;
|
||||
begin
|
||||
Raised := False;
|
||||
L := TLexer.Create(Src);
|
||||
P := TParser.Create(L);
|
||||
try
|
||||
try
|
||||
Prog := P.Parse;
|
||||
Prog.Free;
|
||||
except
|
||||
on EParseError do Raised := True;
|
||||
end;
|
||||
finally
|
||||
P.Free;
|
||||
L.Free;
|
||||
end;
|
||||
AssertTrue('packed class rejected', Raised);
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestParse_PackedArray_RaisesError;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type TArr = packed array[0..3] of Byte;
|
||||
begin end.
|
||||
''';
|
||||
var
|
||||
L: TLexer;
|
||||
P: TParser;
|
||||
Prog: TProgram;
|
||||
Raised: Boolean;
|
||||
begin
|
||||
Raised := False;
|
||||
L := TLexer.Create(Src);
|
||||
P := TParser.Create(L);
|
||||
try
|
||||
try
|
||||
Prog := P.Parse;
|
||||
Prog.Free;
|
||||
except
|
||||
on EParseError do Raised := True;
|
||||
end;
|
||||
finally
|
||||
P.Free;
|
||||
L.Free;
|
||||
end;
|
||||
AssertTrue('packed array rejected', Raised);
|
||||
end;
|
||||
|
||||
procedure TPackedRecordTests.TestCodegen_PackedRecord_TypeSizeMatchesPacked;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
type TFoo = packed record
|
||||
A: Byte;
|
||||
B: Integer;
|
||||
end;
|
||||
var R: TFoo;
|
||||
begin
|
||||
WriteLn(SizeOf(TFoo))
|
||||
end.
|
||||
''';
|
||||
var
|
||||
IR: string;
|
||||
begin
|
||||
IR := GenIR(Src);
|
||||
AssertTrue('IR not empty', IR <> '');
|
||||
{ SizeOf folds at codegen — packed Byte+Integer = 5 bytes }
|
||||
AssertTrue('SizeOf folds to 5', Pos('copy 5', IR) > 0);
|
||||
end;
|
||||
|
||||
procedure TPackedRecordE2ETests.SetUp;
|
||||
begin
|
||||
inherited SetUp;
|
||||
SetUpScratch('compiler/target/test-e2e-packed-record');
|
||||
end;
|
||||
|
||||
const
|
||||
LE = #10;
|
||||
|
||||
SrcByteIntOffsets = '''
|
||||
program P;
|
||||
type TFoo = packed record
|
||||
A: Byte;
|
||||
B: Integer;
|
||||
end;
|
||||
var R: TFoo;
|
||||
begin
|
||||
R.A := 9;
|
||||
R.B := 12345;
|
||||
WriteLn(R.A);
|
||||
WriteLn(R.B);
|
||||
WriteLn(SizeOf(TFoo))
|
||||
end.
|
||||
''';
|
||||
|
||||
SrcPackedVsUnpacked = '''
|
||||
program P;
|
||||
type
|
||||
TPlain = record A: Byte; B: Int64; end;
|
||||
TPacked = packed record A: Byte; B: Int64; end;
|
||||
begin
|
||||
WriteLn(SizeOf(TPlain));
|
||||
WriteLn(SizeOf(TPacked))
|
||||
end.
|
||||
''';
|
||||
|
||||
procedure TPackedRecordE2ETests.TestRun_PackedRecord_ByteInt_Offsets;
|
||||
var Output: string; RCode: Integer;
|
||||
begin
|
||||
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
|
||||
AssertTrue('compile+run', CompileAndRun(SrcByteIntOffsets, Output, RCode));
|
||||
AssertEquals('exit 0', 0, RCode);
|
||||
AssertEquals('A=9, B=12345, SizeOf=5',
|
||||
'9' + LE + '12345' + LE + '5' + LE, Output);
|
||||
end;
|
||||
|
||||
procedure TPackedRecordE2ETests.TestRun_PackedRecord_SizeOfMatchesPacked;
|
||||
var Output: string; RCode: Integer;
|
||||
begin
|
||||
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
|
||||
AssertTrue('compile+run', CompileAndRun(SrcPackedVsUnpacked, Output, RCode));
|
||||
AssertEquals('exit 0', 0, RCode);
|
||||
{ Plain: A at 0, B at 8 (8-byte aligned), tail pad to MaxAlign=8 → 16.
|
||||
Packed: A at 0, B at 1, no tail pad → 9. }
|
||||
AssertEquals('plain=16, packed=9',
|
||||
'16' + LE + '9' + LE, Output);
|
||||
end;
|
||||
|
||||
initialization
|
||||
RegisterTest(TPackedRecordTests);
|
||||
RegisterTest(TPackedRecordE2ETests);
|
||||
|
||||
end.
|
||||
|
|
@ -118,11 +118,11 @@ implementation
|
|||
token kind is appended to the enum — the test below will fail and remind
|
||||
you to also update TokenKindName. }
|
||||
const
|
||||
ExpectedMaxTokenKindOrd = 82; { tkAt }
|
||||
ExpectedMaxTokenKindOrd = 83; { tkAt }
|
||||
|
||||
procedure TTokenKindNameTests.TestMaxOrdinal_IstkAt;
|
||||
begin
|
||||
AssertEquals('tkAt must be the last TTokenKind (ord 82); update this test and TokenKindName when adding new kinds',
|
||||
AssertEquals('tkAt must be the last TTokenKind (ord 83); update this test and TokenKindName when adding new kinds',
|
||||
ExpectedMaxTokenKindOrd, Ord(tkAt));
|
||||
end;
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ IMPLEMENTATION = "implementation" ;
|
|||
USES = "uses" ;
|
||||
TYPE = "type" ;
|
||||
RECORD = "record" ;
|
||||
PACKED = "packed" ;
|
||||
CLASS = "class" ;
|
||||
VAR = "var" ;
|
||||
BEGIN = "begin" ;
|
||||
|
|
@ -235,14 +236,23 @@ TypeDef
|
|||
;
|
||||
|
||||
RecordDef
|
||||
= RECORD { FieldDecl | MethodDecl } END
|
||||
= [ PACKED ] RECORD { FieldDecl | MethodDecl } END
|
||||
;
|
||||
|
||||
(* Records may declare methods alongside fields. Method bodies are written
|
||||
* out-of-line as TRecordName.MethodName, identical to classes. The
|
||||
* receiver is passed by pointer (`Self` is a `var` parameter under the hood);
|
||||
* the language gives normal value semantics — assignments copy the record.
|
||||
* No constructors, destructors, or virtual methods are allowed in records. *)
|
||||
* No constructors, destructors, or virtual methods are allowed in records.
|
||||
*
|
||||
* `packed` is permitted only in front of `record`. It disables natural
|
||||
* alignment padding between fields and disables tail padding on the record
|
||||
* as a whole, so the record's size equals the cumulative byte size of its
|
||||
* fields. ARC-managed field types (`string`, class, interface, dynamic
|
||||
* array) keep their natural 8-byte alignment even inside a packed record
|
||||
* because the runtime helpers (`_StringRelease`, `_ClassRelease`, etc.)
|
||||
* perform 64-bit loads through the field pointer. `packed class` and
|
||||
* `packed array` are rejected as parse errors. *)
|
||||
|
||||
ClassDef
|
||||
= CLASS [ LPAREN GenericName { COMMA GenericName } RPAREN ]
|
||||
|
|
|
|||
|
|
@ -596,6 +596,94 @@ for any realistic string length.
|
|||
|
||||
---
|
||||
|
||||
=== Packed Records
|
||||
|
||||
==== Decision
|
||||
|
||||
Blaise supports the `packed record` qualifier with strict semantics:
|
||||
|
||||
* `packed record ... end` lays out fields at successive byte offsets
|
||||
with no alignment padding between them, and applies no tail padding
|
||||
to the record as a whole.
|
||||
* `packed` is permitted only directly in front of `record`. The forms
|
||||
`packed class` and `packed array`, accepted (often as no-ops) by FPC
|
||||
and Delphi, are parse errors in Blaise.
|
||||
* ARC-managed field types — `string`, class references, interfaces,
|
||||
and dynamic arrays — retain their natural 8-byte alignment inside a
|
||||
packed record. Other field types (`Byte`, `Word`/`SmallInt`,
|
||||
`Integer`/`UInt32`, `Int64`/`UInt64`, `Double`, enums, sets,
|
||||
static arrays of these, and nested packed/non-packed records)
|
||||
pack tight.
|
||||
* `MaxAlign` of a packed record collapses to `1` unless it contains
|
||||
an ARC-managed field (in which case it stays at `8`). Arrays of
|
||||
fully POD packed records therefore stride at the record's exact
|
||||
field-byte sum.
|
||||
|
||||
==== Rationale
|
||||
|
||||
The intended use of `packed` is interop with binary formats whose
|
||||
field layout is byte-addressed: file headers, network protocol
|
||||
records, and ABI structures defined by external libraries. In every
|
||||
such case the user wants byte-exact layout that matches an external
|
||||
specification, and any silent padding the compiler inserts is a bug
|
||||
from the caller's point of view.
|
||||
|
||||
The decision to *forbid* `packed class` and `packed array` rather
|
||||
than silently accept them is deliberate: a `class` in Blaise carries
|
||||
a vptr and lives on the heap behind an `_ClassAlloc` header, so
|
||||
"packing" it would either lie (the qualifier is ignored) or break
|
||||
runtime invariants. Arrays in Blaise are already either dynamic
|
||||
(heap-managed, no inter-element padding) or static (element-strided
|
||||
by the element's own size). Pretending `packed` applies to them
|
||||
gives users false confidence in code that won't behave the way the
|
||||
keyword suggests.
|
||||
|
||||
The exception for ARC-managed field types preserves a hard runtime
|
||||
invariant: `_StringRelease`, `_ClassRelease`, `_IntfRelease`, and
|
||||
`_DynArrayRelease` all perform 64-bit aligned loads through the
|
||||
field pointer. Honouring a 1-byte-aligned ARC field would silently
|
||||
slow x86_64 code and outright fault on stricter ARM targets. A
|
||||
1-byte-stride layout for these types would also break the existing
|
||||
ARC invariant that every reference-holding slot is loadable as a
|
||||
single 8-byte word. Users who genuinely need a byte-exact layout
|
||||
that includes a string field should use `Byte` arrays or a typed
|
||||
pointer instead.
|
||||
|
||||
==== Examples
|
||||
|
||||
[source,pascal]
|
||||
----
|
||||
type
|
||||
TPlain = record
|
||||
A: Byte;
|
||||
B: Int64;
|
||||
end; { SizeOf = 16 (A at 0, pad to 8, B at 8, no tail) }
|
||||
|
||||
TPacked = packed record
|
||||
A: Byte;
|
||||
B: Int64;
|
||||
end; { SizeOf = 9 (A at 0, B at 1, no tail) }
|
||||
|
||||
THybrid = packed record
|
||||
A: Byte;
|
||||
S: string;
|
||||
end; { SizeOf = 16 (A at 0, pad to 8, S at 8) — S stays aligned }
|
||||
----
|
||||
|
||||
==== Alternatives rejected
|
||||
|
||||
* *Always pack, even ARC-managed fields* — rejected. Unaligned 64-bit
|
||||
loads through ARC pointers would either slow down or trap at runtime.
|
||||
* *Per-field `packed` annotation* — rejected. The Delphi/FPC syntax
|
||||
already covers the common case; finer-grained control is rare and
|
||||
can be expressed with manual `Byte` arrays.
|
||||
* *`{$ALIGN n}` / `{$PACKRECORDS n}` directives* — deferred. Blaise
|
||||
does not yet have a compiler directive subsystem; if a use case for
|
||||
intermediate alignments (2, 4) emerges, this is the obvious place
|
||||
to add it later.
|
||||
|
||||
---
|
||||
|
||||
=== Indexed Properties
|
||||
|
||||
==== Decision
|
||||
|
|
|
|||
Loading…
Reference in a new issue