From 21be45a904a0afc767214298721bd2bbce19a054 Mon Sep 17 00:00:00 2001 From: hachem Date: Tue, 8 Sep 2026 11:26:42 +0200 Subject: feat: add signed memory loads --- docs/language.md | 14 ++++++++++-- src/codegen/nasm.c | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/lexer/lexer.c | 2 ++ src/lexer/lexer.h | 1 + src/parser/ast.c | 3 +++ src/parser/ast.h | 9 ++++++++ src/sema/sema.c | 18 ++++++++++++++- tests/codegen_test.c | 37 ++++++++++++++++++++++++++++++ tests/parser_test.c | 40 ++++++++++++++++++++++++++++++++ tests/sema_test.c | 8 +++++++ 10 files changed, 191 insertions(+), 5 deletions(-) diff --git a/docs/language.md b/docs/language.md index 4de054f..3ccdab8 100644 --- a/docs/language.md +++ b/docs/language.md @@ -50,7 +50,7 @@ Unknown keys, a `bits` value other than 32/64, and unknown extensions are errors ## Constants and data -`const` names a constant integer expression — integer literals, character literals, other constants, and `+` `-` `*` `/`. Integers are decimal, `0x` hex, or `0b` binary (these forms work anywhere an integer does). `data` puts a string in `.data`; the name is its address and `.len` is its length in bytes. +`const` names a constant integer expression — integer literals, character literals, other constants, a leading `-`, and `+` `-` `*` `/`. Integers are decimal, `0x` hex, or `0b` binary (these forms work anywhere an integer does). `data` puts a string in `.data`; the name is its address and `.len` is its length in bytes. ```hdass const STDOUT = 1 @@ -138,7 +138,7 @@ if rax > rbx else if rax == rbx rdi = 0 else - rdi = 2 + rdi = -1 ``` ## Dereference (`^`) @@ -170,10 +170,20 @@ rcx = ^dword rsi // mov ecx, [rsi] (32-bit load zero-extends) rdx = ^rsi + 4 // load, then add 4 ``` +`^signed` before the size sign-extends instead, so a narrower value keeps its sign in the full register. It needs a `byte`, `word`, or `dword` size (a full-width load has nothing to extend): + +```hdass +rax = ^signed byte rsi // movsx rax, byte [rsi] +rbx = ^signed word rsi // movsx rbx, word [rsi] +rcx = ^signed dword rsi // movsxd rcx, dword [rsi] +``` + ## Expressions 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. +A leading `-` negates a term (`rax = -5`, `rbx = rax + -3`, `const OFFSET = -8`). It only applies to values that fold to a constant, so it emits a negative immediate; negating a register (`-rbx`) is not supported. + ## Extensions ### logical_registers diff --git a/src/codegen/nasm.c b/src/codegen/nasm.c index 8bcdda3..198cb5c 100644 --- a/src/codegen/nasm.c +++ b/src/codegen/nasm.c @@ -14,6 +14,10 @@ static void emit_const_expr(struct Expr* expr, FILE* out) 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); @@ -370,6 +374,14 @@ static bool fold_const(struct Program* program, struct Expr* expr, uint64_t* out 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; @@ -470,6 +482,14 @@ static bool emit_operand(struct Emitter* emitter, struct Expr* expr) 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; @@ -536,10 +556,22 @@ static bool expr_supported(struct Emitter* emitter, struct Expr* expr) 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_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; @@ -549,10 +581,35 @@ static bool expr_supported(struct Emitter* emitter, struct Expr* expr) return false; } -// dst = [address], zero-extending narrower loads into the full register +// 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: @@ -1073,6 +1130,9 @@ static void collect_floats_expr(struct FloatTable* floats, struct Expr* expr) 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); diff --git a/src/lexer/lexer.c b/src/lexer/lexer.c index e08a458..4d468e6 100644 --- a/src/lexer/lexer.c +++ b/src/lexer/lexer.c @@ -45,6 +45,7 @@ static enum TokenType identifier_type(const char* start, size_t length) { "word", 4, TOKEN_WORD }, { "dword", 5, TOKEN_DWORD }, { "qword", 5, TOKEN_QWORD }, + { "signed", 6, TOKEN_SIGNED }, }; for (size_t i = 0; i < sizeof(keywords) / sizeof(keywords[0]); i += 1) @@ -273,6 +274,7 @@ const char* token_type_name(enum TokenType type) case TOKEN_WORD: return "word"; case TOKEN_DWORD: return "dword"; case TOKEN_QWORD: return "qword"; + case TOKEN_SIGNED: return "signed"; case TOKEN_EQUAL: return "equal"; case TOKEN_PLUS: return "plus"; case TOKEN_MINUS: return "minus"; diff --git a/src/lexer/lexer.h b/src/lexer/lexer.h index 2bc43e0..73de02f 100644 --- a/src/lexer/lexer.h +++ b/src/lexer/lexer.h @@ -26,6 +26,7 @@ enum TokenType TOKEN_WORD, TOKEN_DWORD, TOKEN_QWORD, + TOKEN_SIGNED, TOKEN_EQUAL, TOKEN_PLUS, diff --git a/src/parser/ast.c b/src/parser/ast.c index 9c78d1f..63d8a08 100644 --- a/src/parser/ast.c +++ b/src/parser/ast.c @@ -11,6 +11,9 @@ void free_expr(struct Expr* expr) { case EXPR_PRIMARY: break; + case EXPR_UNARY: + free_expr(expr->unary.operand); + break; case EXPR_BINARY: free_expr(expr->binary.left); free_expr(expr->binary.right); diff --git a/src/parser/ast.h b/src/parser/ast.h index 75d0bdd..288bddf 100644 --- a/src/parser/ast.h +++ b/src/parser/ast.h @@ -58,6 +58,7 @@ struct Param enum ExprKind { EXPR_PRIMARY, + EXPR_UNARY, EXPR_BINARY, EXPR_MEMBER, EXPR_DEREF, @@ -68,6 +69,12 @@ struct PrimaryExpr struct Token token; }; +struct UnaryExpr +{ + struct Token op; + struct Expr* operand; +}; + struct BinaryExpr { struct Expr* left; @@ -83,6 +90,7 @@ struct MemberExpr struct DerefExpr { + bool is_signed; enum StoreSize size; struct Expr* address; }; @@ -93,6 +101,7 @@ struct Expr union { struct PrimaryExpr primary; + struct UnaryExpr unary; struct BinaryExpr binary; struct MemberExpr member; struct DerefExpr deref; diff --git a/src/sema/sema.c b/src/sema/sema.c index 6505077..748ccbe 100644 --- a/src/sema/sema.c +++ b/src/sema/sema.c @@ -103,10 +103,11 @@ static bool is_program_const(const struct Program* program, struct Token name) return false; } -static struct Token first_token(struct Expr* expr) +static struct Token first_token(const struct Expr* expr) { switch (expr->kind) { + case EXPR_UNARY: return expr->unary.op; case EXPR_BINARY: return first_token(expr->binary.left); case EXPR_MEMBER: return first_token(expr->member.object); case EXPR_DEREF: return first_token(expr->deref.address); @@ -116,6 +117,9 @@ static struct Token first_token(struct Expr* expr) static bool check_const_value(struct Source source, struct Program* program, struct Expr* expr) { + if (expr->kind == EXPR_UNARY) + return check_const_value(source, program, expr->unary.operand); + if (expr->kind == EXPR_BINARY) { bool left = check_const_value(source, program, expr->binary.left); @@ -319,9 +323,18 @@ static void check_expr(struct RefCheck* check, const struct Expr* expr) check_expr(check, expr->binary.left); check_expr(check, expr->binary.right); break; + case EXPR_UNARY: + check_expr(check, expr->unary.operand); + break; case EXPR_DEREF: { const struct Expr* address = expr->deref.address; + if (expr->deref.is_signed + && expr->deref.size != STORE_SIZE_BYTE + && expr->deref.size != STORE_SIZE_WORD + && expr->deref.size != STORE_SIZE_DWORD) + ref_error(check, first_token(expr), "signed load requires a byte, word, or dword size"); + if (address->kind == EXPR_PRIMARY && (is_register(check, address->primary.token) || is_param(check, address->primary.token))) break; @@ -406,6 +419,9 @@ static void check_stack_size(struct RefCheck* check, struct Expr* expr) check_stack_size(check, expr->binary.left); check_stack_size(check, expr->binary.right); break; + case EXPR_UNARY: + check_stack_size(check, expr->unary.operand); + break; case EXPR_MEMBER: { const struct Expr* object = expr->member.object; diff --git a/tests/codegen_test.c b/tests/codegen_test.c index db23791..e2969db 100644 --- a/tests/codegen_test.c +++ b/tests/codegen_test.c @@ -100,6 +100,23 @@ static void test_generate_if_else(struct TestContext* context) free_program(&program); } +static void test_generate_negative(struct TestContext* context) +{ + struct Lexer lexer = create_lexer( + "const N = -8\nproc main\n{\nrax = -5\nrbx = rax + -3\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, "%define N (-8)") != NULL); + check(context, strstr(buffer, "mov rax, -5") != NULL); + check(context, strstr(buffer, "add rbx, -3") != NULL); + + free_program(&program); +} + static void test_generate_call(struct TestContext* context) { struct Lexer lexer = create_lexer( @@ -344,6 +361,24 @@ static void test_generate_load(struct TestContext* context) free_program(&program); } +static void test_generate_signed_load(struct TestContext* context) +{ + struct Lexer lexer = create_lexer( + "proc main\n{\nrax = ^signed byte rsi\nrbx = ^signed word rsi\nrcx = ^signed dword rsi\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, "movsx rax, byte [rsi]") != NULL); + check(context, strstr(buffer, "movsx rbx, word [rsi]") != NULL); + check(context, strstr(buffer, "movsxd rcx, dword [rsi]") != NULL); + check(context, strstr(buffer, "; TODO") == NULL); + + free_program(&program); +} + static void test_generate_enum_struct(struct TestContext* context) { struct Lexer lexer = create_lexer( @@ -441,6 +476,7 @@ void run_codegen_tests(struct TestContext* context) test_generate_add_zero_peephole(context); test_generate_enum_struct(context); test_generate_load(context); + test_generate_signed_load(context); test_generate_logical_registers(context); test_generate_logical_disabled(context); test_generate_entry_and_bits(context); @@ -449,6 +485,7 @@ void run_codegen_tests(struct TestContext* context) test_generate_text(context); test_generate_if(context); test_generate_if_else(context); + test_generate_negative(context); test_generate_call(context); test_generate_param_substitution(context); test_generate_divide(context); diff --git a/tests/parser_test.c b/tests/parser_test.c index 32ea422..e1199eb 100644 --- a/tests/parser_test.c +++ b/tests/parser_test.c @@ -46,6 +46,25 @@ static void test_parse_const_expr(struct TestContext* context) free_program(&program); } +static void test_parse_unary(struct TestContext* context) +{ + struct Lexer lexer = create_lexer("const N = -8\nproc main\n{\nrax = -5\n}\n"); + struct Program program; + + check(context, parse_program(&lexer, &program)); + + const struct Expr* konst = program.consts[0].value; + check(context, konst->kind == EXPR_UNARY); + check(context, text_is(konst->unary.op, "-")); + check(context, primary_is(konst->unary.operand, "8")); + + const struct Expr* value = program.procs[0].body[0].assign.value; + check(context, value->kind == EXPR_UNARY); + check(context, primary_is(value->unary.operand, "5")); + + free_program(&program); +} + static void test_parse_data(struct TestContext* context) { struct Lexer lexer = create_lexer("data msg = \"hi\"\n"); @@ -264,6 +283,25 @@ static void test_parse_sized_deref(struct TestContext* context) free_program(&program); } +static void test_parse_signed_deref(struct TestContext* context) +{ + struct Lexer lexer = create_lexer("proc main\n{\nrax = ^signed byte rsi\nrbx = ^byte rsi\n}\n"); + struct Program program; + + check(context, parse_program(&lexer, &program)); + + const struct Expr* signed_load = program.procs[0].body[0].assign.value; + check(context, signed_load->kind == EXPR_DEREF); + check(context, signed_load->deref.is_signed); + check(context, signed_load->deref.size == STORE_SIZE_BYTE); + + const struct Expr* plain_load = program.procs[0].body[1].assign.value; + check(context, plain_load->kind == EXPR_DEREF); + check(context, !plain_load->deref.is_signed); + + free_program(&program); +} + static void test_parse_directives(struct TestContext* context) { struct Lexer lexer = create_lexer("[bits: 32]\n[entry: kmain]\n[enable: logical_registers]\nproc kmain\n{\nsyscall\n}\n"); @@ -349,6 +387,7 @@ void run_parser_tests(struct TestContext* context) { test_parse_consts(context); test_parse_const_expr(context); + test_parse_unary(context); test_parse_data(context); test_parse_proc_params(context); test_parse_proc_body(context); @@ -360,6 +399,7 @@ void run_parser_tests(struct TestContext* context) test_parse_call(context); test_parse_stack(context); test_parse_sized_deref(context); + test_parse_signed_deref(context); test_parse_directives(context); test_parse_bad_directive(context); test_parse_register_size_suffix(context); diff --git a/tests/sema_test.c b/tests/sema_test.c index dc01d47..643ffdf 100644 --- a/tests/sema_test.c +++ b/tests/sema_test.c @@ -97,6 +97,13 @@ static void test_deref_needs_register(struct TestContext* context) check(context, analyze_source("proc main\n{\nrax = ^rsi\n}\n")); } +static void test_signed_load_needs_size(struct TestContext* context) +{ + check(context, analyze_source("proc main\n{\nrax = ^signed byte rsi\n}\n")); + check(context, !analyze_source("proc main\n{\nrax = ^signed rsi\n}\n")); + check(context, !analyze_source("proc main\n{\nrax = ^signed qword rsi\n}\n")); +} + static void test_stack_size_constant(struct TestContext* context) { check(context, analyze_source( @@ -152,6 +159,7 @@ void run_sema_tests(struct TestContext* context) test_const_expr_rejects_register(context); test_const_expr_rejects_data(context); test_deref_needs_register(context); + test_signed_load_needs_size(context); test_stack_size_constant(context); test_enum_struct_members(context); test_references_resolve(context); -- cgit v1.3