Skip to main content
asm blocks and asm functions are not supported on Windows. The MSVC build path does not wire up NASM, so any source file containing asm will fail to produce a working binary on Windows. See Compiler Status and the Windows section of the installation guide.
C△ supports inline assembly through the asm keyword. There are two forms:
  • asm functions — named, callable from C△ code with parameters and a return type. Must end with return unless the return type is void.
  • Anonymous asm {} blocks — bare blocks embedded in surrounding code. Must end with an explicit exit (typically a sys_exit syscall), because control would otherwise fall through into the surrounding C△ code with the CPU still mid-assembly.
Each asm block is compiled to a separate .asm file, assembled with NASM, and linked into the final binary by GCC — fully transparent to the rest of your program. You write assembly with the same function-call interface as any other C△ function, using native C△ type declarations for parameters and return for the exit path.

Basic Syntax

Define an asm function by opening a block with the asm keyword, a function name, a parameter list, and the syntax and section directives.
asm functionName(params) {
    syntax x86_64_linux
    section .text
    // assembly instructions
    return
}
Each element of the block has a specific role:
  • Function name — becomes the global label in the generated .asm file and the callable symbol linked into your binary.
  • Parameters — declared using C△ type syntax (e.g., int a). You do not use assembler directives like %define or equ for parameters.
  • syntax — declares the target architecture and platform. Accepted values are x86_64_linux, x86_64_elf, and arm64_macho.
  • section .text — mandatory for x86_64 NASM targets. Use .section __TEXT,__text for arm64_macho.
  • return; — emits a ret instruction. By itself, return; returns whatever value is currently in the platform return register (rax on x86_64, x0 on ARM64) — you do not need to declare a return type or write return expr; if your assembly has already placed the result there. Use return expr; when you want the compiler to move a C△-level expression into the return register for you before returning. Required for every non-void asm function; a void asm function may omit it (the compiler emits a bare ret).
Variable declarations inside asm blocks may omit semicolons — a newline terminates both declarations and instructions. This keeps the block readable alongside raw assembly mnemonics.

Example: Add Two Integers

The following asm function adds two integers and returns the result through the standard calling convention.
asm int addInts(int a, int b) {
    syntax x86_64_linux
    section .text
    int scratch = 0
    return a + b
}
Call it from the rest of your C△ program exactly as you would any ordinary function:
int result = addInts(3, 7);   // result = 10
The compiler generates a .asm file for addInts, assembles it with NASM, and links the resulting object file alongside the GCC-compiled C output.

Example: Raw Syscall

When you need direct kernel access, write the instructions by hand. Use mov and syscall (or svc on ARM64) as you would in standalone assembly.
asm void exitProcess(int code) {
    syntax x86_64_elf
    section .text
    mov rax, 60    // sys_exit
    mov rdi, code
    syscall
}
exitProcess is declared void and terminates via sys_exit, so no return is needed. For any non-void asm function, return (or return expr;) is required.

Anonymous asm Blocks

Anonymous asm {} blocks can appear at file scope or nested inside any C△ scope — most commonly the body of a function, but also if/else, loops, or any other block. Variables declared inside the block (using C△ declarations like int counter = 0) are registered in the enclosing C△ scope, so the surrounding C△ code can read and write them by name as if they had been declared there directly.
int main() {
    asm {
        syntax x86_64_linux
        section .data
        int counter = 0       // declared inside the asm block
        section .text
        mov [counter], 1
    }

    // `counter` is visible here because the enclosing scope is main()
    counter += 1;
    return counter;           // returns 2
}
An anonymous asm {} block must end with an explicit exit when it is the final thing the program runs — otherwise control falls through into the surrounding C△ code with the CPU still mid-assembly. Use the platform’s exit syscall — mov rax, 60; mov rdi, 0; syscall on x86_64 Linux or mov x16, 1; mov x0, #0; svc #0x80 on ARM64 macOS. When the block is embedded mid-scope (as above) and the surrounding C△ code is expected to keep running, end with a ret-free fallthrough is acceptable — just make sure the CPU state on exit is consistent with the surrounding C△ code’s expectations. Unlike asm functions, return is not valid here — anonymous blocks do not have a return frame.
For embedding raw assembly without defining a named function, use a bare asm {} block. The earlier example above shows a nested block; the example below shows a top-level block that exits the process directly.
// x86_64 Linux — exit the process with status 0
asm {
    syntax x86_64_linux
    section .data
    int counter = 0
    section .text
    mov rax, 60      // sys_exit
    mov rdi, 0       // exit status
    syscall
}
Use anonymous blocks when you need to set up data labels, share locals with the surrounding C△ scope, or emit architecture-specific instructions that are not naturally expressed as a function call.

Syntax Targets

The syntaxdirective tells the compiler which architecture and platform to target when generating the .asm file and choosing the NASM output format.
Syntax TargetArchitecturePlatformText Section Directive
x86_64_linuxx86_64Linuxsection .text
arm64_machoARM64macOS.section __TEXT,__text

Compilation Pipeline

The Hypotenuse Compiler processes asm blocks on a separate path from ordinary C△ code. Both paths converge at the GCC linker step to produce the final binary.
.ctri source
  └─▶ code gen ─▶ .c file ─▶ GCC ──────────────────────┐
  └─▶ asm blocks ─▶ .asm files ─▶ NASM ─▶ GCC linker ─▶ binary
You do not need to invoke NASM or manage object files yourself. Pass your .ctri source to hypc and the entire pipeline runs automatically.

Rules

Each asm block is entirely opaque to the simulation pass. The compiler does not perform last-use analysis or autoremove tracking inside asm bodies. Manage any heap pointers you pass into assembly code manually, or perform a Robbery before the pointer enters the block.
Variables declared inside asm functions are registered both for import access from other modules and within the function’s own scope. Variables declared in bare asm {} blocks are registered in the enclosing C△ scope — whether that’s a function body, a control-flow block, or file scope — and can be referenced by name from the surrounding C△ code as if they had been declared there directly.
Do not use raw assembler data directives such as db, dw, or dd inside asm blocks. Represent data storage with C△ variable declarations instead. The compiler translates these into the correct NASM output for your target.
The text section directive is mandatory in every asm block. Omitting it causes a NASM assembly error at build time. Use section .text for x86_64 targets and .section __TEXT,__text for arm64_macho.
  • Use return; to emit ret — it returns whatever is in the platform return register (rax / x0), so a typed return annotation is optional when your assembly already populates it. Use return expr; to move a C△-level expression into the return register first. Do not write ret directly.
  • Each asm block produces its own .asm file. Name your asm functions clearly — the function name becomes the exported symbol linked into the final binary.