aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-31 01:11:54 +0200
committerhachem <im@hachem.wtf>2026-08-31 01:11:54 +0200
commitcf24f576891ece60bdf04928d3c39dd0bbb9b5f6 (patch)
treef0e5492dca4e0c3bbbff256c2c1fa98b15dcfab5
parent3e20038bbf7206e2270655418d7884ccfe91a4e4 (diff)
feat: add logical_registers
-rw-r--r--examples/logical.hdass19
-rwxr-xr-xscripts/docker_test.sh7
-rwxr-xr-xscripts/run_suite.sh5
-rwxr-xr-xscripts/test_examples.sh11
-rw-r--r--src/codegen/nasm.c104
-rw-r--r--src/parser/parser.c6
-rw-r--r--tests/codegen_test.c48
-rw-r--r--tests/parser_test.c18
8 files changed, 186 insertions, 32 deletions
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 </dev/null
diff --git a/scripts/run_suite.sh b/scripts/run_suite.sh
index 9d40e24..48cf67f 100755
--- a/scripts/run_suite.sh
+++ b/scripts/run_suite.sh
@@ -1,9 +1,4 @@
#!/usr/bin/env bash
-# Build hdass and run the full test suite (unit tests plus the end-to-end
-# example tests). Expects the amd64 Linux environment; it is invoked inside the
-# container by scripts/docker_test.sh, but can also be run directly there.
-#
-# Colours are on by default; set NO_COLOR to disable them.
set -euo pipefail
cd "$(cd "$(dirname "$0")/.." && pwd)"
diff --git a/scripts/test_examples.sh b/scripts/test_examples.sh
index 5ebba65..84299ce 100755
--- a/scripts/test_examples.sh
+++ b/scripts/test_examples.sh
@@ -1,12 +1,4 @@
#!/usr/bin/env bash
-# End-to-end example tests: transpile each program with hdass, assemble it with
-# nasm, link it with ld, run it, and compare its stdout and exit status against
-# the expected values below.
-#
-# The generated programs use Linux x86-64 syscalls, so this must run in the
-# amd64 Linux environment (see the Docker setup in the README), not on macOS.
-#
-# Colours are on by default; set NO_COLOR to disable them.
set -u
root="$(cd "$(dirname "$0")/.." && pwd)"
@@ -38,8 +30,6 @@ trap 'rm -rf "$work"' EXIT
pass=0
fail=0
-# check <name> <description> <source> <expected_exit> <expected_stdout>
-# 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);
}