diff --git a/src/check_asm.cpp b/src/check_asm.cpp index 8508df45767..28884868a50 100644 --- a/src/check_asm.cpp +++ b/src/check_asm.cpp @@ -600,6 +600,26 @@ gb_internal bool check_asm_is_immediate_param(Entity *tmpl_entity, Operand const return false; } +// NOTE(bill): a template opting in to a feature with @(require_target_feature) or @(enable_target_feature) +// must not be rejected because the global build did not enable it +gb_internal bool check_asm_template_feature_is_enabled(Entity *tmpl_entity, String const &feature) { + if (check_target_feature_is_enabled(feature, nullptr)) { + return true; + } + Type *pt = tmpl_entity->AsmTemplate.proc_type; + GB_ASSERT(pt->kind == Type_Proc); + String req = pt->Proc.require_target_feature; + String en = pt->Proc.enable_target_feature; + + if (req.len != 0 && check_target_feature_is_superset_of(req, feature, nullptr)) { + return true; + } + if (en.len != 0 && check_target_feature_is_superset_of(en, feature, nullptr)) { + return true; + } + return false; +} + template gb_internal void check_asm_specs(AsmCtx *asm_ctx, CheckerContext *ctx, Scope *scope, Slice const &specs, Array *asm_template_entity_decls) { @@ -1681,31 +1701,14 @@ gb_internal bool check_mnemonic(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *tm } } { - // Feature gate: the matched form may require a target feature (crc32b needs - // +crc, LSE atomics need +lse, ...). The form carries its Feature. It is - // satisfied if the feature is globally enabled for the target, OR if this - // template's own signature declares it via #require_target_feature / - // #enable_target_feature — a template opting in to a feature shouldn't be - // rejected just because the global build didn't enable it. + // NOTE(bill): Feature Gating + // The matched form may require a target feature (crc32b needs +crc, LSE atomics need +lse, ...). + // The form carries its Feature. It is satisfied if the feature is globally enabled for the target, + // OR if this template's own signature declares it via @(require_target_feature) / @(enable_target_feature) + // see `check_asm_template_feature_is_enabled` String feat = asm_ctx->feature_name_from_form(forms[valid_form_index]); if (feat.len != 0) { - bool ok = check_target_feature_is_enabled(feat, nullptr); - - if (!ok) { - // The proc type may declare the feature itself. - Type *pt = base_type(tmpl_entity->type); - GB_ASSERT(pt->kind == Type_Proc); - String req = pt->Proc.require_target_feature; - String en = pt->Proc.enable_target_feature; - // These are comma-lists; `feat` is a single bare name. is_superset_of - // checks whether the declared list contains it. - if (req.len != 0 && check_target_feature_is_superset_of(req, feat, nullptr)) { - ok = true; - } - if (!ok && en.len != 0 && check_target_feature_is_superset_of(en, feat, nullptr)) { - ok = true; - } - } + bool ok = check_asm_template_feature_is_enabled(tmpl_entity, feat); if (!ok) { error(instr->name, @@ -2272,16 +2275,24 @@ gb_internal void check_asm_instruction_operand(AsmCtx *asm_ctx, CheckerContext * return; } - if (found->kind != Entity_Constant) { + // NOTE(bill): resolved like any other identifier, as the constant may not have been checked yet, + // e.g. one declared later in the same block, or one in a lazy file + found = resolve_alias_entity(ctx, found, nullptr); + if (found->kind == Entity_Constant) { + GB_ASSERT(ctx->scope == param_scope->parent); + check_ident(ctx, operand, expr, nullptr, nullptr, false); + if (operand->mode == Addressing_Invalid) { + // NOTE(bill): already reported, e.g. an illegal declaration cycle + return; + } + } + if (operand->mode != Addressing_Constant) { error(expr, "Only asm parameters or constants are allowed to be used within an 'asm' template"); + operand->mode = Addressing_Invalid; + operand->type = t_invalid; return; } - add_entity_use(ctx, expr, entity); - operand->mode = Addressing_Constant; - operand->value = found->Constant.value; - operand->type = found->type; - add_type_and_value(ctx, expr, operand->mode, operand->type, operand->value); return; case_end; @@ -2867,8 +2878,8 @@ gb_internal void check_asm_instruction_operand(AsmCtx *asm_ctx, CheckerContext * } -template -gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *entity, DeclInfo *d) { +// NOTE(bill): the signature is everything needed to call an asm template, the rest is checked by `check_asm_template_body` +gb_internal bool check_asm_template_signature(CheckerContext *ctx, Entity *entity, DeclInfo *d) { GB_ASSERT(entity->kind == Entity_AsmTemplate); auto *ate = &entity->AsmTemplate; @@ -2880,7 +2891,8 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity GB_ASSERT(at->signature != nullptr); if (at->signature->kind != Ast_ProcType) { error(at->signature, "Expected a valid signature, got %.*s", LIT(ast_strings[at->signature->kind])); - return; + entity->type = t_invalid; + return false; } AstProcType *pt = &at->signature->ProcType; @@ -2898,13 +2910,27 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity // always require the results of `asm` templates type->Proc.require_results = true; } - entity->type = type; + entity->type = type; + ate->proc_type = type; AttributeContext ac = make_attribute_context({}, {}); if (d != nullptr) { check_decl_attributes(ctx, d->attributes, proc_decl_attribute, &ac); } check_target_feature_attributes(ac, entity, type); + return true; +} + +// NOTE(bill): the specification block, the clobbers, and the instructions +template +gb_internal void check_asm_template_body(AsmCtx *asm_ctx, CheckerContext *ctx, Entity *entity, DeclInfo *d) { + GB_ASSERT(entity->kind == Entity_AsmTemplate); + auto *ate = &entity->AsmTemplate; + + ast_node(at, AsmTemplate, d->init_expr); + + Type *type = ate->proc_type; + GB_ASSERT(type != nullptr && type->kind == Type_Proc); check_asm_specs(asm_ctx, ctx, ate->param_scope, at->specs, &ate->decls); @@ -3286,7 +3312,7 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity if (is_type_simd_vector(ed.entity->type)) { i32 vw = cast(i32)(type_size_of(ed.entity->type) * 8); String feat = asm_ctx->required_vector_feature(vw); - if (feat.len != 0 && !check_target_feature_is_enabled(feat, nullptr)) { + if (feat.len != 0 && !check_asm_template_feature_is_enabled(entity, feat)) { error(ed.entity->token, "'asm' vector operand '%.*s' is %d-bit, which requires the '%.*s' target feature", LIT(ed.entity->token.string), vw, LIT(feat)); @@ -3391,7 +3417,7 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity } GB_ASSERT(entity->kind == Entity_AsmTemplate); - if (results->Tuple.variables.count == 0 && !entity->AsmTemplate.is_volatile && + if (type->Proc.result_count == 0 && !entity->AsmTemplate.is_volatile && !entity->AsmTemplate.clobber_memory && entity->AsmTemplate.has_observable_side_effect) { warning(entity->token, @@ -3434,13 +3460,47 @@ gb_internal void check_asm_template(AsmCtx *asm_ctx, CheckerContext *ctx, Entity } gb_internal void check_asm_template_from_entity(CheckerContext *c, Entity *e, DeclInfo *d) { + GB_ASSERT(e->decl_info == d); + if (!check_asm_template_signature(c, e, d)) { + return; + } + switch (build_context.metrics.arch) { + case TargetArch_amd64: + case TargetArch_riscv64: + case TargetArch_arm64: + check_asm_template_body_later(c, e); + break; + default: + error(e->token, "asm templates are not currently supported for this target"); + break; + } +} + +gb_internal void check_asm_template_body_from_entity(CheckerContext *c, Entity *e) { + GB_ASSERT(e->kind == Entity_AsmTemplate); + DeclInfo *d = e->decl_info; + GB_ASSERT(d != nullptr); + + Entity *anonymous_entity = e->AsmTemplate.node->AsmTemplate.anonymous_entity; + if (anonymous_entity != nullptr && anonymous_entity != e) { + // NOTE(bill): the call it is in was checked again and only the entity it made last is used + return; + } + + CheckerContext ctx = *c; + ctx.scope = d->scope; + ctx.decl = d; + if (build_context.metrics.arch == TargetArch_amd64) { - check_asm_template(&g_asm_amd64, c, e, d); + check_asm_template_body(&g_asm_amd64, &ctx, e, d); } else if (build_context.metrics.arch == TargetArch_riscv64) { - check_asm_template(&g_asm_riscv, c, e, d); + check_asm_template_body(&g_asm_riscv, &ctx, e, d); } else if (build_context.metrics.arch == TargetArch_arm64) { - check_asm_template(&g_asm_arm64, c, e, d); + check_asm_template_body(&g_asm_arm64, &ctx, e, d); } else { - error(e->token, "asm templates are not currently supported for this target"); + GB_PANIC("asm templates are not supported for this target"); } + + add_deps_from_child_to_parent(d); + e->flags |= EntityFlag_ProcBodyChecked; } \ No newline at end of file diff --git a/src/check_asm_cfg.cpp b/src/check_asm_cfg.cpp index 51e5fd91fff..86e80922df6 100644 --- a/src/check_asm_cfg.cpp +++ b/src/check_asm_cfg.cpp @@ -377,7 +377,7 @@ gb_internal void check_asm_cfg_analyse(AsmCtx *asm_ctx, AsmCfg *cfg, CheckerCont GB_ASSERT(entity->kind == Entity_AsmTemplate); auto *ate = &entity->AsmTemplate; auto const &decls = ate->decls; - bool diverging = entity->type->Proc.diverging; + bool diverging = ate->proc_type->Proc.diverging; if (cfg->blocks.count == 0) { // With an empty body, the CFG cannot really do nothing diff --git a/src/check_decl.cpp b/src/check_decl.cpp index bd1e102a35a..7b261d346ca 100644 --- a/src/check_decl.cpp +++ b/src/check_decl.cpp @@ -2206,10 +2206,10 @@ gb_internal void check_asm_group_decl(CheckerContext *ctx, Entity *asm_entity, D check_objc_methods(ctx, asm_entity, ac); } -#include "check_asm.cpp" - gb_internal void add_deps_from_child_to_parent(DeclInfo *decl); +#include "check_asm.cpp" + gb_internal void check_entity_decl(CheckerContext *ctx, Entity *e, DeclInfo *d, Type *named_type) { if (e->state == EntityState_Resolved) { return; @@ -2512,6 +2512,8 @@ gb_internal bool check_proc_body(CheckerContext *ctx_, Token token, DeclInfo *de return false; } + ctx->asm_templates_to_check = &decl->proc->asm_templates_to_check; + check_open_scope(ctx, body); { ctx->scope->decl_info = decl; @@ -2570,6 +2572,13 @@ gb_internal bool check_proc_body(CheckerContext *ctx_, Token token, DeclInfo *de } check_close_scope(ctx); + // NOTE(bill): with every name in the body now known, see `check_asm_template_body_later` + ctx->asm_templates_to_check = nullptr; + for (Entity *e : decl->proc->asm_templates_to_check) { + check_asm_template_body_from_entity(ctx, e); + } + array_clear(&decl->proc->asm_templates_to_check); + check_scope_usage(ctx->checker, ctx->scope, check_vet_flags(body)); add_deps_from_child_to_parent(decl); diff --git a/src/check_expr.cpp b/src/check_expr.cpp index b02663aae83..7395b722c0e 100644 --- a/src/check_expr.cpp +++ b/src/check_expr.cpp @@ -9729,6 +9729,8 @@ gb_internal ExprKind check_call_expr(CheckerContext *c, Operand *operand, Ast *c DeclInfo *d = make_decl_info(c->scope, c->decl); Entity *e = alloc_entity_asm_template(d->scope, token, nullptr, unnested_proc); d->init_expr = unnested_proc; + e->decl_info = d; + e->file = c->file; at->anonymous_entity = e; check_asm_template_from_entity(c, e, d); @@ -9987,8 +9989,35 @@ gb_internal ExprKind check_call_expr(CheckerContext *c, Operand *operand, Ast *c error(call, "Called %s enables target feature '%.*s' which is invalid for the build target", kind, LIT(invalid)); } - // NOTE: Due to restrictions in LLVM you can not inline calls with a superset of features. - if (is_call_inlined) { + if (pt->Proc.calling_convention == ProcCC_InlineAsm) { + // NOTE(bill): an asm template is always inlined, so its instructions are assembled with the features + // of the procedure calling it, which must then have every feature the template enables + String scope_enable = {}; + String scope_require = {}; + if (c->curr_proc_sig != nullptr && c->curr_proc_sig->kind == Type_Proc) { + scope_enable = c->curr_proc_sig->Proc.enable_target_feature; + scope_require = c->curr_proc_sig->Proc.require_target_feature; + } + String_Iterator it = {pt->Proc.enable_target_feature, 0}; + String feature = {}; + while (string_split_iterator_next(&it, ',', &feature)) { + if (check_target_feature_is_enabled(feature, nullptr) || + check_target_feature_is_superset_of(scope_enable, feature, nullptr) || + check_target_feature_is_superset_of(scope_require, feature, nullptr)) { + continue; + } + ERROR_BLOCK(); + if (c->curr_proc_sig == nullptr) { + error(call, "Called %s enables target feature '%.*s', which must be enabled globally to call it at file scope", kind, LIT(feature)); + error_line("\tSuggested Example: -target-features:\"%.*s\"\n", LIT(feature)); + } else { + error(call, "Called %s enables target feature '%.*s', this requires the calling procedure to at least enable the same feature, as the template is inlined into it", kind, LIT(feature)); + error_line("\tSuggested Example: @(enable_target_feature=\"%.*s\")\n", LIT(feature)); + } + break; + } + } else if (is_call_inlined) { + // NOTE: Due to restrictions in LLVM you can not inline calls with a superset of features. if (c->curr_proc_decl == nullptr) { error(call, "Calling a '#force_inline' %s that enables target features is not allowed at file scope", kind); } else { diff --git a/src/checker.cpp b/src/checker.cpp index 364e9a5c110..2f11aae86b5 100644 --- a/src/checker.cpp +++ b/src/checker.cpp @@ -218,6 +218,7 @@ gb_internal void init_decl_info_proc(DeclInfo *d, Ast *proc_lit) { if (d->proc == nullptr) { ProcDeclInfo *p = permanent_alloc_item(); p->nested_to_check.allocator = heap_allocator(); + p->asm_templates_to_check.allocator = heap_allocator(); p->labels.allocator = heap_allocator(); p->variadic_reuses.allocator = heap_allocator(); p->variadic_reuse_max_align = 1; @@ -1906,6 +1907,7 @@ gb_internal void init_checker(Checker *c) { // NOTE(bill): 1 Mi elements should be enough on average array_init(&c->procs_to_check, heap_allocator(), 0, 1<<20); + array_init(&c->asm_templates_to_check, heap_allocator()); per_thread_array_init(&c->global_untyped_queue, global_thread_pool.threads.count); mpsc_init(&c->soa_types_to_complete, a); // , 1<<20); @@ -1928,6 +1930,7 @@ gb_internal void destroy_checker(Checker *c) { destroy_checker_context(&c->builtin_ctx); array_free(&c->procs_to_check); + array_free(&c->asm_templates_to_check); per_thread_array_destroy(&c->global_untyped_queue); mpsc_destroy(&c->soa_types_to_complete); } @@ -2831,6 +2834,24 @@ gb_internal void check_procedure_later(Checker *c, AstFile *file, Token token, D check_procedure_later(c, info); } +gb_internal WORKER_TASK_PROC(check_asm_template_body_worker_proc); + +// NOTE(bill): like a procedure literal, only the signature of an asm template is needed to use it, so the rest of it +// is checked once all of the procedure body it is within has been (as the names in that body are not all known +// before then), otherwise with the procedure bodies +gb_internal void check_asm_template_body_later(CheckerContext *c, Entity *e) { + GB_ASSERT(e->kind == Entity_AsmTemplate); + if (c->asm_templates_to_check != nullptr) { + array_add(c->asm_templates_to_check, e); + } else if (global_procedure_body_in_worker_queue.load()) { + thread_pool_add_task(check_asm_template_body_worker_proc, e); + } else { + mutex_lock(&c->checker->asm_templates_to_check_mutex); + array_add(&c->checker->asm_templates_to_check, e); + mutex_unlock(&c->checker->asm_templates_to_check_mutex); + } +} + gb_global Entity *min_dep_basic_equal_procs[Basic_COUNT]; @@ -7231,6 +7252,27 @@ gb_internal WORKER_TASK_PROC(check_proc_info_worker_proc) { return 1; } +// NOTE(bill): the body of an asm template which is not within a procedure body, see `check_asm_template_body_later` +gb_internal void check_queued_asm_template_body(Checker *c, Entity *e, UntypedExprInfoMap *untyped) { + GB_ASSERT(e->file != nullptr); + map_clear(untyped); + + CheckerContext ctx = {}; + init_checker_context(&ctx, c); + defer (destroy_checker_context(&ctx)); + reset_checker_context(&ctx, e->file, untyped); + + check_asm_template_body_from_entity(&ctx, e); + + add_untyped_expressions(&c->info, ctx.untyped); +} + +gb_internal WORKER_TASK_PROC(check_asm_template_body_worker_proc) { + auto *wd = &check_procedure_bodies_worker_data[current_thread_index()]; + check_queued_asm_template_body(wd->c, cast(Entity *)data, &wd->untyped); + return 0; +} + gb_internal void check_init_worker_data(Checker *c) { u32 thread_count = cast(u32)global_thread_pool.threads.count; @@ -7252,10 +7294,18 @@ gb_internal void check_procedure_bodies(Checker *c) { if (thread_count == 1) { UntypedExprInfoMap *untyped = &check_procedure_bodies_worker_data[0].untyped; - for_array(i, c->procs_to_check) { - consume_proc_info(c, c->procs_to_check[i], untyped); + // NOTE(bill): either can queue the other, e.g. a procedure body which uses an asm template of a lazy file + while (c->procs_to_check.count != 0 || c->asm_templates_to_check.count != 0) { + for_array(i, c->procs_to_check) { + consume_proc_info(c, c->procs_to_check[i], untyped); + } + array_clear(&c->procs_to_check); + + for_array(i, c->asm_templates_to_check) { + check_queued_asm_template_body(c, c->asm_templates_to_check[i], untyped); + } + array_clear(&c->asm_templates_to_check); } - array_clear(&c->procs_to_check); debugf("Total Procedure Bodies Checked: %td\n", total_bodies_checked.load(std::memory_order_relaxed)); return; @@ -7270,6 +7320,13 @@ gb_internal void check_procedure_bodies(Checker *c) { GB_ASSERT(prev_procs_to_check_count == c->procs_to_check.count); array_clear(&c->procs_to_check); + isize prev_asm_templates_to_check_count = c->asm_templates_to_check.count; + for_array(i, c->asm_templates_to_check) { + thread_pool_add_task(check_asm_template_body_worker_proc, c->asm_templates_to_check[i]); + } + GB_ASSERT(prev_asm_templates_to_check_count == c->asm_templates_to_check.count); + array_clear(&c->asm_templates_to_check); + thread_pool_wait(); global_procedure_body_in_worker_queue = false; @@ -8253,6 +8310,7 @@ gb_internal void check_parsed_files(Checker *c) { thread_pool_wait(); GB_ASSERT(c->procs_to_check.count == 0); + GB_ASSERT(c->asm_templates_to_check.count == 0); if (DEBUG_CHECK_ALL_PROCEDURES) { TIME_SECTION("check unchecked (safety measure)"); diff --git a/src/checker.hpp b/src/checker.hpp index 73ca6eb6fd9..76c30212125 100644 --- a/src/checker.hpp +++ b/src/checker.hpp @@ -213,6 +213,7 @@ struct ProcDeclInfo { BlockingMutex next_mutex; // for `nested_to_check` Array nested_to_check; // nested procedures to check once this body is checked + Array asm_templates_to_check; // the asm templates whose bodies are checked once this body is checked, see `check_asm_template_body_later` Type * gen_proc_type; // Precalculated Entity *para_poly_original; @@ -937,6 +938,8 @@ struct CheckerContext { bool in_proc_sig; ForeignContext foreign_context; + Array *asm_templates_to_check; // of the procedure body being checked, see `check_asm_template_body_later` + CheckerTypePath *type_path; isize type_level; @@ -985,6 +988,9 @@ struct Checker { BlockingMutex procs_to_check_mutex; Array procs_to_check; + BlockingMutex asm_templates_to_check_mutex; + Array asm_templates_to_check; // those not within a procedure body, see `check_asm_template_body_later` + PerThreadArray global_untyped_queue; MPSCQueue soa_types_to_complete; @@ -1087,3 +1093,4 @@ gb_internal GenTypesData *ensure_polymorphic_record_entity_has_gen_types(Checker gb_internal void init_map_internal_types(Type *type); gb_internal void check_asm_template_from_entity(CheckerContext *c, Entity *e, DeclInfo *d); +gb_internal void check_asm_template_body_from_entity(CheckerContext *c, Entity *e); diff --git a/src/checker_global.cpp b/src/checker_global.cpp index 3817e89d265..36dd8ca629d 100644 --- a/src/checker_global.cpp +++ b/src/checker_global.cpp @@ -862,7 +862,7 @@ gb_internal void global_graph_add_ref(GlobalGraphWalk *w, Entity *e) { } // NOTE: names bound within the expression (parameters, fields, '$T') are also looked up globally, which at -// worst adds an edge; only procedure bodies are skipped, as they are checked after this stage +// worst adds an edge; only the bodies of procedures and asm templates are skipped, as they are checked after this stage gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node) { if (node == nullptr) { return; @@ -1076,41 +1076,8 @@ gb_internal void global_graph_walk(GlobalGraphWalk *w, Ast *node) { break; case Ast_AsmTemplate: + // NOTE: like a procedure literal, the rest of it is checked after this stage, see `check_asm_template_body_later` global_graph_walk(w, node->AsmTemplate.signature); - global_graph_walk_slice(w, node->AsmTemplate.specs); - global_graph_walk_slice(w, node->AsmTemplate.clobbers); - global_graph_walk_slice(w, node->AsmTemplate.instructions); - break; - case Ast_AsmSpec: - global_graph_walk(w, node->AsmSpec.type); - global_graph_walk(w, node->AsmSpec.value); - for (Ast *d : node->AsmSpec.directives) { - global_graph_walk(w, d); - } - break; - case Ast_AsmClobber: - global_graph_walk(w, node->AsmClobber.value); - break; - case Ast_AsmInstruction: - global_graph_walk_slice(w, node->AsmInstruction.operands); - break; - case Ast_AsmMemoryTerm: - global_graph_walk(w, node->AsmMemoryTerm.operand); - global_graph_walk(w, node->AsmMemoryTerm.scale); - break; - case Ast_AsmMemoryOperand: - global_graph_walk(w, node->AsmMemoryOperand.segment_override); - global_graph_walk_slice(w, node->AsmMemoryOperand.terms); - global_graph_walk(w, node->AsmMemoryOperand.type); - break; - case Ast_AsmRegisterGroup: - for (Ast *r : node->AsmRegisterGroup.registers) { - global_graph_walk(w, r); - } - global_graph_walk(w, node->AsmRegisterGroup.type); - break; - case Ast_AsmDirective: - global_graph_walk_slice(w, node->AsmDirective.operands); break; } } diff --git a/src/entity.cpp b/src/entity.cpp index 53b8ad65a77..ef53214e4ee 100644 --- a/src/entity.cpp +++ b/src/entity.cpp @@ -366,6 +366,7 @@ struct Entity { } Label; struct { Ast *node; + Type *proc_type; // of its signature, as an asm group can make the entity's type invalid before its body is checked bool is_volatile; bool is_align_stack; bool is_pure; diff --git a/src/llvm_backend_asm.cpp b/src/llvm_backend_asm.cpp index c1604ba6248..0d0e5fb1dc0 100644 --- a/src/llvm_backend_asm.cpp +++ b/src/llvm_backend_asm.cpp @@ -1815,6 +1815,8 @@ struct lbAsmGenerate_arm64 : lbAsmGenerate { }; gb_internal lbValue lb_emit_asm_template_call(lbProcedure *p, Entity *entity, Array const &args) { + GB_ASSERT_MSG(entity->flags & EntityFlag_ProcBodyChecked, "the body of the asm template '%.*s' was never checked", LIT(entity->token.string)); + lbAsmGenerate_amd64 generator_amd64 = {}; lbAsmGenerate_arm64 generator_arm64 = {}; lbAsmGenerate_riscv64 generator_riscv64 = {}; diff --git a/tests/internal/test_asm_deferred.odin b/tests/internal/test_asm_deferred.odin new file mode 100644 index 00000000000..650d33cd319 --- /dev/null +++ b/tests/internal/test_asm_deferred.odin @@ -0,0 +1,63 @@ +#+build amd64 +package test_internal + +import "core:testing" + +// Like a procedure body, the specification block and body of an `asm` template are checked after its signature: +// once all of the procedure body it is within has been checked, otherwise with the procedure bodies. +// Some of these did not compile when the body was checked along with the signature, and the rest cover +// each of the places a body is now checked from. + +// The body names a constant which needs the template's own signature, in either order +ASM_DEFERRED_K_BEFORE :: size_of(asm_deferred_k_before(0)) +asm_deferred_k_before :: asm(a: u64) -> (r: u64) [a -> r] { add r, ASM_DEFERRED_K_BEFORE } + +asm_deferred_k_after :: asm(a: u64) -> (r: u64) [a -> r] { add r, ASM_DEFERRED_K_AFTER } +ASM_DEFERRED_K_AFTER :: size_of(asm_deferred_k_after(0)) + +// An anonymous template outside of any procedure body +asm_deferred_global_var := asm(a: u64) -> (r: u64) [a -> r] { add r, ASM_DEFERRED_GLOBAL_K }(1) +ASM_DEFERRED_GLOBAL_K :: 6 + +// Names only from `test_asm_deferred_lazy.odin` and a type declared after it, in the specification block and body +asm_deferred_lazy_load :: asm(p: ^u32) -> (r: u32) [t: Asm_Deferred_Scratch] { mov t, [p]:Asm_Deferred_Lazy_U32; add t, ASM_DEFERRED_LAZY_K; mov r, t } +Asm_Deferred_Scratch :: distinct u32 + +ASM_DEFERRED_SHADOWED :: 1 + +@(test) +asm_deferred_global :: proc(t: ^testing.T) { + testing.expect_value(t, ASM_DEFERRED_K_BEFORE, 8) + testing.expect_value(t, ASM_DEFERRED_K_AFTER, 8) + testing.expect_value(t, asm_deferred_k_before(1), u64(9)) + testing.expect_value(t, asm_deferred_k_after(1), u64(9)) + testing.expect_value(t, asm_deferred_global_var, u64(7)) +} + +@(test) +asm_deferred_local :: proc(t: ^testing.T) { + // A local constant declared after the template naming it, which also shadows a global one + add_shadowed :: asm(a: u64) -> (r: u64) [a -> r] { add r, ASM_DEFERRED_SHADOWED } + ASM_DEFERRED_SHADOWED :: 2 + + anonymous := asm(a: u64) -> (r: u64) [a -> r] { add r, ANONYMOUS_LATER }(1) + ANONYMOUS_LATER :: 20 + + nested :: proc() -> u64 { + add_inner :: asm(a: u64) -> (r: u64) [a -> r] { add r, INNER_LATER } + INNER_LATER :: 100 + return add_inner(0) + } + + testing.expect_value(t, add_shadowed(1), u64(3)) + testing.expect_value(t, anonymous, u64(21)) + testing.expect_value(t, nested(), u64(100)) +} + +@(test) +asm_deferred_lazy :: proc(t: ^testing.T) { + x := u32(77) + testing.expect_value(t, asm_deferred_lazy_load(&x), u32(117)) + // A template of a lazy file is first checked from within a procedure body + testing.expect_value(t, asm_deferred_lazy_add(2), u64(42)) +} diff --git a/tests/internal/test_asm_deferred_lazy.odin b/tests/internal/test_asm_deferred_lazy.odin new file mode 100644 index 00000000000..15f1fae3a23 --- /dev/null +++ b/tests/internal/test_asm_deferred_lazy.odin @@ -0,0 +1,11 @@ +#+build amd64 +#+lazy +package test_internal + +// Only named from the `asm` templates of `test_asm_deferred.odin`, and so only checked when their bodies are + +ASM_DEFERRED_LAZY_K :: 40 + +Asm_Deferred_Lazy_U32 :: distinct u32 + +asm_deferred_lazy_add :: asm(a: u64) -> (r: u64) [a -> r] { add r, ASM_DEFERRED_LAZY_K } diff --git a/tests/internal/test_asm_target_features.odin b/tests/internal/test_asm_target_features.odin new file mode 100644 index 00000000000..5a5f50e4f43 --- /dev/null +++ b/tests/internal/test_asm_target_features.odin @@ -0,0 +1,25 @@ +#+build amd64 +package test_internal + +import "core:sys/info" +import "core:testing" + +// An `asm` template is inlined into the procedure calling it, so the target features it enables must also be enabled +// by that procedure, and its operands may then be those which only those features allow, e.g. 256-bit vectors with avx + +@(enable_target_feature="avx") +asm_target_features_add_ps :: asm(a, b: #simd[8]f32) -> (r: #simd[8]f32) { vaddps r, a, b } + +@(test) +asm_target_features_enabled_by_caller :: proc(t: ^testing.T) { + if .avx not_in info.cpu_features() { + return + } + asm_target_features_enabled_by_caller_avx(t) +} + +@(private="file", enable_target_feature="avx") +asm_target_features_enabled_by_caller_avx :: proc(t: ^testing.T) { + r := asm_target_features_add_ps({1, 2, 3, 4, 5, 6, 7, 8}, 0.5) + testing.expect_value(t, transmute([8]f32)r, [8]f32{1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5}) +} diff --git a/tests/issues/run.bat b/tests/issues/run.bat index 6c51429c471..d8c4b9f6fb1 100644 --- a/tests/issues/run.bat +++ b/tests/issues/run.bat @@ -164,6 +164,9 @@ fc /b pr_7848.cbor pr_7848_again.cbor > nul || exit /b @rem `asm` templates are amd64-only, assuming this is going to work on Windows (ARM?) %ODIN% doc "..\test_issue_asm_doc_category" 2>&1 | find /c "asm templates" | findstr /x "1" || exit /b +%ODIN% check "..\test_issue_asm_deferred_errors" %COMMON% 2>&1 | find /c "Unknown mnemonic" | findstr /x "5" || exit /b +%ODIN% check "..\test_issue_asm_target_features" %COMMON% 2>&1 | find /c "Error:" | findstr /x "3" || exit /b +%ODIN% check "..\test_issue_asm_target_features" %COMMON% 2>&1 | find /c "enables target feature 'avx'" | findstr /x "3" || exit /b %ODIN% test "..\test_lifetime_markers" %COMMON% -o:size -lifetime-markers || exit /b %ODIN% test "..\test_lifetime_markers" %COMMON% -o:speed -lifetime-markers || exit /b diff --git a/tests/issues/run.sh b/tests/issues/run.sh index e3832e1bf31..2d3de9f70f3 100755 --- a/tests/issues/run.sh +++ b/tests/issues/run.sh @@ -383,6 +383,19 @@ if [[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "amd64" ]]; then echo "SUCCESSFUL 0/1" exit 1 fi + if [[ $($ODIN check ../test_issue_asm_deferred_errors $COMMON_CHECK 2>&1 >/dev/null | grep -c "Unknown mnemonic") -eq 5 ]]; then + echo "SUCCESSFUL 1/1" + else + echo "SUCCESSFUL 0/1" + exit 1 + fi + if [[ $($ODIN check ../test_issue_asm_target_features $COMMON_CHECK 2>&1 >/dev/null | grep -c "Error:") -eq 3 && + $($ODIN check ../test_issue_asm_target_features $COMMON_CHECK 2>&1 >/dev/null | grep -c "enables target feature 'avx'") -eq 3 ]]; then + echo "SUCCESSFUL 1/1" + else + echo "SUCCESSFUL 0/1" + exit 1 + fi fi $ODIN test ../test_lifetime_markers $COMMON -o:size -lifetime-markers diff --git a/tests/issues/test_issue_asm_deferred_errors/test_issue_asm_deferred_errors.odin b/tests/issues/test_issue_asm_deferred_errors/test_issue_asm_deferred_errors.odin new file mode 100644 index 00000000000..41bfdd44c0f --- /dev/null +++ b/tests/issues/test_issue_asm_deferred_errors/test_issue_asm_deferred_errors.odin @@ -0,0 +1,25 @@ +#+build amd64 +// The specification block and body of an `asm` template are checked after its signature, like a procedure body, +// which must still report each of their errors: every `frobnicate` here is one "Unknown mnemonic" error +package test_issue_asm_deferred_errors + +// Never used +unused_asm :: asm(a: u64) -> (r: u64) [a -> r] { frobnicate r } + +// A duplicate overload, so the group makes its type invalid before its body is checked +dup_a :: asm(a, b: u64) -> (r: u64) [a -> r] { add r, b } +dup_b :: asm(a, b: u64) -> (r: u64) [a -> r] { add r, b; frobnicate r } +dup :: asm { dup_a, dup_b } + +// Not within any procedure body +global_var := asm(a: u64) -> (r: u64) [a -> r] { frobnicate r }(1) + +poly :: proc($N: u64) -> u64 { + return asm(a: u64) -> (r: u64) [a -> r] { add r, N; frobnicate r }(1) +} + +main :: proc() { + unused_local :: asm(a: u64) -> (r: u64) [a -> r] { frobnicate r } + _ = dup(u64(1), 2) + _ = poly(2) +} diff --git a/tests/issues/test_issue_asm_target_features/test_issue_asm_target_features.odin b/tests/issues/test_issue_asm_target_features/test_issue_asm_target_features.odin new file mode 100644 index 00000000000..42448398f74 --- /dev/null +++ b/tests/issues/test_issue_asm_target_features/test_issue_asm_target_features.odin @@ -0,0 +1,30 @@ +#+build amd64 +// An `asm` template is inlined into whatever calls it, so the target features it enables must be enabled there too: +// globally, or by the calling procedure. It enabling 'avx' lets it use 256-bit vectors, and each of the three calls +// which do not have 'avx' is one error, rather than an error from LLVM +package test_issue_asm_target_features + +@(enable_target_feature="avx") +add_ps :: asm(a, b: #simd[8]f32) -> (r: #simd[8]f32) { vaddps r, a, b } + +// At file scope +file_scope := add_ps(1, 2) + +// The caller does not enable 'avx' +not_enabled :: proc() -> #simd[8]f32 { + return add_ps(1, 2) +} + +@(enable_target_feature="avx") +enabled :: proc() -> #simd[8]f32 { + // A procedure literal does not take the features of the procedure it is within + literal := proc() -> #simd[8]f32 { + return add_ps(1, 2) + } + return add_ps(1, 2) + literal() +} + +main :: proc() { + _ = not_enabled() + _ = enabled() +}