From 8b76d35b0a045e0a3277be422513b938ef33dcce Mon Sep 17 00:00:00 2001 From: hachem Date: Wed, 9 Sep 2026 05:14:44 +0200 Subject: feat: fasm backend --- Dockerfile | 1 + README.md | 11 +- docs/language.md | 21 +- examples/fibonacci.hdass | 16 +- examples/loop_sum.hdass | 10 +- examples/records.hdass | 6 +- meson.build | 2 +- scripts/dump_asm.sh | 34 ++ scripts/run_suite.sh | 4 +- scripts/test_examples.sh | 19 +- src/ast.h | 2 + src/codegen.c | 1508 ++++++++++++++++++++++++++++++++++++++++++++++ src/codegen.h | 8 + src/main.c | 11 +- src/nasm.c | 1351 ----------------------------------------- src/nasm.h | 7 - src/parser.c | 10 + tests/codegen_test.c | 68 ++- tests/parser_test.c | 18 + 19 files changed, 1718 insertions(+), 1389 deletions(-) create mode 100755 scripts/dump_asm.sh create mode 100644 src/codegen.c create mode 100644 src/codegen.h delete mode 100644 src/nasm.c delete mode 100644 src/nasm.h diff --git a/Dockerfile b/Dockerfile index e8f37e0..eb8ae2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM --platform=linux/amd64 debian:trixie-slim RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ nasm \ + fasm \ binutils \ meson \ ninja-build \ diff --git a/README.md b/README.md index c3e942e..299b904 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ hachem's dumb assembly super-set, pronounced "HD Ass", or "headass"...depends on The entire point of this project is to provide a middle ground between C and assembly. If we think about why we still write assembly today, it's usually because we need direct control over what the CPU is doing. We want control over the exact instructions being executed, the memory, the stack and everything else that higher-level programming languages normally abstract away. The thing is, not every program written in assembly actually needs that control. Sometimes you want to write something close to the machine without having to manuall deal with every tiny detail yourself. You still want registers, explicit control over memory and a good understanding of what your program is doing, but you don't necessarily need to manually express everything as individual assembly instructions. -This is where this piece of shit comes in. It's not quite high-level enough to be a C-like language, but it's also not low-level enough to be as annoying to write as raw assembly. The goal is to sit somewhere in between, keeping the parts of assembly that make it useful while making the parts that don't need to be painful a little nicer to work with. hdass transpiles into multiple flavours of assembly, such as NASM and MASM, rather than directly producing machine code. The idea is to provide a single language for writing low-level programs while allowing the backend to translate code into the assembler syntax you want to target. You're still ultimately producing assembly, and you're never particularly far away from the code that gets assembled. The goal isn't to hide the machine from you or turn assembly into C. There are already plenty of high-level languages that do that. hdass just fills the gap between the two, where you might want some more convenience whilst writing assembly without taking away the reason you wanted to work close to the machine in the first place. +This is where this piece of shit comes in. It's not quite high-level enough to be a C-like language, but it's also not low-level enough to be as annoying to write as raw assembly. The goal is to sit somewhere in between, keeping the parts of assembly that make it useful while making the parts that don't need to be painful a little nicer to work with. hdass transpiles into multiple flavours of assembly, currently NASM and FASM (MASM is planned), rather than directly producing machine code. The idea is to provide a single language for writing low-level programs while allowing the backend to translate code into the assembler syntax you want to target. You're still ultimately producing assembly, and you're never particularly far away from the code that gets assembled. The goal isn't to hide the machine from you or turn assembly into C. There are already plenty of high-level languages that do that. hdass just fills the gap between the two, where you might want some more convenience whilst writing assembly without taking away the reason you wanted to work close to the machine in the first place. ## Examples Here's a simple "Hello, World!" world program written using hdass' syntax: @@ -53,7 +53,7 @@ meson test -C build ``` If [cppcheck](https://cppcheck.sourceforge.io/) is installed, `ninja -C build cppcheck` runs static analysis over the sources. -hdass emits x86-64 assembly, so to actually assemble and run its output you need an x86-64 Linux toolchain. The bundled Docker environment provides a consistent one on any host, including Apple Silicon, where the amd64 image runs under emulation. The image is a Debian base with `nasm`, `ld` (binutils), a C toolchain and Meson/Ninja preinstalled. +hdass emits x86-64 assembly, so to actually assemble and run its output you need an x86-64 Linux toolchain. The bundled Docker environment provides a consistent one on any host, including Apple Silicon, where the amd64 image runs under emulation. The image is a Debian base with `nasm`, `fasm`, `ld` (binutils), a C toolchain and Meson/Ninja preinstalled. Start the container (this builds the image the first time): ```bash @@ -74,6 +74,13 @@ ld -e main hello.o -o hello ./hello ``` +hdass targets NASM by default; pass `-t fasm` to emit FASM instead, which assembles in a single step: +```bash +./build-linux/hdass -t fasm examples/hello_world.hdass -o hello.asm +fasm hello.asm hello.o +ld -e main hello.o -o hello +``` + To transpile, assemble, link and run every program in [examples/](examples/) and check its output, use the end-to-end test script (also from inside the container): ```bash meson setup build-linux && meson compile -C build-linux && ./scripts/test_examples.sh diff --git a/docs/language.md b/docs/language.md index 42b178e..79d7866 100644 --- a/docs/language.md +++ b/docs/language.md @@ -1,6 +1,6 @@ # hdass language reference -hdass emits NASM for x86-64; fasm and masm are planned. The compiler output itself isn't tied to an OS, but the examples and toolchain here target Linux (Linux syscall numbers, `nasm -f elf64`, `ld`). Pipeline: `lex → parse → analyze → emit`. +hdass emits NASM or FASM for x86-64 (`-t nasm` by default, `-t fasm`); masm is planned. The instruction bodies are the same Intel syntax for both — only the framing (headers, sections, constants, data) differs. The compiler output itself isn't tied to an OS, but the examples and toolchain here target Linux (Linux syscall numbers, ELF64, `ld`). Pipeline: `lex → parse → analyze → emit`. ## A first program @@ -154,6 +154,13 @@ while rcx > 0 } ``` +An optional `.name` right after `while` names the loop's generated labels, so they read as `.name` (top) and `.name_end` (exit) instead of the anonymous `.while_N` — handy for finding a loop in the emitted assembly. Give nested loops distinct names. + +```hdass +while .countdown rcx > 0 // emits `.countdown:` … `jmp .countdown` … `.countdown_end:` + rcx -= 1 +``` + ## Dereference (`^`) `^reg` is the memory at the address in `reg` — NASM's `[reg]`. On the left of `=` it stores there. The store width comes from the value operand, so a sized sub-register picks the size: @@ -262,14 +269,22 @@ See [examples/mandelbrot.hdass](../examples/mandelbrot.hdass) for a float progra ## Building a program ```bash -hdass program.hdass -o program.asm +hdass program.hdass -o program.asm # nasm (default) nasm -f elf64 program.asm -o program.o ld -e main program.o -o program ./program ``` +Or target fasm with `-t fasm`, which assembles in one step: + +```bash +hdass -t fasm program.hdass -o program.asm +fasm program.asm program.o +ld -e main program.o -o program +``` + The [README](../README.md) has a Docker setup with these tools. ## Some stinkies -Clobbering is your responsibility: `syscall` trashes `rcx` and `r11`, while a callee can trash any registers it touches, so nothing is saved automatically. `examples/fibonacci.hdass`, for example, keeps its counter in `r15` for this reason. Register widths must also match, meaning something like `rax = r1.8` would become `mov rax, al`, which will not assemble. Division clobbers extra registers: `/` `%` and their `=` forms use `idiv` through `rax:rdx`, so both are overwritten regardless of the destination. The divisor can be anything — a register, a constant, or an immediate — but an immediate or an `rax`/`rdx` divisor is first copied into `r11`, so those also clobber `r11`. Finally, the entry procedure has no `ret`; it should end with an exit syscall. +Clobbering is your responsibility: `syscall` trashes `rcx` and `r11`, while a callee can trash any registers it touches, so nothing is saved automatically. `examples/fibonacci.hdass`, for example, keeps its counter in `r15` for this reason. Register widths must also match, meaning something like `rax = r1.8` would become `mov rax, al`, which will not assemble. Division clobbers extra registers: `/` `%` and their `=` forms use `idiv` through `rax:rdx`, so both are overwritten regardless of the destination. The divisor can be anything — a register, a constant, or an immediate — but an immediate or an `rax`/`rdx` divisor is first copied into `r11`, so those also clobber `r11`. Labels and procedures become plain assembler symbols, so avoid names the target assembler reserves: `loop`, for instance, is an instruction mnemonic that fasm rejects as a label (nasm allows it). Finally, the entry procedure has no `ret`; it should end with an exit syscall. diff --git a/examples/fibonacci.hdass b/examples/fibonacci.hdass index 9436580..47224fb 100644 --- a/examples/fibonacci.hdass +++ b/examples/fibonacci.hdass @@ -45,16 +45,16 @@ proc main r13 = 1 r15 = 10 // syscall clobbers rcx/r11, so keep the counter in r15 -loop: - print_number(r12) + while .sequence r15 > 0 + { + print_number(r12) - rax = r12 - r12 = r13 - r13 += rax + rax = r12 + r12 = r13 + r13 += rax - r15 -= 1 - if r15 != 0 - goto loop + r15 -= 1 + } rax = SYS_EXIT rdi = 0 diff --git a/examples/loop_sum.hdass b/examples/loop_sum.hdass index 2427b17..130f055 100644 --- a/examples/loop_sum.hdass +++ b/examples/loop_sum.hdass @@ -8,11 +8,11 @@ proc main rbx = 0 // running total rcx = 5 // counter -loop: - rbx += rcx - rcx -= 1 - if rcx != 0 - goto loop + while .countdown rcx > 0 + { + rbx += rcx + rcx -= 1 + } rdi = rbx rax = SYS_EXIT diff --git a/examples/records.hdass b/examples/records.hdass index f8553d6..8e87a4c 100644 --- a/examples/records.hdass +++ b/examples/records.hdass @@ -22,15 +22,17 @@ proc main stack pair[Pair.size] rsi = pair + rsi += Pair.a rbx = 40 - ^rsi = rbx // pair.a (offset 0) + ^rsi = rbx // pair.a rsi = pair rsi += Pair.b rcx = Status.Fail // 2 - ^rsi = rcx // pair.b (offset 8) + ^rsi = rcx // pair.b rsi = pair + rsi += Pair.a rdi = ^rsi // load a = 40 rsi = pair rsi += Pair.b diff --git a/meson.build b/meson.build index b445e94..75226c3 100644 --- a/meson.build +++ b/meson.build @@ -36,7 +36,7 @@ core = static_library( 'src/diag.c', 'src/file.c', 'src/lexer.c', - 'src/nasm.c', + 'src/codegen.c', 'src/parser.c', 'src/sema.c', ), diff --git a/scripts/dump_asm.sh b/scripts/dump_asm.sh new file mode 100755 index 0000000..cb8a710 --- /dev/null +++ b/scripts/dump_asm.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -euo pipefail + +root="$(cd "$(dirname "$0")/.." && pwd)" +cd "$root" + +assemblers="${1:-${ASSEMBLER:-nasm fasm}}" + +docker compose up -d >/dev/null + +docker compose exec -T -e ASMS="$assemblers" hdass bash -c ' + set -e + cd /hdass + [ -d build-linux ] || meson setup build-linux >/dev/null + meson compile -C build-linux >/dev/null + +cat > /tmp/sidebyside.awk <<"AWK" +NR == FNR { left[FNR] = $0; if (length($0) > width) width = length($0); ln = FNR; next } +{ right[FNR] = $0; if (FNR > rn) rn = FNR } +END { + total = ln > rn ? ln : rn + for (i = 1; i <= total; i += 1) + printf "%-*s | %s\n", width, (i in left ? left[i] : ""), (i in right ? right[i] : "") +} +AWK + + for asm in $ASMS; do + for src in examples/*.hdass; do + printf "\n===== %s (%s) =====\n" "$(basename "$src")" "$asm" + ./build-linux/hdass -t "$asm" "$src" -o /tmp/gen.asm + awk -f /tmp/sidebyside.awk <(expand -t 4 "$src") <(expand -t 4 /tmp/gen.asm) + done + done +' /dev/null 2>&1; then echo "${red}error:${reset} '$tool' not found; run this inside the Docker environment" >&2 exit 1 fi done +assemble() +{ + if [ "$assembler" = "fasm" ]; then + fasm "$1" "$2" >/dev/null + else + nasm -f elf64 "$1" -o "$2" + fi +} + work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT @@ -35,9 +46,9 @@ check() local name="$1" desc="$2" source="$3" expected_exit="$4" expected_stdout="$5" local asm="$work/$name.asm" obj="$work/$name.o" bin="$work/$name" stage="" - if ! "$hdass" "$source" -o "$asm" 2>"$work/err"; then + if ! "$hdass" -t "$assembler" "$source" -o "$asm" 2>"$work/err"; then stage="transpile" - elif ! nasm -f elf64 "$asm" -o "$obj" 2>"$work/err"; then + elif ! assemble "$asm" "$obj" 2>"$work/err"; then stage="assemble" elif ! ld -e main "$obj" -o "$bin" 2>"$work/err"; then stage="link" @@ -71,7 +82,7 @@ check() pass=$((pass + 1)) } -printf '%s━━ example programs ━━%s\n' "$bold" "$reset" +printf '%s━━ example programs (%s) ━━%s\n' "$bold" "$assembler" "$reset" check hello_world "writes a greeting to stdout" examples/hello_world.hdass 0 "Hello, World!" check greet "writes a fixed string" examples/greet.hdass 0 "hdass works!" diff --git a/src/ast.h b/src/ast.h index f5eea17..d764c13 100644 --- a/src/ast.h +++ b/src/ast.h @@ -152,6 +152,8 @@ struct IfStatement struct WhileStatement { + bool named; + struct Token name; struct Expr* left; struct Token comparison; struct Expr* right; diff --git a/src/codegen.c b/src/codegen.c new file mode 100644 index 0000000..2eaa8bd --- /dev/null +++ b/src/codegen.c @@ -0,0 +1,1508 @@ +#include +#include +#include +#include +#include +#include + +#include "codegen.h" + +static void emit_const_expr(struct Expr* expr, FILE* out) +{ + switch (expr->kind) + { + case EXPR_PRIMARY: + fprintf(out, "%.*s", (int)expr->primary.token.length, expr->primary.token.start); + break; + case EXPR_UNARY: + fprintf(out, "%.*s", (int)expr->unary.op.length, expr->unary.op.start); + emit_const_expr(expr->unary.operand, out); + break; + case EXPR_BINARY: + emit_const_expr(expr->binary.left, out); + fprintf(out, " %.*s ", (int)expr->binary.op.length, expr->binary.op.start); + emit_const_expr(expr->binary.right, out); + break; + default: + break; + } +} + +static const char* assign_mnemonic(enum TokenType op) +{ + switch (op) + { + case TOKEN_EQUAL: return "mov"; + case TOKEN_PLUS_EQUAL: return "add"; + case TOKEN_MINUS_EQUAL: return "sub"; + case TOKEN_STAR_EQUAL: return "imul"; + default: return NULL; + } +} + +struct FloatTable +{ + struct Token* items; + size_t count; + size_t capacity; +}; + +struct Emitter +{ + struct Program* program; + struct ProcDecl* proc; + struct FloatTable* floats; + FILE* out; + uint32_t label_id; +}; + +static bool is_float_register(struct Token token) +{ + if (token.length < 4 || memcmp(token.start, "xmm", 3) != 0) + return false; + + for (size_t i = 3; i < token.length; i += 1) + if (token.start[i] < '0' || token.start[i] > '9') + return false; + + return true; +} + +static size_t float_index(const struct FloatTable* floats, struct Token literal) +{ + for (size_t i = 0; i < floats->count; i += 1) + if (floats->items[i].length == literal.length + && memcmp(floats->items[i].start, literal.start, literal.length) == 0) + return i; + + return floats->count; +} + +static const char* sized_register(struct Token reg, enum StoreSize size); + +static struct Token resolve_token(struct Emitter* emitter, struct Token token) +{ + if (emitter->proc == NULL) + return token; + + for (size_t i = 0; i < emitter->proc->param_count; i += 1) + { + struct Param param = emitter->proc->params[i]; + if (param.name.length == token.length && memcmp(param.name.start, token.start, token.length) == 0) + return param.reg; + } + + return token; +} + +static struct Token text_token(const char* text) +{ + struct Token token; + token.type = TOKEN_IDENTIFIER; + token.start = text; + token.length = strlen(text); + token.line = 0; + return token; +} + +// with the logical_registers extension, r1..r14 name the general-purpose +// registers; rsp/rbp and the instruction pointer keep their dedicated names. +static const char* logical_register_base(struct Token token) +{ + static const char* registers[] = { + "rax", "rbx", "rcx", "rdx", "rsi", "rdi", + "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", + }; + + if (token.length < 2 || token.start[0] != 'r') + return NULL; + + uint32_t index = 0; + for (size_t i = 1; i < token.length; i += 1) + { + char digit = token.start[i]; + if (digit < '0' || digit > '9') + return NULL; + index = index * 10 + (uint32_t)(digit - '0'); + } + + if (index < 1 || index > sizeof(registers) / sizeof(registers[0])) + return NULL; + + return registers[index - 1]; +} + +static struct Token resolve_register(struct Emitter* emitter, struct Token token) +{ + struct Token resolved = resolve_token(emitter, token); + + if (emitter->program->config.logical_registers) + { + const char* base = logical_register_base(resolved); + if (base != NULL) + return text_token(base); + } + + return resolved; +} + +static uint64_t token_to_u64(struct Token token) +{ + if (token.length > 2 && token.start[0] == '0' && (token.start[1] == 'x' || token.start[1] == 'X')) + { + uint64_t value = 0; + for (size_t i = 2; i < token.length; i += 1) + { + char digit = token.start[i]; + uint64_t nibble = digit <= '9' ? (uint64_t)(digit - '0') + : (uint64_t)((digit | 0x20) - 'a' + 10); + value = value * 16 + nibble; + } + return value; + } + + if (token.length > 2 && token.start[0] == '0' && (token.start[1] == 'b' || token.start[1] == 'B')) + { + uint64_t value = 0; + for (size_t i = 2; i < token.length; i += 1) + value = value * 2 + (uint64_t)(token.start[i] - '0'); + return value; + } + + uint64_t value = 0; + for (size_t i = 0; i < token.length; i += 1) + value = value * 10 + (uint64_t)(token.start[i] - '0'); + + return value; +} + +static bool fold_const(struct Program* program, struct Expr* expr, uint64_t* out); + +static bool buffer_offset(struct Emitter* emitter, struct Token name, uint64_t* out_offset) +{ + struct ProcDecl* proc = emitter->proc; + uint64_t cumulative = 0; + for (size_t i = 0; i < proc->body_count; i += 1) + { + struct Statement* statement = &proc->body[i]; + if (statement->kind != STATEMENT_STACK) + continue; + + uint64_t size = 0; + fold_const(emitter->program, statement->stack.size, &size); + cumulative += size; + if (statement->stack.name.length == name.length + && memcmp(statement->stack.name.start, name.start, name.length) == 0) + { + *out_offset = cumulative; + return true; + } + } + + return false; +} + +static bool is_buffer_name(struct Emitter* emitter, struct Token token) +{ + uint64_t offset; + return emitter->proc != NULL && buffer_offset(emitter, token, &offset); +} + +static enum StoreSize size_from_int(struct Token token) +{ + switch (token_to_u64(token)) + { + case 8: return STORE_SIZE_BYTE; + case 16: return STORE_SIZE_WORD; + case 32: return STORE_SIZE_DWORD; + case 64: return STORE_SIZE_QWORD; + default: return STORE_SIZE_NONE; + } +} + +static bool tokens_equal(struct Token a, struct Token b) +{ + return a.length == b.length && memcmp(a.start, b.start, a.length) == 0; +} + +static bool token_matches(struct Token token, const char* text) +{ + size_t length = strlen(text); + return token.length == length && memcmp(token.start, text, length) == 0; +} + +static struct EnumDecl* find_enum(struct Program* program, struct Token name) +{ + for (size_t i = 0; i < program->enum_count; i += 1) + if (tokens_equal(program->enums[i].name, name)) + return &program->enums[i]; + + return NULL; +} + +static struct StructDecl* find_struct(struct Program* program, struct Token name) +{ + for (size_t i = 0; i < program->struct_count; i += 1) + if (tokens_equal(program->structs[i].name, name)) + return &program->structs[i]; + + return NULL; +} + +static uint64_t store_size_bytes(enum StoreSize size) +{ + switch (size) + { + case STORE_SIZE_BYTE: return 1; + case STORE_SIZE_WORD: return 2; + case STORE_SIZE_DWORD: return 4; + default: return 8; + } +} + +static uint64_t char_literal_value(struct Token token) +{ + if (token.length >= 4 && token.start[1] == '\\') + { + switch (token.start[2]) + { + case 'n': return 10; + case 't': return 9; + case 'r': return 13; + case '0': return 0; + case '\\': return 92; + case '\'': return 39; + default: return (unsigned char)token.start[2]; + } + } + + return (unsigned char)token.start[1]; +} + +static bool fold_member(struct Program* program, struct Expr* object, struct Token member, uint64_t* out) +{ + if (object->kind != EXPR_PRIMARY) + return false; + struct Token name = object->primary.token; + + const struct EnumDecl* enumeration = find_enum(program, name); + if (enumeration != NULL) + { + for (size_t i = 0; i < enumeration->member_count; i += 1) + if (tokens_equal(enumeration->members[i], member)) + { + *out = i; + return true; + } + return false; + } + + const struct StructDecl* layout = find_struct(program, name); + if (layout != NULL) + { + uint64_t offset = 0; + for (size_t i = 0; i < layout->field_count; i += 1) + { + if (tokens_equal(layout->fields[i].name, member)) + { + *out = offset; + return true; + } + offset += store_size_bytes(layout->fields[i].size); + } + if (token_matches(member, "size")) + { + *out = offset; + return true; + } + } + + return false; +} + +// evaluates a compile-time constant expression: integer/char literals, other +// constants, enum values and struct offsets, and + - * / over them +static bool fold_const(struct Program* program, struct Expr* expr, uint64_t* out) +{ + switch (expr->kind) + { + case EXPR_PRIMARY: + { + struct Token token = expr->primary.token; + if (token.type == TOKEN_INTEGER) + { + *out = token_to_u64(token); + return true; + } + if (token.type == TOKEN_CHAR) + { + *out = char_literal_value(token); + return true; + } + if (token.type == TOKEN_IDENTIFIER) + for (size_t i = 0; i < program->const_count; i += 1) + if (tokens_equal(program->consts[i].name, token)) + return fold_const(program, program->consts[i].value, out); + return false; + } + case EXPR_UNARY: + { + uint64_t value; + if (!fold_const(program, expr->unary.operand, &value)) + return false; + *out = 0 - value; + return true; + } + case EXPR_BINARY: + { + uint64_t left; + uint64_t right; + if (!fold_const(program, expr->binary.left, &left) + || !fold_const(program, expr->binary.right, &right)) + return false; + + switch (expr->binary.op.type) + { + case TOKEN_PLUS: *out = left + right; return true; + case TOKEN_MINUS: *out = left - right; return true; + case TOKEN_STAR: *out = left * right; return true; + case TOKEN_SLASH: *out = right != 0 ? left / right : 0; return true; + default: return false; + } + } + case EXPR_MEMBER: + return fold_member(program, expr->member.object, expr->member.member, out); + case EXPR_DEREF: + return false; + } + + return false; +} + +// an enum member folds to its 0-based index; a struct member folds to its byte +// offset (or the total size for `.size`) +static bool emit_named_member(struct Emitter* emitter, struct Token object, struct Token member) +{ + const struct EnumDecl* enumeration = find_enum(emitter->program, object); + if (enumeration != NULL) + { + for (size_t i = 0; i < enumeration->member_count; i += 1) + if (tokens_equal(enumeration->members[i], member)) + { + fprintf(emitter->out, "%zu", i); + return true; + } + } + + const struct StructDecl* layout = find_struct(emitter->program, object); + if (layout != NULL) + { + uint64_t offset = 0; + for (size_t i = 0; i < layout->field_count; i += 1) + { + if (tokens_equal(layout->fields[i].name, member)) + { + fprintf(emitter->out, "%llu", (unsigned long long)offset); + return true; + } + offset += store_size_bytes(layout->fields[i].size); + } + if (token_matches(member, "size")) + { + fprintf(emitter->out, "%llu", (unsigned long long)offset); + return true; + } + } + + return false; +} + +static bool emit_operand(struct Emitter* emitter, struct Expr* expr) +{ + switch (expr->kind) + { + case EXPR_PRIMARY: + { + struct Token token = resolve_register(emitter, expr->primary.token); + fprintf(emitter->out, "%.*s", (int)token.length, token.start); + return true; + } + case EXPR_MEMBER: + { + // a register size suffix: r1.64 -> rax, r1.8 -> al + if (expr->member.member.type == TOKEN_INTEGER && + expr->member.object->kind == EXPR_PRIMARY) + { + enum StoreSize size = size_from_int(expr->member.member); + struct Token base = resolve_register(emitter, expr->member.object->primary.token); + const char* sized = sized_register(base, size); + if (sized != NULL) + fprintf(emitter->out, "%s", sized); + else + fprintf(emitter->out, "%.*s", (int)base.length, base.start); + return true; + } + + // enum value or struct offset + if (expr->member.object->kind == EXPR_PRIMARY + && emit_named_member(emitter, expr->member.object->primary.token, expr->member.member)) + return true; + + if (!emit_operand(emitter, expr->member.object)) + return false; + fprintf(emitter->out, ".%.*s", (int)expr->member.member.length, expr->member.member.start); + return true; + } + case EXPR_UNARY: + { + uint64_t value; + if (!fold_const(emitter->program, expr, &value)) + return false; + fprintf(emitter->out, "%lld", (long long)value); + return true; + } + case EXPR_BINARY: + case EXPR_DEREF: + return false; + } + + return false; +} + +static bool is_gp_register(struct Token token) +{ + static const char* names[] = { + "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", + "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", + }; + + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i += 1) + if (token_matches(token, names[i])) + return true; + + return false; +} + +// idiv can divide by a general-purpose register directly, but not by rax or rdx +// (it overwrites both with the quotient and remainder) nor by an immediate (it +// has no immediate form). Those divisors are copied into r11 first — already a +// volatile register here, clobbered by syscall just like rcx. +static bool divisor_uses_scratch(struct Emitter* emitter, const struct Expr* divisor) +{ + if (divisor->kind == EXPR_PRIMARY) + { + struct Token reg = resolve_register(emitter, divisor->primary.token); + if (is_gp_register(reg)) + return token_matches(reg, "rax") || token_matches(reg, "rdx"); + return true; + } + + // enum/struct members and unary minus fold to immediates; a binary or deref + // divisor is left to the direct path (unsupported, as before) + return divisor->kind == EXPR_MEMBER || divisor->kind == EXPR_UNARY; +} + +// idiv divides rdx:rax by its operand, leaving the quotient in rax and the +// remainder in rdx. The dividend arrives in dst; this loads it into rax, routes +// an awkward divisor through r11, and runs idiv. It clobbers rax, rdx and r11. +static void emit_idiv(struct Emitter* emitter, const char* dst, struct Expr* divisor) +{ + FILE* out = emitter->out; + + if (!divisor_uses_scratch(emitter, divisor)) + { + if (strcmp(dst, "rax") != 0) + fprintf(out, "\tmov rax, %s\n", dst); + fprintf(out, "\tcqo\n\tidiv "); + emit_operand(emitter, divisor); + fprintf(out, "\n"); + return; + } + + // a divisor in rax must be saved before the dividend overwrites rax + bool divisor_in_rax = divisor->kind == EXPR_PRIMARY + && token_matches(resolve_register(emitter, divisor->primary.token), "rax"); + + if (divisor_in_rax && strcmp(dst, "rax") != 0) + { + if (strcmp(dst, "r11") == 0) + fprintf(out, "\txchg rax, r11\n"); + else + fprintf(out, "\tmov r11, rax\n\tmov rax, %s\n", dst); + } + else + { + if (strcmp(dst, "rax") != 0) + fprintf(out, "\tmov rax, %s\n", dst); + fprintf(out, "\tmov r11, "); + emit_operand(emitter, divisor); + fprintf(out, "\n"); + } + + fprintf(out, "\tcqo\n\tidiv r11\n"); +} + +static void emit_division(struct Emitter* emitter, const char* dst, struct Expr* divisor) +{ + emit_idiv(emitter, dst, divisor); + if (strcmp(dst, "rax") != 0) + fprintf(emitter->out, "\tmov %s, rax\n", dst); +} + +static void emit_modulo(struct Emitter* emitter, const char* dst, struct Expr* divisor) +{ + emit_idiv(emitter, dst, divisor); + if (strcmp(dst, "rdx") != 0) + fprintf(emitter->out, "\tmov %s, rdx\n", dst); +} + +static void emit_divide(struct Emitter* emitter, struct AssignStatement* assign) +{ + if (assign->target_deref) + { + fprintf(emitter->out, "\t; TODO: unsupported division\n"); + return; + } + + struct Token target = resolve_register(emitter, assign->target); + char dst[32]; + snprintf(dst, sizeof(dst), "%.*s", (int)target.length, target.start); + + if (assign->op.type == TOKEN_PERCENT_EQUAL) + emit_modulo(emitter, dst, assign->value); + else + emit_division(emitter, dst, assign->value); +} + +// an expression can be evaluated into a register when it is a single term +// (primary or member), or a left-associative chain of binary operators whose +// right operands are plain operands (never a buffer or a nested binary) +static bool expr_supported(struct Emitter* emitter, struct Expr* expr) +{ + switch (expr->kind) + { + case EXPR_PRIMARY: + case EXPR_MEMBER: + case EXPR_DEREF: + return true; + case EXPR_UNARY: + { + uint64_t value; + return fold_const(emitter->program, expr, &value); + } + case EXPR_BINARY: + if (expr->binary.right->kind != EXPR_PRIMARY && + expr->binary.right->kind != EXPR_MEMBER && + expr->binary.right->kind != EXPR_UNARY) + return false; + if (expr->binary.right->kind == EXPR_UNARY) + { + uint64_t value; + if (!fold_const(emitter->program, expr->binary.right, &value)) + return false; + } + if (expr->binary.right->kind == EXPR_PRIMARY && + is_buffer_name(emitter, expr->binary.right->primary.token)) + return false; + return expr_supported(emitter, expr->binary.left); + } + + return false; +} + +// dst = [address], extending narrower loads into the full register. Unsigned +// loads zero-extend (movzx / a 32-bit mov); signed loads sign-extend (movsx, +// or movsxd for a dword). +static void emit_load(struct Emitter* emitter, const char* dst, struct DerefExpr* deref) +{ + FILE* out = emitter->out; + if (deref->is_signed) + { + switch (deref->size) + { + case STORE_SIZE_BYTE: + fprintf(out, "\tmovsx %s, byte [", dst); + break; + case STORE_SIZE_WORD: + fprintf(out, "\tmovsx %s, word [", dst); + break; + case STORE_SIZE_DWORD: + fprintf(out, "\tmovsxd %s, dword [", dst); + break; + default: + fprintf(out, "\tmov %s, [", dst); + break; + } + + emit_operand(emitter, deref->address); + fprintf(out, "]\n"); + return; + } + + switch (deref->size) + { + case STORE_SIZE_BYTE: + fprintf(out, "\tmovzx %s, byte [", dst); + break; + case STORE_SIZE_WORD: + fprintf(out, "\tmovzx %s, word [", dst); + break; + case STORE_SIZE_DWORD: + { + const char* dword = sized_register(text_token(dst), STORE_SIZE_DWORD); + fprintf(out, "\tmov %s, [", dword != NULL ? dword : dst); + break; + } + default: + fprintf(out, "\tmov %s, [", dst); + break; + } + + emit_operand(emitter, deref->address); + fprintf(out, "]\n"); +} + +static void emit_expr_into(struct Emitter* emitter, const char* dst, struct Expr* expr) +{ + if (expr->kind == EXPR_DEREF) + { + emit_load(emitter, dst, &expr->deref); + return; + } + + if (expr->kind == EXPR_BINARY) + { + emit_expr_into(emitter, dst, expr->binary.left); + + if (expr->binary.op.type == TOKEN_SLASH) + { + emit_division(emitter, dst, expr->binary.right); + return; + } + if (expr->binary.op.type == TOKEN_PERCENT) + { + emit_modulo(emitter, dst, expr->binary.right); + return; + } + + const char* mnemonic = + expr->binary.op.type == TOKEN_PLUS ? "add" : + expr->binary.op.type == TOKEN_MINUS ? "sub" : "imul"; + fprintf(emitter->out, "\t%s %s, ", mnemonic, dst); + emit_operand(emitter, expr->binary.right); + fprintf(emitter->out, "\n"); + return; + } + + if (expr->kind == EXPR_PRIMARY && is_buffer_name(emitter, expr->primary.token)) + { + uint64_t offset; + buffer_offset(emitter, expr->primary.token, &offset); + fprintf(emitter->out, "\tlea %s, [rbp - %llu]\n", dst, (unsigned long long)offset); + return; + } + + fprintf(emitter->out, "\tmov %s, ", dst); + emit_operand(emitter, expr); + fprintf(emitter->out, "\n"); +} + +static const char* store_size_keyword(enum StoreSize size) +{ + switch (size) + { + case STORE_SIZE_BYTE: return "byte "; + case STORE_SIZE_WORD: return "word "; + case STORE_SIZE_DWORD: return "dword "; + case STORE_SIZE_QWORD: return "qword "; + default: return ""; + } +} + +// maps a full 64-bit register to its byte/word/dword sub-register for a sized +// store, so `^byte rsi = rdx` writes `dl` rather than the whole register. +// returns NULL when the token is not a full register, or no resizing applies. +static const char* sized_register(struct Token reg, enum StoreSize size) +{ + if (size == STORE_SIZE_NONE || size == STORE_SIZE_QWORD) + return NULL; + + static const struct RegisterSizes + { + const char* quad; + const char* dword; + const char* word; + const char* byte; + } registers[] = + { + { "rax", "eax", "ax", "al" }, + { "rbx", "ebx", "bx", "bl" }, + { "rcx", "ecx", "cx", "cl" }, + { "rdx", "edx", "dx", "dl" }, + { "rsi", "esi", "si", "sil" }, + { "rdi", "edi", "di", "dil" }, + { "rbp", "ebp", "bp", "bpl" }, + { "rsp", "esp", "sp", "spl" }, + { "r8", "r8d", "r8w", "r8b" }, + { "r9", "r9d", "r9w", "r9b" }, + { "r10", "r10d", "r10w", "r10b" }, + { "r11", "r11d", "r11w", "r11b" }, + { "r12", "r12d", "r12w", "r12b" }, + { "r13", "r13d", "r13w", "r13b" }, + { "r14", "r14d", "r14w", "r14b" }, + { "r15", "r15d", "r15w", "r15b" }, + }; + + for (size_t i = 0; i < sizeof(registers) / sizeof(registers[0]); i += 1) + { + const struct RegisterSizes* entry = ®isters[i]; + size_t length = strlen(entry->quad); + if (reg.length != length || memcmp(reg.start, entry->quad, length) != 0) + continue; + + switch (size) + { + case STORE_SIZE_DWORD: return entry->dword; + case STORE_SIZE_WORD: return entry->word; + case STORE_SIZE_BYTE: return entry->byte; + default: return NULL; + } + } + + return NULL; +} + +static const char* float_mnemonic(enum TokenType op) +{ + switch (op) + { + case TOKEN_EQUAL: return "movsd"; + case TOKEN_PLUS_EQUAL: return "addsd"; + case TOKEN_MINUS_EQUAL: return "subsd"; + case TOKEN_STAR_EQUAL: return "mulsd"; + case TOKEN_SLASH_EQUAL: return "divsd"; + default: return NULL; + } +} + +static bool value_is_float(struct Emitter* emitter, const struct Expr* expr) +{ + if (expr->kind != EXPR_PRIMARY) + return false; + if (expr->primary.token.type == TOKEN_FLOAT) + return true; + + return is_float_register(resolve_register(emitter, expr->primary.token)); +} + +// floating point: xmm moves and arithmetic, conversions to/from general-purpose +// registers, and float literals loaded from their .data slot +static bool emit_float_assign(struct Emitter* emitter, struct AssignStatement* assign, + struct Token target, bool target_float) +{ + struct Expr* value = assign->value; + + // float store: ^ptr = xmm -> movsd [ptr], xmm + if (assign->target_deref) + { + if (assign->op.type != TOKEN_EQUAL || value->kind != EXPR_PRIMARY) + return false; + struct Token source = resolve_register(emitter, value->primary.token); + if (!is_float_register(source)) + return false; + fprintf(emitter->out, "\tmovsd [%.*s], %.*s\n", + (int)target.length, target.start, (int)source.length, source.start); + return true; + } + + // float load: xmm = ^ptr -> movsd xmm, [ptr] + if (value->kind == EXPR_DEREF) + { + if (!target_float || assign->op.type != TOKEN_EQUAL) + return false; + fprintf(emitter->out, "\tmovsd %.*s, [", (int)target.length, target.start); + emit_operand(emitter, value->deref.address); + fprintf(emitter->out, "]\n"); + return true; + } + + if (value->kind == EXPR_PRIMARY && value->primary.token.type == TOKEN_FLOAT) + { + if (!target_float || assign->op.type != TOKEN_EQUAL) + return false; + size_t index = float_index(emitter->floats, value->primary.token); + fprintf(emitter->out, "\tmovsd %.*s, [__float%zu]\n", + (int)target.length, target.start, index); + return true; + } + + if (value->kind != EXPR_PRIMARY) + return false; + + struct Token source = resolve_register(emitter, value->primary.token); + bool source_float = is_float_register(source); + + if (target_float && source_float) + { + const char* mnemonic = float_mnemonic(assign->op.type); + if (mnemonic == NULL) + return false; + fprintf(emitter->out, "\t%s %.*s, %.*s\n", mnemonic, + (int)target.length, target.start, (int)source.length, source.start); + return true; + } + + if (assign->op.type != TOKEN_EQUAL) + return false; + + if (target_float) + fprintf(emitter->out, "\tcvtsi2sd %.*s, %.*s\n", + (int)target.length, target.start, (int)source.length, source.start); + else + fprintf(emitter->out, "\tcvttsd2si %.*s, %.*s\n", + (int)target.length, target.start, (int)source.length, source.start); + return true; +} + +static void emit_assign(struct Emitter* emitter, struct AssignStatement* assign) +{ + struct Token float_target = resolve_register(emitter, assign->target); + if (is_float_register(float_target) || value_is_float(emitter, assign->value)) + { + if (!emit_float_assign(emitter, assign, float_target, is_float_register(float_target))) + fprintf(emitter->out, "\t; TODO: unsupported float assignment\n"); + return; + } + + if (assign->op.type == TOKEN_SLASH_EQUAL || assign->op.type == TOKEN_PERCENT_EQUAL) + { + emit_divide(emitter, assign); + return; + } + + struct Token target = resolve_register(emitter, assign->target); + + if (assign->op.type == TOKEN_EQUAL && !assign->target_deref) + { + if (!expr_supported(emitter, assign->value)) + { + fprintf(emitter->out, "\t; TODO: unsupported assignment\n"); + return; + } + + char dst[32]; + snprintf(dst, sizeof(dst), "%.*s", (int)target.length, target.start); + emit_expr_into(emitter, dst, assign->value); + return; + } + + // deref store or compound assignment: needs a plain operand, not a buffer or binary + const char* mnemonic = assign_mnemonic(assign->op.type); + bool value_is_buffer = assign->value->kind == EXPR_PRIMARY + && is_buffer_name(emitter, assign->value->primary.token); + if (mnemonic == NULL || assign->value->kind == EXPR_BINARY + || assign->value->kind == EXPR_DEREF || value_is_buffer) + { + fprintf(emitter->out, "\t; TODO: unsupported assignment\n"); + return; + } + + // adding or subtracting a constant zero (e.g. a struct field at offset 0) is a no-op + uint64_t folded; + if (!assign->target_deref + && (assign->op.type == TOKEN_PLUS_EQUAL || assign->op.type == TOKEN_MINUS_EQUAL) + && fold_const(emitter->program, assign->value, &folded) && folded == 0) + return; + + if (assign->target_deref) + { + fprintf(emitter->out, "\t%s %s[%.*s], ", mnemonic, + store_size_keyword(assign->store_size), (int)target.length, target.start); + + const char* sized = NULL; + if (assign->value->kind == EXPR_PRIMARY) + { + struct Token value = resolve_register(emitter, assign->value->primary.token); + sized = sized_register(value, assign->store_size); + if (sized != NULL) + fprintf(emitter->out, "%s", sized); + } + + if (sized == NULL) + emit_operand(emitter, assign->value); + } + else + { + fprintf(emitter->out, "\t%s %.*s, ", mnemonic, (int)target.length, target.start); + emit_operand(emitter, assign->value); + } + + fprintf(emitter->out, "\n"); +} + +static const char* jump_if_false(enum TokenType comparison) +{ + switch (comparison) + { + case TOKEN_EQUAL_EQUAL: return "jne"; + case TOKEN_BANG_EQUAL: return "je"; + case TOKEN_LESS: return "jge"; + case TOKEN_LESS_EQUAL: return "jg"; + case TOKEN_GREATER: return "jle"; + case TOKEN_GREATER_EQUAL: return "jl"; + default: return NULL; + } +} + +static struct ProcDecl* find_proc(struct Program* program, struct Token name) +{ + for (size_t i = 0; i < program->proc_count; i += 1) + { + struct ProcDecl* proc = &program->procs[i]; + if (proc->name.length == name.length && memcmp(proc->name.start, name.start, name.length) == 0) + return proc; + } + + return NULL; +} + +static void emit_call(struct Emitter* emitter, struct CallStatement* call) +{ + const struct ProcDecl* callee = find_proc(emitter->program, call->name); + if (callee == NULL || callee->param_count != call->arg_count) + { + fprintf(emitter->out, "\t; TODO: unsupported call\n"); + return; + } + + for (size_t i = 0; i < call->arg_count; i += 1) + { + if (call->args[i]->kind == EXPR_BINARY || call->args[i]->kind == EXPR_DEREF) + { + fprintf(emitter->out, "\t; TODO: unsupported call argument\n"); + continue; + } + + struct Token reg = resolve_register(emitter, callee->params[i].reg); + fprintf(emitter->out, "\tmov %.*s, ", (int)reg.length, reg.start); + emit_operand(emitter, call->args[i]); + fprintf(emitter->out, "\n"); + } + + fprintf(emitter->out, "\tcall %.*s\n", (int)call->name.length, call->name.start); +} + +static void emit_statement(struct Emitter* emitter, struct Statement* statement); + +static void emit_block(struct Emitter* emitter, struct Statement* body, size_t count) +{ + for (size_t i = 0; i < count; i += 1) + emit_statement(emitter, &body[i]); +} + +// ucomisd sets the flags like an unsigned compare, so float branches use the +// unsigned jump family (ja/jae/jb/jbe) rather than the signed one +static const char* float_jump_if_false(enum TokenType comparison) +{ + switch (comparison) + { + case TOKEN_EQUAL_EQUAL: return "jne"; + case TOKEN_BANG_EQUAL: return "je"; + case TOKEN_LESS: return "jae"; + case TOKEN_LESS_EQUAL: return "ja"; + case TOKEN_GREATER: return "jbe"; + case TOKEN_GREATER_EQUAL: return "jb"; + default: return NULL; + } +} + +static void emit_float_operand(struct Emitter* emitter, const struct Expr* expr) +{ + if (expr->kind == EXPR_PRIMARY && expr->primary.token.type == TOKEN_FLOAT) + { + fprintf(emitter->out, "[__float%zu]", float_index(emitter->floats, expr->primary.token)); + return; + } + + struct Token token = resolve_register(emitter, expr->primary.token); + fprintf(emitter->out, "%.*s", (int)token.length, token.start); +} + +// Emits the comparison for `left cmp right` and a jump to `target` taken when +// the condition is false, so the code that follows runs when it is true. Both +// if and while build on this. Returns false (after a TODO note) for a form that +// isn't supported yet. +static bool emit_branch_test(struct Emitter* emitter, struct Expr* left, + struct Token comparison, struct Expr* right, const char* target) +{ + bool is_float = value_is_float(emitter, left) || value_is_float(emitter, right); + + if (is_float) + { + const char* jump = float_jump_if_false(comparison.type); + bool left_reg = left->kind == EXPR_PRIMARY + && is_float_register(resolve_register(emitter, left->primary.token)); + if (jump == NULL || !left_reg || !value_is_float(emitter, right)) + { + fprintf(emitter->out, "\t; TODO: unsupported condition\n"); + return false; + } + + fprintf(emitter->out, "\tucomisd "); + emit_float_operand(emitter, left); + fprintf(emitter->out, ", "); + emit_float_operand(emitter, right); + fprintf(emitter->out, "\n\t%s %s\n", jump, target); + return true; + } + + const char* jump = jump_if_false(comparison.type); + if (jump == NULL + || left->kind == EXPR_BINARY || left->kind == EXPR_DEREF + || right->kind == EXPR_BINARY || right->kind == EXPR_DEREF) + { + fprintf(emitter->out, "\t; TODO: unsupported condition\n"); + return false; + } + + fprintf(emitter->out, "\tcmp "); + emit_operand(emitter, left); + fprintf(emitter->out, ", "); + emit_operand(emitter, right); + fprintf(emitter->out, "\n\t%s %s\n", jump, target); + return true; +} + +static void emit_if(struct Emitter* emitter, struct IfStatement* branch) +{ + bool has_else = branch->else_count > 0; + + uint32_t id = emitter->label_id; + emitter->label_id += 1; + + char target[32]; + snprintf(target, sizeof(target), ".if_%s_%u", has_else ? "else" : "end", id); + + if (!emit_branch_test(emitter, branch->left, branch->comparison, branch->right, target)) + return; + + emit_block(emitter, branch->body, branch->body_count); + + if (has_else) + { + fprintf(emitter->out, "\tjmp .if_end_%u\n", id); + fprintf(emitter->out, ".if_else_%u:\n", id); + emit_block(emitter, branch->else_body, branch->else_count); + } + + fprintf(emitter->out, ".if_end_%u:\n", id); +} + +static void emit_while(struct Emitter* emitter, struct WhileStatement* loop) +{ + char top[64]; + char end[64]; + + if (loop->named) + { + snprintf(top, sizeof(top), ".%.*s", (int)loop->name.length, loop->name.start); + snprintf(end, sizeof(end), ".%.*s_end", (int)loop->name.length, loop->name.start); + } + else + { + uint32_t id = emitter->label_id; + emitter->label_id += 1; + snprintf(top, sizeof(top), ".while_%u", id); + snprintf(end, sizeof(end), ".while_end_%u", id); + } + + fprintf(emitter->out, "%s:\n", top); + + if (!emit_branch_test(emitter, loop->left, loop->comparison, loop->right, end)) + return; + + emit_block(emitter, loop->body, loop->body_count); + + fprintf(emitter->out, "\tjmp %s\n", top); + fprintf(emitter->out, "%s:\n", end); +} + +static void emit_statement(struct Emitter* emitter, struct Statement* statement) +{ + FILE* out = emitter->out; + switch (statement->kind) + { + case STATEMENT_ASSIGN: + emit_assign(emitter, &statement->assign); + break; + case STATEMENT_LABEL: + fprintf(out, "%.*s:\n", (int)statement->label.name.length, statement->label.name.start); + break; + case STATEMENT_GOTO: + fprintf(out, "\tjmp %.*s\n", (int)statement->jump.label.length, statement->jump.label.start); + break; + case STATEMENT_SYSCALL: + fprintf(out, "\tsyscall\n"); + break; + case STATEMENT_IF: + emit_if(emitter, &statement->branch); + break; + case STATEMENT_WHILE: + emit_while(emitter, &statement->loop); + break; + case STATEMENT_CALL: + emit_call(emitter, &statement->call); + break; + case STATEMENT_STACK: + break; + default: + fprintf(out, "\t; TODO: unsupported statement\n"); + break; + } +} + +static uint64_t proc_stack_size(struct Program* program, struct ProcDecl* proc) +{ + uint64_t total = 0; + for (size_t i = 0; i < proc->body_count; i += 1) + { + struct Statement* statement = &proc->body[i]; + if (statement->kind == STATEMENT_STACK) + { + uint64_t size = 0; + fold_const(program, statement->stack.size, &size); + total += size; + } + } + + if (total % 16 != 0) + total += 16 - (total % 16); + + return total; +} + +static void collect_float(struct FloatTable* floats, struct Token token) +{ + if (token.type != TOKEN_FLOAT || float_index(floats, token) != floats->count) + return; + + if (floats->count == floats->capacity) + { + size_t capacity = floats->capacity < 8 ? 8 : floats->capacity * 2; + floats->items = realloc(floats->items, capacity * sizeof(struct Token)); + floats->capacity = capacity; + } + + floats->items[floats->count] = token; + floats->count += 1; +} + +static void collect_floats_expr(struct FloatTable* floats, struct Expr* expr) +{ + switch (expr->kind) + { + case EXPR_PRIMARY: + collect_float(floats, expr->primary.token); + break; + case EXPR_UNARY: + collect_floats_expr(floats, expr->unary.operand); + break; + case EXPR_BINARY: + collect_floats_expr(floats, expr->binary.left); + collect_floats_expr(floats, expr->binary.right); + break; + case EXPR_MEMBER: + collect_floats_expr(floats, expr->member.object); + break; + case EXPR_DEREF: + collect_floats_expr(floats, expr->deref.address); + break; + } +} + +static void collect_floats_statement(struct FloatTable* floats, struct Statement* statement) +{ + switch (statement->kind) + { + case STATEMENT_ASSIGN: + collect_floats_expr(floats, statement->assign.value); + break; + case STATEMENT_IF: + collect_floats_expr(floats, statement->branch.left); + collect_floats_expr(floats, statement->branch.right); + for (size_t i = 0; i < statement->branch.body_count; i += 1) + collect_floats_statement(floats, &statement->branch.body[i]); + for (size_t i = 0; i < statement->branch.else_count; i += 1) + collect_floats_statement(floats, &statement->branch.else_body[i]); + break; + case STATEMENT_WHILE: + collect_floats_expr(floats, statement->loop.left); + collect_floats_expr(floats, statement->loop.right); + for (size_t i = 0; i < statement->loop.body_count; i += 1) + collect_floats_statement(floats, &statement->loop.body[i]); + break; + case STATEMENT_CALL: + for (size_t i = 0; i < statement->call.arg_count; i += 1) + collect_floats_expr(floats, statement->call.args[i]); + break; + default: + break; + } +} + +static struct FloatTable collect_floats(struct Program* program) +{ + struct FloatTable floats = { NULL, 0, 0 }; + for (size_t i = 0; i < program->proc_count; i += 1) + for (size_t j = 0; j < program->procs[i].body_count; j += 1) + collect_floats_statement(&floats, &program->procs[i].body[j]); + + return floats; +} + +static void emit_proc(struct Program* program, struct FloatTable* floats, struct ProcDecl* proc, FILE* out) +{ + struct Emitter emitter; + emitter.program = program; + emitter.proc = proc; + emitter.floats = floats; + emitter.out = out; + emitter.label_id = 0; + + struct Config config = program->config; + bool is_entry = config.has_entry + && proc->name.length == config.entry.length + && memcmp(proc->name.start, config.entry.start, proc->name.length) == 0; + + fprintf(out, "%.*s:\n", (int)proc->name.length, proc->name.start); + + uint64_t stack_size = proc_stack_size(program, proc); + if (stack_size > 0) + { + fprintf(out, "\tpush rbp\n"); + fprintf(out, "\tmov rbp, rsp\n"); + fprintf(out, "\tsub rsp, %llu\n", (unsigned long long)stack_size); + } + + for (size_t i = 0; i < proc->body_count; i += 1) + emit_statement(&emitter, &proc->body[i]); + + if (!is_entry) + { + if (stack_size > 0) + fprintf(out, "\tleave\n"); + fprintf(out, "\tret\n"); + } +} + +// The instruction bodies above are plain Intel syntax, identical for every +// target assembler. Only the framing around them — the file header, constants, +// section directives, data definitions and the exported entry symbol — differs, +// so each backend supplies just those. +struct Backend +{ + void (*prologue)(const struct Program* program, FILE* out); + void (*constant)(struct ConstDecl decl, FILE* out); + void (*data_section)(FILE* out); + void (*string_data)(struct DataDecl decl, FILE* out); + void (*float_slot)(size_t index, struct Token literal, FILE* out); + void (*text_section)(FILE* out); + void (*global)(struct Token name, FILE* out); +}; + +static void nasm_prologue(const struct Program* program, FILE* out) +{ + fprintf(out, "bits %u\n", program->config.bits); +} + +static void nasm_constant(struct ConstDecl decl, FILE* out) +{ + fprintf(out, "%%define %.*s (", (int)decl.name.length, decl.name.start); + emit_const_expr(decl.value, out); + fprintf(out, ")\n"); +} + +static void nasm_data_section(FILE* out) +{ + fprintf(out, "section .data\n"); +} + +static void nasm_string_data(struct DataDecl decl, FILE* out) +{ + // the value lexeme keeps its quotes; NASM backtick strings interpret the + // same escapes, so re-wrap the inner content + fprintf(out, "%.*s: db `%.*s`\n", + (int)decl.name.length, decl.name.start, + (int)(decl.value.length - 2), decl.value.start + 1); + fprintf(out, ".len equ $ - %.*s\n", (int)decl.name.length, decl.name.start); +} + +static void nasm_float_slot(size_t index, struct Token literal, FILE* out) +{ + fprintf(out, "__float%zu: dq %.*s\n", index, (int)literal.length, literal.start); +} + +static void nasm_text_section(FILE* out) +{ + fprintf(out, "section .text\n"); +} + +static void nasm_global(struct Token name, FILE* out) +{ + fprintf(out, "global %.*s\n", (int)name.length, name.start); +} + +static const struct Backend nasm_backend = { + nasm_prologue, + nasm_constant, + nasm_data_section, + nasm_string_data, + nasm_float_slot, + nasm_text_section, + nasm_global, +}; + +static void fasm_prologue(const struct Program* program, FILE* out) +{ + fprintf(out, "format ELF%s\n", program->config.bits == 64 ? "64" : ""); +} + +static void fasm_constant(struct ConstDecl decl, FILE* out) +{ + fprintf(out, "%.*s = ", (int)decl.name.length, decl.name.start); + emit_const_expr(decl.value, out); + fprintf(out, "\n"); +} + +static void fasm_data_section(FILE* out) +{ + fprintf(out, "section '.data' writeable\n"); +} + +// fasm string literals are taken verbatim, so the escapes NASM would interpret +// are expanded here into the byte values fasm expects (db "run", 10, "run"). +static void fasm_string_data(struct DataDecl decl, FILE* out) +{ + fprintf(out, "%.*s db ", (int)decl.name.length, decl.name.start); + + const char* text = decl.value.start + 1; + size_t length = decl.value.length - 2; + bool in_quotes = false; + bool first = true; + + for (size_t i = 0; i < length; i += 1) + { + unsigned char byte = (unsigned char)text[i]; + if (byte == '\\' && i + 1 < length) + { + i += 1; + switch (text[i]) + { + case 'n': byte = '\n'; break; + case 't': byte = '\t'; break; + case 'r': byte = '\r'; break; + case '0': byte = '\0'; break; + case 'a': byte = '\a'; break; + case 'b': byte = '\b'; break; + case 'f': byte = '\f'; break; + case 'v': byte = '\v'; break; + case 'e': byte = 27; break; + default: byte = (unsigned char)text[i]; break; + } + + if (in_quotes) + { + fprintf(out, "\""); + in_quotes = false; + } + fprintf(out, "%s%u", first ? "" : ", ", byte); + first = false; + continue; + } + + if (!in_quotes) + { + fprintf(out, "%s\"", first ? "" : ", "); + in_quotes = true; + first = false; + } + fprintf(out, "%c", byte); + } + + if (in_quotes) + fprintf(out, "\""); + if (first) + fprintf(out, "\"\""); + fprintf(out, "\n"); + + fprintf(out, ".len = $ - %.*s\n", (int)decl.name.length, decl.name.start); +} + +static void fasm_float_slot(size_t index, struct Token literal, FILE* out) +{ + fprintf(out, "__float%zu dq %.*s\n", index, (int)literal.length, literal.start); +} + +static void fasm_text_section(FILE* out) +{ + fprintf(out, "section '.text' executable\n"); +} + +static void fasm_global(struct Token name, FILE* out) +{ + fprintf(out, "public %.*s\n", (int)name.length, name.start); +} + +static const struct Backend fasm_backend = { + fasm_prologue, + fasm_constant, + fasm_data_section, + fasm_string_data, + fasm_float_slot, + fasm_text_section, + fasm_global, +}; + +static void generate(struct Program* program, FILE* out, const struct Backend* backend) +{ + struct FloatTable floats = collect_floats(program); + + backend->prologue(program, out); + fprintf(out, "\n"); + + if (program->const_count > 0) + { + for (size_t i = 0; i < program->const_count; i += 1) + backend->constant(program->consts[i], out); + fprintf(out, "\n"); + } + + backend->data_section(out); + for (size_t i = 0; i < program->data_count; i += 1) + backend->string_data(program->data_decls[i], out); + for (size_t i = 0; i < floats.count; i += 1) + backend->float_slot(i, floats.items[i], out); + fprintf(out, "\n"); + + backend->text_section(out); + if (program->config.has_entry) + backend->global(program->config.entry, out); + + for (size_t i = 0; i < program->proc_count; i += 1) + { + fprintf(out, "\n"); + emit_proc(program, &floats, &program->procs[i], out); + } + + free(floats.items); +} + +void generate_nasm(struct Program* program, FILE* out) +{ + generate(program, out, &nasm_backend); +} + +void generate_fasm(struct Program* program, FILE* out) +{ + generate(program, out, &fasm_backend); +} diff --git a/src/codegen.h b/src/codegen.h new file mode 100644 index 0000000..38f37ee --- /dev/null +++ b/src/codegen.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include "ast.h" + +void generate_nasm(struct Program* program, FILE* out); +void generate_fasm(struct Program* program, FILE* out); diff --git a/src/main.c b/src/main.c index 4153a6e..f15af79 100644 --- a/src/main.c +++ b/src/main.c @@ -5,8 +5,8 @@ #include "diag.h" #include "sema.h" #include "lexer.h" -#include "nasm.h" #include "parser.h" +#include "codegen.h" int main(int argc, char** argv) { @@ -18,9 +18,9 @@ int main(int argc, char** argv) if (result == PARSE_ERROR) return 1; - if (args.target != ASSEMBLER_NASM) + if (args.target == ASSEMBLER_MASM) { - report_error_message("only the nasm target is supported"); + report_error_message("the masm target is not implemented yet"); return 1; } @@ -62,7 +62,10 @@ int main(int argc, char** argv) } } - generate_nasm(&program, out); + if (args.target == ASSEMBLER_FASM) + generate_fasm(&program, out); + else + generate_nasm(&program, out); if (out != stdout) fclose(out); diff --git a/src/nasm.c b/src/nasm.c deleted file mode 100644 index 2a98e11..0000000 --- a/src/nasm.c +++ /dev/null @@ -1,1351 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "nasm.h" - -static void emit_const_expr(struct Expr* expr, FILE* out) -{ - switch (expr->kind) - { - case EXPR_PRIMARY: - fprintf(out, "%.*s", (int)expr->primary.token.length, expr->primary.token.start); - break; - case EXPR_UNARY: - fprintf(out, "%.*s", (int)expr->unary.op.length, expr->unary.op.start); - emit_const_expr(expr->unary.operand, out); - break; - case EXPR_BINARY: - emit_const_expr(expr->binary.left, out); - fprintf(out, " %.*s ", (int)expr->binary.op.length, expr->binary.op.start); - emit_const_expr(expr->binary.right, out); - break; - default: - break; - } -} - -static void emit_consts(struct Program* program, FILE* out) -{ - for (size_t i = 0; i < program->const_count; i += 1) - { - struct ConstDecl decl = program->consts[i]; - fprintf(out, "%%define %.*s (", (int)decl.name.length, decl.name.start); - emit_const_expr(decl.value, out); - fprintf(out, ")\n"); - } -} - -static void emit_data(struct Program* program, FILE* out) -{ - fprintf(out, "section .data\n"); - - for (size_t i = 0; i < program->data_count; i += 1) - { - struct DataDecl decl = program->data_decls[i]; - - // the value lexeme keeps its surrounding double quotes; NASM backtick - // strings interpret the same escapes, so re-wrap the inner content - fprintf(out, "%.*s: db `%.*s`\n", - (int)decl.name.length, decl.name.start, - (int)(decl.value.length - 2), decl.value.start + 1); - fprintf(out, ".len equ $ - %.*s\n", - (int)decl.name.length, decl.name.start); - } -} - -static const char* assign_mnemonic(enum TokenType op) -{ - switch (op) - { - case TOKEN_EQUAL: return "mov"; - case TOKEN_PLUS_EQUAL: return "add"; - case TOKEN_MINUS_EQUAL: return "sub"; - case TOKEN_STAR_EQUAL: return "imul"; - default: return NULL; - } -} - -struct FloatTable -{ - struct Token* items; - size_t count; - size_t capacity; -}; - -struct Emitter -{ - struct Program* program; - struct ProcDecl* proc; - struct FloatTable* floats; - FILE* out; - uint32_t label_id; -}; - -static bool is_float_register(struct Token token) -{ - if (token.length < 4 || memcmp(token.start, "xmm", 3) != 0) - return false; - - for (size_t i = 3; i < token.length; i += 1) - if (token.start[i] < '0' || token.start[i] > '9') - return false; - - return true; -} - -static size_t float_index(const struct FloatTable* floats, struct Token literal) -{ - for (size_t i = 0; i < floats->count; i += 1) - if (floats->items[i].length == literal.length - && memcmp(floats->items[i].start, literal.start, literal.length) == 0) - return i; - - return floats->count; -} - -static const char* sized_register(struct Token reg, enum StoreSize size); - -static struct Token resolve_token(struct Emitter* emitter, struct Token token) -{ - if (emitter->proc == NULL) - return token; - - for (size_t i = 0; i < emitter->proc->param_count; i += 1) - { - struct Param param = emitter->proc->params[i]; - if (param.name.length == token.length && memcmp(param.name.start, token.start, token.length) == 0) - return param.reg; - } - - return token; -} - -static struct Token text_token(const char* text) -{ - struct Token token; - token.type = TOKEN_IDENTIFIER; - token.start = text; - token.length = strlen(text); - token.line = 0; - return token; -} - -// with the logical_registers extension, r1..r14 name the general-purpose -// registers; rsp/rbp and the instruction pointer keep their dedicated names. -static const char* logical_register_base(struct Token token) -{ - static const char* registers[] = { - "rax", "rbx", "rcx", "rdx", "rsi", "rdi", - "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", - }; - - if (token.length < 2 || token.start[0] != 'r') - return NULL; - - uint32_t index = 0; - for (size_t i = 1; i < token.length; i += 1) - { - char digit = token.start[i]; - if (digit < '0' || digit > '9') - return NULL; - index = index * 10 + (uint32_t)(digit - '0'); - } - - if (index < 1 || index > sizeof(registers) / sizeof(registers[0])) - return NULL; - - return registers[index - 1]; -} - -static struct Token resolve_register(struct Emitter* emitter, struct Token token) -{ - struct Token resolved = resolve_token(emitter, token); - - if (emitter->program->config.logical_registers) - { - const char* base = logical_register_base(resolved); - if (base != NULL) - return text_token(base); - } - - return resolved; -} - -static uint64_t token_to_u64(struct Token token) -{ - if (token.length > 2 && token.start[0] == '0' && (token.start[1] == 'x' || token.start[1] == 'X')) - { - uint64_t value = 0; - for (size_t i = 2; i < token.length; i += 1) - { - char digit = token.start[i]; - uint64_t nibble = digit <= '9' ? (uint64_t)(digit - '0') - : (uint64_t)((digit | 0x20) - 'a' + 10); - value = value * 16 + nibble; - } - return value; - } - - if (token.length > 2 && token.start[0] == '0' && (token.start[1] == 'b' || token.start[1] == 'B')) - { - uint64_t value = 0; - for (size_t i = 2; i < token.length; i += 1) - value = value * 2 + (uint64_t)(token.start[i] - '0'); - return value; - } - - uint64_t value = 0; - for (size_t i = 0; i < token.length; i += 1) - value = value * 10 + (uint64_t)(token.start[i] - '0'); - - return value; -} - -static bool fold_const(struct Program* program, struct Expr* expr, uint64_t* out); - -static bool buffer_offset(struct Emitter* emitter, struct Token name, uint64_t* out_offset) -{ - struct ProcDecl* proc = emitter->proc; - uint64_t cumulative = 0; - for (size_t i = 0; i < proc->body_count; i += 1) - { - struct Statement* statement = &proc->body[i]; - if (statement->kind != STATEMENT_STACK) - continue; - - uint64_t size = 0; - fold_const(emitter->program, statement->stack.size, &size); - cumulative += size; - if (statement->stack.name.length == name.length - && memcmp(statement->stack.name.start, name.start, name.length) == 0) - { - *out_offset = cumulative; - return true; - } - } - - return false; -} - -static bool is_buffer_name(struct Emitter* emitter, struct Token token) -{ - uint64_t offset; - return emitter->proc != NULL && buffer_offset(emitter, token, &offset); -} - -static enum StoreSize size_from_int(struct Token token) -{ - switch (token_to_u64(token)) - { - case 8: return STORE_SIZE_BYTE; - case 16: return STORE_SIZE_WORD; - case 32: return STORE_SIZE_DWORD; - case 64: return STORE_SIZE_QWORD; - default: return STORE_SIZE_NONE; - } -} - -static bool tokens_equal(struct Token a, struct Token b) -{ - return a.length == b.length && memcmp(a.start, b.start, a.length) == 0; -} - -static bool token_matches(struct Token token, const char* text) -{ - size_t length = strlen(text); - return token.length == length && memcmp(token.start, text, length) == 0; -} - -static struct EnumDecl* find_enum(struct Program* program, struct Token name) -{ - for (size_t i = 0; i < program->enum_count; i += 1) - if (tokens_equal(program->enums[i].name, name)) - return &program->enums[i]; - - return NULL; -} - -static struct StructDecl* find_struct(struct Program* program, struct Token name) -{ - for (size_t i = 0; i < program->struct_count; i += 1) - if (tokens_equal(program->structs[i].name, name)) - return &program->structs[i]; - - return NULL; -} - -static uint64_t store_size_bytes(enum StoreSize size) -{ - switch (size) - { - case STORE_SIZE_BYTE: return 1; - case STORE_SIZE_WORD: return 2; - case STORE_SIZE_DWORD: return 4; - default: return 8; - } -} - -static uint64_t char_literal_value(struct Token token) -{ - if (token.length >= 4 && token.start[1] == '\\') - { - switch (token.start[2]) - { - case 'n': return 10; - case 't': return 9; - case 'r': return 13; - case '0': return 0; - case '\\': return 92; - case '\'': return 39; - default: return (unsigned char)token.start[2]; - } - } - - return (unsigned char)token.start[1]; -} - -static bool fold_member(struct Program* program, struct Expr* object, struct Token member, uint64_t* out) -{ - if (object->kind != EXPR_PRIMARY) - return false; - struct Token name = object->primary.token; - - const struct EnumDecl* enumeration = find_enum(program, name); - if (enumeration != NULL) - { - for (size_t i = 0; i < enumeration->member_count; i += 1) - if (tokens_equal(enumeration->members[i], member)) - { - *out = i; - return true; - } - return false; - } - - const struct StructDecl* layout = find_struct(program, name); - if (layout != NULL) - { - uint64_t offset = 0; - for (size_t i = 0; i < layout->field_count; i += 1) - { - if (tokens_equal(layout->fields[i].name, member)) - { - *out = offset; - return true; - } - offset += store_size_bytes(layout->fields[i].size); - } - if (token_matches(member, "size")) - { - *out = offset; - return true; - } - } - - return false; -} - -// evaluates a compile-time constant expression: integer/char literals, other -// constants, enum values and struct offsets, and + - * / over them -static bool fold_const(struct Program* program, struct Expr* expr, uint64_t* out) -{ - switch (expr->kind) - { - case EXPR_PRIMARY: - { - struct Token token = expr->primary.token; - if (token.type == TOKEN_INTEGER) - { - *out = token_to_u64(token); - return true; - } - if (token.type == TOKEN_CHAR) - { - *out = char_literal_value(token); - return true; - } - if (token.type == TOKEN_IDENTIFIER) - for (size_t i = 0; i < program->const_count; i += 1) - if (tokens_equal(program->consts[i].name, token)) - return fold_const(program, program->consts[i].value, out); - return false; - } - case EXPR_UNARY: - { - uint64_t value; - if (!fold_const(program, expr->unary.operand, &value)) - return false; - *out = 0 - value; - return true; - } - case EXPR_BINARY: - { - uint64_t left; - uint64_t right; - if (!fold_const(program, expr->binary.left, &left) - || !fold_const(program, expr->binary.right, &right)) - return false; - - switch (expr->binary.op.type) - { - case TOKEN_PLUS: *out = left + right; return true; - case TOKEN_MINUS: *out = left - right; return true; - case TOKEN_STAR: *out = left * right; return true; - case TOKEN_SLASH: *out = right != 0 ? left / right : 0; return true; - default: return false; - } - } - case EXPR_MEMBER: - return fold_member(program, expr->member.object, expr->member.member, out); - case EXPR_DEREF: - return false; - } - - return false; -} - -// an enum member folds to its 0-based index; a struct member folds to its byte -// offset (or the total size for `.size`) -static bool emit_named_member(struct Emitter* emitter, struct Token object, struct Token member) -{ - const struct EnumDecl* enumeration = find_enum(emitter->program, object); - if (enumeration != NULL) - { - for (size_t i = 0; i < enumeration->member_count; i += 1) - if (tokens_equal(enumeration->members[i], member)) - { - fprintf(emitter->out, "%zu", i); - return true; - } - } - - const struct StructDecl* layout = find_struct(emitter->program, object); - if (layout != NULL) - { - uint64_t offset = 0; - for (size_t i = 0; i < layout->field_count; i += 1) - { - if (tokens_equal(layout->fields[i].name, member)) - { - fprintf(emitter->out, "%llu", (unsigned long long)offset); - return true; - } - offset += store_size_bytes(layout->fields[i].size); - } - if (token_matches(member, "size")) - { - fprintf(emitter->out, "%llu", (unsigned long long)offset); - return true; - } - } - - return false; -} - -static bool emit_operand(struct Emitter* emitter, struct Expr* expr) -{ - switch (expr->kind) - { - case EXPR_PRIMARY: - { - struct Token token = resolve_register(emitter, expr->primary.token); - fprintf(emitter->out, "%.*s", (int)token.length, token.start); - return true; - } - case EXPR_MEMBER: - { - // a register size suffix: r1.64 -> rax, r1.8 -> al - if (expr->member.member.type == TOKEN_INTEGER && - expr->member.object->kind == EXPR_PRIMARY) - { - enum StoreSize size = size_from_int(expr->member.member); - struct Token base = resolve_register(emitter, expr->member.object->primary.token); - const char* sized = sized_register(base, size); - if (sized != NULL) - fprintf(emitter->out, "%s", sized); - else - fprintf(emitter->out, "%.*s", (int)base.length, base.start); - return true; - } - - // enum value or struct offset - if (expr->member.object->kind == EXPR_PRIMARY - && emit_named_member(emitter, expr->member.object->primary.token, expr->member.member)) - return true; - - if (!emit_operand(emitter, expr->member.object)) - return false; - fprintf(emitter->out, ".%.*s", (int)expr->member.member.length, expr->member.member.start); - return true; - } - case EXPR_UNARY: - { - uint64_t value; - if (!fold_const(emitter->program, expr, &value)) - return false; - fprintf(emitter->out, "%lld", (long long)value); - return true; - } - case EXPR_BINARY: - case EXPR_DEREF: - return false; - } - - return false; -} - -static bool is_gp_register(struct Token token) -{ - static const char* names[] = { - "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", - "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15", - }; - - for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i += 1) - if (token_matches(token, names[i])) - return true; - - return false; -} - -// idiv can divide by a general-purpose register directly, but not by rax or rdx -// (it overwrites both with the quotient and remainder) nor by an immediate (it -// has no immediate form). Those divisors are copied into r11 first — already a -// volatile register here, clobbered by syscall just like rcx. -static bool divisor_uses_scratch(struct Emitter* emitter, const struct Expr* divisor) -{ - if (divisor->kind == EXPR_PRIMARY) - { - struct Token reg = resolve_register(emitter, divisor->primary.token); - if (is_gp_register(reg)) - return token_matches(reg, "rax") || token_matches(reg, "rdx"); - return true; - } - - // enum/struct members and unary minus fold to immediates; a binary or deref - // divisor is left to the direct path (unsupported, as before) - return divisor->kind == EXPR_MEMBER || divisor->kind == EXPR_UNARY; -} - -// idiv divides rdx:rax by its operand, leaving the quotient in rax and the -// remainder in rdx. The dividend arrives in dst; this loads it into rax, routes -// an awkward divisor through r11, and runs idiv. It clobbers rax, rdx and r11. -static void emit_idiv(struct Emitter* emitter, const char* dst, struct Expr* divisor) -{ - FILE* out = emitter->out; - - if (!divisor_uses_scratch(emitter, divisor)) - { - if (strcmp(dst, "rax") != 0) - fprintf(out, "\tmov rax, %s\n", dst); - fprintf(out, "\tcqo\n\tidiv "); - emit_operand(emitter, divisor); - fprintf(out, "\n"); - return; - } - - // a divisor in rax must be saved before the dividend overwrites rax - bool divisor_in_rax = divisor->kind == EXPR_PRIMARY - && token_matches(resolve_register(emitter, divisor->primary.token), "rax"); - - if (divisor_in_rax && strcmp(dst, "rax") != 0) - { - if (strcmp(dst, "r11") == 0) - fprintf(out, "\txchg rax, r11\n"); - else - fprintf(out, "\tmov r11, rax\n\tmov rax, %s\n", dst); - } - else - { - if (strcmp(dst, "rax") != 0) - fprintf(out, "\tmov rax, %s\n", dst); - fprintf(out, "\tmov r11, "); - emit_operand(emitter, divisor); - fprintf(out, "\n"); - } - - fprintf(out, "\tcqo\n\tidiv r11\n"); -} - -static void emit_division(struct Emitter* emitter, const char* dst, struct Expr* divisor) -{ - emit_idiv(emitter, dst, divisor); - if (strcmp(dst, "rax") != 0) - fprintf(emitter->out, "\tmov %s, rax\n", dst); -} - -static void emit_modulo(struct Emitter* emitter, const char* dst, struct Expr* divisor) -{ - emit_idiv(emitter, dst, divisor); - if (strcmp(dst, "rdx") != 0) - fprintf(emitter->out, "\tmov %s, rdx\n", dst); -} - -static void emit_divide(struct Emitter* emitter, struct AssignStatement* assign) -{ - if (assign->target_deref) - { - fprintf(emitter->out, "\t; TODO: unsupported division\n"); - return; - } - - struct Token target = resolve_register(emitter, assign->target); - char dst[32]; - snprintf(dst, sizeof(dst), "%.*s", (int)target.length, target.start); - - if (assign->op.type == TOKEN_PERCENT_EQUAL) - emit_modulo(emitter, dst, assign->value); - else - emit_division(emitter, dst, assign->value); -} - -// an expression can be evaluated into a register when it is a single term -// (primary or member), or a left-associative chain of binary operators whose -// right operands are plain operands (never a buffer or a nested binary) -static bool expr_supported(struct Emitter* emitter, struct Expr* expr) -{ - switch (expr->kind) - { - case EXPR_PRIMARY: - case EXPR_MEMBER: - case EXPR_DEREF: - return true; - case EXPR_UNARY: - { - uint64_t value; - return fold_const(emitter->program, expr, &value); - } - case EXPR_BINARY: - if (expr->binary.right->kind != EXPR_PRIMARY && - expr->binary.right->kind != EXPR_MEMBER && - expr->binary.right->kind != EXPR_UNARY) - return false; - if (expr->binary.right->kind == EXPR_UNARY) - { - uint64_t value; - if (!fold_const(emitter->program, expr->binary.right, &value)) - return false; - } - if (expr->binary.right->kind == EXPR_PRIMARY && - is_buffer_name(emitter, expr->binary.right->primary.token)) - return false; - return expr_supported(emitter, expr->binary.left); - } - - return false; -} - -// dst = [address], extending narrower loads into the full register. Unsigned -// loads zero-extend (movzx / a 32-bit mov); signed loads sign-extend (movsx, -// or movsxd for a dword). -static void emit_load(struct Emitter* emitter, const char* dst, struct DerefExpr* deref) -{ - FILE* out = emitter->out; - if (deref->is_signed) - { - switch (deref->size) - { - case STORE_SIZE_BYTE: - fprintf(out, "\tmovsx %s, byte [", dst); - break; - case STORE_SIZE_WORD: - fprintf(out, "\tmovsx %s, word [", dst); - break; - case STORE_SIZE_DWORD: - fprintf(out, "\tmovsxd %s, dword [", dst); - break; - default: - fprintf(out, "\tmov %s, [", dst); - break; - } - - emit_operand(emitter, deref->address); - fprintf(out, "]\n"); - return; - } - - switch (deref->size) - { - case STORE_SIZE_BYTE: - fprintf(out, "\tmovzx %s, byte [", dst); - break; - case STORE_SIZE_WORD: - fprintf(out, "\tmovzx %s, word [", dst); - break; - case STORE_SIZE_DWORD: - { - const char* dword = sized_register(text_token(dst), STORE_SIZE_DWORD); - fprintf(out, "\tmov %s, [", dword != NULL ? dword : dst); - break; - } - default: - fprintf(out, "\tmov %s, [", dst); - break; - } - - emit_operand(emitter, deref->address); - fprintf(out, "]\n"); -} - -static void emit_expr_into(struct Emitter* emitter, const char* dst, struct Expr* expr) -{ - if (expr->kind == EXPR_DEREF) - { - emit_load(emitter, dst, &expr->deref); - return; - } - - if (expr->kind == EXPR_BINARY) - { - emit_expr_into(emitter, dst, expr->binary.left); - - if (expr->binary.op.type == TOKEN_SLASH) - { - emit_division(emitter, dst, expr->binary.right); - return; - } - if (expr->binary.op.type == TOKEN_PERCENT) - { - emit_modulo(emitter, dst, expr->binary.right); - return; - } - - const char* mnemonic = - expr->binary.op.type == TOKEN_PLUS ? "add" : - expr->binary.op.type == TOKEN_MINUS ? "sub" : "imul"; - fprintf(emitter->out, "\t%s %s, ", mnemonic, dst); - emit_operand(emitter, expr->binary.right); - fprintf(emitter->out, "\n"); - return; - } - - if (expr->kind == EXPR_PRIMARY && is_buffer_name(emitter, expr->primary.token)) - { - uint64_t offset; - buffer_offset(emitter, expr->primary.token, &offset); - fprintf(emitter->out, "\tlea %s, [rbp - %llu]\n", dst, (unsigned long long)offset); - return; - } - - fprintf(emitter->out, "\tmov %s, ", dst); - emit_operand(emitter, expr); - fprintf(emitter->out, "\n"); -} - -static const char* store_size_keyword(enum StoreSize size) -{ - switch (size) - { - case STORE_SIZE_BYTE: return "byte "; - case STORE_SIZE_WORD: return "word "; - case STORE_SIZE_DWORD: return "dword "; - case STORE_SIZE_QWORD: return "qword "; - default: return ""; - } -} - -// maps a full 64-bit register to its byte/word/dword sub-register for a sized -// store, so `^byte rsi = rdx` writes `dl` rather than the whole register. -// returns NULL when the token is not a full register, or no resizing applies. -static const char* sized_register(struct Token reg, enum StoreSize size) -{ - if (size == STORE_SIZE_NONE || size == STORE_SIZE_QWORD) - return NULL; - - static const struct RegisterSizes - { - const char* quad; - const char* dword; - const char* word; - const char* byte; - } registers[] = - { - { "rax", "eax", "ax", "al" }, - { "rbx", "ebx", "bx", "bl" }, - { "rcx", "ecx", "cx", "cl" }, - { "rdx", "edx", "dx", "dl" }, - { "rsi", "esi", "si", "sil" }, - { "rdi", "edi", "di", "dil" }, - { "rbp", "ebp", "bp", "bpl" }, - { "rsp", "esp", "sp", "spl" }, - { "r8", "r8d", "r8w", "r8b" }, - { "r9", "r9d", "r9w", "r9b" }, - { "r10", "r10d", "r10w", "r10b" }, - { "r11", "r11d", "r11w", "r11b" }, - { "r12", "r12d", "r12w", "r12b" }, - { "r13", "r13d", "r13w", "r13b" }, - { "r14", "r14d", "r14w", "r14b" }, - { "r15", "r15d", "r15w", "r15b" }, - }; - - for (size_t i = 0; i < sizeof(registers) / sizeof(registers[0]); i += 1) - { - const struct RegisterSizes* entry = ®isters[i]; - size_t length = strlen(entry->quad); - if (reg.length != length || memcmp(reg.start, entry->quad, length) != 0) - continue; - - switch (size) - { - case STORE_SIZE_DWORD: return entry->dword; - case STORE_SIZE_WORD: return entry->word; - case STORE_SIZE_BYTE: return entry->byte; - default: return NULL; - } - } - - return NULL; -} - -static const char* float_mnemonic(enum TokenType op) -{ - switch (op) - { - case TOKEN_EQUAL: return "movsd"; - case TOKEN_PLUS_EQUAL: return "addsd"; - case TOKEN_MINUS_EQUAL: return "subsd"; - case TOKEN_STAR_EQUAL: return "mulsd"; - case TOKEN_SLASH_EQUAL: return "divsd"; - default: return NULL; - } -} - -static bool value_is_float(struct Emitter* emitter, const struct Expr* expr) -{ - if (expr->kind != EXPR_PRIMARY) - return false; - if (expr->primary.token.type == TOKEN_FLOAT) - return true; - - return is_float_register(resolve_register(emitter, expr->primary.token)); -} - -// floating point: xmm moves and arithmetic, conversions to/from general-purpose -// registers, and float literals loaded from their .data slot -static bool emit_float_assign(struct Emitter* emitter, struct AssignStatement* assign, - struct Token target, bool target_float) -{ - struct Expr* value = assign->value; - - // float store: ^ptr = xmm -> movsd [ptr], xmm - if (assign->target_deref) - { - if (assign->op.type != TOKEN_EQUAL || value->kind != EXPR_PRIMARY) - return false; - struct Token source = resolve_register(emitter, value->primary.token); - if (!is_float_register(source)) - return false; - fprintf(emitter->out, "\tmovsd [%.*s], %.*s\n", - (int)target.length, target.start, (int)source.length, source.start); - return true; - } - - // float load: xmm = ^ptr -> movsd xmm, [ptr] - if (value->kind == EXPR_DEREF) - { - if (!target_float || assign->op.type != TOKEN_EQUAL) - return false; - fprintf(emitter->out, "\tmovsd %.*s, [", (int)target.length, target.start); - emit_operand(emitter, value->deref.address); - fprintf(emitter->out, "]\n"); - return true; - } - - if (value->kind == EXPR_PRIMARY && value->primary.token.type == TOKEN_FLOAT) - { - if (!target_float || assign->op.type != TOKEN_EQUAL) - return false; - size_t index = float_index(emitter->floats, value->primary.token); - fprintf(emitter->out, "\tmovsd %.*s, [__float%zu]\n", - (int)target.length, target.start, index); - return true; - } - - if (value->kind != EXPR_PRIMARY) - return false; - - struct Token source = resolve_register(emitter, value->primary.token); - bool source_float = is_float_register(source); - - if (target_float && source_float) - { - const char* mnemonic = float_mnemonic(assign->op.type); - if (mnemonic == NULL) - return false; - fprintf(emitter->out, "\t%s %.*s, %.*s\n", mnemonic, - (int)target.length, target.start, (int)source.length, source.start); - return true; - } - - if (assign->op.type != TOKEN_EQUAL) - return false; - - if (target_float) - fprintf(emitter->out, "\tcvtsi2sd %.*s, %.*s\n", - (int)target.length, target.start, (int)source.length, source.start); - else - fprintf(emitter->out, "\tcvttsd2si %.*s, %.*s\n", - (int)target.length, target.start, (int)source.length, source.start); - return true; -} - -static void emit_assign(struct Emitter* emitter, struct AssignStatement* assign) -{ - struct Token float_target = resolve_register(emitter, assign->target); - if (is_float_register(float_target) || value_is_float(emitter, assign->value)) - { - if (!emit_float_assign(emitter, assign, float_target, is_float_register(float_target))) - fprintf(emitter->out, "\t; TODO: unsupported float assignment\n"); - return; - } - - if (assign->op.type == TOKEN_SLASH_EQUAL || assign->op.type == TOKEN_PERCENT_EQUAL) - { - emit_divide(emitter, assign); - return; - } - - struct Token target = resolve_register(emitter, assign->target); - - if (assign->op.type == TOKEN_EQUAL && !assign->target_deref) - { - if (!expr_supported(emitter, assign->value)) - { - fprintf(emitter->out, "\t; TODO: unsupported assignment\n"); - return; - } - - char dst[32]; - snprintf(dst, sizeof(dst), "%.*s", (int)target.length, target.start); - emit_expr_into(emitter, dst, assign->value); - return; - } - - // deref store or compound assignment: needs a plain operand, not a buffer or binary - const char* mnemonic = assign_mnemonic(assign->op.type); - bool value_is_buffer = assign->value->kind == EXPR_PRIMARY - && is_buffer_name(emitter, assign->value->primary.token); - if (mnemonic == NULL || assign->value->kind == EXPR_BINARY - || assign->value->kind == EXPR_DEREF || value_is_buffer) - { - fprintf(emitter->out, "\t; TODO: unsupported assignment\n"); - return; - } - - // adding or subtracting a constant zero (e.g. a struct field at offset 0) is a no-op - uint64_t folded; - if (!assign->target_deref - && (assign->op.type == TOKEN_PLUS_EQUAL || assign->op.type == TOKEN_MINUS_EQUAL) - && fold_const(emitter->program, assign->value, &folded) && folded == 0) - return; - - if (assign->target_deref) - { - fprintf(emitter->out, "\t%s %s[%.*s], ", mnemonic, - store_size_keyword(assign->store_size), (int)target.length, target.start); - - const char* sized = NULL; - if (assign->value->kind == EXPR_PRIMARY) - { - struct Token value = resolve_register(emitter, assign->value->primary.token); - sized = sized_register(value, assign->store_size); - if (sized != NULL) - fprintf(emitter->out, "%s", sized); - } - - if (sized == NULL) - emit_operand(emitter, assign->value); - } - else - { - fprintf(emitter->out, "\t%s %.*s, ", mnemonic, (int)target.length, target.start); - emit_operand(emitter, assign->value); - } - - fprintf(emitter->out, "\n"); -} - -static const char* jump_if_false(enum TokenType comparison) -{ - switch (comparison) - { - case TOKEN_EQUAL_EQUAL: return "jne"; - case TOKEN_BANG_EQUAL: return "je"; - case TOKEN_LESS: return "jge"; - case TOKEN_LESS_EQUAL: return "jg"; - case TOKEN_GREATER: return "jle"; - case TOKEN_GREATER_EQUAL: return "jl"; - default: return NULL; - } -} - -static struct ProcDecl* find_proc(struct Program* program, struct Token name) -{ - for (size_t i = 0; i < program->proc_count; i += 1) - { - struct ProcDecl* proc = &program->procs[i]; - if (proc->name.length == name.length && memcmp(proc->name.start, name.start, name.length) == 0) - return proc; - } - - return NULL; -} - -static void emit_call(struct Emitter* emitter, struct CallStatement* call) -{ - const struct ProcDecl* callee = find_proc(emitter->program, call->name); - if (callee == NULL || callee->param_count != call->arg_count) - { - fprintf(emitter->out, "\t; TODO: unsupported call\n"); - return; - } - - for (size_t i = 0; i < call->arg_count; i += 1) - { - if (call->args[i]->kind == EXPR_BINARY || call->args[i]->kind == EXPR_DEREF) - { - fprintf(emitter->out, "\t; TODO: unsupported call argument\n"); - continue; - } - - struct Token reg = resolve_register(emitter, callee->params[i].reg); - fprintf(emitter->out, "\tmov %.*s, ", (int)reg.length, reg.start); - emit_operand(emitter, call->args[i]); - fprintf(emitter->out, "\n"); - } - - fprintf(emitter->out, "\tcall %.*s\n", (int)call->name.length, call->name.start); -} - -static void emit_statement(struct Emitter* emitter, struct Statement* statement); - -static void emit_block(struct Emitter* emitter, struct Statement* body, size_t count) -{ - for (size_t i = 0; i < count; i += 1) - emit_statement(emitter, &body[i]); -} - -// ucomisd sets the flags like an unsigned compare, so float branches use the -// unsigned jump family (ja/jae/jb/jbe) rather than the signed one -static const char* float_jump_if_false(enum TokenType comparison) -{ - switch (comparison) - { - case TOKEN_EQUAL_EQUAL: return "jne"; - case TOKEN_BANG_EQUAL: return "je"; - case TOKEN_LESS: return "jae"; - case TOKEN_LESS_EQUAL: return "ja"; - case TOKEN_GREATER: return "jbe"; - case TOKEN_GREATER_EQUAL: return "jb"; - default: return NULL; - } -} - -static void emit_float_operand(struct Emitter* emitter, const struct Expr* expr) -{ - if (expr->kind == EXPR_PRIMARY && expr->primary.token.type == TOKEN_FLOAT) - { - fprintf(emitter->out, "[__float%zu]", float_index(emitter->floats, expr->primary.token)); - return; - } - - struct Token token = resolve_register(emitter, expr->primary.token); - fprintf(emitter->out, "%.*s", (int)token.length, token.start); -} - -// Emits the comparison for `left cmp right` and a jump to `target` taken when -// the condition is false, so the code that follows runs when it is true. Both -// if and while build on this. Returns false (after a TODO note) for a form that -// isn't supported yet. -static bool emit_branch_test(struct Emitter* emitter, struct Expr* left, - struct Token comparison, struct Expr* right, const char* target) -{ - bool is_float = value_is_float(emitter, left) || value_is_float(emitter, right); - - if (is_float) - { - const char* jump = float_jump_if_false(comparison.type); - bool left_reg = left->kind == EXPR_PRIMARY - && is_float_register(resolve_register(emitter, left->primary.token)); - if (jump == NULL || !left_reg || !value_is_float(emitter, right)) - { - fprintf(emitter->out, "\t; TODO: unsupported condition\n"); - return false; - } - - fprintf(emitter->out, "\tucomisd "); - emit_float_operand(emitter, left); - fprintf(emitter->out, ", "); - emit_float_operand(emitter, right); - fprintf(emitter->out, "\n\t%s %s\n", jump, target); - return true; - } - - const char* jump = jump_if_false(comparison.type); - if (jump == NULL - || left->kind == EXPR_BINARY || left->kind == EXPR_DEREF - || right->kind == EXPR_BINARY || right->kind == EXPR_DEREF) - { - fprintf(emitter->out, "\t; TODO: unsupported condition\n"); - return false; - } - - fprintf(emitter->out, "\tcmp "); - emit_operand(emitter, left); - fprintf(emitter->out, ", "); - emit_operand(emitter, right); - fprintf(emitter->out, "\n\t%s %s\n", jump, target); - return true; -} - -static void emit_if(struct Emitter* emitter, struct IfStatement* branch) -{ - bool has_else = branch->else_count > 0; - - uint32_t id = emitter->label_id; - emitter->label_id += 1; - - char target[32]; - snprintf(target, sizeof(target), ".if_%s_%u", has_else ? "else" : "end", id); - - if (!emit_branch_test(emitter, branch->left, branch->comparison, branch->right, target)) - return; - - emit_block(emitter, branch->body, branch->body_count); - - if (has_else) - { - fprintf(emitter->out, "\tjmp .if_end_%u\n", id); - fprintf(emitter->out, ".if_else_%u:\n", id); - emit_block(emitter, branch->else_body, branch->else_count); - } - - fprintf(emitter->out, ".if_end_%u:\n", id); -} - -static void emit_while(struct Emitter* emitter, struct WhileStatement* loop) -{ - uint32_t id = emitter->label_id; - emitter->label_id += 1; - - char target[32]; - snprintf(target, sizeof(target), ".while_end_%u", id); - - fprintf(emitter->out, ".while_%u:\n", id); - - if (!emit_branch_test(emitter, loop->left, loop->comparison, loop->right, target)) - return; - - emit_block(emitter, loop->body, loop->body_count); - - fprintf(emitter->out, "\tjmp .while_%u\n", id); - fprintf(emitter->out, ".while_end_%u:\n", id); -} - -static void emit_statement(struct Emitter* emitter, struct Statement* statement) -{ - FILE* out = emitter->out; - switch (statement->kind) - { - case STATEMENT_ASSIGN: - emit_assign(emitter, &statement->assign); - break; - case STATEMENT_LABEL: - fprintf(out, "%.*s:\n", (int)statement->label.name.length, statement->label.name.start); - break; - case STATEMENT_GOTO: - fprintf(out, "\tjmp %.*s\n", (int)statement->jump.label.length, statement->jump.label.start); - break; - case STATEMENT_SYSCALL: - fprintf(out, "\tsyscall\n"); - break; - case STATEMENT_IF: - emit_if(emitter, &statement->branch); - break; - case STATEMENT_WHILE: - emit_while(emitter, &statement->loop); - break; - case STATEMENT_CALL: - emit_call(emitter, &statement->call); - break; - case STATEMENT_STACK: - break; - default: - fprintf(out, "\t; TODO: unsupported statement\n"); - break; - } -} - -static uint64_t proc_stack_size(struct Program* program, struct ProcDecl* proc) -{ - uint64_t total = 0; - for (size_t i = 0; i < proc->body_count; i += 1) - { - struct Statement* statement = &proc->body[i]; - if (statement->kind == STATEMENT_STACK) - { - uint64_t size = 0; - fold_const(program, statement->stack.size, &size); - total += size; - } - } - - if (total % 16 != 0) - total += 16 - (total % 16); - - return total; -} - -static void collect_float(struct FloatTable* floats, struct Token token) -{ - if (token.type != TOKEN_FLOAT || float_index(floats, token) != floats->count) - return; - - if (floats->count == floats->capacity) - { - size_t capacity = floats->capacity < 8 ? 8 : floats->capacity * 2; - floats->items = realloc(floats->items, capacity * sizeof(struct Token)); - floats->capacity = capacity; - } - - floats->items[floats->count] = token; - floats->count += 1; -} - -static void collect_floats_expr(struct FloatTable* floats, struct Expr* expr) -{ - switch (expr->kind) - { - case EXPR_PRIMARY: - collect_float(floats, expr->primary.token); - break; - case EXPR_UNARY: - collect_floats_expr(floats, expr->unary.operand); - break; - case EXPR_BINARY: - collect_floats_expr(floats, expr->binary.left); - collect_floats_expr(floats, expr->binary.right); - break; - case EXPR_MEMBER: - collect_floats_expr(floats, expr->member.object); - break; - case EXPR_DEREF: - collect_floats_expr(floats, expr->deref.address); - break; - } -} - -static void collect_floats_statement(struct FloatTable* floats, struct Statement* statement) -{ - switch (statement->kind) - { - case STATEMENT_ASSIGN: - collect_floats_expr(floats, statement->assign.value); - break; - case STATEMENT_IF: - collect_floats_expr(floats, statement->branch.left); - collect_floats_expr(floats, statement->branch.right); - for (size_t i = 0; i < statement->branch.body_count; i += 1) - collect_floats_statement(floats, &statement->branch.body[i]); - for (size_t i = 0; i < statement->branch.else_count; i += 1) - collect_floats_statement(floats, &statement->branch.else_body[i]); - break; - case STATEMENT_WHILE: - collect_floats_expr(floats, statement->loop.left); - collect_floats_expr(floats, statement->loop.right); - for (size_t i = 0; i < statement->loop.body_count; i += 1) - collect_floats_statement(floats, &statement->loop.body[i]); - break; - case STATEMENT_CALL: - for (size_t i = 0; i < statement->call.arg_count; i += 1) - collect_floats_expr(floats, statement->call.args[i]); - break; - default: - break; - } -} - -static struct FloatTable collect_floats(struct Program* program) -{ - struct FloatTable floats = { NULL, 0, 0 }; - for (size_t i = 0; i < program->proc_count; i += 1) - for (size_t j = 0; j < program->procs[i].body_count; j += 1) - collect_floats_statement(&floats, &program->procs[i].body[j]); - - return floats; -} - -static void emit_float_data(const struct FloatTable* floats, FILE* out) -{ - for (size_t i = 0; i < floats->count; i += 1) - fprintf(out, "__float%zu: dq %.*s\n", i, - (int)floats->items[i].length, floats->items[i].start); -} - -static void emit_proc(struct Program* program, struct FloatTable* floats, struct ProcDecl* proc, FILE* out) -{ - struct Emitter emitter; - emitter.program = program; - emitter.proc = proc; - emitter.floats = floats; - emitter.out = out; - emitter.label_id = 0; - - struct Config config = program->config; - bool is_entry = config.has_entry - && proc->name.length == config.entry.length - && memcmp(proc->name.start, config.entry.start, proc->name.length) == 0; - - fprintf(out, "%.*s:\n", (int)proc->name.length, proc->name.start); - - uint64_t stack_size = proc_stack_size(program, proc); - if (stack_size > 0) - { - fprintf(out, "\tpush rbp\n"); - fprintf(out, "\tmov rbp, rsp\n"); - fprintf(out, "\tsub rsp, %llu\n", (unsigned long long)stack_size); - } - - for (size_t i = 0; i < proc->body_count; i += 1) - emit_statement(&emitter, &proc->body[i]); - - if (!is_entry) - { - if (stack_size > 0) - fprintf(out, "\tleave\n"); - fprintf(out, "\tret\n"); - } -} - -void generate_nasm(struct Program* program, FILE* out) -{ - struct FloatTable floats = collect_floats(program); - - fprintf(out, "bits %u\n\n", program->config.bits); - - if (program->const_count > 0) - { - emit_consts(program, out); - fprintf(out, "\n"); - } - - emit_data(program, out); - emit_float_data(&floats, out); - fprintf(out, "\n"); - - fprintf(out, "section .text\n"); - if (program->config.has_entry) - fprintf(out, "global %.*s\n", (int)program->config.entry.length, program->config.entry.start); - - for (size_t i = 0; i < program->proc_count; i += 1) - { - fprintf(out, "\n"); - emit_proc(program, &floats, &program->procs[i], out); - } - - free(floats.items); -} diff --git a/src/nasm.h b/src/nasm.h deleted file mode 100644 index 1a93b31..0000000 --- a/src/nasm.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include - -#include "ast.h" - -void generate_nasm(struct Program* program, FILE* out); diff --git a/src/parser.c b/src/parser.c index ddaee72..ae7c1b3 100644 --- a/src/parser.c +++ b/src/parser.c @@ -552,6 +552,14 @@ static bool parse_if(struct Parser* parser, struct Statement* out) static bool parse_while(struct Parser* parser, struct Statement* out) { + // an optional .name makes the loop's asm labels readable (.name / .name_end) + struct Token name = { 0 }; + bool named = match_token(parser, TOKEN_DOT); + if (named && !consume(parser, TOKEN_IDENTIFIER, "expected a loop name after '.'")) + return false; + if (named) + name = parser->previous; + struct Expr* left; struct Token comparison; struct Expr* right; @@ -568,6 +576,8 @@ static bool parse_while(struct Parser* parser, struct Statement* out) } out->kind = STATEMENT_WHILE; + out->loop.named = named; + out->loop.name = name; out->loop.left = left; out->loop.comparison = comparison; out->loop.right = right; diff --git a/tests/codegen_test.c b/tests/codegen_test.c index 2ccba98..24cac6d 100644 --- a/tests/codegen_test.c +++ b/tests/codegen_test.c @@ -1,9 +1,9 @@ #include #include -#include "nasm.h" #include "tests.h" #include "parser.h" +#include "codegen.h" static void generate_to_buffer(struct Program* program, char* buffer, size_t size) { @@ -23,6 +23,24 @@ static void generate_to_buffer(struct Program* program, char* buffer, size_t siz fclose(out); } +static void generate_fasm_to_buffer(struct Program* program, char* buffer, size_t size) +{ + FILE* out = tmpfile(); + if (out == NULL) + { + buffer[0] = '\0'; + return; + } + + generate_fasm(program, out); + fflush(out); + rewind(out); + + size_t read = fread(buffer, 1, size - 1, out); + buffer[read] = '\0'; + fclose(out); +} + static void test_generate_consts_and_data(struct TestContext* context) { struct Lexer lexer = create_lexer("const N = 5\ndata msg = \"hi\"\n"); @@ -41,6 +59,33 @@ static void test_generate_consts_and_data(struct TestContext* context) free_program(&program); } +static void test_generate_fasm(struct TestContext* context) +{ + struct Lexer lexer = create_lexer( + "[entry: main]\nconst N = 5\ndata msg = \"hi\\n\"\nproc main\n{\nrax = N\nsyscall\n}\n"); + struct Program program; + check(context, parse_program(&lexer, &program)); + + char buffer[1024]; + generate_fasm_to_buffer(&program, buffer, sizeof(buffer)); + + // fasm framing differs from nasm + check(context, strstr(buffer, "format ELF64") != NULL); + check(context, strstr(buffer, "N = 5") != NULL); + check(context, strstr(buffer, "section '.data' writeable") != NULL); + check(context, strstr(buffer, "msg db \"hi\", 10") != NULL); // escape expanded to a byte + check(context, strstr(buffer, ".len = $ - msg") != NULL); + check(context, strstr(buffer, "section '.text' executable") != NULL); + check(context, strstr(buffer, "public main") != NULL); + // instruction bodies are identical to nasm + check(context, strstr(buffer, "mov rax, N") != NULL); + // nasm-only forms must be absent + check(context, strstr(buffer, "%define") == NULL); + check(context, strstr(buffer, "bits 64") == NULL); + + free_program(&program); +} + static void test_generate_text(struct TestContext* context) { struct Lexer lexer = create_lexer( @@ -121,6 +166,25 @@ static void test_generate_while(struct TestContext* context) free_program(&program); } +static void test_generate_while_named(struct TestContext* context) +{ + struct Lexer lexer = create_lexer( + "proc main\n{\nwhile .drain rcx > 0\n{\nrcx -= 1\n}\n}\n"); + struct Program program; + check(context, parse_program(&lexer, &program)); + + char buffer[1024]; + generate_to_buffer(&program, buffer, sizeof(buffer)); + + check(context, strstr(buffer, ".drain:") != NULL); + check(context, strstr(buffer, "jle .drain_end") != NULL); + check(context, strstr(buffer, "jmp .drain") != NULL); + check(context, strstr(buffer, ".drain_end:") != NULL); + check(context, strstr(buffer, ".while_0") == NULL); + + free_program(&program); +} + static void test_generate_negative(struct TestContext* context) { struct Lexer lexer = create_lexer( @@ -524,10 +588,12 @@ void run_codegen_tests(struct TestContext* context) test_generate_entry_and_bits(context); test_generate_no_entry(context); test_generate_consts_and_data(context); + test_generate_fasm(context); test_generate_text(context); test_generate_if(context); test_generate_if_else(context); test_generate_while(context); + test_generate_while_named(context); test_generate_negative(context); test_generate_call(context); test_generate_param_substitution(context); diff --git a/tests/parser_test.c b/tests/parser_test.c index 88e32a1..a88a319 100644 --- a/tests/parser_test.c +++ b/tests/parser_test.c @@ -241,6 +241,23 @@ static void test_parse_while(struct TestContext* context) check(context, primary_is(loop.loop.right, "0")); check(context, loop.loop.body_count == 2); check(context, text_is(loop.loop.body[1].assign.target, "rcx")); + check(context, !loop.loop.named); + + free_program(&program); +} + +static void test_parse_while_named(struct TestContext* context) +{ + struct Lexer lexer = create_lexer("proc main\n{\nwhile .drain rcx > 0\n{\nrcx -= 1\n}\n}\n"); + struct Program program; + + check(context, parse_program(&lexer, &program)); + + struct Statement loop = program.procs[0].body[0]; + check(context, loop.kind == STATEMENT_WHILE); + check(context, loop.loop.named); + check(context, text_is(loop.loop.name, "drain")); + check(context, primary_is(loop.loop.left, "rcx")); free_program(&program); } @@ -417,6 +434,7 @@ void run_parser_tests(struct TestContext* context) test_parse_if_block(context); test_parse_else_if(context); test_parse_while(context); + test_parse_while_named(context); test_parse_call(context); test_parse_stack(context); test_parse_sized_deref(context); -- cgit v1.3