From cf24f576891ece60bdf04928d3c39dd0bbb9b5f6 Mon Sep 17 00:00:00 2001 From: hachem Date: Mon, 31 Aug 2026 01:11:54 +0200 Subject: feat: add logical_registers --- examples/logical.hdass | 19 +++++++++ scripts/docker_test.sh | 7 ---- scripts/run_suite.sh | 5 --- scripts/test_examples.sh | 11 +---- src/codegen/nasm.c | 104 +++++++++++++++++++++++++++++++++++++++++++---- src/parser/parser.c | 6 ++- tests/codegen_test.c | 48 ++++++++++++++++++++++ tests/parser_test.c | 18 ++++++++ 8 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 examples/logical.hdass diff --git a/examples/logical.hdass b/examples/logical.hdass new file mode 100644 index 0000000..5e82b42 --- /dev/null +++ b/examples/logical.hdass @@ -0,0 +1,19 @@ +[entry: main] +[enable: logical_registers] + +// With the logical_registers extension, r1..r14 name the general-purpose +// registers (r1 = rax, r2 = rbx, ...) instead of the architecture names. +// Computes (4 + 3) * 3 = 21 and returns it as the exit status. +const SYS_EXIT = 60 + +proc main +{ + r1 = 4 // rax + r2 = 3 // rbx + r1 += r2 // 7 + r1 *= r2 // 21 + + r6 = r1 // rdi = 21 (the exit status) + r1 = SYS_EXIT + syscall +} diff --git a/scripts/docker_test.sh b/scripts/docker_test.sh index 2ded903..5730e1d 100755 --- a/scripts/docker_test.sh +++ b/scripts/docker_test.sh @@ -1,9 +1,4 @@ #!/usr/bin/env bash -# Convenience wrapper: build hdass and run the full test suite (unit tests plus -# the end-to-end example tests) inside the amd64 Linux Docker environment. -# Run this from the host; it brings the container up if it is not already. -# -# Colours are on by default; set NO_COLOR to disable them. set -euo pipefail root="$(cd "$(dirname "$0")/.." && pwd)" @@ -11,6 +6,4 @@ cd "$root" docker compose up -d >/dev/null -# stdin is redirected from /dev/null so `docker compose exec -T` does not hang -# waiting on the stream after the suite finishes. docker compose exec -T -e "NO_COLOR=${NO_COLOR:-}" hdass bash /hdass/scripts/run_suite.sh -# expected_stdout is compared after trailing newlines are stripped (as $() does). check() { local name="$1" desc="$2" source="$3" expected_exit="$4" expected_stdout="$5" @@ -90,6 +80,7 @@ check arithmetic "integer compound-assignment math" examples/arithmetic.hd check loop_sum "sums 1..5 with a countdown loop" examples/loop_sum.hdass 15 "" check branch "selects the larger of two values" examples/branch.hdass 8 "" check call "passes an argument through a proc" examples/call.hdass 21 "" +check logical "logical register names (r1..r14)" examples/logical.hdass 21 "" check fibonacci "prints the first ten Fibonacci numbers" examples/fibonacci.hdasm 0 "0 1 1 diff --git a/src/codegen/nasm.c b/src/codegen/nasm.c index adae143..0db60dd 100644 --- a/src/codegen/nasm.c +++ b/src/codegen/nasm.c @@ -55,6 +55,8 @@ struct Emitter uint32_t label_id; }; +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) @@ -70,6 +72,57 @@ static struct Token resolve_token(struct Emitter* emitter, struct Token token) 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) { uint64_t value = 0; @@ -106,21 +159,49 @@ static bool is_buffer_name(struct Emitter* emitter, struct Token token) return emitter->proc != NULL && buffer_offset(emitter->proc, 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 emit_operand(struct Emitter* emitter, struct Expr* expr) { switch (expr->kind) { case EXPR_PRIMARY: { - struct Token token = resolve_token(emitter, expr->primary.token); + 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; + } + 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_BINARY: return false; } @@ -130,7 +211,7 @@ static bool emit_operand(struct Emitter* emitter, struct Expr* expr) static void emit_divide(struct Emitter* emitter, struct AssignStatement* assign) { - struct Token target = resolve_token(emitter, assign->target); + struct Token target = resolve_register(emitter, assign->target); bool target_is_rax = target.length == 3 && memcmp(target.start, "rax", 3) == 0; if (assign->target_deref || !target_is_rax) { @@ -155,11 +236,14 @@ static bool expr_supported(struct Emitter* emitter, struct Expr* expr) case EXPR_MEMBER: return true; case EXPR_BINARY: - if (expr->binary.op.type != TOKEN_PLUS && expr->binary.op.type != TOKEN_MINUS) + if (expr->binary.op.type != TOKEN_PLUS && + expr->binary.op.type != TOKEN_MINUS) return false; - if (expr->binary.right->kind != EXPR_PRIMARY && expr->binary.right->kind != EXPR_MEMBER) + if (expr->binary.right->kind != EXPR_PRIMARY && + expr->binary.right->kind != EXPR_MEMBER) return false; - if (expr->binary.right->kind == EXPR_PRIMARY && is_buffer_name(emitter, expr->binary.right->primary.token)) + 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); } @@ -266,7 +350,7 @@ static void emit_assign(struct Emitter* emitter, struct AssignStatement* assign) return; } - struct Token target = resolve_token(emitter, assign->target); + struct Token target = resolve_register(emitter, assign->target); if (assign->op.type == TOKEN_EQUAL && !assign->target_deref) { @@ -284,7 +368,8 @@ static void emit_assign(struct Emitter* emitter, struct AssignStatement* assign) // 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); + 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 || value_is_buffer) { fprintf(emitter->out, "\t; TODO: unsupported assignment\n"); @@ -299,7 +384,7 @@ static void emit_assign(struct Emitter* emitter, struct AssignStatement* assign) const char* sized = NULL; if (assign->value->kind == EXPR_PRIMARY) { - struct Token value = resolve_token(emitter, assign->value->primary.token); + 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); @@ -360,7 +445,8 @@ static void emit_call(struct Emitter* emitter, struct CallStatement* call) continue; } - fprintf(emitter->out, "\tmov %.*s, ", (int)callee->params[i].reg.length, callee->params[i].reg.start); + 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"); } diff --git a/src/parser/parser.c b/src/parser/parser.c index 8603c46..7ec4474 100644 --- a/src/parser/parser.c +++ b/src/parser/parser.c @@ -154,11 +154,15 @@ static struct Expr* parse_postfix(struct Parser* parser) while (match_token(parser, TOKEN_DOT)) { - if (!consume(parser, TOKEN_IDENTIFIER, "expected member name after '.'")) + // an identifier is a member (data.len); an integer is a register size + // suffix (r1.64), meaningful with the logical_registers extension + if (!check(parser, TOKEN_IDENTIFIER) && !check(parser, TOKEN_INTEGER)) { + error_at(parser, parser->current, "expected a member name or size after '.'"); free_expr(expr); return NULL; } + advance_parser(parser); struct Expr* member = alloc_expr(EXPR_MEMBER); member->member.object = expr; diff --git a/tests/codegen_test.c b/tests/codegen_test.c index 5236268..ab771f7 100644 --- a/tests/codegen_test.c +++ b/tests/codegen_test.c @@ -275,8 +275,56 @@ static void test_generate_no_entry(struct TestContext* context) free_program(&program); } +static void test_generate_logical_registers(struct TestContext* context) +{ + struct Lexer lexer = create_lexer( + "[enable: logical_registers]\nproc main\n{\nr1 = 5\nr4 = r10\nr6 = r1.8\n}\n"); + struct Program program; + check(context, parse_program(&lexer, &program)); + + FILE* out = tmpfile(); + generate_nasm(&program, out); + fflush(out); + rewind(out); + + char buffer[1024]; + size_t read = fread(buffer, 1, sizeof(buffer) - 1, out); + buffer[read] = '\0'; + fclose(out); + + check(context, strstr(buffer, "mov rax, 5") != NULL); // r1 -> rax + check(context, strstr(buffer, "mov rdx, r11") != NULL); // r4 -> rdx, r10 -> r11 + check(context, strstr(buffer, "mov rdi, al") != NULL); // r6 -> rdi, r1.8 -> al + + free_program(&program); +} + +static void test_generate_logical_disabled(struct TestContext* context) +{ + struct Lexer lexer = create_lexer("proc main\n{\nr1 = 5\n}\n"); + struct Program program; + check(context, parse_program(&lexer, &program)); + + FILE* out = tmpfile(); + generate_nasm(&program, out); + fflush(out); + rewind(out); + + char buffer[1024]; + size_t read = fread(buffer, 1, sizeof(buffer) - 1, out); + buffer[read] = '\0'; + fclose(out); + + // without the extension, r1 is passed through untouched + check(context, strstr(buffer, "mov r1, 5") != NULL); + + free_program(&program); +} + void run_codegen_tests(struct TestContext* context) { + test_generate_logical_registers(context); + test_generate_logical_disabled(context); test_generate_entry_and_bits(context); test_generate_no_entry(context); test_generate_consts_and_data(context); diff --git a/tests/parser_test.c b/tests/parser_test.c index 3b60261..3fa3342 100644 --- a/tests/parser_test.c +++ b/tests/parser_test.c @@ -234,6 +234,23 @@ static void test_parse_bad_directive(struct TestContext* context) free_program(&program); } +static void test_parse_register_size_suffix(struct TestContext* context) +{ + struct Lexer lexer = create_lexer("proc main\n{\nr1 = r2.8\n}\n"); + struct Program program; + + check(context, parse_program(&lexer, &program)); + + struct Expr* value = program.procs[0].body[0].assign.value; + check(context, value->kind == EXPR_MEMBER); + check(context, value->member.member.type == TOKEN_INTEGER); + check(context, text_is(value->member.member, "8")); + check(context, value->member.object->kind == EXPR_PRIMARY); + check(context, text_is(value->member.object->primary.token, "r2")); + + free_program(&program); +} + static void test_parse_errors(struct TestContext* context) { struct Program program; @@ -265,5 +282,6 @@ void run_parser_tests(struct TestContext* context) test_parse_sized_deref(context); test_parse_directives(context); test_parse_bad_directive(context); + test_parse_register_size_suffix(context); test_parse_errors(context); } -- cgit v1.3