diff --git a/internal/cbm/lsp/generated/ruby_stdlib_data.c b/internal/cbm/lsp/generated/ruby_stdlib_data.c index 83e853e65..f94ccd764 100644 --- a/internal/cbm/lsp/generated/ruby_stdlib_data.c +++ b/internal/cbm/lsp/generated/ruby_stdlib_data.c @@ -40,55 +40,59 @@ cbm_registry_add_type(reg, rt); \ } while (0) -/* Register an instance method on receiver type `recv_` returning `ret_`. */ -#define REG_METHOD(recv_, name_, ret_) \ - do { \ - memset(&rf, 0, sizeof(rf)); \ - rf.min_params = -1; \ - rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \ - rf.short_name = (name_); \ - rf.receiver_type = (recv_); \ - { \ - const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); \ - rets[0] = (ret_); \ - rets[1] = NULL; \ - rf.signature = cbm_type_func(arena, NULL, NULL, rets); \ - } \ - cbm_registry_add_func(reg, rf); \ +/* Signature `() -> ret`. Every seed allocation can fail (arena exhausted): + * a NULL return vector is left out, so the signature types as unknown (the + * same fallback ruby_register_method uses) instead of being written through. */ +static const CBMType *ruby_seed_signature(CBMArena *arena, const CBMType *ret) { + const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); + if (rets) { + rets[0] = ret; + rets[1] = NULL; + } + return cbm_type_func(arena, NULL, NULL, rets); +} + +/* Register an instance method on receiver type `recv_` returning `ret_`. + * An entry whose formatted QN came back NULL is not registered: a NULL QN + * would reach strcmp in finalize and in every later lookup. */ +#define REG_METHOD(recv_, name_, ret_) \ + do { \ + memset(&rf, 0, sizeof(rf)); \ + rf.min_params = -1; \ + rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \ + if (!rf.qualified_name) \ + break; \ + rf.short_name = (name_); \ + rf.receiver_type = (recv_); \ + rf.signature = ruby_seed_signature(arena, (ret_)); \ + cbm_registry_add_func(reg, rf); \ } while (0) /* Register a singleton (class-side) method: receiver key ".self" - * matches ruby_lookup_singleton_method's keying convention. */ -#define REG_SMETHOD(recv_, name_, ret_) \ - do { \ - memset(&rf, 0, sizeof(rf)); \ - rf.min_params = -1; \ - rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \ - rf.short_name = (name_); \ - rf.receiver_type = cbm_arena_sprintf(arena, "%s.self", (recv_)); \ - { \ - const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); \ - rets[0] = (ret_); \ - rets[1] = NULL; \ - rf.signature = cbm_type_func(arena, NULL, NULL, rets); \ - } \ - cbm_registry_add_func(reg, rf); \ + * matches ruby_lookup_singleton_method's keying convention. Skipped when + * either formatted name came back NULL (see REG_METHOD). */ +#define REG_SMETHOD(recv_, name_, ret_) \ + do { \ + memset(&rf, 0, sizeof(rf)); \ + rf.min_params = -1; \ + rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \ + rf.receiver_type = cbm_arena_sprintf(arena, "%s.self", (recv_)); \ + if (!rf.qualified_name || !rf.receiver_type) \ + break; \ + rf.short_name = (name_); \ + rf.signature = ruby_seed_signature(arena, (ret_)); \ + cbm_registry_add_func(reg, rf); \ } while (0) /* Register a global (Kernel) built-in function returning `ret_`. */ -#define REG_BUILTIN(name_, ret_) \ - do { \ - memset(&rf, 0, sizeof(rf)); \ - rf.min_params = -1; \ - rf.qualified_name = (name_); \ - rf.short_name = (name_); \ - { \ - const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets)); \ - rets[0] = (ret_); \ - rets[1] = NULL; \ - rf.signature = cbm_type_func(arena, NULL, NULL, rets); \ - } \ - cbm_registry_add_func(reg, rf); \ +#define REG_BUILTIN(name_, ret_) \ + do { \ + memset(&rf, 0, sizeof(rf)); \ + rf.min_params = -1; \ + rf.qualified_name = (name_); \ + rf.short_name = (name_); \ + rf.signature = ruby_seed_signature(arena, (ret_)); \ + cbm_registry_add_func(reg, rf); \ } while (0) void cbm_ruby_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena) { diff --git a/tests/test_ruby_lsp.c b/tests/test_ruby_lsp.c index 4aa00b9f8..c5499f78e 100644 --- a/tests/test_ruby_lsp.c +++ b/tests/test_ruby_lsp.c @@ -1498,6 +1498,32 @@ TEST(rubylsp_const_path_is_linear) { PASS(); } +/* Review ask (c): the stdlib seed must survive allocation failure. A + * zero-initialised arena has no blocks and no grow size, so every + * allocation from it returns NULL. The seed must not write through a NULL + * return-type vector, and must not register an entry whose formatted name + * came back NULL (a NULL QN would reach strcmp in finalize and in every later + * lookup). */ +TEST(rubylsp_stdlib_seed_survives_alloc_failure) { + CBMArena reg_arena; + cbm_arena_init(®_arena); + CBMTypeRegistry reg; + cbm_registry_init(®, ®_arena); + CBMArena dead; + memset(&dead, 0, sizeof(dead)); + cbm_ruby_stdlib_register(®, &dead); + ASSERT(reg.type_count > 0); + ASSERT(reg.func_count > 0); /* Kernel built-ins carry literal names */ + for (int i = 0; i < reg.func_count; i++) { + ASSERT(reg.funcs[i].qualified_name != NULL); + ASSERT(reg.funcs[i].short_name != NULL); + } + cbm_registry_finalize(®); + ASSERT(cbm_registry_lookup_func(®, "puts") != NULL); + cbm_arena_destroy(®_arena); + PASS(); +} + /* ── suite ──────────────────────────────────────────────────────── */ void suite_ruby_lsp(void) { @@ -1536,4 +1562,5 @@ void suite_ruby_lsp(void) { RUN_TEST(rubylsp_self_rebinding_blocks_no_edge); RUN_TEST(rubylsp_mixin_walk_visits_each_module_once); RUN_TEST(rubylsp_const_path_is_linear); + RUN_TEST(rubylsp_stdlib_seed_survives_alloc_failure); }