aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-31 16:24:28 +0200
committerhachem <im@hachem.wtf>2026-08-31 16:24:28 +0200
commitf8a2d02c75df62f74e5907696d5a99a024d59018 (patch)
tree7f374fb0e408ea2dab15dc628b7bb6b0cee0d9e8
parentcaf904c9a8a30a44fb3fca6df93e97c5cabdb0c0 (diff)
feat: codegen for * and / in expressions and /= on any register
-rw-r--r--docs/language.md8
-rw-r--r--examples/mul_div.hdass19
-rwxr-xr-xscripts/test_examples.sh1
-rw-r--r--src/codegen/nasm.c49
-rw-r--r--tests/codegen_test.c53
5 files changed, 112 insertions, 18 deletions
diff --git a/docs/language.md b/docs/language.md
index a2b4ffa..bfb41f3 100644
--- a/docs/language.md
+++ b/docs/language.md
@@ -78,8 +78,9 @@ Written by their architecture names — `rax`–`rdi`, `rbp`, `rsp`, `r8`–`r15
```hdass
rax = SYS_WRITE // mov
-rcx -= 1 // += -= *= /= -> add sub imul idiv (/= targets rax)
-rdx = buffer + 31 // address math with + and -
+rcx -= 1 // += -= *= /= -> add sub imul idiv
+rax = rbx * rcx // + - * / in a value; / and /= use rax:rdx (see Gotchas)
+rdx = buffer + 31 // address math
loop: // label
goto loop
if rcx != 0 // == != < <= > >= ; runs the next statement only
@@ -113,7 +114,7 @@ A leading size keyword sets the width explicitly. It down-converts a full regist
## Expressions
-Assignment values and `if` operands: registers, integers, chars (`'0'`), constants, data names, member access (`data.len`), and `+`/`-`. Multiply and divide come from the `*=` and `/=` compound assignments, not from `*`/`/` inside a value.
+Assignment values and `if` operands: registers, integers, chars (`'0'`), constants, data names, member access (`data.len`), and `+` `-` `*` `/`. Operators are left-associative and each right-hand operand must be a single term, so `a * b + c` works but `a + b * c` (a nested right operand) doesn't yet.
## Extensions
@@ -160,4 +161,5 @@ The [README](../README.md) has a Docker setup with these tools.
- **Clobbering is yours.** `syscall` trashes `rcx`/`r11`; a callee trashes what it touches. Nothing is saved for you — `examples/fibonacci.hdass` keeps its counter in `r15` for this reason.
- **Widths must match.** `rax = r1.8` becomes `mov rax, al`, which won't assemble.
+- **Division uses `rax:rdx`.** `/` and `/=` go through `idiv`, so they clobber `rax` and `rdx` regardless of the target, and the divisor can't be `rax`, `rdx`, or an immediate — put it in another register first.
- **The entry procedure has no `ret`** — end it with an exit syscall. \ No newline at end of file
diff --git a/examples/mul_div.hdass b/examples/mul_div.hdass
new file mode 100644
index 0000000..8c813b6
--- /dev/null
+++ b/examples/mul_div.hdass
@@ -0,0 +1,19 @@
+[entry: main]
+
+// (7 * 6) / 5 = 8 with a value-expression multiply and a non-rax division,
+// returned as the exit status.
+const SYS_EXIT = 60
+
+proc main
+{
+ rbx = 7
+ rcx = 6
+ rbx = rbx * rcx // 42
+
+ rcx = 5
+ rbx /= rcx // 8
+
+ rdi = rbx
+ rax = SYS_EXIT
+ syscall
+}
diff --git a/scripts/test_examples.sh b/scripts/test_examples.sh
index 4a01a0e..d732e49 100755
--- a/scripts/test_examples.sh
+++ b/scripts/test_examples.sh
@@ -81,6 +81,7 @@ check loop_sum "sums 1..5 with a countdown loop" examples/loop_sum.hdas
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 mul_div "multiply and non-rax division" examples/mul_div.hdass 8 ""
check fibonacci "prints the first ten Fibonacci numbers" examples/fibonacci.hdass 0 "0
1
1
diff --git a/src/codegen/nasm.c b/src/codegen/nasm.c
index 0db60dd..e55c791 100644
--- a/src/codegen/nasm.c
+++ b/src/codegen/nasm.c
@@ -209,25 +209,39 @@ static bool emit_operand(struct Emitter* emitter, struct Expr* expr)
return false;
}
+// idiv divides rdx:rax by its operand and leaves the quotient in rax, so a
+// division computes `dst = dst / divisor` through rax (clobbering rax and rdx).
+static void emit_division(struct Emitter* emitter, const char* dst, struct Expr* divisor)
+{
+ bool dst_is_rax = strcmp(dst, "rax") == 0;
+
+ if (!dst_is_rax)
+ fprintf(emitter->out, "\tmov rax, %s\n", dst);
+ fprintf(emitter->out, "\tcqo\n");
+ fprintf(emitter->out, "\tidiv ");
+ emit_operand(emitter, divisor);
+ fprintf(emitter->out, "\n");
+ if (!dst_is_rax)
+ fprintf(emitter->out, "\tmov %s, rax\n", dst);
+}
+
static void emit_divide(struct Emitter* emitter, struct AssignStatement* assign)
{
- 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)
+ if (assign->target_deref)
{
fprintf(emitter->out, "\t; TODO: unsupported division\n");
return;
}
- fprintf(emitter->out, "\tcqo\n");
- fprintf(emitter->out, "\tidiv ");
- emit_operand(emitter, assign->value);
- fprintf(emitter->out, "\n");
+ struct Token target = resolve_register(emitter, assign->target);
+ char dst[32];
+ snprintf(dst, sizeof(dst), "%.*s", (int)target.length, target.start);
+ 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 '+'/'-' whose right
-// operands are plain operands (never a buffer or a nested binary)
+// (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)
@@ -236,13 +250,10 @@ 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)
- return false;
- if (expr->binary.right->kind != EXPR_PRIMARY &&
+ if (expr->binary.right->kind != EXPR_PRIMARY &&
expr->binary.right->kind != EXPR_MEMBER)
return false;
- if (expr->binary.right->kind == EXPR_PRIMARY &&
+ 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);
@@ -257,7 +268,15 @@ static void emit_expr_into(struct Emitter* emitter, const char* dst, struct Expr
{
emit_expr_into(emitter, dst, expr->binary.left);
- const char* mnemonic = expr->binary.op.type == TOKEN_PLUS ? "add" : "sub";
+ if (expr->binary.op.type == TOKEN_SLASH)
+ {
+ emit_division(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");
diff --git a/tests/codegen_test.c b/tests/codegen_test.c
index ab771f7..b69d79e 100644
--- a/tests/codegen_test.c
+++ b/tests/codegen_test.c
@@ -151,6 +151,57 @@ static void test_generate_divide(struct TestContext* context)
free_program(&program);
}
+static void test_generate_multiply(struct TestContext* context)
+{
+ struct Lexer lexer = create_lexer("proc main\n{\nrax = rbx * rcx\nrbx = rcx * 3 + rdx\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, rbx") != NULL);
+ check(context, strstr(buffer, "imul rax, rcx") != NULL);
+ check(context, strstr(buffer, "imul rbx, 3") != NULL);
+ check(context, strstr(buffer, "add rbx, rdx") != NULL);
+ check(context, strstr(buffer, "; TODO") == NULL);
+
+ free_program(&program);
+}
+
+static void test_generate_divide_nonrax(struct TestContext* context)
+{
+ // division into a register other than rax routes through rax:rdx
+ struct Lexer lexer = create_lexer("proc main\n{\nrbx /= rcx\nrsi = rdi / rcx\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, rbx") != NULL);
+ check(context, strstr(buffer, "idiv rcx") != NULL);
+ check(context, strstr(buffer, "mov rbx, rax") != NULL);
+ check(context, strstr(buffer, "mov rsi, rax") != NULL);
+ check(context, strstr(buffer, "; TODO") == NULL);
+
+ free_program(&program);
+}
+
static void test_generate_stack_frame(struct TestContext* context)
{
struct Lexer lexer = create_lexer("proc work\n{\nstack buffer[32]\nsyscall\n}\n");
@@ -333,6 +384,8 @@ void run_codegen_tests(struct TestContext* context)
test_generate_call(context);
test_generate_param_substitution(context);
test_generate_divide(context);
+ test_generate_multiply(context);
+ test_generate_divide_nonrax(context);
test_generate_stack_frame(context);
test_generate_address_expr(context);
test_generate_sized_store(context);