From 228f61397a936b6e02e94ee136390075ef20b0c0 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 14 Sep 2026 07:54:47 +0200 Subject: [PATCH 1/6] string.c: Further String#tr fixes and refactors * `tr_trans_pair` is now responsible for consuming the matches, akin to the ERB change in 4ca48a192c225b887ac2e5d6d30486bb6850887f. This makes it impossible to shift by 64 or more, hence removes a branch in `next_match`. Also ensure `s` and `matches_bitmap` members stays in sync. * Add missing `needles_count` check in `tr_trans_pairs_search_sse2`. Reported-By: Ashley Allen Reported-By: Asten Rooky --- string.c | 91 ++++++++++++++++++++++------------------ test/ruby/test_string.rb | 7 ++++ 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/string.c b/string.c index b3e18565820295..8aae42ce78f11b 100644 --- a/string.c +++ b/string.c @@ -9330,9 +9330,13 @@ tr_trans_pairs_search_basic(struct tr_trans_pairs_search *search) static inline VALUE tr_trans_pairs_next_match_sse2(struct tr_trans_pairs_search *search) { - size_t next_match_offset = ntz_int32(search->matches_bitmap); - search->matches_bitmap >>= (next_match_offset + 1); - search->s += next_match_offset; + RUBY_ASSERT(search->matches_bitmap > 0); + size_t trailing_zeros = (size_t)ntz_int32(search->matches_bitmap); + + RUBY_ASSERT(trailing_zeros < (sizeof(search->matches_bitmap) * CHAR_BIT)); + search->matches_bitmap >>= trailing_zeros; + search->s += trailing_zeros; + RUBY_ASSERT(search->s <= search->send); return search->trans_table[*search->s]; } @@ -9340,40 +9344,42 @@ tr_trans_pairs_next_match_sse2(struct tr_trans_pairs_search *search) static inline VALUE tr_trans_pairs_search_sse2(struct tr_trans_pairs_search *search) { - RBIMPL_ASSERT_OR_ASSUME(search->needles_count > 0); - RBIMPL_ASSERT_OR_ASSUME(search->needles_count < TR_TRANS_PAIRS_SIMD_MAX_NEEDLES); - - if (search->matches_bitmap) { - return tr_trans_pairs_next_match_sse2(search); - } + if (search->needles_count) { + RBIMPL_ASSERT_OR_ASSUME(search->needles_count > 0); + RBIMPL_ASSERT_OR_ASSUME(search->needles_count < TR_TRANS_PAIRS_SIMD_MAX_NEEDLES); - if ((size_t)(search->send - search->s) >= sizeof(__m128i)) { - int i; - __m128i masks[TR_TRANS_PAIRS_SIMD_MAX_NEEDLES]; - for (i = 0; i < search->needles_count; i++) { - masks[i] = _mm_set1_epi8(search->needles[i]); + if (search->matches_bitmap) { + return tr_trans_pairs_next_match_sse2(search); } - do { - const __m128i bytes = _mm_loadu_si128((__m128i const *)search->s); - - __m128i matches[TR_TRANS_PAIRS_SIMD_MAX_NEEDLES]; + if ((size_t)(search->send - search->s) >= sizeof(__m128i)) { + int i; + __m128i masks[TR_TRANS_PAIRS_SIMD_MAX_NEEDLES]; for (i = 0; i < search->needles_count; i++) { - matches[i] = _mm_cmpeq_epi8(bytes, masks[i]); + masks[i] = _mm_set1_epi8(search->needles[i]); } - for (i = 1; i < search->needles_count; i++) { - matches[0] = _mm_or_si128(matches[0], matches[i]); - } + do { + const __m128i bytes = _mm_loadu_si128((__m128i const *)search->s); - const int bitmap = _mm_movemask_epi8(matches[0]); + __m128i matches[TR_TRANS_PAIRS_SIMD_MAX_NEEDLES]; + for (i = 0; i < search->needles_count; i++) { + matches[i] = _mm_cmpeq_epi8(bytes, masks[i]); + } - if (bitmap) { - search->matches_bitmap = bitmap; - return tr_trans_pairs_next_match_sse2(search); - } - search->s += sizeof(__m128i); - } while ((size_t)(search->send - search->s) >= sizeof(__m128i)); + for (i = 1; i < search->needles_count; i++) { + matches[0] = _mm_or_si128(matches[0], matches[i]); + } + + const int bitmap = _mm_movemask_epi8(matches[0]); + + if (bitmap) { + search->matches_bitmap = bitmap; + return tr_trans_pairs_next_match_sse2(search); + } + search->s += sizeof(__m128i); + } while ((size_t)(search->send - search->s) >= sizeof(__m128i)); + } } return tr_trans_pairs_search_basic(search); } @@ -9385,17 +9391,13 @@ tr_trans_pairs_search_sse2(struct tr_trans_pairs_search *search) static inline VALUE tr_trans_pairs_next_match_neon(struct tr_trans_pairs_search *search) { - int trailing_zeros = ntz_int64(search->matches_bitmap); + RUBY_ASSERT(search->matches_bitmap > 0); + size_t trailing_zeros = (size_t)ntz_int64(search->matches_bitmap); // uint64_t >>= 64 would be undefined behaviour - if (trailing_zeros >= 63) { - search->matches_bitmap = 0; - search->s += 15; - } - else { - search->matches_bitmap >>= (trailing_zeros + 1); - search->s += trailing_zeros / 4; - } + RUBY_ASSERT(trailing_zeros < (sizeof(search->matches_bitmap) * CHAR_BIT)); + search->matches_bitmap >>= trailing_zeros; + search->s += trailing_zeros / 4; RUBY_ASSERT(search->s <= search->send); return search->trans_table[*search->s]; @@ -9452,6 +9454,15 @@ tr_trans_pairs_search_neon(struct tr_trans_pairs_search *search) #define tr_trans_pairs_search_impl tr_trans_pairs_search_basic #endif +static inline void +tr_trans_pairs_consume_match(struct tr_trans_pairs_search *search) +{ + search->s++; +#ifdef HAVE_SIMD + search->matches_bitmap >>= 1; +#endif +} + static VALUE tr_trans_pairs(VALUE str, VALUE pairs_val) { @@ -9530,7 +9541,7 @@ tr_trans_pairs(VALUE str, VALUE pairs_val) clen = rb_enc_codelen(c, e1); repl = rb_hash_lookup2(hash, UINT2NUM(c), 0); if (!repl) { - search.s += clen; + tr_trans_pairs_consume_match(&search); continue; } } @@ -9543,7 +9554,7 @@ tr_trans_pairs(VALUE str, VALUE pairs_val) } tr_buffer_append_str(&buffer, repl); checkpoint = search.s + clen; - search.s++; + tr_trans_pairs_consume_match(&search); if (cr == ENC_CODERANGE_7BIT && rb_enc_str_coderange(repl) != ENC_CODERANGE_7BIT) { cr = ENC_CODERANGE_VALID; diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index 3280bf83df7579..bb92a2b85aed6e 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -2690,6 +2690,13 @@ def test_tr_hash assert_equal(S("01@3456789abcdefgHij"), S("0123456789abcdefghij").tr("h" => "H", "2" => "@")) assert_equal(S("UL" * 16 ), S("\u2028<" * 16).tr("\u2028" => "U", "<" => "L")) + assert_equal(S("U\u2029" * 16 ), S("\u2028\u2029" * 16).tr("\u2028" => "U")) + + # Many keys + expected = S(("a".."z").to_a.join) + replacements = ("A".."Z").to_h { |k| [k, k.downcase] } + actual = S(("A".."Z").to_a.join.tr(replacements)) + assert_equal(expected, actual) end def test_tr! From f30fdb01f363d1ea96f033a9803bad943976bbed Mon Sep 17 00:00:00 2001 From: git Date: Mon, 14 Sep 2026 07:07:43 +0000 Subject: [PATCH 2/6] Update bundled gems list as of 2026-09-14 --- NEWS.md | 5 +++-- gems/bundled_gems | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 6741268187b100..f4d263bbcb3764 100644 --- a/NEWS.md +++ b/NEWS.md @@ -231,12 +231,12 @@ They are still available on rubygems.org and can be installed with * rss 0.3.3 * 0.3.2 to [0.3.3][rss-0.3.3] * net-imap 0.6.7 - * 0.6.2 to [v0.6.3][net-imap-v0.6.3], [v0.6.4][net-imap-v0.6.4], [v0.6.4.1][net-imap-v0.6.4.1], [v0.6.5][net-imap-v0.6.5], [v0.6.6][net-imap-v0.6.6] + * 0.6.2 to [v0.6.3][net-imap-v0.6.3], [v0.6.4][net-imap-v0.6.4], [v0.6.4.1][net-imap-v0.6.4.1], [v0.6.5][net-imap-v0.6.5], [v0.6.6][net-imap-v0.6.6], [v0.6.7][net-imap-v0.6.7] * rbs 4.2.0 * 3.10.0 to [v3.10.1][rbs-v3.10.1], [v3.10.2][rbs-v3.10.2], [v3.10.3][rbs-v3.10.3], [v3.10.4][rbs-v3.10.4], [v4.0.0.dev.1][rbs-v4.0.0.dev.1], [v4.0.0.dev.2][rbs-v4.0.0.dev.2], [v4.0.0.dev.3][rbs-v4.0.0.dev.3], [v4.0.0.dev.4][rbs-v4.0.0.dev.4], [v4.0.0.dev.5][rbs-v4.0.0.dev.5], [v4.0.0][rbs-v4.0.0], [v4.0.1.dev.1][rbs-v4.0.1.dev.1], [v4.0.1.dev.2][rbs-v4.0.1.dev.2], [v4.0.1][rbs-v4.0.1], [v4.0.2][rbs-v4.0.2], [v4.0.3][rbs-v4.0.3], [v4.1.0.pre.1][rbs-v4.1.0.pre.1], [v4.1.0.pre.2][rbs-v4.1.0.pre.2], [v4.1.0][rbs-v4.1.0], [v4.1.1.pre.1][rbs-v4.1.1.pre.1], [v4.1.1][rbs-v4.1.1], [v4.1.2][rbs-v4.1.2], [v4.1.3][rbs-v4.1.3], [v4.2.0.pre.1][rbs-v4.2.0.pre.1], [v4.2.0][rbs-v4.2.0] * typeprof 0.33.1 * mutex_m 0.3.0 -* bigdecimal 4.1.2 +* bigdecimal 4.1.3 * 4.0.1 to [v4.1.0][bigdecimal-v4.1.0], [v4.1.1][bigdecimal-v4.1.1], [v4.1.2][bigdecimal-v4.1.2] * resolv-replace 0.2.0 * 0.1.1 to [v0.2.0][resolv-replace-v0.2.0] @@ -579,6 +579,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [net-imap-v0.6.4.1]: https://github.com/ruby/net-imap/releases/tag/v0.6.4.1 [net-imap-v0.6.5]: https://github.com/ruby/net-imap/releases/tag/v0.6.5 [net-imap-v0.6.6]: https://github.com/ruby/net-imap/releases/tag/v0.6.6 +[net-imap-v0.6.7]: https://github.com/ruby/net-imap/releases/tag/v0.6.7 [rbs-v3.10.1]: https://github.com/ruby/rbs/releases/tag/v3.10.1 [rbs-v3.10.2]: https://github.com/ruby/rbs/releases/tag/v3.10.2 [rbs-v3.10.3]: https://github.com/ruby/rbs/releases/tag/v3.10.3 diff --git a/gems/bundled_gems b/gems/bundled_gems index 7079bff6ff273f..9920300870c9b6 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -23,7 +23,7 @@ racc 1.8.1 https://github.com/ruby/racc mutex_m 0.3.0 https://github.com/ruby/mutex_m getoptlong 0.2.1 https://github.com/ruby/getoptlong base64 0.3.0 https://github.com/ruby/base64 -bigdecimal 4.1.2 https://github.com/ruby/bigdecimal +bigdecimal 4.1.3 https://github.com/ruby/bigdecimal observer 0.1.2 https://github.com/ruby/observer abbrev 0.1.2 https://github.com/ruby/abbrev resolv-replace 0.2.0 https://github.com/ruby/resolv-replace From e96cea4e03ddd35f176a0e7e4b95f209bc958ee5 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 14 Sep 2026 19:27:52 +1200 Subject: [PATCH 3/6] coroutine: add native behavior tests. (#18817) --- .gitignore | 1 + common.mk | 43 ++++++- coroutine/test/main.c | 31 +++++ coroutine/test/stack.c | 49 ++++++++ coroutine/test/stack.h | 11 ++ coroutine/test/test_initialize_destroy.c | 41 +++++++ coroutine/test/test_initialize_destroy.h | 3 + coroutine/test/test_transfer_repeat.c | 141 ++++++++++++++++++++++ coroutine/test/test_transfer_repeat.h | 3 + coroutine/test/test_transfer_return.c | 142 +++++++++++++++++++++++ coroutine/test/test_transfer_return.h | 3 + depend | 5 + template/Makefile.in | 2 + thread_sched.h | 4 +- win32/Makefile.sub | 2 + 15 files changed, 477 insertions(+), 4 deletions(-) create mode 100644 coroutine/test/main.c create mode 100644 coroutine/test/stack.c create mode 100644 coroutine/test/stack.h create mode 100644 coroutine/test/test_initialize_destroy.c create mode 100644 coroutine/test/test_initialize_destroy.h create mode 100644 coroutine/test/test_transfer_repeat.c create mode 100644 coroutine/test/test_transfer_repeat.h create mode 100644 coroutine/test/test_transfer_return.c create mode 100644 coroutine/test/test_transfer_return.h diff --git a/.gitignore b/.gitignore index 1ea0a323d92558..5706c86ae3fa50 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ lcov*.info /*-fake.rb /*.dll /*.exe +/coroutine-test /*.ilk /*.res /*.pc diff --git a/common.mk b/common.mk index e6559c6ca41160..c6c767177501f2 100644 --- a/common.mk +++ b/common.mk @@ -94,6 +94,13 @@ MAKE_ENC = -f $(ENC_MK) V="$(V)" UNICODE_HDR_DIR="$(UNICODE_HDR_DIR)" \ PRISM_BUILD_DIR = prism +COROUTINE_TEST = coroutine-test$(EXEEXT) +COROUTINE_TEST_OBJS = coroutine-main.$(OBJEXT) \ + coroutine-stack.$(OBJEXT) \ + coroutine-test_initialize_destroy.$(OBJEXT) \ + coroutine-test_transfer_repeat.$(OBJEXT) \ + coroutine-test_transfer_return.$(OBJEXT) + LIBPRISM_OBJS = \ prism/arena.$(OBJEXT) \ prism/buffer.$(OBJEXT) \ @@ -691,6 +698,7 @@ noarch_config_h = tmp/include/noarch/ruby/config.h clean: clean-ext clean-enc clean-golf clean-docs clean-extout clean-modular-gc clean-local clean-platform clean-spec clean-local:: clean-runnable $(Q)$(RM) $(ALLOBJS) $(LIBRUBY_A) $(LIBRUBY_SO) $(LIBRUBY) $(LIBRUBY_ALIASES) + $(Q)$(RM) $(COROUTINE_TEST) $(COROUTINE_TEST_OBJS) $(Q)$(RM) $(PROGRAM) $(WPROGRAM) miniruby$(EXEEXT) dmyext.$(OBJEXT) dmyenc.$(OBJEXT) $(ARCHFILE) .*.time $(Q)$(RM) y.tab.c y.output encdb.h transdb.h config.log rbconfig.rb $(ruby_pc) $(COROUTINE_H:/Context.h=/.time) $(Q)$(RM) probes.h probes.$(OBJEXT) probes.stamp ruby-glommed.$(OBJEXT) ruby.imp ChangeLog $(STATIC_RUBY)$(EXEEXT) @@ -931,8 +939,15 @@ yes-test-tool: prog PHONY $(ACTIONS_ENDGROUP) no-test-tool: PHONY +test-coroutine: $(TEST_RUNNABLE)-test-coroutine +yes-test-coroutine: $(COROUTINE_TEST) PHONY + $(ACTIONS_GROUP) + $(Q)$(exec) $(COROUTINE_TEST_RUN) + $(ACTIONS_ENDGROUP) +no-test-coroutine: PHONY + test-sample: test-basic # backward compatibility for mswin-build -test-short: btest-ruby $(DOT_WAIT) test-knownbug $(DOT_WAIT) test-basic +test-short: test-coroutine $(DOT_WAIT) btest-ruby $(DOT_WAIT) test-knownbug $(DOT_WAIT) test-basic test: test-short # Separate to skip updating encs and exts by `make -o test-precheck` @@ -1043,7 +1058,7 @@ $(ENC_MK): $(srcdir)/enc/make_encmake.rb $(srcdir)/enc/Makefile.in $(srcdir)/enc .PHONY: distclean-srcs distclean-srcs-local distclean-srcs-ext .PHONY: realclean realclean-ext realclean-local realclean-enc realclean-golf realclean-extout .PHONY: realclean-srcs realclean-srcs-local realclean-srcs-ext -.PHONY: exam check test test-short test-all btest btest-ruby test-basic test-knownbug +.PHONY: exam check test test-short test-all test-coroutine btest btest-ruby test-basic test-knownbug .PHONY: run runruby parse benchmark gdb gdb-ruby .PHONY: update-mspec update-rubyspec test-rubyspec test-spec .PHONY: touch-unicode-files @@ -1137,6 +1152,30 @@ $(COROUTINE_H:/Context.h=/.time): $(Q) $(MAKEDIRS) $(@D) @$(NULLCMD) > $@ +$(COROUTINE_TEST): $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ) + $(ECHO) linking $@ + $(Q) $(COROUTINE_TEST_LINK) + +coroutine-main.$(OBJEXT): {$(VPATH)}coroutine/test/main.c + $(ECHO) compiling $(srcdir)/coroutine/test/main.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/main.c + +coroutine-stack.$(OBJEXT): {$(VPATH)}coroutine/test/stack.c + $(ECHO) compiling $(srcdir)/coroutine/test/stack.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/stack.c + +coroutine-test_initialize_destroy.$(OBJEXT): {$(VPATH)}coroutine/test/test_initialize_destroy.c + $(ECHO) compiling $(srcdir)/coroutine/test/test_initialize_destroy.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_initialize_destroy.c + +coroutine-test_transfer_repeat.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_repeat.c + $(ECHO) compiling $(srcdir)/coroutine/test/test_transfer_repeat.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_transfer_repeat.c + +coroutine-test_transfer_return.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_return.c + $(ECHO) compiling $(srcdir)/coroutine/test/test_transfer_return.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_transfer_return.c + ### # dependencies for generated C sources. diff --git a/coroutine/test/main.c b/coroutine/test/main.c new file mode 100644 index 00000000000000..7340ab7e736c97 --- /dev/null +++ b/coroutine/test/main.c @@ -0,0 +1,31 @@ +#include "test_initialize_destroy.h" +#include "test_transfer_repeat.h" +#include "test_transfer_return.h" + +#include + +static int +run_tests(void) +{ + int result = EXIT_SUCCESS; + + if (test_initialize_destroy() != EXIT_SUCCESS) result = EXIT_FAILURE; + if (test_transfer_repeat() != EXIT_SUCCESS) result = EXIT_FAILURE; + if (test_transfer_return() != EXIT_SUCCESS) result = EXIT_FAILURE; + + return result; +} + +int +main(void) +{ + return run_tests(); +} + +#if defined(_WIN32) +int +wmain(void) +{ + return run_tests(); +} +#endif diff --git a/coroutine/test/stack.c b/coroutine/test/stack.c new file mode 100644 index 00000000000000..0bc987996548e6 --- /dev/null +++ b/coroutine/test/stack.c @@ -0,0 +1,49 @@ +#include "ruby/internal/config.h" + +#include "stack.h" + +#include + +#if defined(_WIN32) +# include +#elif defined(HAVE_SYS_MMAN_H) +# include +#endif + +#if !defined(MAP_ANONYMOUS) && defined(MAP_ANON) +# define MAP_ANONYMOUS MAP_ANON +#endif + +int +coroutine_stack_allocate(struct coroutine_stack *stack, size_t size) +{ +#if defined(_WIN32) + stack->base = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +#elif defined(HAVE_MMAP) && defined(MAP_ANONYMOUS) + stack->base = mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (stack->base == MAP_FAILED) stack->base = NULL; +#else + stack->base = malloc(size); +#endif + + stack->size = stack->base == NULL ? 0 : size; + return stack->base == NULL ? -1 : 0; +} + +void +coroutine_stack_free(struct coroutine_stack *stack) +{ + if (stack->base == NULL) return; + +#if defined(_WIN32) + VirtualFree(stack->base, 0, MEM_RELEASE); +#elif defined(HAVE_MMAP) && defined(MAP_ANONYMOUS) + munmap(stack->base, stack->size); +#else + free(stack->base); +#endif + + stack->base = NULL; + stack->size = 0; +} diff --git a/coroutine/test/stack.h b/coroutine/test/stack.h new file mode 100644 index 00000000000000..1ec63730e7534a --- /dev/null +++ b/coroutine/test/stack.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +struct coroutine_stack { + void *base; + size_t size; +}; + +int coroutine_stack_allocate(struct coroutine_stack *stack, size_t size); +void coroutine_stack_free(struct coroutine_stack *stack); diff --git a/coroutine/test/test_initialize_destroy.c b/coroutine/test/test_initialize_destroy.c new file mode 100644 index 00000000000000..2762ae70c06c8c --- /dev/null +++ b/coroutine/test/test_initialize_destroy.c @@ -0,0 +1,41 @@ +#include "ruby/internal/config.h" + +#include COROUTINE_H + +#include "stack.h" +#include "test_initialize_destroy.h" + +#include +#include + +#define STACK_SIZE (1024 * 1024) + +static COROUTINE +never_started(struct coroutine_context *from, struct coroutine_context *self) +{ + (void)from; + (void)self; + abort(); +} + +int +test_initialize_destroy(void) +{ + struct coroutine_stack stack = {0}; + struct coroutine_context main_context; + struct coroutine_context context; + + if (coroutine_stack_allocate(&stack, STACK_SIZE) != 0) { + fprintf(stderr, "failed to allocate coroutine stack\n"); + return EXIT_FAILURE; + } + + coroutine_initialize_main(&main_context); + coroutine_initialize(&context, never_started, stack.base, stack.size); + + coroutine_destroy(&context); + coroutine_destroy(&main_context); + coroutine_stack_free(&stack); + + return EXIT_SUCCESS; +} diff --git a/coroutine/test/test_initialize_destroy.h b/coroutine/test/test_initialize_destroy.h new file mode 100644 index 00000000000000..5bb4a2a5671160 --- /dev/null +++ b/coroutine/test/test_initialize_destroy.h @@ -0,0 +1,3 @@ +#pragma once + +int test_initialize_destroy(void); diff --git a/coroutine/test/test_transfer_repeat.c b/coroutine/test/test_transfer_repeat.c new file mode 100644 index 00000000000000..50a6f971116c52 --- /dev/null +++ b/coroutine/test/test_transfer_repeat.c @@ -0,0 +1,141 @@ +#include "ruby/internal/config.h" + +#include COROUTINE_H + +#include "stack.h" +#include "test_transfer_repeat.h" + +#include +#include +#include + +#define STACK_SIZE (1024 * 1024) +#define TRANSFER_COUNT 1000 + +static struct coroutine_context main_context; +static struct coroutine_context worker_context; +static unsigned int yielded_iteration; +static uint32_t yielded_state; +static int completed; + +static void +check_context(const char *message, struct coroutine_context *actual, + struct coroutine_context *expected) +{ + if (actual != expected) { + fprintf(stderr, "%s: expected %p, got %p\n", + message, (void *)expected, (void *)actual); + abort(); + } +} + +static void +context_start(struct coroutine_context *from, struct coroutine_context *self) +{ +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_finish_switch_fiber(self->fake_stack, + (const void **)&from->stack_base, + &from->stack_size); +#else + (void)from; + (void)self; +#endif +} + +static struct coroutine_context * +transfer(struct coroutine_context *current, struct coroutine_context *target) +{ +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_start_switch_fiber(¤t->fake_stack, + target->stack_base, target->stack_size); +#endif + +#if defined(COROUTINE_SANITIZE_THREAD) + __tsan_switch_to_fiber(target->tsan_fiber, 0); +#endif + + struct coroutine_context *from = coroutine_transfer(current, target); + +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_finish_switch_fiber(current->fake_stack, NULL, NULL); +#endif + + return from; +} + +static COROUTINE +worker_entry(struct coroutine_context *from, struct coroutine_context *self) +{ + context_start(from, self); + + check_context("worker context was entered by", from, &main_context); + check_context("worker context received self", self, &worker_context); + + uint32_t state = UINT32_C(0x12345678); + + for (unsigned int iteration = 1; iteration <= TRANSFER_COUNT; iteration++) { + state = state * UINT32_C(1664525) + UINT32_C(1013904223); + yielded_iteration = iteration; + yielded_state = state; + + from = transfer(self, &main_context); + check_context("worker context was resumed by", from, &main_context); + + if (yielded_iteration != iteration || yielded_state != state) { + fprintf(stderr, "worker local state was not preserved\n"); + abort(); + } + } + + completed = 1; + transfer(self, &main_context); + abort(); +} + +int +test_transfer_repeat(void) +{ + struct coroutine_stack stack = {0}; + int result = EXIT_FAILURE; + + if (coroutine_stack_allocate(&stack, STACK_SIZE) != 0) { + fprintf(stderr, "failed to allocate coroutine stack\n"); + return EXIT_FAILURE; + } + + yielded_iteration = 0; + yielded_state = 0; + completed = 0; + + coroutine_initialize_main(&main_context); + coroutine_initialize(&worker_context, worker_entry, stack.base, stack.size); + + uint32_t expected_state = UINT32_C(0x12345678); + + for (unsigned int iteration = 1; iteration <= TRANSFER_COUNT; iteration++) { + expected_state = expected_state * UINT32_C(1664525) + UINT32_C(1013904223); + + struct coroutine_context *from = transfer(&main_context, &worker_context); + + if (from != &worker_context || yielded_iteration != iteration || + yielded_state != expected_state || completed) { + fprintf(stderr, "repeated coroutine transfer failed at iteration %u\n", + iteration); + goto finish; + } + } + + if (transfer(&main_context, &worker_context) != &worker_context || !completed) { + fprintf(stderr, "worker context did not complete repeated transfers\n"); + goto finish; + } + + result = EXIT_SUCCESS; + +finish: + coroutine_destroy(&worker_context); + coroutine_destroy(&main_context); + coroutine_stack_free(&stack); + + return result; +} diff --git a/coroutine/test/test_transfer_repeat.h b/coroutine/test/test_transfer_repeat.h new file mode 100644 index 00000000000000..56c00799378139 --- /dev/null +++ b/coroutine/test/test_transfer_repeat.h @@ -0,0 +1,3 @@ +#pragma once + +int test_transfer_repeat(void); diff --git a/coroutine/test/test_transfer_return.c b/coroutine/test/test_transfer_return.c new file mode 100644 index 00000000000000..657e00387decfd --- /dev/null +++ b/coroutine/test/test_transfer_return.c @@ -0,0 +1,142 @@ +#include "ruby/internal/config.h" + +#include COROUTINE_H + +#include "stack.h" +#include "test_transfer_return.h" + +#include +#include + +#define STACK_SIZE (1024 * 1024) + +static struct coroutine_context main_context; +static struct coroutine_context first_context; +static struct coroutine_context second_context; + +static void +check_context(const char *message, struct coroutine_context *actual, + struct coroutine_context *expected) +{ + if (actual != expected) { + fprintf(stderr, "%s: expected %p, got %p\n", + message, (void *)expected, (void *)actual); + abort(); + } +} + +static void +context_start(struct coroutine_context *from, struct coroutine_context *self) +{ +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_finish_switch_fiber(self->fake_stack, + (const void **)&from->stack_base, + &from->stack_size); +#else + (void)from; + (void)self; +#endif +} + +static struct coroutine_context * +transfer(struct coroutine_context *current, struct coroutine_context *target) +{ +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_start_switch_fiber(¤t->fake_stack, + target->stack_base, target->stack_size); +#endif + +#if defined(COROUTINE_SANITIZE_THREAD) + __tsan_switch_to_fiber(target->tsan_fiber, 0); +#endif + + struct coroutine_context *from = coroutine_transfer(current, target); + +#if defined(COROUTINE_SANITIZE_ADDRESS) + __sanitizer_finish_switch_fiber(current->fake_stack, NULL, NULL); +#endif + + return from; +} + +static COROUTINE +second_entry(struct coroutine_context *from, struct coroutine_context *self) +{ + context_start(from, self); + + check_context("second context was entered by", from, &first_context); + check_context("second context received self", self, &second_context); + + from = transfer(self, &main_context); + check_context("second context was resumed by", from, &main_context); + + transfer(self, &first_context); + abort(); +} + +static COROUTINE +first_entry(struct coroutine_context *from, struct coroutine_context *self) +{ + context_start(from, self); + + check_context("first context was entered by", from, &main_context); + check_context("first context received self", self, &first_context); + + from = transfer(self, &second_context); + check_context("first context was resumed by", from, &second_context); + + transfer(self, &main_context); + abort(); +} + +int +test_transfer_return(void) +{ + struct coroutine_stack first_stack = {0}; + struct coroutine_stack second_stack = {0}; + int result = EXIT_FAILURE; + + if (coroutine_stack_allocate(&first_stack, STACK_SIZE) != 0 || + coroutine_stack_allocate(&second_stack, STACK_SIZE) != 0) { + fprintf(stderr, "failed to allocate coroutine stacks\n"); + goto finish; + } + + coroutine_initialize_main(&main_context); + coroutine_initialize(&first_context, first_entry, + first_stack.base, first_stack.size); + coroutine_initialize(&second_context, second_entry, + second_stack.base, second_stack.size); + + result = EXIT_SUCCESS; + + /* The original target is first_context, but second_context resumes us. */ + struct coroutine_context *from = transfer(&main_context, &first_context); + + if (from != &second_context) { + fprintf(stderr, + "coroutine_transfer returned %p, expected actual resumer %p\n", + (void *)from, (void *)&second_context); + result = EXIT_FAILURE; + } + + /* Resume second_context, which resumes first_context, which resumes us. */ + from = transfer(&main_context, &second_context); + + if (from != &first_context) { + fprintf(stderr, + "coroutine_transfer returned %p, expected actual resumer %p\n", + (void *)from, (void *)&first_context); + result = EXIT_FAILURE; + } + + coroutine_destroy(&first_context); + coroutine_destroy(&second_context); + coroutine_destroy(&main_context); + +finish: + coroutine_stack_free(&first_stack); + coroutine_stack_free(&second_stack); + + return result; +} diff --git a/coroutine/test/test_transfer_return.h b/coroutine/test/test_transfer_return.h new file mode 100644 index 00000000000000..228d611cde3447 --- /dev/null +++ b/coroutine/test/test_transfer_return.h @@ -0,0 +1,3 @@ +#pragma once + +int test_transfer_return(void); diff --git a/depend b/depend index a18eebe3fa96b1..4099bd57bacdb7 100644 --- a/depend +++ b/depend @@ -55,6 +55,11 @@ compile.$(OBJEXT): {$(VPATH)}compile.c complex.$(OBJEXT): {$(VPATH)}complex.c concurrent_set.$(OBJEXT): {$(VPATH)}concurrent_set.c cont.$(OBJEXT): {$(VPATH)}cont.c +coroutine-main.$(OBJEXT): {$(VPATH)}coroutine/test/main.c +coroutine-stack.$(OBJEXT): {$(VPATH)}coroutine/test/stack.c +coroutine-test_initialize_destroy.$(OBJEXT): {$(VPATH)}coroutine/test/test_initialize_destroy.c +coroutine-test_transfer_repeat.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_repeat.c +coroutine-test_transfer_return.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_return.c debug.$(OBJEXT): {$(VPATH)}debug.c debug_counter.$(OBJEXT): {$(VPATH)}debug_counter.c dir.$(OBJEXT): {$(VPATH)}dir.c diff --git a/template/Makefile.in b/template/Makefile.in index a519dfb858c6ea..68e53e379b4445 100644 --- a/template/Makefile.in +++ b/template/Makefile.in @@ -185,6 +185,8 @@ BOOTSTRAPRUBY_FAKE = $(yes_baseruby:yes=$(arch)-fake.rb) COROUTINE_H = @X_COROUTINE_H@ COROUTINE_OBJ = $(COROUTINE_H:.h=.$(OBJEXT)) COROUTINE_SRC = @X_COROUTINE_SRC@ +COROUTINE_TEST_LINK = $(CC) $(EXE_LDFLAGS) $(XLDFLAGS) $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ) $(MAINLIBS) $(OUTFLAG)$(COROUTINE_TEST) +COROUTINE_TEST_RUN = ./$(COROUTINE_TEST) #### End of system configuration section. #### diff --git a/thread_sched.h b/thread_sched.h index e56f5d7dd19c32..2b38f08a71a009 100644 --- a/thread_sched.h +++ b/thread_sched.h @@ -161,8 +161,8 @@ struct rb_native_thread { bool retiring; // A terminating coroutine records its context here before its final - // transfer; this nt's loop reclaims it. (Not via coroutine_transfer()'s - // return value: its meaning differs between the amd64 asm and ucontext.) + // transfer; this nt's loop reclaims it. coroutine_transfer() cannot be + // used because a terminating coroutine never resumes to return a value. struct coroutine_context *dead_co; }; diff --git a/win32/Makefile.sub b/win32/Makefile.sub index ccc3e918f509e5..897964ab02827d 100644 --- a/win32/Makefile.sub +++ b/win32/Makefile.sub @@ -344,6 +344,8 @@ COROUTINE_SRC = $(COROUTINE_OBJ:.obj=.asm) !error copy coroutine has been replaced with pthread implementation at 42130a64f02294dc8025af3a51bda518c67ab33d !endif COROUTINE_H = $(COROUTINE_OBJ:.obj=.h) +COROUTINE_TEST_LINK = $(CC) $(COROUTINE_TEST_OBJS) $(COROUTINE_OBJ) $(MAINLIBS) $(OUTFLAG)$(COROUTINE_TEST) -link $(LDFLAGS) $(XLDFLAGS) +COROUTINE_TEST_RUN = .\$(COROUTINE_TEST) ARFLAGS = -machine:$(MACHINE) -out: LD = $(CC) From 03a426ec8d30929558bf1244b4ad95eb7e1f54d2 Mon Sep 17 00:00:00 2001 From: HASUMI Hitoshi Date: Mon, 14 Sep 2026 10:30:15 +0200 Subject: [PATCH 4/6] [Feature #22279] Add region arguments to String bit operations (#18562) This patch extends the following String methods: * String#bit_set(offset, length, lsb_first: true) -> self * String#bit_set(range, lsb_first: true) -> self * String#bit_clear(offset, length, lsb_first: true) -> self * String#bit_clear(range, lsb_first: true) -> self * String#bit_flip(offset, length, lsb_first: true) -> self * String#bit_flip(range, lsb_first: true) -> self * String#bit_count(offset, length, lsb_first: true) -> Integer * String#bit_count(range, lsb_first: true) -> Integer Link: [Feature #22279] --- doc/string/bit_clear.rdoc | 24 +- doc/string/bit_count.rdoc | 34 ++- doc/string/bit_flip.rdoc | 24 +- doc/string/bit_set.rdoc | 22 +- spec/ruby/core/string/bit_clear_spec.rb | 21 ++ spec/ruby/core/string/bit_count_spec.rb | 38 ++- spec/ruby/core/string/bit_flip_spec.rb | 21 ++ spec/ruby/core/string/bit_set_spec.rb | 21 ++ string.c | 333 ++++++++++++++++++++++-- test/ruby/test_string.rb | 185 ++++++++++++- 10 files changed, 691 insertions(+), 32 deletions(-) diff --git a/doc/string/bit_clear.rdoc b/doc/string/bit_clear.rdoc index 737577258c2121..2be34366b47523 100644 --- a/doc/string/bit_clear.rdoc +++ b/doc/string/bit_clear.rdoc @@ -4,13 +4,31 @@ Sets the bit at zero-based bit +offset+ to 0; returns +self+: s.bit_clear(1) # => "\xFD" s # => "\xFD" +With +length+, clears the +length+ consecutive bits beginning at +offset+; +with a Range, clears the bits covered by +range+: + + s = "\xFF\xFF" + s.bit_clear(4, 8) # => "\x0F\xF0" + s = "\xFF\xFF" + s.bit_clear(4..11) # => "\x0F\xF0" + +The entire region must lie within +self+; a region that extends beyond the +end raises +IndexError+ rather than being clamped. Writing zero bits is a +no-op, but even an empty region raises +IndexError+ when it begins beyond +the end of +self+. + By default, bits within each byte are numbered from least-significant to most-significant. If +lsb_first+ is +false+, byte order is unchanged but bits within each byte are numbered from most-significant to least-significant: s = "\xFF" - s.bit_clear(1, lsb_first: false) # => "\xBF" + s.bit_clear(1, lsb_first: false) # => "\xBF" + s = "\xFF\xFF" + s.bit_clear(4, 8, lsb_first: false) # => "\xF0\x0F" -Raises +IndexError+ if +offset+ is out of range. -Raises +ArgumentError+ if +offset+ is too large to be represented. +Raises +IndexError+ if +offset+ is out of range, or if any part of the +region lies outside +self+. +Raises +ArgumentError+ if +length+ is negative. +Raises +ArgumentError+ if a bit position is too large to be represented. Raises +ArgumentError+ if +lsb_first+ is neither +true+ nor +false+. +Raises +FrozenError+ if +self+ is frozen, even when writing zero bits. diff --git a/doc/string/bit_count.rdoc b/doc/string/bit_count.rdoc index 021cf37101e6ae..4f44d9b7e5d76a 100644 --- a/doc/string/bit_count.rdoc +++ b/doc/string/bit_count.rdoc @@ -1,8 +1,38 @@ -Returns the number of set bits in +self+: +Returns the number of set bits in +self+. + +With no arguments, counts over all bytes of +self+: "\x00".bit_count # => 0 "\xFF".bit_count # => 8 "\xAA".bit_count # => 4 +With +offset+ and +length+, counts only the +length+ bits beginning at +zero-based bit +offset+; with a Range, counts the bits covered by +range+: + + data = "\xFF\x00\xF0" + data.bit_count(0, 8) # => 8 + data.bit_count(8, 8) # => 0 + data.bit_count(0..7) # => 8 + data.bit_count(8...16) # => 0 + data.bit_count(16..) # => 4 + +A region that extends beyond the end of +self+ is clamped to the bits that +exist; a region that begins at or beyond the end counts zero: + + data.bit_count(16, 100) # => 4 + data.bit_count(100, 8) # => 0 + +By default, bits within each byte are numbered from least-significant to +most-significant. If +lsb_first+ is +false+, byte order is unchanged but bits +within each byte are numbered from most-significant to least-significant. +The numbering matters only when the region is not byte-aligned; the +no-argument form is independent of it: + + "\xF0".bit_count(0, 4) # => 0 + "\xF0".bit_count(0, 4, lsb_first: false) # => 4 + The count is over the bytes of +self+ and is independent of string encoding. -Raises +ArgumentError+ if any argument is given. +Raises +IndexError+ if +offset+ or a Range endpoint is negative. +Raises +ArgumentError+ if +length+ is negative. +Raises +ArgumentError+ if a bit position is too large to be represented. +Raises +ArgumentError+ if +lsb_first+ is neither +true+ nor +false+. diff --git a/doc/string/bit_flip.rdoc b/doc/string/bit_flip.rdoc index 7a480b03d6f532..472bb18f30bebc 100644 --- a/doc/string/bit_flip.rdoc +++ b/doc/string/bit_flip.rdoc @@ -4,13 +4,31 @@ Flips the bit at zero-based bit +offset+; returns +self+: s.bit_flip(1) # => "\x02" s.bit_flip(1) # => "\x00" +With +length+, flips the +length+ consecutive bits beginning at +offset+; +with a Range, flips the bits covered by +range+: + + s = "\x00\xFF" + s.bit_flip(4, 8) # => "\xF0\xF0" + s = "\x00\xFF" + s.bit_flip(4..11) # => "\xF0\xF0" + +The entire region must lie within +self+; a region that extends beyond the +end raises +IndexError+ rather than being clamped. Writing zero bits is a +no-op, but even an empty region raises +IndexError+ when it begins beyond +the end of +self+. + By default, bits within each byte are numbered from least-significant to most-significant. If +lsb_first+ is +false+, byte order is unchanged but bits within each byte are numbered from most-significant to least-significant: s = "\x00" - s.bit_flip(1, lsb_first: false) # => "\x40" + s.bit_flip(1, lsb_first: false) # => "\x40" + s = "\x00\xFF" + s.bit_flip(4, 8, lsb_first: false) # => "\x0F\x0F" -Raises +IndexError+ if +offset+ is out of range. -Raises +ArgumentError+ if +offset+ is too large to be represented. +Raises +IndexError+ if +offset+ is out of range, or if any part of the +region lies outside +self+. +Raises +ArgumentError+ if +length+ is negative. +Raises +ArgumentError+ if a bit position is too large to be represented. Raises +ArgumentError+ if +lsb_first+ is neither +true+ nor +false+. +Raises +FrozenError+ if +self+ is frozen, even when writing zero bits. diff --git a/doc/string/bit_set.rdoc b/doc/string/bit_set.rdoc index 82c4cb25e4ca0b..28709196ef10e7 100644 --- a/doc/string/bit_set.rdoc +++ b/doc/string/bit_set.rdoc @@ -4,13 +4,31 @@ Sets the bit at zero-based bit +offset+ to 1; returns +self+: s.bit_set(1) # => "\x02" s # => "\x02" +With +length+, sets the +length+ consecutive bits beginning at +offset+; +with a Range, sets the bits covered by +range+: + + s = "\x00\x00" + s.bit_set(4, 8) # => "\xF0\x0F" + s = "\x00\x00" + s.bit_set(4..11) # => "\xF0\x0F" + +The entire region must lie within +self+; a region that extends beyond the +end raises +IndexError+ rather than being clamped. Writing zero bits is a +no-op, but even an empty region raises +IndexError+ when it begins beyond +the end of +self+. + By default, bits within each byte are numbered from least-significant to most-significant. If +lsb_first+ is +false+, byte order is unchanged but bits within each byte are numbered from most-significant to least-significant: s = "\x00" s.bit_set(1, lsb_first: false) # => "\x40" + s = "\x00\x00" + s.bit_set(4, 8, lsb_first: false) # => "\x0F\xF0" -Raises +IndexError+ if +offset+ is out of range. -Raises +ArgumentError+ if +offset+ is too large to be represented. +Raises +IndexError+ if +offset+ is out of range, or if any part of the +region lies outside +self+. +Raises +ArgumentError+ if +length+ is negative. +Raises +ArgumentError+ if a bit position is too large to be represented. Raises +ArgumentError+ if +lsb_first+ is neither +true+ nor +false+. +Raises +FrozenError+ if +self+ is frozen, even when writing zero bits. diff --git a/spec/ruby/core/string/bit_clear_spec.rb b/spec/ruby/core/string/bit_clear_spec.rb index 1e8851d93231ec..1a73167a7f5e70 100644 --- a/spec/ruby/core/string/bit_clear_spec.rb +++ b/spec/ruby/core/string/bit_clear_spec.rb @@ -21,11 +21,32 @@ str.should == "\xFF\x7F" end + it "clears a region given as offset and length and returns self" do + str = +"\xFF\xFF" + str.bit_clear(4, 8).should.equal?(str) + str.should == "\x0F\xF0" + end + + it "clears a region given as a Range" do + str = +"\xFF\xFF" + str.bit_clear(4..11) + str.should == "\x0F\xF0" + end + it "raises an IndexError for an out of range bit offset" do -> { "\x00".bit_clear(8) }.should.raise(IndexError) -> { "\x00".bit_clear(-1) }.should.raise(IndexError) end + it "raises an IndexError when a region extends past the end" do + -> { "\x00".bit_clear(0, 9) }.should.raise(IndexError) + -> { "\x00".bit_clear(0..8) }.should.raise(IndexError) + end + + it "raises an ArgumentError for a negative length" do + -> { "\x00".bit_clear(0, -1) }.should.raise(ArgumentError) + end + it "raises a FrozenError if self is frozen" do -> { "\x00".freeze.bit_clear(0) }.should.raise(FrozenError) end diff --git a/spec/ruby/core/string/bit_count_spec.rb b/spec/ruby/core/string/bit_count_spec.rb index 3c77d6f7f20cfb..bd3146c85ea8b8 100644 --- a/spec/ruby/core/string/bit_count_spec.rb +++ b/spec/ruby/core/string/bit_count_spec.rb @@ -10,9 +10,43 @@ "\xAA\xF0".bit_count.should == 8 end - it "raises an ArgumentError when given an argument" do + it "counts the set bits in a region given as offset and length" do + data = "\xFF\x00\xF0" + data.bit_count(0, 8).should == 8 + data.bit_count(8, 8).should == 0 + data.bit_count(4, 8).should == 4 + end + + it "counts the set bits in a region given as a Range" do + data = "\xFF\x00\xF0" + data.bit_count(0..7).should == 8 + data.bit_count(8...16).should == 0 + data.bit_count(16..).should == 4 + end + + it "clamps a region that extends past the end of the string" do + data = "\xFF\x00\xF0" + data.bit_count(16, 100).should == 4 + data.bit_count(100, 8).should == 0 + end + + it "interprets a non-byte-aligned region according to lsb_first" do + "\xF0".bit_count(0, 4).should == 0 + "\xF0".bit_count(0, 4, lsb_first: false).should == 4 + end + + it "accepts lsb_first for a whole-string count but does not use it" do + "\xFF".bit_count(lsb_first: false).should == 8 + end + + it "raises an ArgumentError for a lone offset without a length" do -> { "\x00".bit_count(0) }.should.raise(ArgumentError) - -> { "\x00".bit_count(lsb_first: false) }.should.raise(ArgumentError) + end + + it "raises for an invalid region" do + -> { "\x00".bit_count(-1, 4) }.should.raise(IndexError) + -> { "\x00".bit_count(0, -1) }.should.raise(ArgumentError) + -> { "\x00".bit_count(0, 4, lsb_first: nil) }.should.raise(ArgumentError) end end end diff --git a/spec/ruby/core/string/bit_flip_spec.rb b/spec/ruby/core/string/bit_flip_spec.rb index 7f443d064575ff..9bf3136f2e168f 100644 --- a/spec/ruby/core/string/bit_flip_spec.rb +++ b/spec/ruby/core/string/bit_flip_spec.rb @@ -23,11 +23,32 @@ str.should == "\x00\x80" end + it "flips a region given as offset and length and returns self" do + str = +"\x00\xFF" + str.bit_flip(4, 8).should.equal?(str) + str.should == "\xF0\xF0" + end + + it "flips a region given as a Range" do + str = +"\x00\xFF" + str.bit_flip(4..11) + str.should == "\xF0\xF0" + end + it "raises an IndexError for an out of range bit offset" do -> { "\x00".bit_flip(8) }.should.raise(IndexError) -> { "\x00".bit_flip(-1) }.should.raise(IndexError) end + it "raises an IndexError when a region extends past the end" do + -> { "\x00".bit_flip(0, 9) }.should.raise(IndexError) + -> { "\x00".bit_flip(0..8) }.should.raise(IndexError) + end + + it "raises an ArgumentError for a negative length" do + -> { "\x00".bit_flip(0, -1) }.should.raise(ArgumentError) + end + it "raises a FrozenError if self is frozen" do -> { "\x00".freeze.bit_flip(0) }.should.raise(FrozenError) end diff --git a/spec/ruby/core/string/bit_set_spec.rb b/spec/ruby/core/string/bit_set_spec.rb index 1f44dc7f801a5c..6059c9f2459d27 100644 --- a/spec/ruby/core/string/bit_set_spec.rb +++ b/spec/ruby/core/string/bit_set_spec.rb @@ -21,11 +21,32 @@ str.should == "\x00\x80" end + it "sets a region given as offset and length and returns self" do + str = +"\x00\x00" + str.bit_set(4, 8).should.equal?(str) + str.should == "\xF0\x0F" + end + + it "sets a region given as a Range" do + str = +"\x00\x00" + str.bit_set(4..11) + str.should == "\xF0\x0F" + end + it "raises an IndexError for an out of range bit offset" do -> { "\x00".bit_set(8) }.should.raise(IndexError) -> { "\x00".bit_set(-1) }.should.raise(IndexError) end + it "raises an IndexError when a region extends past the end" do + -> { "\x00".bit_set(0, 9) }.should.raise(IndexError) + -> { "\x00".bit_set(0..8) }.should.raise(IndexError) + end + + it "raises an ArgumentError for a negative length" do + -> { "\x00".bit_set(0, -1) }.should.raise(ArgumentError) + end + it "raises a FrozenError if self is frozen" do -> { "\x00".freeze.bit_set(0) }.should.raise(FrozenError) end diff --git a/string.c b/string.c index 8aae42ce78f11b..641553b8e3594a 100644 --- a/string.c +++ b/string.c @@ -6849,17 +6849,114 @@ str_bit_offset_from_index(VALUE index) return offset; } +/* + * Bit lengths share the offset's representable range. + * A negative length is an ArgumentError rather than an IndexError. + */ +static uint64_t +str_bit_length_from_index(VALUE index) +{ + VALUE integer = rb_to_int(index); + + if (FIXNUM_P(integer)) { + long value = FIX2LONG(integer); + if (value < 0) { + rb_raise(rb_eArgError, "negative bit length"); + } + return (uint64_t)value; + } + + RUBY_ASSERT(RB_TYPE_P(integer, T_BIGNUM)); + if (rb_int_negative_p(integer)) { + rb_raise(rb_eArgError, "negative bit length"); + } + if (rb_cmpint(rb_int_cmp(integer, ULL2NUM(UINT64_MAX)), integer, ULL2NUM(UINT64_MAX)) > 0) { + rb_raise(rb_eArgError, "bit length out of representable range"); + } + return (uint64_t)NUM2ULL(integer); +} + +static inline uint64_t +str_bit_size(long byte_len) +{ + /* + * byte_len * CHAR_BIT overflows uint64_t only for byte_len >= 2**61 which cannot + * be allocated. Saturate so that unreachable cases cannot wrap. + */ + if ((uint64_t)byte_len > UINT64_MAX / CHAR_BIT) return UINT64_MAX; + return (uint64_t)byte_len * CHAR_BIT; +} + +struct str_bit_range { + uint64_t beg; + uint64_t end_exclusive; /* meaningful only when end_open is false */ + bool end_open; /* a nil end: the region runs to the end of self */ +}; + +/* + * Coerce a bit Range's endpoints to bit offsets. This may run arbitrary Ruby + * (Integer#to_int on the endpoints), so it does NOT read the string's length: + * The caller must resolve the length only after this returns, otherwise + * to_int that reallocates self would leave a stale size. + */ +static void +str_bit_range_to_offsets(VALUE range, struct str_bit_range *out) +{ + VALUE beg_v, end_v; + int excl; + + /* + * We don't use rb_range_beg_len: it counts negative endpoints from the end, + * which is an IndexError for bit positions, and it is limited to long instead + * of uint64_t. + */ + rb_range_values(range, &beg_v, &end_v, &excl); + + out->beg = NIL_P(beg_v) ? 0 : str_bit_offset_from_index(beg_v).value; + if (NIL_P(end_v)) { + out->end_open = true; + out->end_exclusive = 0; + } + else { + uint64_t end = str_bit_offset_from_index(end_v).value; + out->end_open = false; + /* + * The saturation loses one position only for an inclusive end of + * 2**64-1, which lies beyond any real string either way. + */ + out->end_exclusive = (excl || end == UINT64_MAX) ? end : end + 1; + } +} + +/* + * Turn a coerced Range into (beg, len) against the now-current total bit size. + * The length is deliberately not clamped to the bits available, so a reading + * caller can clamp while a writing caller detects the overrun and raises. + */ static bool -str_lsb_first(int argc, VALUE *argv, VALUE *index) +str_bit_range_resolve(const struct str_bit_range *range, uint64_t total_bits, uint64_t *begp, uint64_t *lenp) +{ + uint64_t beg = range->beg; + if (beg > total_bits) return false; + + uint64_t end_exclusive = range->end_open ? total_bits : range->end_exclusive; + if (end_exclusive < beg) end_exclusive = beg; + + *begp = beg; + *lenp = end_exclusive - beg; + return true; +} + +static bool +str_lsb_first_from_opts(VALUE opts) { static ID keywords[1]; - VALUE opts, vlsb_first; + VALUE vlsb_first; if (!keywords[0]) { keywords[0] = rb_intern_const("lsb_first"); } - rb_scan_args(argc, argv, "1:", index, &opts); rb_get_kwargs(opts, keywords, 0, 1, &vlsb_first); if (vlsb_first == Qundef || vlsb_first == Qtrue) { return true; @@ -6871,6 +6968,15 @@ str_lsb_first(int argc, VALUE *argv, VALUE *index) UNREACHABLE_RETURN(false); } +static bool +str_lsb_first(int argc, VALUE *argv, VALUE *index) +{ + VALUE opts; + + rb_scan_args(argc, argv, "1:", index, &opts); + return str_lsb_first_from_opts(opts); +} + static inline uint64_t str_logical_to_physical_bit64(uint64_t logical, bool lsb_first) { @@ -6967,11 +7073,83 @@ enum str_bit_mutation { STR_BIT_FLIP }; +/* + * Mask for the logical in-byte positions lo..hi (0 <= lo <= hi <= 7) of one + * byte. A contiguous logical run stays contiguous within a byte under both + * numbering conventions; MSB-first only mirrors it. + */ +static inline unsigned char +str_bit_region_byte_mask(unsigned int lo, unsigned int hi, bool lsb_first) +{ + if (lsb_first) { + return (unsigned char)((0xFFu >> (7 - hi)) & (0xFFu << lo)); + } + else { + return (unsigned char)((0xFFu >> lo) & (0xFFu << (7 - hi))); + } +} + +static inline void +str_apply_bit_mask(unsigned char *byte, unsigned char mask, enum str_bit_mutation mutation) +{ + switch (mutation) { + case STR_BIT_SET: + *byte |= mask; + break; + case STR_BIT_CLEAR: + *byte &= (unsigned char)~mask; + break; + case STR_BIT_FLIP: + *byte ^= mask; + break; + } +} + +/* The caller has bounds-checked [beg, beg+len) and called rb_str_modify. */ +static void +str_mutate_bit_region(unsigned char *ptr, uint64_t beg, uint64_t len, bool lsb_first, enum str_bit_mutation mutation) +{ + uint64_t first_bit = beg; + uint64_t last_bit = beg + len - 1; + long first_byte = (long)(first_bit / CHAR_BIT); + long last_byte = (long)(last_bit / CHAR_BIT); + unsigned int first_off = (unsigned int)(first_bit % CHAR_BIT); + unsigned int last_off = (unsigned int)(last_bit % CHAR_BIT); + + if (first_byte == last_byte) { + str_apply_bit_mask(ptr + first_byte, str_bit_region_byte_mask(first_off, last_off, lsb_first), mutation); + return; + } + + str_apply_bit_mask(ptr + first_byte, str_bit_region_byte_mask(first_off, 7, lsb_first), mutation); + long middle_len = last_byte - first_byte - 1; + if (middle_len > 0) { + unsigned char *middle = ptr + first_byte + 1; + switch (mutation) { + case STR_BIT_SET: + memset(middle, 0xFF, middle_len); + break; + case STR_BIT_CLEAR: + memset(middle, 0, middle_len); + break; + case STR_BIT_FLIP: + /* + * Byte loop on purpose: the compiler auto-vectorizes it (verified on gcc 13.3 + * and clang 18.1 with x86_64), and being read-modify-write, the flip is memory-bound, + * so a manual word-at-a-time XOR loop was measured to be no faster. + */ + for (long i = 0; i < middle_len; i++) { + middle[i] ^= 0xFF; + } + break; + } + } + str_apply_bit_mask(ptr + last_byte, str_bit_region_byte_mask(0, last_off, lsb_first), mutation); +} + static VALUE -str_mutate_bit(int argc, VALUE *argv, VALUE str, enum str_bit_mutation mutation) +str_mutate_single_bit(VALUE str, VALUE index, bool lsb_first, enum str_bit_mutation mutation) { - VALUE index; - bool lsb_first = str_lsb_first(argc, argv, &index); struct str_bit_offset offset = str_bit_offset_from_index(index); struct str_bit_location location; long bit_index; @@ -6994,24 +7172,70 @@ str_mutate_bit(int argc, VALUE *argv, VALUE str, enum str_bit_mutation mutation) mask = (unsigned char)(1u << location.bit_offset); } - switch (mutation) { - case STR_BIT_SET: - ptr[location.byte_index] |= mask; - break; - case STR_BIT_CLEAR: - ptr[location.byte_index] &= (unsigned char)~mask; - break; - case STR_BIT_FLIP: - ptr[location.byte_index] ^= mask; - break; + str_apply_bit_mask(ptr + location.byte_index, mask, mutation); + return str; +} + +static VALUE +str_mutate_bit(int argc, VALUE *argv, VALUE str, enum str_bit_mutation mutation) +{ + VALUE target, length_v, opts; + uint64_t beg = 0, len = 0; + + /* Count positional arguments so that an explicit nil is not mistaken for an omitted one. */ + int nargs = rb_scan_args(argc, argv, "11:", &target, &length_v, &opts); + bool lsb_first = str_lsb_first_from_opts(opts); + + bool is_range = rb_obj_is_kind_of(target, rb_cRange); + if (nargs == 1 && !is_range) { + return str_mutate_single_bit(str, target, lsb_first, mutation); + } + + struct str_bit_range range = {0}; + struct str_bit_offset offset; + if (is_range) { + if (nargs == 2) { + rb_raise(rb_eArgError, "bit length not allowed with a Range"); + } + str_bit_range_to_offsets(target, &range); + } + else { + offset = str_bit_offset_from_index(target); + len = str_bit_length_from_index(length_v); } + /* + * A region that begins past the end is out of range even when it is + * empty, and one that runs past the end is not allowed to silently + * shrink: both are errors for a mutation, unlike the clamping reads. + * An empty region whose start is within 0..bitsize writes nothing. + */ + uint64_t total_bits = str_bit_size(RSTRING_LEN(str)); + if (is_range) { + if (!str_bit_range_resolve(&range, total_bits, &beg, &len) || len > total_bits - beg) { + rb_raise(rb_eIndexError, "bit range out of range"); + } + } + else { + beg = offset.value; + if (beg > total_bits || len > total_bits - beg) { + rb_raise(rb_eIndexError, "bit range out of range"); + } + } + /* Even a zero-length write requires a mutable receiver. */ + rb_check_frozen(str); + if (len == 0) return str; + + rb_str_modify(str); + str_mutate_bit_region((unsigned char *)RSTRING_PTR(str), beg, len, lsb_first, mutation); return str; } /* * call-seq: * bit_set(offset, lsb_first: true) -> self + * bit_set(offset, length, lsb_first: true) -> self + * bit_set(range, lsb_first: true) -> self * * :include: doc/string/bit_set.rdoc * @@ -7025,6 +7249,8 @@ rb_str_bit_set(int argc, VALUE *argv, VALUE str) /* * call-seq: * bit_clear(offset, lsb_first: true) -> self + * bit_clear(offset, length, lsb_first: true) -> self + * bit_clear(range, lsb_first: true) -> self * * :include: doc/string/bit_clear.rdoc * @@ -7038,6 +7264,8 @@ rb_str_bit_clear(int argc, VALUE *argv, VALUE str) /* * call-seq: * bit_flip(offset, lsb_first: true) -> self + * bit_flip(offset, length, lsb_first: true) -> self + * bit_flip(range, lsb_first: true) -> self * * :include: doc/string/bit_flip.rdoc * @@ -7089,17 +7317,84 @@ str_count_bits(const unsigned char *ptr, long len) return count; } +static uint64_t +str_count_bits_region(const unsigned char *ptr, uint64_t beg, uint64_t len, bool lsb_first) +{ + uint64_t first_bit = beg; + uint64_t last_bit = beg + len - 1; + long first_byte = (long)(first_bit / CHAR_BIT); + long last_byte = (long)(last_bit / CHAR_BIT); + unsigned int first_off = (unsigned int)(first_bit % CHAR_BIT); + unsigned int last_off = (unsigned int)(last_bit % CHAR_BIT); + + if (first_byte == last_byte) { + return rb_popcount32((uint32_t)(ptr[first_byte] & str_bit_region_byte_mask(first_off, last_off, lsb_first))); + } + + uint64_t count = rb_popcount32((uint32_t)(ptr[first_byte] & str_bit_region_byte_mask(first_off, 7, lsb_first))); + count += str_count_bits(ptr + first_byte + 1, last_byte - first_byte - 1); + count += rb_popcount32((uint32_t)(ptr[last_byte] & str_bit_region_byte_mask(0, last_off, lsb_first))); + return count; +} + /* * call-seq: * bit_count -> integer + * bit_count(offset, length, lsb_first: true) -> integer + * bit_count(range, lsb_first: true) -> integer * * :include: doc/string/bit_count.rdoc * */ static VALUE -rb_str_bit_count(VALUE str) +rb_str_bit_count(int argc, VALUE *argv, VALUE str) { - return ULL2NUM(str_count_bits((const unsigned char *)RSTRING_PTR(str), RSTRING_LEN(str))); + VALUE v0, v1, opts; + uint64_t beg = 0, len = 0; + + /* Count positional arguments so that an explicit nil is not mistaken for an omitted one. */ + int nargs = rb_scan_args(argc, argv, "02:", &v0, &v1, &opts); + /* + * A whole-string popcount is independent of bit numbering. + * no-(offset|range)-argument form only validates lsb_first. + */ + bool lsb_first = str_lsb_first_from_opts(opts); + + if (nargs == 0) { + return ULL2NUM(str_count_bits((const unsigned char *)RSTRING_PTR(str), RSTRING_LEN(str))); + } + + bool is_range = rb_obj_is_kind_of(v0, rb_cRange); + struct str_bit_range range = {0}; + if (is_range) { + if (nargs == 2) { + rb_raise(rb_eArgError, "bit length not allowed with a Range"); + } + str_bit_range_to_offsets(v0, &range); + } + else if (nargs == 1) { + rb_raise(rb_eArgError, "no bit length given"); + } + else { + beg = str_bit_offset_from_index(v0).value; + len = str_bit_length_from_index(v1); + } + + const unsigned char *ptr = (const unsigned char *)RSTRING_PTR(str); + uint64_t total_bits = str_bit_size(RSTRING_LEN(str)); + if (is_range) { + if (!str_bit_range_resolve(&range, total_bits, &beg, &len)) { + return INT2FIX(0); + } + } + else if (beg >= total_bits) { + return INT2FIX(0); + } + + /* Reads clamp: only the part of the region that exists is counted. */ + if (len > total_bits - beg) len = total_bits - beg; + if (len == 0) return INT2FIX(0); + return ULL2NUM(str_count_bits_region(ptr, beg, len, lsb_first)); } static void @@ -13939,7 +14234,7 @@ Init_String(void) rb_define_method(rb_cString, "bit_set", rb_str_bit_set, -1); rb_define_method(rb_cString, "bit_clear", rb_str_bit_clear, -1); rb_define_method(rb_cString, "bit_flip", rb_str_bit_flip, -1); - rb_define_method(rb_cString, "bit_count", rb_str_bit_count, 0); + rb_define_method(rb_cString, "bit_count", rb_str_bit_count, -1); rb_define_method(rb_cString, "bitwise_not", rb_str_bitwise_not, 0); rb_define_method(rb_cString, "bitwise_not!", rb_str_bitwise_not_bang, 0); rb_define_method(rb_cString, "bitwise_and", rb_str_bitwise_and, 1); diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb index bb92a2b85aed6e..899ff08692aacb 100644 --- a/test/ruby/test_string.rb +++ b/test/ruby/test_string.rb @@ -1104,13 +1104,196 @@ def test_bit_set_clear_flip assert_equal(S("car"), shared) end + def test_bit_set_clear_flip_region + s = S("\x00\x00") + assert_same(s, s.bit_set(4, 8)) + assert_equal(S("\xF0\x0F"), s) + s = S("\x00\x00") + s.bit_set(4..11) + assert_equal(S("\xF0\x0F"), s) + s = S("\xFF\xFF") + assert_same(s, s.bit_clear(4, 8)) + assert_equal(S("\x0F\xF0"), s) + s = S("\xFF\xFF") + s.bit_clear(4..11) + assert_equal(S("\x0F\xF0"), s) + s = S("\x00\xFF") + assert_same(s, s.bit_flip(4, 8)) + assert_equal(S("\xF0\xF0"), s) + s = S("\x00\xFF") + s.bit_flip(4..11) + assert_equal(S("\xF0\xF0"), s) + + # Range variants + s = S("\x00") + s.bit_set(0...8) + assert_equal(S("\xFF"), s) + s = S("\x00\x00") + s.bit_set(8..) + assert_equal(S("\x00\xFF"), s) + s = S("\x00") + s.bit_set(..3) + assert_equal(S("\x0F"), s) + s = S("\x00") + s.bit_set(nil..nil) + assert_equal(S("\xFF"), s) + + # A region spanning several bytes exercises the byte-fill path. + s = S("\x00" * 5) + s.bit_set(4, 32) + assert_equal(S("\xF0\xFF\xFF\xFF\x0F"), s) + s.bit_flip(0..) + assert_equal(S("\x0F\x00\x00\x00\xF0"), s) + + # One-bit and zero-bit forms + s = S("\x00") + s.bit_set(3, 1) + assert_equal(S("\x08"), s) + s = S("\xAA") + assert_same(s, s.bit_set(0, 0)) + s.bit_clear(0, 0) + s.bit_flip(0, 0) + assert_equal(S("\xAA"), s) + s = S("\x00") + assert_same(s, s.bit_set(8..)) # empty region at the very end + s.bit_set(8, 0) + s.bit_set(0...0) + assert_equal(S("\x00"), s) + + # lsb_first: false interprets the same logical positions MSB-first. + s = S("\x00\x00") + s.bit_set(6..9, lsb_first: false) + assert_equal(S("\x03\xC0"), s) + s = S("\x00\x00") + s.bit_set(6, 4, lsb_first: false) + assert_equal(S("\x03\xC0"), s) + + # Writes do not clamp: any part of the region outside self raises. + assert_raise(IndexError) { S("\x00").bit_set(0, 9) } + assert_raise(IndexError) { S("\x00").bit_set(8, 1) } + assert_raise(IndexError) { S("\x00").bit_set(0..8) } + assert_raise(IndexError) { S("\x00").bit_set(0...9) } + assert_raise(IndexError) { S("\x00").bit_set(8..8) } + assert_raise(IndexError) { S("\x00").bit_set(9..) } + # An empty region is no exception when it begins past the end. + assert_raise(IndexError) { S("\x00").bit_set(9, 0) } + assert_raise(IndexError) { S("\x00").bit_set(9...9) } + assert_raise(IndexError) { S("\x00").bit_clear(9, 0) } + assert_raise(IndexError) { S("\x00").bit_flip(9, 0) } + assert_raise(IndexError) { S("\x00").bit_clear(0..100) } + assert_raise(IndexError) { S("\x00").bit_flip(0..100) } + assert_raise(IndexError) { S("").bit_set(0..7) } + assert_raise(IndexError) { S("\x00").bit_set(..-1) } + assert_raise(IndexError) { S("\x00").bit_set(-1..2) } + assert_raise(IndexError) { S("\x00").bit_set(2**62, 1) } + assert_raise(IndexError) { S("\x00").bit_set(2**62..2**62 + 4) } + assert_raise(ArgumentError) { S("\x00").bit_set(0, -1) } + assert_raise(ArgumentError) { S("\x00").bit_set(0, 2**100) } + assert_raise(ArgumentError) { S("\x00").bit_set(0..2**100) } + assert_raise(ArgumentError) { S("\x00").bit_set(0..3, 4) } + # An explicit nil is an argument, not an omitted one. + assert_raise(TypeError) { S("\x00").bit_set(0, nil) } + assert_raise(ArgumentError) { S("\x00").bit_set(0..3, nil) } + assert_raise(ArgumentError) { S("\x00").bit_set(0..3, lsb_first: nil) } + assert_raise(FrozenError) { S("\x00").freeze.bit_set(0..3) } + # A zero-length write still requires a mutable receiver, but an + # out-of-range region is detected before the frozen check. + assert_raise(FrozenError) { S("\x00").freeze.bit_set(0, 0) } + assert_raise(FrozenError) { S("\x00").freeze.bit_clear(0...0) } + assert_raise(FrozenError) { S("\x00").freeze.bit_flip(8..) } + assert_raise(IndexError) { S("\x00").freeze.bit_set(9, 0) } + + # Copy-on-write: mutating must not affect a shared sibling. + shared = S("fooXbar").split(S("X")).last + shared.bit_set(0..7) + assert_equal(S("\xFFar").b, shared.b) + end + def test_bit_count assert_equal(0, S("").bit_count) assert_equal(0, S("\x00").bit_count) assert_equal(8, S("\xFF").bit_count) assert_equal(8, S("\xAA\xF0").bit_count) + # A full-string popcount is bit-order independent; the keyword is + # validated but has no effect. + assert_equal(8, S("\xFF").bit_count(lsb_first: false)) assert_raise(ArgumentError) { S("\x00").bit_count(0) } - assert_raise(ArgumentError) { S("\x00").bit_count(lsb_first: false) } + assert_raise(ArgumentError) { S("\x00").bit_count(lsb_first: nil) } + end + + def test_bit_count_region + data = S("\xFF\x00\xF0") + assert_equal(8, data.bit_count(0, 8)) + assert_equal(0, data.bit_count(8, 8)) + assert_equal(0, data.bit_count(16, 4)) + assert_equal(4, data.bit_count(20, 4)) + assert_equal(4, data.bit_count(4, 8)) + assert_equal(8, data.bit_count(0..7)) + assert_equal(0, data.bit_count(8..15)) + assert_equal(8, data.bit_count(0...8)) + assert_equal(4, data.bit_count(16..)) + assert_equal(8, data.bit_count(..7)) + assert_equal(12, data.bit_count(nil..nil)) + assert_equal(0, data.bit_count(0, 0)) + assert_equal(0, data.bit_count(0...0)) + + # Reads clamp: only the part of the region that exists is counted. + assert_equal(4, data.bit_count(16, 100)) + assert_equal(0, data.bit_count(24, 8)) + assert_equal(0, data.bit_count(100, 8)) + assert_equal(4, data.bit_count(16..100)) + assert_equal(0, data.bit_count(100..200)) + assert_equal(0, data.bit_count(2**62, 8)) + + # lsb_first: selects which physical bits a non-byte-aligned region means. + assert_equal(0, S("\xF0").bit_count(0, 4)) + assert_equal(4, S("\xF0").bit_count(0, 4, lsb_first: false)) + assert_equal(4, S("\xF0").bit_count(0..3, lsb_first: false)) + assert_equal(4, S("\xF0").bit_count(4, 4)) + + assert_raise(IndexError) { S("\x00").bit_count(-1, 4) } + assert_raise(IndexError) { S("\x00").bit_count(-1..3) } + assert_raise(IndexError) { S("\x00").bit_count(..-1) } + assert_raise(ArgumentError) { S("\x00").bit_count(0, -1) } + assert_raise(ArgumentError) { S("\x00").bit_count(2**100, 1) } + assert_raise(ArgumentError) { S("\x00").bit_count(0, 2**100) } + assert_raise(ArgumentError) { S("\x00").bit_count(0..2**100) } + assert_raise(ArgumentError) { S("\x00").bit_count(0..3, 4) } + assert_raise_with_message(ArgumentError, "no bit length given") { S("\x00").bit_count(0) } + # An explicit nil is an argument, not an omitted one. + assert_raise(ArgumentError) { S("\x00").bit_count(nil) } + assert_raise(TypeError) { S("\x00").bit_count(nil, 3) } + assert_raise(TypeError) { S("\x00").bit_count(0, nil) } + assert_raise(ArgumentError) { S("\x00").bit_count(0..3, nil) } + assert_raise(ArgumentError) { S("\x00").bit_count(0, 4, lsb_first: nil) } + end + + def test_bit_region_argument_side_effect + # Coercing an argument (Integer#to_int, or a Range endpoint) may run user + # code that resizes the receiver. The bounds check and the memory access + # must both see the post-coercion length, or a stale size lets the region + # method read or write out of bounds. + shrink = Class.new do + def initialize(str, value); @str, @value = str, value; end + def to_int; @str.replace("\xFF".b); @value; end + end + + # Writes: the region no longer fits the shrunken string, so this must raise + # rather than write past the reallocated buffer. + s = S("\x00") * 8 + assert_raise(IndexError) { s.bit_set(0, shrink.new(s, 64)) } + assert_equal("\xFF".b, s.b) + + # A Range endpoint is coerced the same way; a beginless range keeps the + # custom object out of Range's begin <=> end construction check. + s = S("\x00") * 8 + assert_raise(IndexError) { s.bit_set(..shrink.new(s, 63)) } + assert_equal("\xFF".b, s.b) + + # Reads clamp to the post-coercion length instead of reading freed memory. + s = S("\xFF") * 8 + assert_equal(8, s.bit_count(0, shrink.new(s, 64))) + assert_equal("\xFF".b, s.b) end def test_bitwise From e4534b8cd0fd488d7d415c1cbb3fc01e6d94566b Mon Sep 17 00:00:00 2001 From: KBS Date: Mon, 14 Sep 2026 18:23:07 +0900 Subject: [PATCH 5/6] [ruby/strscan] Fix matched_size when the stored string is shrunk (https://github.com/ruby/strscan/pull/217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `matched_size` returns the raw register width without clamping it to the current length of the stored string, so shrinking the string leaves it reporting the old match: ```ruby s = StringScanner.new("abcdef") s.scan(/abcdef/) s.string = "abc" s.matched_size # => 6, should be 3 s.matched # => "abc" (this one is already correct) ``` `extract_range` (`ext/strscan/strscan.c:168-169`) already clamps, which is why `matched` answers correctly. #212 added the same two lines to `integer_at`, `charpos` got them in `7b77f30`, and `bol?` carries its own guard — `matched_size` was missed. Fixed in all three backends (CRuby, JRuby, TruffleRuby) with a shared regression test. --------- https://github.com/ruby/strscan/commit/0906399858 Co-authored-by: Sutou Kouhei --- ext/strscan/strscan.c | 6 +++++- test/strscan/test_stringscanner.rb | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ext/strscan/strscan.c b/ext/strscan/strscan.c index 6c7339090b308a..5f5ad1b9bff231 100644 --- a/ext/strscan/strscan.c +++ b/ext/strscan/strscan.c @@ -1673,10 +1673,14 @@ static VALUE strscan_matched_size(VALUE self) { struct strscanner *p; + long beg, end; GET_SCANNER(self, p); if (! MATCHED_P(p)) return Qnil; - return LONG2NUM(p->regs.end[0] - p->regs.beg[0]); + beg = adjust_register_position(p, p->regs.beg[0]); + if (beg > S_LEN(p)) return Qnil; + end = minl(adjust_register_position(p, p->regs.end[0]), S_LEN(p)); + return LONG2NUM(end - beg); } static int diff --git a/test/strscan/test_stringscanner.rb b/test/strscan/test_stringscanner.rb index 79784b59f50b45..8bf24c73012202 100644 --- a/test/strscan/test_stringscanner.rb +++ b/test/strscan/test_stringscanner.rb @@ -738,6 +738,27 @@ def test_matched_size assert_nil(s.matched_size) end + def test_matched_size_when_shrunk + # matched_size must agree with matched, which extract_range clamps to the + # current length of the stored string. + s = create_string_scanner(+"before 29 after") + s.skip_until(" ") + assert_equal("29", s.scan(/\d+/)) + assert_equal(2, s.matched_size) + + s.string.replace("before 2") + assert_equal("2", s.matched) + assert_equal(1, s.matched_size) + + s.string.replace("before ") + assert_equal("", s.matched) + assert_equal(0, s.matched_size) + + s.string.replace("before") + assert_nil(s.matched) + assert_nil(s.matched_size) + end + def test_empty_encoding_utf8 ss = create_string_scanner('') assert_equal(Encoding::UTF_8, ss.rest.encoding) From e7551c08831c26cdfca6c9ca4bface71e621c93b Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Mon, 14 Sep 2026 22:15:24 +1200 Subject: [PATCH 6/6] coroutine: allow pthread resumption across threads. (#18820) --- common.mk | 5 + coroutine/pthread/Context.c | 208 +++++++++++++-------------- coroutine/pthread/Context.h | 30 ++-- coroutine/test/main.c | 2 + coroutine/test/test_pthread_resume.c | 185 ++++++++++++++++++++++++ coroutine/test/test_pthread_resume.h | 3 + depend | 1 + 7 files changed, 320 insertions(+), 114 deletions(-) create mode 100644 coroutine/test/test_pthread_resume.c create mode 100644 coroutine/test/test_pthread_resume.h diff --git a/common.mk b/common.mk index c6c767177501f2..e1357be829449c 100644 --- a/common.mk +++ b/common.mk @@ -98,6 +98,7 @@ COROUTINE_TEST = coroutine-test$(EXEEXT) COROUTINE_TEST_OBJS = coroutine-main.$(OBJEXT) \ coroutine-stack.$(OBJEXT) \ coroutine-test_initialize_destroy.$(OBJEXT) \ + coroutine-test_pthread_resume.$(OBJEXT) \ coroutine-test_transfer_repeat.$(OBJEXT) \ coroutine-test_transfer_return.$(OBJEXT) @@ -1168,6 +1169,10 @@ coroutine-test_initialize_destroy.$(OBJEXT): {$(VPATH)}coroutine/test/test_initi $(ECHO) compiling $(srcdir)/coroutine/test/test_initialize_destroy.c $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_initialize_destroy.c +coroutine-test_pthread_resume.$(OBJEXT): {$(VPATH)}coroutine/test/test_pthread_resume.c + $(ECHO) compiling $(srcdir)/coroutine/test/test_pthread_resume.c + $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_pthread_resume.c + coroutine-test_transfer_repeat.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_repeat.c $(ECHO) compiling $(srcdir)/coroutine/test/test_transfer_repeat.c $(Q) $(CC) $(CFLAGS) $(XCFLAGS) $(CPPFLAGS) $(COUTFLAG)$@ -c $(CSRCFLAG)$(srcdir)/coroutine/test/test_transfer_repeat.c diff --git a/coroutine/pthread/Context.c b/coroutine/pthread/Context.c index e42f4b9bb42ad8..a1c227b2e7344f 100644 --- a/coroutine/pthread/Context.c +++ b/coroutine/pthread/Context.c @@ -6,7 +6,6 @@ */ #include "Context.h" -#include #include #include @@ -32,48 +31,20 @@ int check(const char * message, int result) { void coroutine_initialize_main(struct coroutine_context * context) { context->id = pthread_self(); + context->start = NULL; + + check("coroutine_initialize_main:pthread_mutex_init", + pthread_mutex_init(&context->guard, NULL) + ); check("coroutine_initialize_main:pthread_cond_init", pthread_cond_init(&context->schedule, NULL) ); - context->shared = (struct coroutine_shared*)malloc(sizeof(struct coroutine_shared)); - assert(context->shared); - - context->shared->main = context; - context->shared->count = 1; - - if (DEBUG) { - pthread_mutexattr_t attr; - pthread_mutexattr_init(&attr); - pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ERRORCHECK); - - check("coroutine_initialize_main:pthread_mutex_init", - pthread_mutex_init(&context->shared->guard, &attr) - ); - } else { - check("coroutine_initialize_main:pthread_mutex_init", - pthread_mutex_init(&context->shared->guard, NULL) - ); - } -} - -static -void coroutine_release(struct coroutine_context *context) { - if (context->shared) { - size_t count = (context->shared->count -= 1); - - if (count == 0) { - if (DEBUG) fprintf(stderr, "coroutine_release:pthread_mutex_destroy(%p)\n", &context->shared->guard); - pthread_mutex_destroy(&context->shared->guard); - free(context->shared); - } - - context->shared = NULL; - - if (DEBUG) fprintf(stderr, "coroutine_release:pthread_cond_destroy(%p)\n", &context->schedule); - pthread_cond_destroy(&context->schedule); - } + context->suspended = 0; + context->initialized = 1; + context->thread_created = 0; + context->from = NULL; } void coroutine_initialize( @@ -85,10 +56,24 @@ void coroutine_initialize( assert(start && stack && size >= 1024); // We will create the thread when we first transfer, but save the details now: - context->shared = NULL; context->start = start; context->stack = stack; context->size = size; + + check("coroutine_initialize:pthread_mutex_init", + pthread_mutex_init(&context->guard, NULL) + ); + + check("coroutine_initialize:pthread_cond_init", + pthread_cond_init(&context->schedule, NULL) + ); + + /* A worker is initially resumable even though its pthread is created + * lazily by the first transfer. */ + context->suspended = 1; + context->initialized = 1; + context->thread_created = 0; + context->from = NULL; } static @@ -114,41 +99,16 @@ void coroutine_guard_unlock(void * _context) if (DEBUG) fprintf(stderr, "coroutine_guard_unlock:pthread_mutex_unlock\n"); check("coroutine_guard_unlock:pthread_mutex_unlock", - pthread_mutex_unlock(&context->shared->guard) + pthread_mutex_unlock(&context->guard) ); } -static -void coroutine_wait(struct coroutine_context *context) -{ - if (DEBUG) fprintf(stderr, "coroutine_wait:pthread_mutex_lock(guard=%p is_locked=%d)\n", &context->shared->guard, is_locked(&context->shared->guard)); - check("coroutine_wait:pthread_mutex_lock", - pthread_mutex_lock(&context->shared->guard) - ); - - if (DEBUG) fprintf(stderr, "coroutine_wait:pthread_mutex_unlock(guard)\n"); - pthread_mutex_unlock(&context->shared->guard); -} - -static -void coroutine_trampoline_cleanup(void *_context) { - struct coroutine_context * context = _context; - coroutine_release(context); -} - void * coroutine_trampoline(void * _context) { struct coroutine_context * context = _context; - assert(context->shared); - - pthread_cleanup_push(coroutine_trampoline_cleanup, context); - - coroutine_wait(context); context->start(context->from, context); - pthread_cleanup_pop(1); - return NULL; } @@ -169,71 +129,110 @@ int coroutine_create_thread(struct coroutine_context *context) return result; } - result = pthread_cond_init(&context->schedule, NULL); - if (result != 0) { - pthread_attr_destroy(&attr); - return result; - } - result = pthread_create(&context->id, &attr, coroutine_trampoline, context); + pthread_attr_destroy(&attr); + if (result != 0) { - pthread_attr_destroy(&attr); - if (DEBUG) fprintf(stderr, "coroutine_create_thread:pthread_cond_destroy(%p)\n", &context->schedule); - pthread_cond_destroy(&context->schedule); return result; } - context->shared->count += 1; + context->thread_created = 1; return result; } +static +void coroutine_lock_pair(struct coroutine_context *current, struct coroutine_context *target) +{ + /* A valid transfer targets a suspended context, so it cannot be trying to + * acquire current->guard while we acquire target->guard. */ + check("coroutine_transfer:pthread_mutex_lock(current)", + pthread_mutex_lock(¤t->guard) + ); + + check("coroutine_transfer:pthread_mutex_lock(target)", + pthread_mutex_lock(&target->guard) + ); +} + +static +void coroutine_unlock_pair(struct coroutine_context *current, struct coroutine_context *target) +{ + check("coroutine_transfer:pthread_mutex_unlock(target)", + pthread_mutex_unlock(&target->guard) + ); + + check("coroutine_transfer:pthread_mutex_unlock(current)", + pthread_mutex_unlock(¤t->guard) + ); +} + struct coroutine_context * coroutine_transfer(struct coroutine_context * current, struct coroutine_context * target) { - assert(current->shared); + assert(current->initialized); + assert(target->initialized); + assert(current != target); - struct coroutine_context * previous = target->from; int result = 0; - target->from = current; - if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_mutex_lock(guard=%p is_locked=%d)\n", ¤t->shared->guard, is_locked(¤t->shared->guard)); - pthread_mutex_lock(¤t->shared->guard); - pthread_cleanup_push(coroutine_guard_unlock, current); + coroutine_lock_pair(current, target); - // First transfer: - if (target->shared == NULL) { - target->shared = current->shared; + if (current->start == NULL) { + /* A main context follows its caller, which may change when Ruby's M:N + * scheduler moves a Ruby thread to another native thread. */ + current->id = pthread_self(); + } + else { + assert(current->thread_created); + assert(pthread_equal(current->id, pthread_self())); + } + assert(!current->suspended); + assert(target->suspended); + + struct coroutine_context * previous = target->from; + + current->suspended = 1; + target->suspended = 0; + target->from = current; + // First transfer: + if (target->start != NULL && !target->thread_created) { if (DEBUG) fprintf(stderr, "coroutine_transfer:coroutine_create_thread...\n"); result = coroutine_create_thread(target); if (result != 0) { if (DEBUG) fprintf(stderr, "coroutine_transfer:coroutine_create_thread failed\n"); - target->shared = NULL; - target->from = previous; } } else { if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cond_signal(target)\n"); - pthread_cond_signal(&target->schedule); + result = pthread_cond_signal(&target->schedule); } - if (result == 0) { + if (result != 0) { + target->from = previous; + target->suspended = 1; + current->suspended = 0; + coroutine_unlock_pair(current, target); + errno = result; + return NULL; + } + + check("coroutine_transfer:pthread_mutex_unlock(target)", + pthread_mutex_unlock(&target->guard) + ); + + pthread_cleanup_push(coroutine_guard_unlock, current); + + while (current->suspended) { // A side effect of acting upon a cancellation request while in a condition wait is that the mutex is (in effect) re-acquired before calling the first cancellation cleanup handler. If cancelled, pthread_cond_wait immediately invokes cleanup handlers. - if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cond_wait(schedule=%p, guard=%p, is_locked=%d)\n", ¤t->schedule, ¤t->shared->guard, is_locked(¤t->shared->guard)); + if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cond_wait(schedule=%p, guard=%p, is_locked=%d)\n", ¤t->schedule, ¤t->guard, is_locked(¤t->guard)); check("coroutine_transfer:pthread_cond_wait", - pthread_cond_wait(¤t->schedule, ¤t->shared->guard) + pthread_cond_wait(¤t->schedule, ¤t->guard) ); } if (DEBUG) fprintf(stderr, "coroutine_transfer:pthread_cleanup_pop\n"); pthread_cleanup_pop(1); - /* Keep the push/pop pair in the same lexical scope and unlock the guard - * before reporting a setup failure. */ - if (result != 0) { - errno = result; - return NULL; - } - #ifdef __FreeBSD__ // Apparently required for FreeBSD: pthread_testcancel(); @@ -268,14 +267,15 @@ void coroutine_destroy(struct coroutine_context * context) assert(context); - // We are already destroyed or never created: - if (context->shared == NULL) return; + if (!context->initialized) return; - if (context == context->shared->main) { - context->shared->main = NULL; - coroutine_release(context); - } else { + if (context->thread_created) { coroutine_join(context); - assert(context->shared == NULL); + context->thread_created = 0; } + + if (DEBUG) fprintf(stderr, "coroutine_destroy:pthread_cond_destroy(%p)\n", &context->schedule); + pthread_cond_destroy(&context->schedule); + pthread_mutex_destroy(&context->guard); + context->initialized = 0; } diff --git a/coroutine/pthread/Context.h b/coroutine/pthread/Context.h index 6d551ee9df7676..f584090837599b 100644 --- a/coroutine/pthread/Context.h +++ b/coroutine/pthread/Context.h @@ -24,28 +24,38 @@ struct coroutine_context; -struct coroutine_shared -{ - pthread_mutex_t guard; - struct coroutine_context * main; - - size_t count; -}; - typedef COROUTINE(* coroutine_start)(struct coroutine_context *from, struct coroutine_context *self); struct coroutine_context { - struct coroutine_shared * shared; - + /* NULL for a main context; otherwise the worker pthread entry point. */ coroutine_start start; void *argument; void *stack; size_t size; + /* The current caller for a main context, or the context's worker pthread. */ pthread_t id; + + /* Whether the lazily created worker pthread must be cancelled and joined. + * This remains false for a main context, whose id is the caller's pthread. */ + int thread_created; + + /* Serializes updates to suspended and from, and is paired with schedule. */ + pthread_mutex_t guard; + + /* Wakes this context when another context transfers control to it. */ pthread_cond_t schedule; + + /* Whether this context is inactive and can be resumed. This is also the + * predicate protected by guard and checked when waiting on schedule. */ + int suspended; + + /* Whether guard and schedule have been initialized and remain valid. */ + int initialized; + + /* The context that most recently transferred control to this context. */ struct coroutine_context * from; }; diff --git a/coroutine/test/main.c b/coroutine/test/main.c index 7340ab7e736c97..ae6a70f2c0d315 100644 --- a/coroutine/test/main.c +++ b/coroutine/test/main.c @@ -1,4 +1,5 @@ #include "test_initialize_destroy.h" +#include "test_pthread_resume.h" #include "test_transfer_repeat.h" #include "test_transfer_return.h" @@ -10,6 +11,7 @@ run_tests(void) int result = EXIT_SUCCESS; if (test_initialize_destroy() != EXIT_SUCCESS) result = EXIT_FAILURE; + if (test_pthread_resume() != EXIT_SUCCESS) result = EXIT_FAILURE; if (test_transfer_repeat() != EXIT_SUCCESS) result = EXIT_FAILURE; if (test_transfer_return() != EXIT_SUCCESS) result = EXIT_FAILURE; diff --git a/coroutine/test/test_pthread_resume.c b/coroutine/test/test_pthread_resume.c new file mode 100644 index 00000000000000..d343ab42af82ba --- /dev/null +++ b/coroutine/test/test_pthread_resume.c @@ -0,0 +1,185 @@ +#include "ruby/internal/config.h" + +#include COROUTINE_H + +#include "stack.h" +#include "test_pthread_resume.h" + +#include +#include + +#ifdef COROUTINE_PTHREAD_CONTEXT + +#include + +#define STACK_SIZE (1024 * 1024) +#define RESUME_COUNT 100 + +static struct coroutine_context main_context; +static struct coroutine_context worker_context; +static struct coroutine_context *expected_resumer; +static pthread_t main_thread; +static pthread_t worker_thread; +static unsigned int iteration; +static int completed; +static int resumer_result; + +static void +check_context(const char *message, struct coroutine_context *actual, + struct coroutine_context *expected) +{ + if (actual != expected) { + fprintf(stderr, "%s: expected %p, got %p\n", + message, (void *)expected, (void *)actual); + abort(); + } +} + +static struct coroutine_context * +transfer(struct coroutine_context *current, struct coroutine_context *target) +{ + struct coroutine_context *from = coroutine_transfer(current, target); + + if (from == NULL) { + perror("coroutine_transfer"); + abort(); + } + + return from; +} + +static COROUTINE +worker_entry(struct coroutine_context *from, struct coroutine_context *self) +{ + check_context("worker context was entered by", from, &main_context); + check_context("worker context received self", self, &worker_context); + worker_thread = pthread_self(); + + for (iteration = 0; iteration < RESUME_COUNT; iteration++) { + from = transfer(self, from); + check_context("worker context was resumed by another pthread", from, + expected_resumer); + + if (!pthread_equal(worker_thread, pthread_self())) { + fprintf(stderr, "worker coroutine migrated between pthreads\n"); + abort(); + } + + from = transfer(self, from); + check_context("worker context was resumed by the main pthread", from, + &main_context); + + if (!pthread_equal(worker_thread, pthread_self())) { + fprintf(stderr, "worker coroutine migrated between pthreads\n"); + abort(); + } + } + + completed = 1; + transfer(self, from); + abort(); +} + +static void * +resume_worker(void *argument) +{ + (void)argument; + + struct coroutine_context resumer_context; + coroutine_initialize_main(&resumer_context); + expected_resumer = &resumer_context; + + if (pthread_equal(main_thread, pthread_self()) || + pthread_equal(worker_thread, pthread_self())) { + fprintf(stderr, "pthread resumer did not run on a distinct pthread\n"); + resumer_result = EXIT_FAILURE; + coroutine_destroy(&resumer_context); + return NULL; + } + + struct coroutine_context *from = transfer(&resumer_context, &worker_context); + if (from != &worker_context) { + fprintf(stderr, "pthread resumer was resumed by an unexpected context\n"); + resumer_result = EXIT_FAILURE; + } + + coroutine_destroy(&resumer_context); + return NULL; +} + +int +test_pthread_resume(void) +{ + struct coroutine_stack stack = {0}; + int result = EXIT_FAILURE; + + if (coroutine_stack_allocate(&stack, STACK_SIZE) != 0) { + fprintf(stderr, "failed to allocate coroutine stack\n"); + return EXIT_FAILURE; + } + + iteration = 0; + completed = 0; + resumer_result = EXIT_SUCCESS; + main_thread = pthread_self(); + + coroutine_initialize_main(&main_context); + coroutine_initialize(&worker_context, worker_entry, stack.base, stack.size); + + if (transfer(&main_context, &worker_context) != &worker_context || iteration != 0) { + fprintf(stderr, "worker did not initially yield to the main pthread\n"); + goto finish; + } + + for (unsigned int expected_iteration = 0; + expected_iteration < RESUME_COUNT; + expected_iteration++) { + pthread_t resumer; + int error = pthread_create(&resumer, NULL, resume_worker, NULL); + if (error != 0) { + fprintf(stderr, "failed to create pthread resumer: %d\n", error); + goto finish; + } + + error = pthread_join(resumer, NULL); + if (error != 0 || resumer_result != EXIT_SUCCESS) { + fprintf(stderr, "pthread resumer failed: %d\n", error); + goto finish; + } + + if (transfer(&main_context, &worker_context) != &worker_context) { + fprintf(stderr, "worker did not yield after changing pthread resumer\n"); + goto finish; + } + + if (expected_iteration + 1 < RESUME_COUNT) { + if (iteration != expected_iteration + 1 || completed) { + fprintf(stderr, "worker resumed at an unexpected iteration\n"); + goto finish; + } + } + else if (!completed) { + fprintf(stderr, "worker did not complete pthread resume test\n"); + goto finish; + } + } + + result = EXIT_SUCCESS; + +finish: + coroutine_destroy(&worker_context); + coroutine_destroy(&main_context); + coroutine_stack_free(&stack); + + return result; +} + +#else + +int +test_pthread_resume(void) +{ + return EXIT_SUCCESS; +} + +#endif diff --git a/coroutine/test/test_pthread_resume.h b/coroutine/test/test_pthread_resume.h new file mode 100644 index 00000000000000..76a7cb1eeb10c8 --- /dev/null +++ b/coroutine/test/test_pthread_resume.h @@ -0,0 +1,3 @@ +#pragma once + +int test_pthread_resume(void); diff --git a/depend b/depend index 4099bd57bacdb7..a07aa3a302abe3 100644 --- a/depend +++ b/depend @@ -58,6 +58,7 @@ cont.$(OBJEXT): {$(VPATH)}cont.c coroutine-main.$(OBJEXT): {$(VPATH)}coroutine/test/main.c coroutine-stack.$(OBJEXT): {$(VPATH)}coroutine/test/stack.c coroutine-test_initialize_destroy.$(OBJEXT): {$(VPATH)}coroutine/test/test_initialize_destroy.c +coroutine-test_pthread_resume.$(OBJEXT): {$(VPATH)}coroutine/test/test_pthread_resume.c coroutine-test_transfer_repeat.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_repeat.c coroutine-test_transfer_return.$(OBJEXT): {$(VPATH)}coroutine/test/test_transfer_return.c debug.$(OBJEXT): {$(VPATH)}debug.c