feat(lang): inline assembler blocks (asm … end routine bodies)
A routine body may now be written as inline assembly:
function GetSelf: Pointer; assembler; nostackframe;
asm
movq %rdi, %rax
ret
end;
The block is opaque GNU/AT&T assembly: the lexer captures the whole asm … end
as one tkAsmBlock token (verbatim text, never tokenised as Pascal), the parser
wraps it in a TAsmStmt, the semantic pass treats it as a black box, and the
native backend emits it verbatim into the assembly stream where the existing
internal/external assembler parses it. `nostackframe` suppresses the compiler
prologue/epilogue so the asm body owns the whole frame. asm routines mix
freely with ordinary Pascal routines in a standard .pas unit (no .inc needed).
This is the FPC model (rtl/linux/x86_64/si_c.inc) and the path to retiring the
hand-written runtime/src/main/asm/*.s files (assembled by `cc -c` today) — once
each body moves into an asm routine the RTL builds with no external assembler.
Design follows ports-and-adapters: x86-64 knowledge stays at the backend/
assembler edge, the portable core never interprets the block. The QBE backend
rejects asm bodies (it emits no assembly text); native is the inline-asm target
and the default. TAsmStmt round-trips through the .bif unit cache.
Pipeline: lexer (tkAsmBlock + ReadAsmBody raw capture), uAST (TAsmStmt,
TMethodDecl.NoStackFrame), parser (nostackframe directive + asm-body path),
semantic (opaque no-op), native codegen (verbatim emit + nostackframe null-frame
guard), QBE rejection, uUnitInterfaceIO encode/decode, bif-coverage entry.
`asm` becomes a reserved word (one local var named Asm renamed in a test).
Fixes a native sret-Result field-read codegen bug the feature exposed: reading a
field of an sret function's Result at offset 0 (e.g. `Result.Kind` in a record-
returning function) read the Result frame slot DIRECTLY instead of dereferencing
the caller-buffer pointer it holds, so `Result.Field = const` was always false.
The offset-0 fast path in the integer field-read leaf now routes through
EmitLocalRecordBase like the offset>0 path, so the sret indirection happens in
both. QBE was already correct; this was a native-only divergence.
Tests: lexer raw-capture (3), native verbatim-body/no-prologue IR test (1),
internal-assembler e2e returns-value + adds-two-args (2). All four fixpoints
and both QBE- and native-built test runners pass (3764 tests).
Design note: docs/inline-asm-design.adoc; grammar + rationale updated.
This commit is contained in:
parent
7ac533ca15
commit
0396ed7b1c
|
|
@ -7322,10 +7322,13 @@ begin
|
|||
else
|
||||
Self.Emit(Format(#9'leaq %d(%%rcx), %%rax', [FAE.FieldInfo.Offset]));
|
||||
end
|
||||
else if FAE.FieldInfo.Offset = 0 then
|
||||
Self.EmitLoadVar(Self.VarOperand(FAE.RecordName), FAE.FieldInfo.TypeDesc)
|
||||
else
|
||||
begin
|
||||
{ Route through EmitLocalRecordBase for BOTH offset 0 and > 0: for the
|
||||
sret-function Result the slot holds the caller-buffer POINTER, so the
|
||||
field must be read indirectly (movq slot -> %rcx; load N(%rcx)). The
|
||||
old offset-0 fast path read the slot directly, which for sret Result
|
||||
read the pointer value as the field (Result.K = const always false). }
|
||||
Self.EmitLocalRecordBase(FAE.RecordName, '%rcx');
|
||||
Self.EmitLoadVar(Format('%d(%%rcx)', [FAE.FieldInfo.Offset]),
|
||||
FAE.FieldInfo.TypeDesc);
|
||||
|
|
@ -10560,6 +10563,13 @@ var
|
|||
AliasBuf: Integer;
|
||||
begin
|
||||
Self.DbgStmtLabel(AStmt);
|
||||
if AStmt is TAsmStmt then
|
||||
begin
|
||||
{ Inline assembler: emit the verbatim block. The internal/external
|
||||
assembler parses it; the front end never did. }
|
||||
Self.Emit(TAsmStmt(AStmt).Code);
|
||||
Exit;
|
||||
end;
|
||||
if AStmt is TAssignment then
|
||||
begin
|
||||
Asgn := TAssignment(AStmt);
|
||||
|
|
@ -15799,6 +15809,21 @@ begin
|
|||
if AExported then
|
||||
Self.Emit('.globl ' + Sym);
|
||||
Self.Emit(Sym + ':');
|
||||
{ nostackframe: the body is an inline-asm block that owns the entire frame
|
||||
(prologue, args-from-registers, ret). Emit no compiler prologue/epilogue,
|
||||
no param spill, no ARC — just the verbatim block. This is the null-object
|
||||
frame strategy (see docs/inline-asm-design.adoc). }
|
||||
if ADecl.NoStackFrame then
|
||||
begin
|
||||
if (ADecl.Body <> nil) then
|
||||
for I := 0 to ADecl.Body.Stmts.Count - 1 do
|
||||
Self.EmitStmt(TASTStmt(ADecl.Body.Stmts.Items[I]));
|
||||
Self.DbgEndFunc();
|
||||
Self.Emit('.type ' + Sym + ', @function');
|
||||
FExitLabel := '';
|
||||
FCapturedVars := nil;
|
||||
Exit;
|
||||
end;
|
||||
Self.Emit(#9'pushq %rbp');
|
||||
Self.Emit(#9'movq %rsp, %rbp');
|
||||
if FFrameSize > 0 then
|
||||
|
|
|
|||
|
|
@ -2460,6 +2460,10 @@ var
|
|||
begin
|
||||
if AStmt = nil then
|
||||
raise ECodeGenError.Create('EmitStmt called with nil statement');
|
||||
if AStmt is TAsmStmt then
|
||||
raise ECodeGenError.Create(
|
||||
'inline asm blocks require the native backend (--backend native); '
|
||||
+ 'the QBE backend cannot emit assembly');
|
||||
if AStmt is TTryFinallyStmt then
|
||||
EmitTryFinallyStmt(TTryFinallyStmt(AStmt))
|
||||
else if AStmt is TTryExceptStmt then
|
||||
|
|
|
|||
|
|
@ -247,6 +247,14 @@ type
|
|||
destructor Destroy; override;
|
||||
end;
|
||||
|
||||
{ An inline-assembler block: the verbatim text of one `asm` ... `end` body.
|
||||
The front end never interprets Code — it is GNU/AT&T assembly handed to the
|
||||
backend's assembler verbatim (see docs/inline-asm-design.adoc). }
|
||||
TAsmStmt = class(TASTStmt)
|
||||
public
|
||||
Code: string; { verbatim assembly text between asm and end }
|
||||
end;
|
||||
|
||||
TWhileStmt = class(TASTStmt)
|
||||
public
|
||||
Condition: TASTExpr; { owned }
|
||||
|
|
@ -806,6 +814,9 @@ type
|
|||
consumed by codegen for cross-unit references. }
|
||||
IsExternal: Boolean; { declared with 'external' directive — no body }
|
||||
ExternalName: string; { C symbol name from 'external name ''c_foo'''; empty = use Pascal name }
|
||||
NoStackFrame: Boolean; { declared 'nostackframe' — body is an asm
|
||||
block that owns the whole frame; codegen
|
||||
emits no compiler prologue/epilogue }
|
||||
IsRecordMethod: Boolean; { set by uSemantic — owner type is a record (not a class) }
|
||||
VTableSlot: Integer; { -1 = static; >=0 = vtable index (set by uSemantic) }
|
||||
TypeParams: TStringList; { non-nil = generic function template; owns param names }
|
||||
|
|
|
|||
|
|
@ -111,7 +111,12 @@ type
|
|||
tkDot,
|
||||
tkDotDot, { .. — array bounds separator and case range }
|
||||
tkCaret, { ^ — pointer dereference / pointer type prefix }
|
||||
tkAt { @ — address-of operator }
|
||||
tkAt, { @ — address-of operator }
|
||||
tkAsmBlock { whole 'asm' ... 'end' block; Value holds the verbatim
|
||||
assembly text (the front end never tokenises it).
|
||||
Appended at the enum's END so adding it shifts no
|
||||
existing ordinal (set-of-TTokenKind literals elsewhere
|
||||
depend on stable ordinals). }
|
||||
);
|
||||
|
||||
TToken = record
|
||||
|
|
@ -137,6 +142,7 @@ type
|
|||
procedure SeedPredefines;
|
||||
procedure SkipToElseOrEndif;
|
||||
procedure SkipToEndif;
|
||||
function ReadAsmBody: string;
|
||||
public
|
||||
constructor Create(const ASource: string; const AFilename: string = '');
|
||||
destructor Destroy; override;
|
||||
|
|
@ -224,6 +230,7 @@ begin
|
|||
else if AUpper = 'THREADVAR' then Result := tkThreadVar
|
||||
else if AUpper = 'BEGIN' then Result := tkBegin
|
||||
else if AUpper = 'END' then Result := tkEnd
|
||||
else if AUpper = 'ASM' then Result := tkAsmBlock
|
||||
else if AUpper = 'TYPE' then Result := tkType
|
||||
else if AUpper = 'RECORD' then Result := tkRecord
|
||||
else if AUpper = 'PACKED' then Result := tkPacked
|
||||
|
|
@ -524,6 +531,42 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
{ Called immediately after the 'asm' keyword token was produced (FTok.Token is
|
||||
that keyword). Drives the tokeniser forward to the matching 'end' keyword and
|
||||
returns the verbatim source text between the two — assembly is not Pascal, so
|
||||
nothing in the block is re-interpreted. Only a standalone 'end' KEYWORD token
|
||||
terminates the block (an 'end' inside an asm '#' line comment is a comment
|
||||
token, never a keyword, so it is ignored). The terminating 'end' token is
|
||||
consumed; normal tokenising resumes after it. }
|
||||
function TLexer.ReadAsmBody: string;
|
||||
var
|
||||
raw: TFpgPasToken;
|
||||
BlockStart: Integer; { 1-based source index of the first char after 'asm' }
|
||||
EndStart: Integer; { 1-based source index of the terminating 'end' }
|
||||
begin
|
||||
{ FTok.Token is the 'asm' keyword; the body begins right after it. }
|
||||
BlockStart := FTok.Token.TextStart + FTok.Token.Len;
|
||||
EndStart := -1;
|
||||
while True do
|
||||
begin
|
||||
raw := FTok.NextToken();
|
||||
if raw.Kind = fptkEOF then
|
||||
begin
|
||||
{ Unterminated block: capture to end of source. }
|
||||
EndStart := Length(FTok.Source) + 1;
|
||||
Break;
|
||||
end;
|
||||
if (raw.Kind = fptkKeyword) and (FTok.TokenTextUpper() = 'END') then
|
||||
begin
|
||||
EndStart := FTok.Token.TextStart;
|
||||
Break;
|
||||
end;
|
||||
end;
|
||||
{ Slice [BlockStart, EndStart) — Copy is 0-based, so subtract 1 from the
|
||||
1-based start; length is EndStart - BlockStart. }
|
||||
Result := Copy(FTok.Source, BlockStart - 1, EndStart - BlockStart);
|
||||
end;
|
||||
|
||||
function TLexer.Next: TToken;
|
||||
var
|
||||
raw: TFpgPasToken;
|
||||
|
|
@ -583,7 +626,14 @@ begin
|
|||
begin
|
||||
text := FTok.TokenTextUpper();
|
||||
Result.Kind := MapKeyword(text);
|
||||
Result.Value := FTok.TokenText();
|
||||
if Result.Kind = tkAsmBlock then
|
||||
{ Capture the whole 'asm' ... 'end' block as verbatim text — its
|
||||
content is GNU/AT&T assembly, not Pascal, so it must not be
|
||||
tokenised. ReadAsmBody drives the tokeniser to the matching
|
||||
'end' and slices the raw source. }
|
||||
Result.Value := Self.ReadAsmBody()
|
||||
else
|
||||
Result.Value := FTok.TokenText();
|
||||
end;
|
||||
|
||||
fptkIdentifier:
|
||||
|
|
@ -737,6 +787,7 @@ begin
|
|||
tkOut: Result := 'out';
|
||||
tkConstructor: Result := 'constructor';
|
||||
tkDestructor: Result := 'destructor';
|
||||
tkAsmBlock: Result := 'asm block';
|
||||
tkIdent: Result := 'identifier';
|
||||
tkPlus: Result := '+';
|
||||
tkMinus: Result := '-';
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ type
|
|||
procedure ParseUsesList(AList: TStringList);
|
||||
procedure ParseUses(AProg: TProgram);
|
||||
function ParseBlock: TBlock;
|
||||
function ParseAsmBody: TBlock;
|
||||
procedure ParseTypeSection(ABlock: TBlock);
|
||||
procedure ParseTypeDecl(ABlock: TBlock);
|
||||
procedure ParseConstBlock(AList: TObjectList);
|
||||
|
|
@ -2098,6 +2099,7 @@ begin
|
|||
SameText(FCurrent.Value, 'static') or
|
||||
SameText(FCurrent.Value, 'final') or
|
||||
SameText(FCurrent.Value, 'assembler') or
|
||||
SameText(FCurrent.Value, 'nostackframe') or
|
||||
SameText(FCurrent.Value, 'forward') or
|
||||
SameText(FCurrent.Value, 'deprecated') or
|
||||
SameText(FCurrent.Value, 'platform') or
|
||||
|
|
@ -2105,6 +2107,8 @@ begin
|
|||
begin
|
||||
if SameText(FCurrent.Value, 'inline') then
|
||||
Result.IsInline := True
|
||||
else if SameText(FCurrent.Value, 'nostackframe') then
|
||||
Result.NoStackFrame := True
|
||||
else if SameText(FCurrent.Value, 'forward') then
|
||||
IsForward := True
|
||||
else if SameText(FCurrent.Value, 'cdecl') or
|
||||
|
|
@ -2129,7 +2133,16 @@ begin
|
|||
proc. Without this guard the forward decl swallowed the real
|
||||
implementation (and everything up to the program's 'end.') as its body
|
||||
(issue #130 bug2). }
|
||||
if (not Result.IsExternal) and (not IsForward) and
|
||||
if (not Result.IsExternal) and (not IsForward) and Check(tkAsmBlock) then
|
||||
begin
|
||||
{ Inline-assembler body: the lexer already captured the whole asm ... end
|
||||
block as one tkAsmBlock token whose Value is the verbatim text. Wrap it
|
||||
in a TBlock holding a single TAsmStmt; semantic treats it as opaque and
|
||||
codegen emits the text verbatim. }
|
||||
Result.Body := Self.ParseAsmBody();
|
||||
Expect(tkSemicolon);
|
||||
end
|
||||
else if (not Result.IsExternal) and (not IsForward) and
|
||||
(Check(tkBegin) or Check(tkVar) or Check(tkType) or Check(tkConst) or
|
||||
(ACanHaveNestedProcs and (Check(tkProcedure) or Check(tkFunction)))) then
|
||||
begin
|
||||
|
|
@ -2142,6 +2155,21 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
{ Build a TBlock whose sole statement is the TAsmStmt for the current
|
||||
tkAsmBlock token, then consume that token. }
|
||||
function TParser.ParseAsmBody: TBlock;
|
||||
var
|
||||
Stmt: TAsmStmt;
|
||||
begin
|
||||
Result := TBlock.Create();
|
||||
Stmt := TAsmStmt.Create();
|
||||
Stmt.Line := FCurrent.Line;
|
||||
Stmt.Col := FCurrent.Col;
|
||||
Stmt.Code := FCurrent.Value;
|
||||
Result.Stmts.Add(Stmt);
|
||||
Advance(); { consume the tkAsmBlock token }
|
||||
end;
|
||||
|
||||
procedure TParser.ParseParamList(AParams: TObjectList);
|
||||
var
|
||||
Par: TMethodParam;
|
||||
|
|
|
|||
|
|
@ -6843,6 +6843,10 @@ var
|
|||
L: Integer;
|
||||
DupLocal: Boolean;
|
||||
begin
|
||||
{ Inline-assembler block: opaque to the front end — its content is assembly,
|
||||
not Pascal, so no statement/type/ARC analysis runs over it. }
|
||||
if AStmt is TAsmStmt then
|
||||
Exit;
|
||||
if AStmt is TForStmt then
|
||||
begin
|
||||
ForS := TForStmt(AStmt);
|
||||
|
|
|
|||
|
|
@ -903,6 +903,9 @@ begin
|
|||
Result := EncodeLpstr('inh') +
|
||||
EncodeLpstr(TInheritedCallStmt(AStmt).Name) +
|
||||
EncodeExprList(TInheritedCallStmt(AStmt).Args)
|
||||
else if AStmt is TAsmStmt then
|
||||
Result := EncodeLpstr('asm') +
|
||||
EncodeLpstr(TAsmStmt(AStmt).Code)
|
||||
else
|
||||
raise EIfaceFormatError.Create(
|
||||
'EncodeStmt: unhandled statement node ' + AStmt.ClassName);
|
||||
|
|
@ -1535,6 +1538,7 @@ var
|
|||
PCn: TProcCall;
|
||||
MCSn: TMethodCallStmt;
|
||||
ICSn: TInheritedCallStmt;
|
||||
ASMn: TAsmStmt;
|
||||
C, I: Integer;
|
||||
Body: TASTStmt;
|
||||
begin
|
||||
|
|
@ -1702,6 +1706,12 @@ begin
|
|||
ReadExprList(AText, APos, ICSn.Args);
|
||||
Result := ICSn;
|
||||
end
|
||||
else if Kind = 'asm' then
|
||||
begin
|
||||
ASMn := TAsmStmt.Create();
|
||||
ASMn.Code := ReadLpstrAt(AText, APos);
|
||||
Result := ASMn;
|
||||
end
|
||||
else
|
||||
raise EIfaceFormatError.Create(
|
||||
'ReadStmt: unknown statement kind ''' + Kind + '''');
|
||||
|
|
|
|||
|
|
@ -106,6 +106,10 @@ type
|
|||
argc/argv forwarding and exit-code propagation through __libc_start_main. }
|
||||
procedure TestEntry_ExitCodePropagates;
|
||||
procedure TestEntry_ArgsForwarded;
|
||||
{ Inline assembler blocks (asm ... end): a nostackframe asm-body function is
|
||||
assembled verbatim by the internal assembler and called from Pascal. }
|
||||
procedure TestInlineAsm_ReturnsValue;
|
||||
procedure TestInlineAsm_AddsTwoArgs;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -1077,6 +1081,59 @@ begin
|
|||
AssertEquals('2' + LineEnding + 'alpha' + LineEnding + 'beta' + LineEnding, Out_);
|
||||
end;
|
||||
|
||||
procedure TInternalAsmE2ETests.TestInlineAsm_ReturnsValue;
|
||||
var
|
||||
Out_: string;
|
||||
EC: Integer;
|
||||
begin
|
||||
{ A nostackframe asm-body function returns 42 in %eax; the internal assembler
|
||||
assembles the verbatim block, and Pascal calls it normally. }
|
||||
if not CompileAndRun(
|
||||
'program test_asmret;' + LineEnding +
|
||||
'function GetFortyTwo: Integer; assembler; nostackframe;' + LineEnding +
|
||||
'asm' + LineEnding +
|
||||
' movl $42, %eax' + LineEnding +
|
||||
' ret' + LineEnding +
|
||||
'end;' + LineEnding +
|
||||
'begin' + LineEnding +
|
||||
' WriteLn(GetFortyTwo())' + LineEnding +
|
||||
'end.',
|
||||
Out_, EC) then
|
||||
begin
|
||||
Ignore('<toolchain-missing>');
|
||||
Exit;
|
||||
end;
|
||||
AssertEquals(0, EC);
|
||||
AssertEquals('42' + LineEnding, Out_);
|
||||
end;
|
||||
|
||||
procedure TInternalAsmE2ETests.TestInlineAsm_AddsTwoArgs;
|
||||
var
|
||||
Out_: string;
|
||||
EC: Integer;
|
||||
begin
|
||||
{ Two integer args arrive in %edi/%esi (SysV); the asm body sums them. Proves
|
||||
a nostackframe asm function reads its parameters from the arg registers. }
|
||||
if not CompileAndRun(
|
||||
'program test_asmadd;' + LineEnding +
|
||||
'function AddTwo(A, B: Integer): Integer; assembler; nostackframe;' + LineEnding +
|
||||
'asm' + LineEnding +
|
||||
' movl %edi, %eax' + LineEnding +
|
||||
' addl %esi, %eax' + LineEnding +
|
||||
' ret' + LineEnding +
|
||||
'end;' + LineEnding +
|
||||
'begin' + LineEnding +
|
||||
' WriteLn(AddTwo(40, 2))' + LineEnding +
|
||||
'end.',
|
||||
Out_, EC) then
|
||||
begin
|
||||
Ignore('<toolchain-missing>');
|
||||
Exit;
|
||||
end;
|
||||
AssertEquals(0, EC);
|
||||
AssertEquals('42' + LineEnding, Out_);
|
||||
end;
|
||||
|
||||
{ ---- Registration ---- }
|
||||
|
||||
initialization
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ type
|
|||
procedure TestKeywords_CaseInsensitive;
|
||||
procedure TestIdent_NotKeyword_Prefix;
|
||||
|
||||
{ Inline assembler blocks — asm ... end captured as one raw-text token }
|
||||
procedure TestAsmBlock_CapturesRawText;
|
||||
procedure TestAsmBlock_EndMentionedInComment_NotTerminator;
|
||||
procedure TestAsmBlock_FollowedByDecl_StopsAtEnd;
|
||||
|
||||
{ Identifiers }
|
||||
procedure TestIdent_Simple;
|
||||
procedure TestIdent_WithUnderscore;
|
||||
|
|
@ -313,6 +318,50 @@ begin
|
|||
AssertEquals('Value', 'beginning', tok.Value);
|
||||
end;
|
||||
|
||||
{ Inline assembler blocks }
|
||||
|
||||
procedure TLexerTests.TestAsmBlock_CapturesRawText;
|
||||
var
|
||||
tok: TToken;
|
||||
begin
|
||||
{ An asm ... end block is returned as ONE tkAsmBlock token whose Value is
|
||||
the verbatim text between (exclusive) the asm keyword and the end keyword. }
|
||||
SetLexer('asm' + #10 + ' movl %eax, %ebx' + #10 + ' ret' + #10 + 'end');
|
||||
tok := FLexer.Next();
|
||||
AssertEquals('Kind', Ord(tkAsmBlock), Ord(tok.Kind));
|
||||
AssertTrue('has movl', Pos('movl %eax, %ebx', tok.Value) >= 0);
|
||||
AssertTrue('has ret', Pos('ret', tok.Value) >= 0);
|
||||
AssertTrue('no trailing end keyword captured', Pos('end', tok.Value) < 0);
|
||||
end;
|
||||
|
||||
procedure TLexerTests.TestAsmBlock_EndMentionedInComment_NotTerminator;
|
||||
var
|
||||
tok: TToken;
|
||||
begin
|
||||
{ 'end' appearing inside a # line comment must NOT terminate the block —
|
||||
only a standalone end keyword does. }
|
||||
SetLexer('asm' + #10 + ' ret # this ends the block' + #10 + 'end');
|
||||
tok := FLexer.Next();
|
||||
AssertEquals('Kind', Ord(tkAsmBlock), Ord(tok.Kind));
|
||||
AssertTrue('comment text preserved', Pos('this ends the block', tok.Value) >= 0);
|
||||
AssertTrue('has ret', Pos('ret', tok.Value) >= 0);
|
||||
end;
|
||||
|
||||
procedure TLexerTests.TestAsmBlock_FollowedByDecl_StopsAtEnd;
|
||||
var
|
||||
tok: TToken;
|
||||
begin
|
||||
{ After the block's end, normal tokenising resumes — the trailing ';' and the
|
||||
next token are produced as usual. }
|
||||
SetLexer('asm' + #10 + ' ret' + #10 + 'end; begin');
|
||||
tok := FLexer.Next();
|
||||
AssertEquals('asm block', Ord(tkAsmBlock), Ord(tok.Kind));
|
||||
tok := FLexer.Next();
|
||||
AssertEquals('semicolon after end', Ord(tkSemicolon), Ord(tok.Kind));
|
||||
tok := FLexer.Next();
|
||||
AssertEquals('begin resumes', Ord(tkBegin), Ord(tok.Kind));
|
||||
end;
|
||||
|
||||
{ Identifiers }
|
||||
|
||||
procedure TLexerTests.TestIdent_Simple;
|
||||
|
|
|
|||
|
|
@ -65,6 +65,9 @@ type
|
|||
into an SSE (xmm) argument register per the System V ABI, not an integer
|
||||
register. Asserts the call site emits movsd into %xmm0. }
|
||||
procedure TestMethodFloatArg_LoadedIntoXmm;
|
||||
{ Inline asm: a nostackframe asm-body function emits the verbatim block and
|
||||
NO compiler prologue (no pushq %rbp / frame subq). }
|
||||
procedure TestInlineAsm_VerbatimBody_NoPrologue;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -643,14 +646,40 @@ const
|
|||
end.
|
||||
''';
|
||||
var
|
||||
Asm: string;
|
||||
AsmText: string;
|
||||
begin
|
||||
Asm := GenAsm(Src);
|
||||
AsmText := GenAsm(Src);
|
||||
{ The Double argument must reach the SSE arg register, not an integer one. }
|
||||
AssertTrue('float method arg loaded into %xmm0',
|
||||
Pos('movsd', Asm) >= 0);
|
||||
Pos('movsd', AsmText) >= 0);
|
||||
AssertTrue('float method arg targets the SSE arg register',
|
||||
Pos('%xmm0', Asm) >= 0);
|
||||
Pos('%xmm0', AsmText) >= 0);
|
||||
end;
|
||||
|
||||
procedure TNativeConstArgTests.TestInlineAsm_VerbatimBody_NoPrologue;
|
||||
const
|
||||
Src = '''
|
||||
program P;
|
||||
function GetFortyTwo: Integer; assembler; nostackframe;
|
||||
asm
|
||||
movl $42, %eax
|
||||
ret
|
||||
end;
|
||||
begin
|
||||
WriteLn(GetFortyTwo())
|
||||
end.
|
||||
''';
|
||||
var
|
||||
AsmText: string;
|
||||
Region: string;
|
||||
begin
|
||||
AsmText := GenAsm(Src);
|
||||
Region := Self.FuncRegion(AsmText, 'GetFortyTwo');
|
||||
{ The verbatim asm instructions appear in the function body. }
|
||||
AssertTrue('verbatim movl $42', Pos('movl $42, %eax', Region) >= 0);
|
||||
AssertTrue('verbatim ret', Pos('ret', Region) >= 0);
|
||||
{ nostackframe: no compiler-emitted prologue inside the function. }
|
||||
AssertTrue('no pushq %rbp prologue', Pos('pushq %rbp', Region) < 0);
|
||||
end;
|
||||
|
||||
initialization
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ MethodDecl
|
|||
[ COLON TypeName ] (* return type for functions *)
|
||||
SEMICOLON
|
||||
{ MethodDirective SEMICOLON }
|
||||
[ Block SEMICOLON ] (* body absent for forward, external, or abstract decls *)
|
||||
[ ( Block | AsmBlock ) SEMICOLON ] (* body absent for forward, external, or abstract decls *)
|
||||
;
|
||||
|
||||
MethodDirective
|
||||
|
|
@ -377,7 +377,17 @@ MethodDirective
|
|||
| OVERRIDE
|
||||
| ExternalDirective
|
||||
| "overload" (* required on every overload of a same-named proc/func/method *)
|
||||
| IDENT (* inline, stdcall, cdecl, abstract, reintroduce, … *)
|
||||
| "nostackframe" (* asm body owns the frame; no compiler prologue/epilogue *)
|
||||
| IDENT (* inline, stdcall, cdecl, abstract, reintroduce, assembler, … *)
|
||||
;
|
||||
|
||||
(* Inline-assembler body. The lexer captures everything between ASM and the
|
||||
* matching END as one opaque token (verbatim GNU/AT&T assembly text); the front
|
||||
* end never tokenises the contents. Used for the runtime's startup/setjmp/
|
||||
* atomic routines. Requires the native backend. See
|
||||
* docs/inline-asm-design.adoc. *)
|
||||
AsmBlock
|
||||
= ASM (* verbatim assembly text *) END
|
||||
;
|
||||
|
||||
(* Calling-convention directives (cdecl, stdcall, register, pascal, safecall)
|
||||
|
|
|
|||
266
docs/inline-asm-design.adoc
Normal file
266
docs/inline-asm-design.adoc
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
= Inline Assembler Blocks (`asm … end`)
|
||||
:toc: macro
|
||||
:toclevels: 3
|
||||
|
||||
toc::[]
|
||||
|
||||
== Summary
|
||||
|
||||
Add an inline-assembler statement form so a routine body may be written in
|
||||
assembly:
|
||||
|
||||
[source,pascal]
|
||||
----
|
||||
procedure BlaiseStart; assembler; nostackframe;
|
||||
asm
|
||||
endbr64
|
||||
xorl %ebp, %ebp
|
||||
...
|
||||
call __libc_start_main@PLT
|
||||
end;
|
||||
----
|
||||
|
||||
This is the FPC model (`rtl/linux/x86_64/si_c.inc`): assembly lives inside an
|
||||
ordinary Pascal routine in a standard `.pas` unit, freely mixed with normal
|
||||
Pascal routines in the same unit. Blaise needs no `.inc` include mechanism for
|
||||
this — `.inc` is only FPC's textual-splice convenience and is orthogonal to
|
||||
inline assembly.
|
||||
|
||||
The motivation is the last gcc dependency in the runtime build: the hand-written
|
||||
`runtime/src/main/asm/*.s` files (`_start`, setjmp/longjmp, atomics, UTF-8) are
|
||||
assembled with `cc -c`. Once those bodies can be expressed as `asm` blocks in
|
||||
Pascal units, the whole RTL builds through Blaise's own pipeline with no external
|
||||
assembler (the Pascal units already do, since the Makefile uses
|
||||
`--assembler internal`).
|
||||
|
||||
== Scope
|
||||
|
||||
In scope:
|
||||
|
||||
* A new statement node `TAsmStmt` carrying the verbatim assembly text of one
|
||||
`asm … end` block.
|
||||
* Lexer support to capture an `asm` block as raw text (the body is GNU/AT&T
|
||||
assembly, not Pascal tokens).
|
||||
* Parser support to attach an `asm` body to a routine and to accept the
|
||||
`nostackframe` directive.
|
||||
* Semantic pass: treat an `asm` body as opaque — no statement analysis, but
|
||||
still register the routine's signature/symbol like any other.
|
||||
* Both codegen backends:
|
||||
** *native* — emit the routine label and the verbatim asm lines into the
|
||||
assembly stream; honour `nostackframe` (no compiler-generated prologue/
|
||||
epilogue).
|
||||
** *QBE* — reject `asm` bodies with a clear diagnostic (QBE has no inline-asm
|
||||
escape; the native backend is the inline-asm target and is the default).
|
||||
* `.bif` serialisation of `TAsmStmt` so an `asm`-bodied routine in a unit
|
||||
interface round-trips through the unit cache.
|
||||
* `bif-coverage` registration for the new node.
|
||||
|
||||
Out of scope (explicitly):
|
||||
|
||||
* Parsing or validating the assembly instructions. The block is opaque text
|
||||
handed to the existing internal assembler (or `cc -c`), which already parses
|
||||
GNU/AT&T syntax. Blaise does not learn an assembler grammar.
|
||||
* Intel syntax, register-name remapping, or FPC's `@result` / typed local
|
||||
references inside asm. The block references symbols by their emitted name
|
||||
exactly as the hand-written `.s` files do today. (A later iteration may add
|
||||
name substitution; it is not required to retire the `.s` files.)
|
||||
* Inline asm as an *expression* or mid-statement; only a whole-routine `asm`
|
||||
body is supported (matches the RTL's needs and FPC's `assembler;` routines).
|
||||
|
||||
== Why opaque-text, not a parsed instruction model
|
||||
|
||||
The internal assembler (`blaise.assembler.x86_64.AssembleToObject`) already
|
||||
consumes a full GNU/AT&T assembly *text* and produces a relocatable object; the
|
||||
native backend already emits its whole output as assembly *text*. An `asm`
|
||||
block therefore needs only to reach the backend's text stream verbatim — the
|
||||
existing assembler does the real work. Parsing instructions into the AST would
|
||||
duplicate the assembler's job, couple the front end to one architecture's
|
||||
instruction set, and buy nothing: the bytes still end up as text the assembler
|
||||
re-reads.
|
||||
|
||||
The block is thus modelled as an *opaque leaf* in the AST — text in, text out,
|
||||
inspected by no pass between parser and backend.
|
||||
|
||||
== Design patterns applied
|
||||
|
||||
The change is small, but two established patterns make the boundaries clean and
|
||||
keep the architecture honest. They are called out because the user asked, and
|
||||
because naming them pins the intended seams for future maintainers.
|
||||
|
||||
=== Strategy (GoF) — already the backend seam, extended not bent
|
||||
|
||||
Code generation is already a Strategy: `ICodeGen` is the strategy interface and
|
||||
`TCodeGenNative` / `TCodeGenQBE` are concrete strategies selected per `--backend`
|
||||
(`blaise.codegen.driver`). An `asm` body is a new *body kind* each strategy
|
||||
decides how to handle:
|
||||
|
||||
* native translates it (emit verbatim),
|
||||
* QBE refuses it (diagnostic).
|
||||
|
||||
No new dispatch mechanism is introduced — `TAsmStmt` flows through the same
|
||||
`EmitStmt` strategy method every other statement uses. This is the litmus test
|
||||
that the feature fits: the only per-backend code is one new `case`/`is`
|
||||
arm inside each existing strategy, not a new branch in the pipeline.
|
||||
|
||||
=== Hexagonal / Ports-and-Adapters — the assembler is a driven adapter
|
||||
|
||||
The compiler core (lexer → parser → AST → semantic) is the *domain*; the
|
||||
toolchain it drives out to (internal assembler, `cc`, internal linker) sits
|
||||
behind a *port*. `TAsmStmt` carries an opaque payload that the domain never
|
||||
interprets; only the *driven adapter* — the assembler behind the port —
|
||||
understands it. This is exactly the hexagonal intent: the core stays ignorant
|
||||
of x86-64 instruction encoding (a detail of the outside world), and the
|
||||
architecture-specific knowledge lives at the adapter edge
|
||||
(`blaise.assembler.x86_64`, `blaise.codegen.native.x86_64`).
|
||||
|
||||
Concretely, this dictates *where the text may and may not be touched*: the
|
||||
front end and semantic pass must treat the block as a black box. Any future
|
||||
feature that needs to understand the asm (e.g. `@result` substitution) belongs
|
||||
in the architecture adapter, behind the same port — never in the parser or
|
||||
semantic pass. Keeping that rule is what stops inline asm from leaking
|
||||
x86-64 assumptions into the portable core as more architectures are added.
|
||||
|
||||
=== Null Object (GoF) — `nostackframe` as the no-op frame strategy
|
||||
|
||||
Frame setup is currently unconditional in `EmitFunctionDef`
|
||||
(`pushq %rbp` / `movq %rsp,%rbp` / `subq`). `nostackframe` selects a *null*
|
||||
frame strategy: emit no prologue/epilogue, the asm body owns the entire frame.
|
||||
Implemented as a guard around the existing prologue/epilogue emission rather
|
||||
than a parallel code path, so the two cases cannot drift.
|
||||
|
||||
== Detailed design
|
||||
|
||||
=== Lexing — capturing the raw block
|
||||
|
||||
The tokeniser (`uPasTokeniser.TFpgPasToken`) records `TextStart` (1-based source
|
||||
index) and `Len` for every token, and exposes the whole `Source`. This is
|
||||
sufficient to slice the verbatim block without a second scanner:
|
||||
|
||||
. The lexer recognises `asm` as a keyword token (`tkAsm`).
|
||||
. On `tkAsm`, a new lexer entry point `ReadAsmBody` drives the tokeniser
|
||||
forward, tracking the source offset, until it reaches the matching `end`
|
||||
keyword token. The raw text between the end of the `asm` token and the start
|
||||
of the `end` token is returned via `Source` + offsets — comments and
|
||||
whitespace preserved, nothing re-tokenised as Pascal.
|
||||
. Nesting: assembly has no `asm … end` nesting, so the first `end` keyword
|
||||
terminates the block. The one caveat is a `#` line comment or a string in
|
||||
the asm that contains the word `end`; the GNU assembler's own tokens are
|
||||
line-oriented, so the scan matches `end` only as a standalone keyword token
|
||||
at statement position (the tokeniser already classifies it as `fptkKeyword`),
|
||||
which an `end` inside a comment is not. This mirrors how the tokeniser
|
||||
already distinguishes a keyword from an identifier substring.
|
||||
|
||||
Rationale recorded in `language-rationale.adoc`; the `asm` keyword and the
|
||||
block production added to `grammar.ebnf`.
|
||||
|
||||
=== AST — `TAsmStmt`
|
||||
|
||||
[source,pascal]
|
||||
----
|
||||
TAsmStmt = class(TASTStmt)
|
||||
public
|
||||
Code: string; { verbatim assembly text of the block }
|
||||
end;
|
||||
----
|
||||
|
||||
A routine with an `asm` body holds, in its `Body.Stmts`, a single `TAsmStmt`
|
||||
(the simplest representation that reuses the existing `TBlock`/`Stmts`
|
||||
container and the existing body-present/absent logic). `TMethodDecl` gains a
|
||||
`NoStackFrame: Boolean` flag for the `nostackframe` directive.
|
||||
|
||||
=== Parsing
|
||||
|
||||
. Directive loop already accepts `assembler`; add `nostackframe` there, setting
|
||||
`TMethodDecl.NoStackFrame`.
|
||||
. Body parsing: when the next token is `tkAsm`, call the lexer's raw-capture
|
||||
path and wrap the returned text in a `TAsmStmt` inside a fresh `TBlock`,
|
||||
instead of `ParseBlock`. `assembler` is advisory in FPC (an `asm` body
|
||||
implies it); Blaise keys off the `asm` token, so `assembler;` without an
|
||||
`asm` body, or an `asm` body without `assembler;`, both behave sensibly.
|
||||
|
||||
=== Semantic analysis
|
||||
|
||||
`TAsmStmt` is opaque: `AnalyseStmt` has a no-op arm for it. The routine's
|
||||
parameters, return type, calling convention, and emitted symbol name are
|
||||
resolved exactly as for any routine, so call sites bind normally. No
|
||||
liveness, type, or ARC analysis runs over the block.
|
||||
|
||||
=== Codegen — native
|
||||
|
||||
In `EmitFunctionDef`:
|
||||
|
||||
. Emit `.text`, the `.globl`/label as today.
|
||||
. If `NoStackFrame`: skip prologue, frame `subq`, and the epilogue; emit the
|
||||
`TAsmStmt.Code` verbatim (the body is responsible for `ret`).
|
||||
. Else (an `asm` body that still wants the standard frame): emit the normal
|
||||
prologue, then the verbatim asm, then the normal epilogue — supported, but
|
||||
the RTL bodies use `nostackframe`.
|
||||
|
||||
`EmitStmt` gains a `TAsmStmt` arm that writes the verbatim lines, used when an
|
||||
`asm` block appears as the sole body statement.
|
||||
|
||||
=== Codegen — QBE
|
||||
|
||||
`TAsmStmt` raises a clear "inline asm requires the native backend" codegen
|
||||
error. QBE is not the inline-asm target; the native backend is the default and
|
||||
the only one that emits assembly text.
|
||||
|
||||
=== Unit interface I/O (`.bif`)
|
||||
|
||||
`TAsmStmt` gains encode/decode in `uUnitInterfaceIO` (one string field) and a
|
||||
`bif-coverage.status` entry, so an `asm`-bodied routine exported by a unit
|
||||
round-trips through the unit cache. `NoStackFrame` joins the `TMethodDecl`
|
||||
directive flags already serialised (`IsVirtual`, `IsOverride`, …).
|
||||
|
||||
== Migration of the `.s` files
|
||||
|
||||
Once `asm` blocks land, each `runtime/src/main/asm/*_x86_64.s` body can move
|
||||
into an `asm` routine in a `.pas` unit (e.g. `blaise_start.pas`,
|
||||
`blaise_cpu.pas`), the Makefile's `$(CC) -c` asm rule and `$(ASM_DIR)` go away,
|
||||
and the RTL builds with zero external-assembler invocations. That migration is
|
||||
a *follow-up* commit per file, gated on this feature; it is not part of the
|
||||
feature commit, so a regression is bisectable to one body at a time.
|
||||
|
||||
The `_start` body is the natural first migration and its e2e guards already
|
||||
exist (`TInternalAsmE2ETests.TestEntry_*`).
|
||||
|
||||
== Testing
|
||||
|
||||
Per project policy, both layers:
|
||||
|
||||
. *IR/asm unit test* — compile a tiny `asm`-body routine and assert the emitted
|
||||
native assembly contains the verbatim instructions and, for `nostackframe`,
|
||||
omits the `pushq %rbp` prologue.
|
||||
. *E2E (internal assembler + linker)* — a program that calls an `asm` routine
|
||||
returning a known value in `%rax`, run end-to-end, asserting stdout/exit.
|
||||
This is the only layer that proves the block assembles and executes; it runs
|
||||
through `TInternalAsmE2ETests`’ real-compiler harness.
|
||||
. *QBE rejection* — assert the QBE backend errors on an `asm` body.
|
||||
. *.bif round-trip* — a unit exporting an `asm` routine, imported via the unit
|
||||
cache, still links and runs (guards the serialiser).
|
||||
. *Fixpoints* — once a `.s` body is migrated, the native fixpoints exercise it
|
||||
(the compiler links its own RTL).
|
||||
|
||||
== Risks
|
||||
|
||||
* *Block-termination scanning* — a mis-scan that swallows past the real `end`
|
||||
would corrupt the following declarations. Mitigated by matching `end` only
|
||||
as a standalone keyword token and by the parser re-synchronising on the
|
||||
routine's trailing `;`. Covered by a test with an `end`-mentioning comment
|
||||
inside the block.
|
||||
* *Symbol naming* — the asm references callees/globals by their emitted symbol
|
||||
name; a mismatch is a link error, caught by the e2e/fixpoint link step.
|
||||
* *`nostackframe` misuse* — a body that omits `ret` or corrupts the stack
|
||||
crashes at run time; the e2e execution test is the guard.
|
||||
|
||||
== Alternatives considered
|
||||
|
||||
* *Keep the `.s` files, add a `blaise --assemble file.s` mode (Route A).*
|
||||
Smaller, and reaches "no gcc in the RTL build" too, but leaves assembly
|
||||
outside the language and keeps a parallel build path. Inline `asm` blocks
|
||||
(this design, Route B) fold the assembly into ordinary units, matching FPC and
|
||||
removing the `$(ASM_DIR)` special case entirely. Route A remains a valid
|
||||
stepping stone but is not pursued.
|
||||
* *Parse instructions into a structured AST.* Rejected — duplicates the
|
||||
assembler, couples the front end to x86-64, and provides no benefit for the
|
||||
text-to-text path (see "Why opaque-text").
|
||||
|
|
@ -5209,3 +5209,62 @@ anything that can be read as a designator can be assigned to.
|
|||
* *Require an intermediate variable (status quo).* Rejected — it is a
|
||||
gratuitous restriction with no semantic justification; the read side already
|
||||
supported the full chain, so write-side parity is the consistent choice.
|
||||
|
||||
== Inline Assembler — `asm … end` Routine Bodies
|
||||
|
||||
=== Decision
|
||||
|
||||
A routine body may be written as an inline-assembler block instead of a Pascal
|
||||
`begin … end` block:
|
||||
|
||||
[source,pascal]
|
||||
----
|
||||
function GetSelf: Pointer; assembler; nostackframe;
|
||||
asm
|
||||
movq %rdi, %rax
|
||||
ret
|
||||
end;
|
||||
----
|
||||
|
||||
The block content is verbatim GNU/AT&T assembly. The front end (lexer, parser,
|
||||
semantic pass) never interprets it — the lexer captures the whole `asm … end`
|
||||
as one opaque token, and the native backend emits the text verbatim into the
|
||||
assembly stream, where the existing internal (or external) assembler parses it.
|
||||
The `nostackframe` directive suppresses the compiler prologue/epilogue so the
|
||||
asm body owns the entire frame (it provides its own `ret`). Inline asm requires
|
||||
the native backend; the QBE backend rejects it with a diagnostic. `asm`
|
||||
routines mix freely with ordinary Pascal routines in a standard `.pas` unit
|
||||
(no `.inc` include mechanism is involved).
|
||||
|
||||
=== Rationale
|
||||
|
||||
The motivation is toolchain independence. The runtime's startup (`_start`),
|
||||
`setjmp`/`longjmp`, and atomic primitives are hand-written assembly. Shipping
|
||||
them as standalone `.s` files assembled by `cc -c` reintroduces a build-time gcc
|
||||
dependency, defeating the internal assembler/linker. Expressing them as `asm`
|
||||
bodies in Pascal units folds that assembly into Blaise's own pipeline — the same
|
||||
approach FPC takes (`rtl/linux/x86_64/si_c.inc`).
|
||||
|
||||
The block is modelled as opaque text rather than a parsed instruction AST: the
|
||||
internal assembler already consumes GNU/AT&T text and the native backend already
|
||||
emits text, so the block needs only to reach the backend's stream verbatim.
|
||||
Parsing instructions into the AST would duplicate the assembler and couple the
|
||||
front end to one architecture's instruction set for no benefit. This keeps the
|
||||
architecture-specific knowledge at the backend/assembler edge (a ports-and-
|
||||
adapters boundary), so the portable core stays ignorant of x86-64 encoding. See
|
||||
docs/inline-asm-design.adoc for the full design and the design patterns applied.
|
||||
|
||||
=== Alternatives considered
|
||||
|
||||
* *A standalone `blaise --assemble file.s -o file.o` mode (Route A).* Reaches
|
||||
"no gcc in the RTL build" too and is smaller, but leaves assembly outside the
|
||||
language and keeps a parallel build path. Inline `asm` blocks fold the
|
||||
assembly into ordinary units, matching FPC and removing the special-case
|
||||
build directory. Route A remains a valid stepping stone but was not pursued.
|
||||
* *Parse instructions into a structured AST.* Rejected — duplicates the
|
||||
assembler, couples the front end to x86-64, and provides nothing for the
|
||||
text-to-text path.
|
||||
* *Reserve `asm` only contextually (keep it usable as an identifier).* Rejected
|
||||
— `asm` is a reserved word in FPC/Delphi and in the grammar; the codebase had
|
||||
a single local variable named `Asm` (renamed), and contextual keywording would
|
||||
need parser state the lexer does not have.
|
||||
|
|
|
|||
|
|
@ -112,6 +112,9 @@ TIfStmt.ElseStmt serialise
|
|||
# TCompoundStmt (uAST.pas:243)
|
||||
TCompoundStmt.Stmts serialise
|
||||
|
||||
# TAsmStmt (uAST.pas:255)
|
||||
TAsmStmt.Code serialise
|
||||
|
||||
# TWhileStmt (uAST.pas:250)
|
||||
TWhileStmt.Condition serialise
|
||||
TWhileStmt.Body serialise
|
||||
|
|
|
|||
Loading…
Reference in a new issue