feat(types): add SmallInt / Word 16-bit integer types

Introduces tySmallInt and tyWord first-class types alongside the
existing Integer / Int64 / Byte / UInt32 / UInt64 family.  Storage is
2 bytes (storeh / loadsh / loaduh) but values are widened to QBE 'w'
in registers, matching the Byte pattern.  Int16 and UInt16 are
accepted as aliases.

Implicit widening into Integer, Int64, UInt32 and UInt64 is permitted;
all 16-bit values fit losslessly in those wider types.

Tests: 11 IR-level + 5 e2e in cp.test.smallint_word.pas.

Grammar and language-rationale updated to remove the "deferred" note
on SmallInt/Word.
This commit is contained in:
Graeme Geldenhuys 2026-05-22 08:51:21 +01:00
parent b43a999f80
commit 5d092cafee
8 changed files with 542 additions and 56 deletions

View file

@ -596,7 +596,8 @@ end;
function TCodeGenQBE.QbeTypeOf(AType: TTypeDesc): string;
begin
case AType.Kind of
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum: Result := 'w';
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum,
tySmallInt, tyWord: Result := 'w';
tySet: if TSetTypeDesc(AType).BitCount <= 32 then Result := 'w' else Result := 'l';
tyInt64, tyUInt64, tyString: Result := 'l';
tyRecord: Result := 'l'; { pointer to aggregate }
@ -623,6 +624,8 @@ function TCodeGenQBE.LoadInstrFor(AType: TTypeDesc): string;
begin
case AType.Kind of
tyByte, tyBoolean: Result := 'loadub';
tySmallInt: Result := 'loadsh';
tyWord: Result := 'loaduh';
tyInteger, tyUInt32, tyEnum: Result := 'loadw';
tySet: if TSetTypeDesc(AType).BitCount <= 32 then Result := 'loadw' else Result := 'loadl';
tyDouble: Result := 'loadd';
@ -638,6 +641,7 @@ function TCodeGenQBE.StoreInstrFor(AType: TTypeDesc): string;
begin
case AType.Kind of
tyByte, tyBoolean: Result := 'storeb';
tySmallInt, tyWord: Result := 'storeh';
tyInteger, tyUInt32, tyEnum: Result := 'storew';
tySet: if TSetTypeDesc(AType).BitCount <= 32 then Result := 'storew' else Result := 'storel';
tyDouble: Result := 'stored';
@ -775,6 +779,7 @@ begin
Result := AKind in [
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum,
tyInt64, tyUInt64,
tySmallInt, tyWord,
tyDouble, tySingle,
tySet,
tyPointer, tyPChar
@ -784,7 +789,8 @@ end;
function TCodeGenQBE.PromotedQType(AKind: TTypeKind; AType: TTypeDesc): string;
begin
case AKind of
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum: Result := 'w';
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum,
tySmallInt, tyWord: Result := 'w';
tyInt64, tyUInt64, tyString, tyClass, tyPointer, tyPChar, tyMetaClass: Result := 'l';
tyDouble: Result := 'd';
tySingle: Result := 's';
@ -1466,7 +1472,8 @@ begin
end;
case Decl.ResolvedType.Kind of
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum:
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum,
tySmallInt, tyWord:
begin
EmitLine(Format(' %%_var_%s =l alloc4 1', [VarName]));
EmitLine(Format(' storew 0, %%_var_%s', [VarName]));
@ -1612,6 +1619,8 @@ begin
case Decl.ResolvedType.Kind of
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum:
EmitLine(Format('export data $%s = { w 0 }', [VarName]));
tySmallInt, tyWord:
EmitLine(Format('export data $%s = { h 0 }', [VarName]));
tySet:
if TSetTypeDesc(Decl.ResolvedType).BitCount <= 32 then
EmitLine(Format('export data $%s = { w 0 }', [VarName]))
@ -2388,6 +2397,8 @@ begin
ElemSize := SAT.ElementType.RawSize;
case SAT.ElementType.Kind of
tyByte, tyBoolean: QLoad := 'loadub';
tySmallInt: QLoad := 'loadsh';
tyWord: QLoad := 'loaduh';
tyInteger, tyUInt32, tyEnum: QLoad := 'loadw';
else
QLoad := 'loadl';
@ -2513,6 +2524,8 @@ begin
ElemSize := DAT.ElementType.RawSize;
case DAT.ElementType.Kind of
tyByte, tyBoolean: QLoad := 'loadub';
tySmallInt: QLoad := 'loadsh';
tyWord: QLoad := 'loaduh';
tyInteger, tyUInt32, tyEnum: QLoad := 'loadw';
else
QLoad := 'loadl';
@ -4543,7 +4556,8 @@ begin
end
else
case Par.ResolvedType.Kind of
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum:
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum,
tySmallInt, tyWord:
begin
EmitLine(Format(' %%_var_%s =l alloc4 1', [Par.ParamName]));
EmitLine(Format(' storew %%_par_%s, %%_var_%s',
@ -5338,7 +5352,8 @@ begin
else
begin
case Par.ResolvedType.Kind of
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum:
tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum,
tySmallInt, tyWord:
begin
EmitLine(Format(' %%_var_%s =l alloc4 1', [Par.ParamName]));
EmitLine(Format(' storew %%_par_%s, %%_var_%s',
@ -6825,13 +6840,15 @@ begin
begin
ArgTemp := EmitExpr(TASTExpr(TArrayLiteralExpr(FC.Args.Items[1]).Elements.Items[I]));
IsIntArg := TASTExpr(TArrayLiteralExpr(FC.Args.Items[1]).Elements.Items[I]).ResolvedType.Kind in
[tyInteger, tyBoolean, tyByte, tyUInt32, tyInt64, tyUInt64, tyEnum];
[tyInteger, tyBoolean, tyByte, tyUInt32, tyInt64, tyUInt64,
tySmallInt, tyWord, tyEnum];
end
else
begin
ArgTemp := EmitExpr(TASTExpr(FC.Args.Items[I + 1]));
IsIntArg := TASTExpr(FC.Args.Items[I + 1]).ResolvedType.Kind in
[tyInteger, tyBoolean, tyByte, tyUInt32, tyInt64, tyUInt64, tyEnum];
[tyInteger, tyBoolean, tyByte, tyUInt32, tyInt64, tyUInt64,
tySmallInt, tyWord, tyEnum];
end;
FmtSlotTemp := AllocTemp;
EmitLine(Format(' %s =l add %s, %d', [FmtSlotTemp, FmtArrTemp, I * 16]));
@ -7238,6 +7255,10 @@ begin
if FC.ResolvedType.Kind = tyByte then
{ Byte(X): truncate to 8 bits — mask to [0..255] }
EmitLine(Format(' %s =w and %s, 255', [T, ArgTemp]))
else if FC.ResolvedType.Kind in [tySmallInt, tyWord] then
{ SmallInt(X) / Word(X): truncate to 16 bits. Sign extension on
read is the load instruction's job (loadsh vs loaduh). }
EmitLine(Format(' %s =w and %s, 65535', [T, ArgTemp]))
else if QType = 'w' then
EmitLine(Format(' %s =w copy %s', [T, ArgTemp]))
else
@ -8061,12 +8082,7 @@ begin
EmitLine(Format(' %s =l add $%s, %s', [L, FldAccess.ConstArraySymbol, Ptr]));
QType := QbeTypeOf(SAT.ElementType);
T := AllocTemp;
case SAT.ElementType.Kind of
tyByte, tyBoolean: LoadInstr := 'loadub';
tyInteger, tyUInt32, tyEnum: LoadInstr := 'loadw';
else
LoadInstr := 'loadl';
end;
LoadInstr := LoadInstrFor(SAT.ElementType);
EmitLine(Format(' %s =%s %s %s', [T, QType, LoadInstr, L]));
Result := T;
Exit;
@ -9688,14 +9704,8 @@ begin
Result := ElemPtr;
Exit;
end;
case SAT.ElementType.Kind of
tyByte, tyBoolean: QLoad := 'loadub';
tyInteger, tyUInt32, tyEnum: QLoad := 'loadw';
tyInt64, tyUInt64, tyString, tyClass, tyPointer, tyPChar, tyMetaClass: QLoad := 'loadl';
else
QLoad := 'loadl';
end;
QType := QbeTypeOf(SAT.ElementType);
QLoad := LoadInstrFor(SAT.ElementType);
QType := QbeTypeOf(SAT.ElementType);
ByteVal := AllocTemp;
EmitLine(Format(' %s =%s %s %s', [ByteVal, QType, QLoad, ElemPtr]));
Result := ByteVal;
@ -9727,13 +9737,7 @@ begin
if AExpr.StrExpr.ResolvedType.Kind = tyDynArray then
begin
ElemSize := TDynArrayTypeDesc(AExpr.StrExpr.ResolvedType).ElementType.RawSize;
case TDynArrayTypeDesc(AExpr.StrExpr.ResolvedType).ElementType.Kind of
tyByte, tyBoolean: QLoad := 'loadub';
tyInteger, tyUInt32, tyEnum: QLoad := 'loadw';
tyInt64, tyUInt64, tyString, tyClass, tyPointer, tyPChar, tyMetaClass: QLoad := 'loadl';
else
QLoad := 'loadl';
end;
QLoad := LoadInstrFor(TDynArrayTypeDesc(AExpr.StrExpr.ResolvedType).ElementType);
QType := QbeTypeOf(TDynArrayTypeDesc(AExpr.StrExpr.ResolvedType).ElementType);
StrPtr := EmitExpr(AExpr.StrExpr);
IdxW := EmitExpr(AExpr.IndexExpr);
@ -9966,6 +9970,7 @@ begin
EmitLine(Format(' %s =l add %s, %s', [ElemPtr, PCharBase, Offset]));
case ElemType.Kind of
tyByte, tyBoolean: StoreInstr := 'storeb';
tySmallInt, tyWord: StoreInstr := 'storeh';
tyInteger, tyUInt32, tyEnum: StoreInstr := 'storew';
else
begin
@ -10035,6 +10040,7 @@ begin
ElemVal := EmitExpr(AStmt.ValueExpr);
case ElemType.Kind of
tyByte, tyBoolean: StoreInstr := 'storeb';
tySmallInt, tyWord: StoreInstr := 'storeh';
tyInteger, tyUInt32, tyEnum: StoreInstr := 'storew';
tyInt64, tyUInt64, tyString, tyClass, tyPointer, tyPChar, tyMetaClass: StoreInstr := 'storel';
else

View file

@ -298,7 +298,8 @@ begin
CName := CanonicalName(AType);
if HasBeenEmitted(CName) then Exit;
case AType.Kind of
tyInteger, tyInt64, tyUInt32, tyUInt64, tyByte, tyBoolean:
tyInteger, tyInt64, tyUInt32, tyUInt64,
tySmallInt, tyWord, tyByte, tyBoolean:
EmitPrimitive(AType);
tyString:
EmitUtf8Str;
@ -334,7 +335,8 @@ begin
SzB := AType.RawSize;
case AType.Kind of
tyBoolean: begin SubKind := SK_BOOLEAN; IsSigned := 0; end;
tyInt64, tyInteger:begin SubKind := SK_INTEGER; IsSigned := 1; end;
tyInt64, tyInteger, tySmallInt:
begin SubKind := SK_INTEGER; IsSigned := 1; end;
else
SubKind := SK_INTEGER; IsSigned := 0;
end;

View file

@ -331,7 +331,8 @@ begin
if SameText(AConstraint, 'record') then
begin
if not (ArgType.Kind in [tyRecord, tyInteger, tyInt64, tyUInt32, tyUInt64,
tyByte, tyBoolean, tyString, tyPointer]) then
tySmallInt, tyWord, tyByte, tyBoolean, tyString,
tyPointer]) then
raise ESemanticError.Create(Format(
'Type ''%s'' does not satisfy constraint ''%s: record'' in %s',
[AArgName, AParamName, AContext]));
@ -3503,7 +3504,8 @@ begin
begin
K := ADecl.ResolvedReturnType.Kind;
if not (K in [tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum,
tyInt64, tyUInt64, tyDouble, tySingle, tyPointer, tyPChar]) then
tyInt64, tyUInt64, tySmallInt, tyWord,
tyDouble, tySingle, tyPointer, tyPChar]) then
Exit;
end;
@ -3515,7 +3517,8 @@ begin
if Par.ResolvedType = nil then Exit;
K := Par.ResolvedType.Kind;
if not (K in [tyInteger, tyUInt32, tyBoolean, tyByte, tyEnum,
tyInt64, tyUInt64, tyDouble, tySingle, tyPointer, tyPChar]) then
tyInt64, tyUInt64, tySmallInt, tyWord,
tyDouble, tySingle, tyPointer, tyPChar]) then
Exit;
end;
@ -4567,6 +4570,8 @@ begin
tyInt64: Base := 'l';
tyUInt32: Base := 'u';
tyUInt64: Base := 'Q';
tySmallInt: Base := 'h';
tyWord: Base := 'H';
tyByte: Base := 'y';
tyBoolean: Base := 'b';
tyDouble: Base := 'd';
@ -4770,7 +4775,8 @@ begin
when both are candidates. }
if (AArgExpr is TIntLiteral) and AParam.IsNumeric then
begin
if AParam.Kind in [tyInteger, tyInt64, tyUInt32, tyUInt64, tyByte] then
if AParam.Kind in [tyInteger, tyInt64, tyUInt32, tyUInt64,
tySmallInt, tyWord, tyByte] then
Result := 2
else
Result := 1; { Double, Single — widening }

View file

@ -23,6 +23,8 @@ type
tyInt64, { Int64 — QBE 'l' }
tyUInt32, { UInt32 — QBE 'w' unsigned }
tyUInt64, { UInt64 / QWord — QBE 'l' unsigned }
tySmallInt, { SmallInt / Int16 — QBE 'h' (signed half-word) }
tyWord, { Word / UInt16 — QBE 'h' (unsigned half-word) }
tyByte, { Byte — QBE 'b' }
tyBoolean, { Boolean — stored as byte, 0/1 only }
tyString, { ARC-managed UTF-8 string }
@ -349,11 +351,13 @@ type
FAllTypes: TObjectList; { owned TTypeDesc }
FGenerics: TStringList; { non-owning: base name → TGenericTypeDef* (void ptr) }
FTypeInteger: TTypeDesc;
FTypeInt64: TTypeDesc;
FTypeUInt32: TTypeDesc;
FTypeUInt64: TTypeDesc;
FTypeByte: TTypeDesc;
FTypeInteger: TTypeDesc;
FTypeInt64: TTypeDesc;
FTypeUInt32: TTypeDesc;
FTypeUInt64: TTypeDesc;
FTypeSmallInt: TTypeDesc;
FTypeWord: TTypeDesc;
FTypeByte: TTypeDesc;
FTypeBoolean: TTypeDesc;
FTypeString: TTypeDesc;
FTypeVoid: TTypeDesc;
@ -420,10 +424,12 @@ type
function FindGeneric(const AName: string): TObject;
{ Convenience type accessors }
property TypeInteger: TTypeDesc read FTypeInteger;
property TypeInt64: TTypeDesc read FTypeInt64;
property TypeUInt32: TTypeDesc read FTypeUInt32;
property TypeUInt64: TTypeDesc read FTypeUInt64;
property TypeInteger: TTypeDesc read FTypeInteger;
property TypeInt64: TTypeDesc read FTypeInt64;
property TypeUInt32: TTypeDesc read FTypeUInt32;
property TypeUInt64: TTypeDesc read FTypeUInt64;
property TypeSmallInt: TTypeDesc read FTypeSmallInt;
property TypeWord: TTypeDesc read FTypeWord;
property TypeByte: TTypeDesc read FTypeByte;
property TypeBoolean: TTypeDesc read FTypeBoolean;
property TypeString: TTypeDesc read FTypeString;
@ -443,7 +449,7 @@ implementation
function TTypeDesc.IsNumeric: Boolean;
begin
Result := Kind in [tyInteger, tyInt64, tyUInt32, tyUInt64, tyByte, tyEnum, tyDouble, tySingle];
Result := Kind in [tyInteger, tyInt64, tyUInt32, tyUInt64, tySmallInt, tyWord, tyByte, tyEnum, tyDouble, tySingle];
end;
function TTypeDesc.IsFloat: Boolean;
@ -458,7 +464,7 @@ end;
function TTypeDesc.IsOrdinal: Boolean;
begin
Result := Kind in [tyInteger, tyInt64, tyUInt32, tyUInt64, tyByte, tyBoolean, tyEnum];
Result := Kind in [tyInteger, tyInt64, tyUInt32, tyUInt64, tySmallInt, tyWord, tyByte, tyBoolean, tyEnum];
end;
function TTypeDesc.IsRecord: Boolean;
@ -470,6 +476,7 @@ function TTypeDesc.RawSize: Integer;
begin
case Kind of
tyByte, tyBoolean: Result := 1;
tySmallInt, tyWord: Result := 2;
tyInteger, tyUInt32, tyEnum: Result := 4;
tyInt64, tyUInt64, tyString, tyClass, tyPointer, tyNil, tyDouble: Result := 8;
tySingle: Result := 4;
@ -492,6 +499,7 @@ begin
case Kind of
tyInteger, tyUInt32, tyEnum: Result := 4;
tyInt64, tyUInt64: Result := 8;
tySmallInt, tyWord: Result := 2;
tyByte, tyBoolean: Result := 1; { true byte; stack slots are alloc4-padded by codegen }
tyString: Result := 8; { pointer size on 64-bit }
tyRecord: Result := TRecordTypeDesc(Self).TotalSize;
@ -516,6 +524,7 @@ begin
case Kind of
tyInteger, tyUInt32, tyEnum: Result := 4;
tyByte, tyBoolean: Result := 1; { natural alignment }
tySmallInt, tyWord: Result := 2;
tyInt64, tyUInt64, tyString: Result := 8;
tyRecord: Result := TRecordTypeDesc(Self).MaxAlign;
tySet: if TSetTypeDesc(Self).BitCount <= 32 then Result := 4 else Result := 8;
@ -1205,11 +1214,13 @@ var
TMethodDesc: TRecordTypeDesc;
begin
{ Primitive types }
FTypeInteger := NewType(tyInteger, 'Integer');
FTypeInt64 := NewType(tyInt64, 'Int64');
FTypeUInt32 := NewType(tyUInt32, 'UInt32');
FTypeUInt64 := NewType(tyUInt64, 'UInt64');
FTypeByte := NewType(tyByte, 'Byte');
FTypeInteger := NewType(tyInteger, 'Integer');
FTypeInt64 := NewType(tyInt64, 'Int64');
FTypeUInt32 := NewType(tyUInt32, 'UInt32');
FTypeUInt64 := NewType(tyUInt64, 'UInt64');
FTypeSmallInt := NewType(tySmallInt, 'SmallInt');
FTypeWord := NewType(tyWord, 'Word');
FTypeByte := NewType(tyByte, 'Byte');
FTypeBoolean := NewType(tyBoolean, 'Boolean');
FTypeString := NewType(tyString, 'string');
FTypeVoid := NewType(tyVoid, 'void');
@ -1227,6 +1238,10 @@ begin
Define(TSymbol.Create('UInt64', skType, FTypeUInt64));
Define(TSymbol.Create('QWord', skType, FTypeUInt64)); { FPC alias }
Define(TSymbol.Create('PtrUInt', skType, FTypeUInt64)); { pointer-sized unsigned on 64-bit }
Define(TSymbol.Create('SmallInt', skType, FTypeSmallInt));
Define(TSymbol.Create('Int16', skType, FTypeSmallInt)); { Delphi alias }
Define(TSymbol.Create('Word', skType, FTypeWord));
Define(TSymbol.Create('UInt16', skType, FTypeWord)); { Delphi alias }
Define(TSymbol.Create('Byte', skType, FTypeByte));
Define(TSymbol.Create('Boolean', skType, FTypeBoolean));
Define(TSymbol.Create('string', skType, FTypeString));

View file

@ -13,6 +13,7 @@ uses
blaise.testing.runner.text,
cp.test.runner_filters,
cp.test.uint64,
cp.test.smallint_word,
cp.test.lexer,
cp.test.parser,
cp.test.codegen,

View file

@ -0,0 +1,434 @@
{
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.smallint_word;
{ Tests for the 16-bit integer types SmallInt (signed) and Word (unsigned),
plus their Delphi-style aliases Int16 and UInt16.
Coverage:
- All four names resolve, SmallInt/Int16 and Word/UInt16 share descriptors.
- SizeOf is 2 for every name.
- Fields are packed at 2-byte stride; mixed records align correctly.
- Load/store use loadsh/loaduh/storeh (16-bit half-word ops).
- Integer/UInt32 widen implicitly in expressions.
- WriteLn and IntToStr handle both signed and unsigned values. }
interface
uses
blaise.testing, cp.test.e2e.base,
uLexer, uParser, uAST, uSymbolTable, uSemantic, uCodeGenQBE;
type
TSmallIntWordTests = class(TTestCase)
private
function AnalyseSrc(const ASrc: string): TProgram;
function GenIR(const ASrc: string): string;
published
procedure TestSemantic_SmallInt_TypeRegistered;
procedure TestSemantic_Word_TypeRegistered;
procedure TestSemantic_Int16_AliasOfSmallInt;
procedure TestSemantic_UInt16_AliasOfWord;
procedure TestSemantic_SizeOf_SmallInt_Is2;
procedure TestSemantic_SizeOf_Word_Is2;
procedure TestSemantic_SmallIntField_RecordPacks;
procedure TestSemantic_MixedRecord_AlignsTo16;
procedure TestCodegen_SmallIntField_UsesLoadsh;
procedure TestCodegen_WordField_UsesLoaduh;
procedure TestCodegen_SmallIntField_UsesStoreh;
end;
[Threaded]
TSmallIntWordE2ETests = class(TE2ETestCase)
protected
procedure SetUp; override;
published
procedure TestRun_SmallInt_RoundTrip;
procedure TestRun_Word_RoundTrip;
procedure TestRun_SmallInt_Negative;
procedure TestRun_Word_MaxValue;
procedure TestRun_MixedRecord_RoundTrip;
end;
implementation
function TSmallIntWordTests.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 TSmallIntWordTests.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 TSmallIntWordTests.TestSemantic_SmallInt_TypeRegistered;
const
Src = '''
program P;
var X: SmallInt;
begin end.
''';
var
Prog: TProgram;
T: TTypeDesc;
begin
Prog := AnalyseSrc(Src);
try
T := TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType;
AssertTrue('resolves', T <> nil);
AssertEquals('Kind = tySmallInt', Ord(tySmallInt), Ord(T.Kind));
finally
Prog.Free;
end;
end;
procedure TSmallIntWordTests.TestSemantic_Word_TypeRegistered;
const
Src = '''
program P;
var X: Word;
begin end.
''';
var
Prog: TProgram;
T: TTypeDesc;
begin
Prog := AnalyseSrc(Src);
try
T := TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType;
AssertTrue('resolves', T <> nil);
AssertEquals('Kind = tyWord', Ord(tyWord), Ord(T.Kind));
finally
Prog.Free;
end;
end;
procedure TSmallIntWordTests.TestSemantic_Int16_AliasOfSmallInt;
const
Src = '''
program P;
var A: SmallInt;
var B: Int16;
begin end.
''';
var
Prog: TProgram;
begin
Prog := AnalyseSrc(Src);
try
AssertSame('SmallInt and Int16 share descriptor',
TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType,
TVarDecl(Prog.Block.Decls.Items[1]).ResolvedType);
finally
Prog.Free;
end;
end;
procedure TSmallIntWordTests.TestSemantic_UInt16_AliasOfWord;
const
Src = '''
program P;
var A: Word;
var B: UInt16;
begin end.
''';
var
Prog: TProgram;
begin
Prog := AnalyseSrc(Src);
try
AssertSame('Word and UInt16 share descriptor',
TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType,
TVarDecl(Prog.Block.Decls.Items[1]).ResolvedType);
finally
Prog.Free;
end;
end;
procedure TSmallIntWordTests.TestSemantic_SizeOf_SmallInt_Is2;
var
IR: string;
begin
IR := GenIR(
'''
program P;
var N: Integer;
begin N := SizeOf(SmallInt) end.
''');
AssertTrue('SizeOf(SmallInt) is 2', Pos('copy 2', IR) > 0);
end;
procedure TSmallIntWordTests.TestSemantic_SizeOf_Word_Is2;
var
IR: string;
begin
IR := GenIR(
'''
program P;
var N: Integer;
begin N := SizeOf(Word) end.
''');
AssertTrue('SizeOf(Word) is 2', Pos('copy 2', IR) > 0);
end;
procedure TSmallIntWordTests.TestSemantic_SmallIntField_RecordPacks;
const
Src = '''
program P;
type
TFour = record
A: SmallInt;
B: SmallInt;
C: SmallInt;
D: SmallInt;
end;
var R: TFour;
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('B at 2', 2, RT.FindField('B').Offset);
AssertEquals('C at 4', 4, RT.FindField('C').Offset);
AssertEquals('D at 6', 6, RT.FindField('D').Offset);
AssertEquals('total 8', 8, RT.TotalSize);
finally
Prog.Free;
end;
end;
procedure TSmallIntWordTests.TestSemantic_MixedRecord_AlignsTo16;
const
{ Byte + SmallInt + Integer = 1 + (pad 1) + 2 + 4 = 8 bytes. }
Src = '''
program P;
type
TMixed = record
Tag: Byte;
ID: SmallInt;
Val: Integer;
end;
var R: TMixed;
begin end.
''';
var
Prog: TProgram;
RT: TRecordTypeDesc;
begin
Prog := AnalyseSrc(Src);
try
RT := TRecordTypeDesc(TVarDecl(Prog.Block.Decls.Items[0]).ResolvedType);
AssertEquals('Tag at 0', 0, RT.FindField('Tag').Offset);
AssertEquals('ID at 2 (after 1-byte pad)', 2, RT.FindField('ID').Offset);
AssertEquals('Val at 4', 4, RT.FindField('Val').Offset);
AssertEquals('total 8', 8, RT.TotalSize);
finally
Prog.Free;
end;
end;
procedure TSmallIntWordTests.TestCodegen_SmallIntField_UsesLoadsh;
var
IR: string;
begin
IR := GenIR(
'''
program P;
type TR = record V: SmallInt; end;
var R: TR;
var X: Integer;
begin X := R.V end.
''');
AssertTrue('SmallInt field load uses loadsh', Pos('loadsh', IR) > 0);
end;
procedure TSmallIntWordTests.TestCodegen_WordField_UsesLoaduh;
var
IR: string;
begin
IR := GenIR(
'''
program P;
type TR = record V: Word; end;
var R: TR;
var X: Integer;
begin X := R.V end.
''');
AssertTrue('Word field load uses loaduh', Pos('loaduh', IR) > 0);
end;
procedure TSmallIntWordTests.TestCodegen_SmallIntField_UsesStoreh;
var
IR: string;
begin
IR := GenIR(
'''
program P;
type TR = record V: SmallInt; end;
var R: TR;
begin R.V := 42 end.
''');
AssertTrue('SmallInt field store uses storeh', Pos('storeh', IR) > 0);
end;
{ ---------- e2e ---------- }
procedure TSmallIntWordE2ETests.SetUp;
begin
inherited SetUp;
SetUpScratch('compiler/target/test-e2e-smallint-word');
end;
const
LE = #10;
SrcSmallIntRoundTrip = '''
program P;
var X: SmallInt;
begin
X := 12345;
WriteLn(X)
end.
''';
SrcWordRoundTrip = '''
program P;
var X: Word;
begin
X := 60000;
WriteLn(X)
end.
''';
SrcSmallIntNegative = '''
program P;
var X: SmallInt;
begin
X := -1;
WriteLn(X)
end.
''';
SrcWordMax = '''
program P;
var X: Word;
begin
X := 65535;
WriteLn(X)
end.
''';
SrcMixedRecord = '''
program P;
type
TMixed = record
Tag: Byte;
ID: SmallInt;
Val: Integer;
end;
var R: TMixed;
begin
R.Tag := 9;
R.ID := -100;
R.Val := 12345;
WriteLn(R.Tag);
WriteLn(R.ID);
WriteLn(R.Val);
WriteLn(SizeOf(TMixed))
end.
''';
procedure TSmallIntWordE2ETests.TestRun_SmallInt_RoundTrip;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcSmallIntRoundTrip, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('12345', '12345' + LE, Output);
end;
procedure TSmallIntWordE2ETests.TestRun_Word_RoundTrip;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcWordRoundTrip, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('60000', '60000' + LE, Output);
end;
procedure TSmallIntWordE2ETests.TestRun_SmallInt_Negative;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcSmallIntNegative, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('-1 sign-extended', '-1' + LE, Output);
end;
procedure TSmallIntWordE2ETests.TestRun_Word_MaxValue;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcWordMax, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('65535 zero-extended', '65535' + LE, Output);
end;
procedure TSmallIntWordE2ETests.TestRun_MixedRecord_RoundTrip;
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit; end;
AssertTrue('compile+run', CompileAndRun(SrcMixedRecord, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('Tag, ID, Val, SizeOf',
'9' + LE + '-100' + LE + '12345' + LE + '8' + LE, Output);
end;
initialization
RegisterTest(TSmallIntWordTests);
RegisterTest(TSmallIntWordE2ETests);
end.

View file

@ -787,8 +787,12 @@ ArrayLiteral
* Alias: Cardinal
* UInt64 — 64-bit unsigned integer (QBE type: l)
* Aliases: QWord, PtrUInt (the pointer-sized unsigned)
* SmallInt — 16-bit signed integer (QBE type: w, stored as h)
* Alias: Int16
* Word — 16-bit unsigned integer (QBE type: w, stored as h)
* Alias: UInt16
* Boolean — 1-byte boolean (QBE type: w; 0 = false, 1 = true)
* Byte — 8-bit unsigned integer (QBE type: w)
* Byte — 8-bit unsigned integer (QBE type: w, stored as b)
* String — ARC-managed UTF-8 string pointer (QBE type: l)
* Pointer — untyped pointer (QBE type: l)
* ^T — typed pointer to T; created on demand (QBE type: l)

View file

@ -490,6 +490,8 @@ Blaise defines five integer types with fixed, platform-independent widths:
| `Int64` | 64-bit signed | `l` |
| `UInt32` | 32-bit unsigned | `w` | `Cardinal`
| `UInt64` | 64-bit unsigned | `l` | `QWord`, `PtrUInt`
| `SmallInt`| 16-bit signed | `w` (2 bytes in storage) | `Int16`
| `Word` | 16-bit unsigned | `w` (2 bytes in storage) | `UInt16`
| `Byte` | 8-bit unsigned | `w` (1 byte in storage) |
|===
@ -521,6 +523,22 @@ negative `Int64` becomes a huge `UInt64` (or vice versa) without
warning. All narrower integer types (`Integer`, `UInt32`, `Byte`) widen
implicitly into both `Int64` and `UInt64`.
==== 16-bit integer types: SmallInt / Word
`SmallInt` (16-bit signed, range −32768..32767) and `Word` (16-bit
unsigned, range 0..65535) are provided alongside the established
32-/64-bit integer types so that code ported from FPC or Delphi
compiles unchanged and so that `record` field layouts can match
fixed-width binary formats without packing tricks.
In registers and temporaries both types are widened to `w` (32-bit
QBE word); only field and array element storage uses 2 bytes per
element (`storeh` / `loadsh` for `SmallInt`, `loaduh` for `Word`).
Implicit widening into `Integer`, `Int64`, `UInt32` and `UInt64` is
permitted in the same way `Byte` widens, since 16-bit values fit
losslessly in all of those. `Int16` and `UInt16` are accepted as
aliases for `SmallInt` and `Word` respectively.
==== Integer literal typing
Untyped integer literals are typed by magnitude:
@ -570,11 +588,11 @@ for any realistic string length.
interoperating with existing 32-bit FPC code or C functions that expect
`int32_t`. Explicit `Int64` covers the cases that genuinely need 64 bits.
* *`LongInt`, `SmallInt`, `Word` aliases* — deferred. These FPC/Delphi
aliases map to fixed widths (32-bit, 16-bit, 16-bit unsigned) and
could be added as type aliases in a future release alongside true
`SmallInt` / `Word` types. `Cardinal` is provided as an alias for
`UInt32` since the unsigned-32 type is already present.
* *`LongInt` alias* — deferred. The FPC/Delphi `LongInt` name maps to
a 32-bit signed integer (identical to Blaise's `Integer`) and could
be added as a plain type alias in a future release. `SmallInt`,
`Int16`, `Word` and `UInt16` are now first-class 16-bit types, and
`Cardinal` remains an alias for `UInt32`.
---