= 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").
