From c3c3aa1533927f343983676fec0560a17b2a501f Mon Sep 17 00:00:00 2001 From: gingerBill Date: Mon, 24 Aug 2026 11:46:07 +0100 Subject: [PATCH] Add `#pure` directive for `asm` templates --- core/rexcode/isa/riscv/clobber_types.odin | 1 - .../riscv/tablegen/cpp-compiler/cpp-gen.odin | 20 +++- .../x86/tablegen/cpp-compiler/cpp-gen.odin | 22 +++- src/asm_tables_amd64.cpp | 34 ++++-- src/asm_tables_riscv.cpp | 19 ++- src/check_asm.cpp | 108 +++++++++++++++++- src/entity.cpp | 1 + src/parser.cpp | 3 +- 8 files changed, 180 insertions(+), 28 deletions(-) diff --git a/core/rexcode/isa/riscv/clobber_types.odin b/core/rexcode/isa/riscv/clobber_types.odin index 3a9c54d98..6d2b37bbd 100644 --- a/core/rexcode/isa/riscv/clobber_types.odin +++ b/core/rexcode/isa/riscv/clobber_types.odin @@ -32,7 +32,6 @@ Side_Effect :: enum u8 { IFENCE, // instruction-fetch synchronization (FENCE.I) ATOMIC, // indivisible memory RMW (AMO*, and the LR/SC pair) RESERVATION, // sets or tests an LR/SC reservation - NONDETERMINISTIC, } Clobber :: struct { diff --git a/core/rexcode/isa/riscv/tablegen/cpp-compiler/cpp-gen.odin b/core/rexcode/isa/riscv/tablegen/cpp-compiler/cpp-gen.odin index 8ab2a4a00..149daecc0 100644 --- a/core/rexcode/isa/riscv/tablegen/cpp-compiler/cpp-gen.odin +++ b/core/rexcode/isa/riscv/tablegen/cpp-compiler/cpp-gen.odin @@ -236,6 +236,9 @@ main :: proc() { bool is_conditional() const { return has_control(); } + bool is_nondeterministic() const { + return false; + } }; void clobber_implicit_regs(StringSet *clobber_registers_set, u16 implicit_regs) { @@ -273,6 +276,17 @@ main :: proc() { u16 csr; // CSR address when a src slot is AliasSrc_CSR_LIT u8 nargs; // operands the user supplies (ARG0..operands) { + if (op->kind == Ast_AsmLabelDecl) { + saw_label = true; // resolved against label_scope during operand checking + } + } + return saw_label; +} + template gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tmpl_entity, AstAsmInstruction *instr, @@ -1471,12 +1486,31 @@ gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm // Handle clobbering from mnemonic auto clobber = clobber_forms[valid_form_index]; + // NOTE(bill): reads_mem/writes_mem are per-FORM capability bits. + // A form with an r/m slot (e.g. add r/m32, imm32) carries them even + // when the operand resolved to a register, e.g. `add x, 123`. + // Count a real access only when an operand actually resolved to memory, + // or the access is implicit (no r/m slot exists to carry the bit: movs/stos/...). + bool has_mem_operand = false; + bool has_rm_slot = false; + auto const &valid_form = forms[valid_form_index]; + for_array(i, operands) { + if (determine_asm_operand_kind(&operands[i]) == AsmOperand_Memory) { + has_mem_operand = true; + } + AsmOperandKind op_kind = asm_ctx->kind_from_operand_type(operand_slot_type(valid_form, cast(int)i)); + if (op_kind == AsmOperand_Memory || op_kind == AsmOperand_Register_Or_Memory) { + has_rm_slot = true; + } + } + bool mem_is_real = has_mem_operand || !has_rm_slot; + tmpl_entity->AsmTemplate.clobber_flags |= clobber.implies_clobber_flags(); - tmpl_entity->AsmTemplate.clobber_memory |= clobber.implies_clobber_memory(); + tmpl_entity->AsmTemplate.clobber_memory |= clobber.implies_clobber_memory() && mem_is_real; tmpl_entity->AsmTemplate.is_volatile |= clobber.implies_side_effects(); tmpl_entity->AsmTemplate.has_observable_side_effect |= clobber.implies_side_effects() != 0; - tmpl_entity->AsmTemplate.has_observable_side_effect |= clobber.writes_mem; + tmpl_entity->AsmTemplate.has_observable_side_effect |= clobber.writes_mem && mem_is_real; // #align_stack only matters if the body makes a call (which requires the stack // aligned at the call boundary) or manipulates RSP directly. Plain memory access @@ -1646,6 +1680,39 @@ gb_internal void check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm } asm_ctx->clobber_implicit_regs(&tmpl_entity->AsmTemplate.clobber_registers_set, produced); + // Purity inference + if (asm_acc->can_be_pure) { + // NOTE(bill): Only the first violating instruction is recorded + // The later ones don't overwrite the reason. + char const *why = nullptr; + + if (clobber.writes_mem && mem_is_real) { + why = "it writes to memory"; + } else if (clobber.reads_mem && mem_is_real) { + // A load's result depends on memory, which is not a value input. + why = "it reads from memory"; + } else if (clobber.is_nondeterministic() || + (pseudo_mnemonic > 0 && alias.is_nondeterministic())) { + // rdtsc / rdrand / cpuid on x86 + // counter/entropy CSR reads on RISC-V. + why = "it is nondeterministic"; + } else if (clobber.implies_clobber_memory() && mem_is_real) { + why = "it accesses memory the compiler cannot see"; + } else if (clobber.implies_side_effects()) { + why = "it has an observable side effect"; + } else if (clobber.has_control() && !check_asm_instr_targets_internal_label(instr)) { + // Internal jmp/jcc/ret over the template's own labels stays pure; a call + // or an indirect/external transfer does not. + why = "it possibly transfers control outside the inline 'asm' template"; + } + + if (why != nullptr) { + asm_acc->can_be_pure = false; + asm_acc->impure_reason = why; + asm_acc->impure_reason_node = instr->name; + } + } + return; } @@ -2188,8 +2255,9 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity entity->type = type; - bool is_volatile = false; - bool is_align_stack = false; + bool is_volatile = false; + bool is_align_stack = false; + bool is_pure_annotated = false; auto *clobber_registers_set = &entity->AsmTemplate.clobber_registers_set; check_asm_specs(asm_ctx, ctx, ate->param_scope, at->specs, &ate->decls); @@ -2211,6 +2279,11 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity error(clobber->name, "#align_stack has already been defined as an asm specification"); } is_align_stack = true; + } else if (clobber->name.string == "pure") { + if (is_pure_annotated) { + error(clobber->name, "#pure has already been defined as an asm specification"); + } + is_pure_annotated = true; } else { error(clobber->name, "Unknown clobber directive '#%.*s'", LIT(clobber->name.string)); } @@ -2317,6 +2390,7 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity // jump target / back-edge, after which a read can precede its textual def; from // there on we stop emitting the implicit-read diagnostic. asm_acc.straight_line = true; + asm_acc.can_be_pure = true; // collect label decls for (Ast *instruction_ : at->instructions) { @@ -2657,6 +2731,32 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity "end it with an unconditional jump, return, or halt"); } } + + { + bool declared_effects = entity->AsmTemplate.is_volatile || + entity->AsmTemplate.clobber_memory || + entity->AsmTemplate.has_observable_side_effect; + bool is_pure = asm_acc.can_be_pure && !declared_effects && !type->Proc.diverging; + entity->AsmTemplate.is_pure = is_pure; + + if (is_pure_annotated && !is_pure) { + Ast *node = asm_acc.impure_reason_node; + char const *why = asm_acc.impure_reason; + if (why == nullptr) { + if (type->Proc.diverging) { + why = "it is declared diverging (-> !) and computes no outputs"; + } else if (entity->AsmTemplate.clobber_memory) { + why = "it declares '#clobber memory'"; + } else if (entity->AsmTemplate.is_volatile) { + why = "it is declared '#volatile'"; + } else { + why = "it declares an observable effect"; + } + } + Token tok = node ? ast_token(node): entity->token; + error(tok, "'asm' template is marked #pure but it is not pure: %s", why); + } + } } gb_internal void check_asm_template_from_entity(CheckerContext *c, Entity *e, DeclInfo *d) { diff --git a/src/entity.cpp b/src/entity.cpp index e21ec4483..f6045e341 100644 --- a/src/entity.cpp +++ b/src/entity.cpp @@ -345,6 +345,7 @@ struct Entity { Ast *node; bool is_volatile; bool is_align_stack; + bool is_pure; bool has_observable_side_effect; diff --git a/src/parser.cpp b/src/parser.cpp index d9d07cf57..1173305f8 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -2758,7 +2758,8 @@ gb_internal Ast *parse_asm_template(AstFile *f) { Token name = expect_token(f, Token_Ident); if (name.string == "volatile" || - name.string == "align_stack") { + name.string == "align_stack" || + name.string == "pure") { Ast *clobber = alloc_ast_node(f, Ast_AsmClobber); clobber->AsmClobber.token = hash; clobber->AsmClobber.name = name;