rubyduino 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1628) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +1 -0
  3. data/CHANGELOG.md +12 -0
  4. data/README.md +1 -2
  5. data/examples/hc_sr04.rb +39 -0
  6. data/lib/rubyduino/arduino_uno.rb +76 -0
  7. data/lib/rubyduino/sp_runtime.h +287 -0
  8. data/lib/rubyduino/spinel.rb +1 -1
  9. data/lib/rubyduino/spinel_arduino_codegen.rb +53 -13
  10. data/lib/rubyduino/version.rb +1 -1
  11. data/vendor/spinel/Makefile +463 -54
  12. data/vendor/spinel/README.md +248 -68
  13. data/vendor/spinel/benchmark/bm_jekyll_lite.rb +241 -0
  14. data/vendor/spinel/benchmark/bm_micro_lisp.rb +282 -0
  15. data/vendor/spinel/benchmark/bm_poly_cells.rb +75 -0
  16. data/vendor/spinel/benchmark/bm_range_each.rb +15 -0
  17. data/vendor/spinel/benchmark/bm_sinatra_mini.rb +126 -0
  18. data/vendor/spinel/compiler_helpers.rb +1506 -0
  19. data/vendor/spinel/docs/ANALYZE-IR.md +411 -0
  20. data/vendor/spinel/docs/AST.md +360 -0
  21. data/vendor/spinel/docs/CLASS-OBJECT.md +521 -0
  22. data/vendor/spinel/docs/FFI.md +9 -0
  23. data/vendor/spinel/docs/FLOAT-ROUNDING.md +47 -0
  24. data/vendor/spinel/docs/HASH-NULLABLE.md +379 -0
  25. data/vendor/spinel/docs/INCOMPATIBILITIES.md +102 -0
  26. data/vendor/spinel/docs/RBS-EXTRACT.md +128 -0
  27. data/vendor/spinel/docs/fast-bootstrap.md +74 -0
  28. data/vendor/spinel/docs/issue-646-plan.md +159 -0
  29. data/vendor/spinel/docs/promote-mode-plan.md +227 -0
  30. data/vendor/spinel/docs/setbyte-semantic-plan.md +203 -0
  31. data/vendor/spinel/examples/ffi/sqlite/README.md +7 -6
  32. data/vendor/spinel/examples/ffi/sqlite/sqlite3_lib.rb +7 -4
  33. data/vendor/spinel/experiments/rbs/box.rb +32 -0
  34. data/vendor/spinel/experiments/rbs/box.seed +18 -0
  35. data/vendor/spinel/experiments/rbs/harness.rb +117 -0
  36. data/vendor/spinel/lib/regexp/re_compile.c +265 -25
  37. data/vendor/spinel/lib/regexp/re_exec.c +80 -19
  38. data/vendor/spinel/lib/regexp/re_internal.h +22 -2
  39. data/vendor/spinel/lib/regexp/re_utf8.c +21 -2
  40. data/vendor/spinel/lib/sp_bigint.c +67 -3
  41. data/vendor/spinel/lib/sp_core.c +248 -0
  42. data/vendor/spinel/lib/sp_core.h +37 -0
  43. data/vendor/spinel/lib/sp_crypto.c +583 -0
  44. data/vendor/spinel/lib/sp_crypto.h +79 -0
  45. data/vendor/spinel/lib/sp_fiber.c +95 -0
  46. data/vendor/spinel/lib/sp_fiber.h +41 -0
  47. data/vendor/spinel/lib/sp_gc.c +89 -0
  48. data/vendor/spinel/lib/sp_gc.h +81 -0
  49. data/vendor/spinel/lib/sp_io.c +98 -0
  50. data/vendor/spinel/lib/sp_io.h +31 -0
  51. data/vendor/spinel/lib/sp_net.c +350 -0
  52. data/vendor/spinel/lib/sp_net.h +97 -0
  53. data/vendor/spinel/lib/sp_pack.c +431 -0
  54. data/vendor/spinel/lib/sp_runtime.h +3485 -341
  55. data/vendor/spinel/lib/sp_strscan.c +320 -0
  56. data/vendor/spinel/lib/sp_system.c +173 -0
  57. data/vendor/spinel/lib/sp_system.h +16 -0
  58. data/vendor/spinel/lib/sp_time.c +204 -0
  59. data/vendor/spinel/lib/sp_time.h +63 -0
  60. data/vendor/spinel/lib/sp_types.h +144 -0
  61. data/vendor/spinel/lib/stringio.c +31 -3
  62. data/vendor/spinel/lib/strscan.c +34 -7
  63. data/vendor/spinel/lib/strscan.rb +6 -53
  64. data/vendor/spinel/node_table_loader.rb +150 -0
  65. data/vendor/spinel/spinel +319 -21
  66. data/vendor/spinel/spinel_analyze.rb +37556 -0
  67. data/vendor/spinel/spinel_codegen.rb +47620 -33346
  68. data/vendor/spinel/spinel_parse +0 -0
  69. data/vendor/spinel/spinel_parse.c +801 -91
  70. data/vendor/spinel/spinel_rbs_extract.c +931 -0
  71. data/vendor/spinel/test/alias_attr_reader_source.rb +38 -0
  72. data/vendor/spinel/test/alias_attr_reader_source.rb.expected +7 -0
  73. data/vendor/spinel/test/alias_inherited_method.rb +20 -0
  74. data/vendor/spinel/test/alias_inherited_method.rb.expected +3 -0
  75. data/vendor/spinel/test/alias_method_dispatch.rb +14 -0
  76. data/vendor/spinel/test/analyze_fail/attributes_non_symbol.rb +3 -0
  77. data/vendor/spinel/test/analyze_fail/attributes_non_symbol.stderr.expected +1 -0
  78. data/vendor/spinel/test/analyze_fail/dynamic_define_method.rb +4 -0
  79. data/vendor/spinel/test/analyze_fail/dynamic_define_method.stderr.expected +1 -0
  80. data/vendor/spinel/test/analyze_fail/instance_exec_def_in_block.rb +14 -0
  81. data/vendor/spinel/test/analyze_fail/instance_exec_def_in_block.stderr.expected +1 -0
  82. data/vendor/spinel/test/analyze_fail/instance_exec_define_method.rb +15 -0
  83. data/vendor/spinel/test/analyze_fail/instance_exec_define_method.stderr.expected +1 -0
  84. data/vendor/spinel/test/analyze_fail/instance_exec_no_block.rb +13 -0
  85. data/vendor/spinel/test/analyze_fail/instance_exec_no_block.stderr.expected +1 -0
  86. data/vendor/spinel/test/analyze_fail/nonliteral_define_method_each.rb +7 -0
  87. data/vendor/spinel/test/analyze_fail/nonliteral_define_method_each.stderr.expected +1 -0
  88. data/vendor/spinel/test/argv_gc.rb +45 -0
  89. data/vendor/spinel/test/argv_gc.rb.args +1 -0
  90. data/vendor/spinel/test/argv_gc.rb.expected +6 -0
  91. data/vendor/spinel/test/array_at_out_of_bounds.rb +23 -0
  92. data/vendor/spinel/test/array_at_out_of_bounds.rb.expected +12 -0
  93. data/vendor/spinel/test/array_clear_expr.rb +26 -0
  94. data/vendor/spinel/test/array_clear_expr.rb.expected +10 -0
  95. data/vendor/spinel/test/array_combination_block.rb +11 -0
  96. data/vendor/spinel/test/array_compact_eq.rb +16 -0
  97. data/vendor/spinel/test/array_compact_eq.rb.expected +10 -0
  98. data/vendor/spinel/test/array_fill_poly.rb.expected +1 -0
  99. data/vendor/spinel/test/array_first_last_empty.rb +12 -0
  100. data/vendor/spinel/test/array_first_last_empty.rb.expected +8 -0
  101. data/vendor/spinel/test/array_flatten_typed_elements.rb +19 -0
  102. data/vendor/spinel/test/array_frozen_mutate_raises.rb +36 -0
  103. data/vendor/spinel/test/array_frozen_mutate_raises.rb.expected +20 -0
  104. data/vendor/spinel/test/array_frozen_slice_bang.rb +19 -0
  105. data/vendor/spinel/test/array_frozen_slice_bang.rb.expected +2 -0
  106. data/vendor/spinel/test/array_get_oob_negative.rb +22 -0
  107. data/vendor/spinel/test/array_get_oob_negative.rb.expected +6 -0
  108. data/vendor/spinel/test/array_grep.rb +12 -0
  109. data/vendor/spinel/test/array_grep.rb.expected +8 -0
  110. data/vendor/spinel/test/array_index_int_opt_narrow.rb +30 -0
  111. data/vendor/spinel/test/array_index_int_opt_narrow.rb.expected +7 -0
  112. data/vendor/spinel/test/array_map_bang.rb +28 -0
  113. data/vendor/spinel/test/array_map_bang.rb.expected +7 -0
  114. data/vendor/spinel/test/array_minmax_block.rb +14 -0
  115. data/vendor/spinel/test/array_negative_index_too_small.rb +22 -0
  116. data/vendor/spinel/test/array_negative_index_too_small.rb.expected +5 -0
  117. data/vendor/spinel/test/array_object_id.rb +12 -0
  118. data/vendor/spinel/test/array_object_id.rb.expected +6 -0
  119. data/vendor/spinel/test/array_pack_returns_string.expected +2 -0
  120. data/vendor/spinel/test/array_pack_returns_string.rb +4 -0
  121. data/vendor/spinel/test/array_pack_string_unpack.rb +12 -0
  122. data/vendor/spinel/test/array_pattern_match_basic.rb +27 -0
  123. data/vendor/spinel/test/array_predicate_no_block.rb +14 -0
  124. data/vendor/spinel/test/array_predicate_no_block.rb.expected +11 -0
  125. data/vendor/spinel/test/array_predicate_no_block_param.rb +14 -0
  126. data/vendor/spinel/test/array_predicate_no_block_param.rb.expected +10 -0
  127. data/vendor/spinel/test/array_product.rb +5 -0
  128. data/vendor/spinel/test/array_push_multi_arg.rb +32 -0
  129. data/vendor/spinel/test/array_push_multi_arg.rb.expected +10 -0
  130. data/vendor/spinel/test/array_select_bang.rb +34 -0
  131. data/vendor/spinel/test/array_select_bang.rb.expected +8 -0
  132. data/vendor/spinel/test/array_slice_before_after.rb +6 -0
  133. data/vendor/spinel/test/array_slice_when_chunk.rb +7 -0
  134. data/vendor/spinel/test/array_to_h_sort_bang_block.rb +26 -0
  135. data/vendor/spinel/test/array_to_h_sort_bang_block.rb.expected +10 -0
  136. data/vendor/spinel/test/array_transpose_poly_elements.rb +34 -0
  137. data/vendor/spinel/test/array_transpose_typed_elements.rb +19 -0
  138. data/vendor/spinel/test/array_unresolved_inspect_no_segv.rb +9 -0
  139. data/vendor/spinel/test/array_unresolved_inspect_no_segv.rb.expected +2 -0
  140. data/vendor/spinel/test/at_exit_lifo.rb +6 -0
  141. data/vendor/spinel/test/at_exit_lifo.rb.expected +4 -0
  142. data/vendor/spinel/test/back_ref.rb +15 -1
  143. data/vendor/spinel/test/back_ref.rb.expected +5 -0
  144. data/vendor/spinel/test/bang_op_ruby_truthy.expected +7 -0
  145. data/vendor/spinel/test/bang_op_ruby_truthy.rb +10 -0
  146. data/vendor/spinel/test/bare_arg_in_module_body.rb +29 -0
  147. data/vendor/spinel/test/bare_sibling_cls_method_call.rb +67 -0
  148. data/vendor/spinel/test/bare_sibling_cls_method_call.rb.expected +4 -0
  149. data/vendor/spinel/test/begin_rescue_as_expression.expected +5 -0
  150. data/vendor/spinel/test/begin_rescue_as_expression.rb +14 -0
  151. data/vendor/spinel/test/begin_rescue_else_clause.rb +63 -0
  152. data/vendor/spinel/test/begin_rescue_else_clause.rb.expected +7 -0
  153. data/vendor/spinel/test/bigint_propagation_chain.rb +33 -0
  154. data/vendor/spinel/test/bigint_propagation_chain.rb.expected +9 -0
  155. data/vendor/spinel/test/bigint_volatile_combo.rb +69 -0
  156. data/vendor/spinel/test/bigint_volatile_combo.rb.expected +18 -0
  157. data/vendor/spinel/test/block_given_block_param.rb +21 -3
  158. data/vendor/spinel/test/block_given_block_param.rb.expected +2 -0
  159. data/vendor/spinel/test/block_given_inline_call_site.expected +2 -0
  160. data/vendor/spinel/test/block_given_inline_call_site.rb +9 -0
  161. data/vendor/spinel/test/block_optional_unused.rb +18 -0
  162. data/vendor/spinel/test/block_optional_unused.rb.expected +4 -0
  163. data/vendor/spinel/test/block_param_call_block_given.rb +46 -0
  164. data/vendor/spinel/test/block_param_call_block_given.rb.expected +6 -0
  165. data/vendor/spinel/test/block_param_call_value_return.rb +32 -0
  166. data/vendor/spinel/test/block_param_call_value_return.rb.expected +5 -0
  167. data/vendor/spinel/test/bm_instance_eval.rb +9 -8
  168. data/vendor/spinel/test/bool_case_equality.rb +10 -0
  169. data/vendor/spinel/test/bool_logical_operators.rb +9 -0
  170. data/vendor/spinel/test/bool_logical_operators.rb.expected +8 -0
  171. data/vendor/spinel/test/builtin_array_method_capture.rb +38 -0
  172. data/vendor/spinel/test/builtin_array_method_capture.rb.expected +11 -0
  173. data/vendor/spinel/test/bundle_array_e.rb +250 -0
  174. data/vendor/spinel/test/bundle_array_e.rb.expected +57 -0
  175. data/vendor/spinel/test/bundle_array_f.rb +169 -0
  176. data/vendor/spinel/test/bundle_class_01.rb +43 -0
  177. data/vendor/spinel/test/bundle_class_01.rb.expected +4 -0
  178. data/vendor/spinel/test/bundle_class_03.rb +57 -0
  179. data/vendor/spinel/test/bundle_class_03.rb.expected +6 -0
  180. data/vendor/spinel/test/bundle_class_05.rb +187 -0
  181. data/vendor/spinel/test/bundle_class_05.rb.expected +14 -0
  182. data/vendor/spinel/test/bundle_class_06.rb +201 -0
  183. data/vendor/spinel/test/bundle_class_06.rb.expected +12 -0
  184. data/vendor/spinel/test/bundle_class_07.rb +72 -0
  185. data/vendor/spinel/test/bundle_class_07.rb.expected +6 -0
  186. data/vendor/spinel/test/bundle_class_08.rb +101 -0
  187. data/vendor/spinel/test/bundle_class_08.rb.expected +9 -0
  188. data/vendor/spinel/test/bundle_class_09.rb +171 -0
  189. data/vendor/spinel/test/bundle_class_09.rb.expected +9 -0
  190. data/vendor/spinel/test/bundle_class_10.rb +189 -0
  191. data/vendor/spinel/test/bundle_class_10.rb.expected +5 -0
  192. data/vendor/spinel/test/bundle_class_11.rb +191 -0
  193. data/vendor/spinel/test/bundle_class_11.rb.expected +8 -0
  194. data/vendor/spinel/test/bundle_class_12.rb +234 -0
  195. data/vendor/spinel/test/bundle_class_12.rb.expected +15 -0
  196. data/vendor/spinel/test/bundle_class_13.rb +61 -0
  197. data/vendor/spinel/test/bundle_class_13.rb.expected +2 -0
  198. data/vendor/spinel/test/bundle_class_14.rb +162 -0
  199. data/vendor/spinel/test/bundle_class_14.rb.expected +10 -0
  200. data/vendor/spinel/test/bundle_class_15.rb +184 -0
  201. data/vendor/spinel/test/bundle_class_15.rb.expected +8 -0
  202. data/vendor/spinel/test/bundle_class_18.rb +101 -0
  203. data/vendor/spinel/test/bundle_class_18.rb.expected +13 -0
  204. data/vendor/spinel/test/bundle_class_19.rb +67 -0
  205. data/vendor/spinel/test/bundle_class_19.rb.expected +8 -0
  206. data/vendor/spinel/test/bundle_class_20.rb +122 -0
  207. data/vendor/spinel/test/bundle_class_20.rb.expected +6 -0
  208. data/vendor/spinel/test/bundle_class_21.rb +208 -0
  209. data/vendor/spinel/test/bundle_class_21.rb.expected +27 -0
  210. data/vendor/spinel/test/bundle_class_22.rb +218 -0
  211. data/vendor/spinel/test/bundle_class_22.rb.expected +23 -0
  212. data/vendor/spinel/test/bundle_class_23.rb +187 -0
  213. data/vendor/spinel/test/bundle_class_23.rb.expected +19 -0
  214. data/vendor/spinel/test/bundle_class_a.rb +111 -0
  215. data/vendor/spinel/test/bundle_class_a.rb.expected +14 -0
  216. data/vendor/spinel/test/bundle_classd_04.rb +149 -0
  217. data/vendor/spinel/test/bundle_classd_04.rb.expected +15 -0
  218. data/vendor/spinel/test/bundle_classd_05.rb +65 -0
  219. data/vendor/spinel/test/bundle_classd_05.rb.expected +15 -0
  220. data/vendor/spinel/test/bundle_classd_08.rb +44 -0
  221. data/vendor/spinel/test/bundle_classd_08.rb.expected +4 -0
  222. data/vendor/spinel/test/bundle_classd_09.rb +221 -0
  223. data/vendor/spinel/test/bundle_classd_09.rb.expected +16 -0
  224. data/vendor/spinel/test/bundle_classd_10.rb +77 -0
  225. data/vendor/spinel/test/bundle_classd_10.rb.expected +10 -0
  226. data/vendor/spinel/test/bundle_classd_11.rb +89 -0
  227. data/vendor/spinel/test/bundle_classd_11.rb.expected +6 -0
  228. data/vendor/spinel/test/bundle_classd_12.rb +211 -0
  229. data/vendor/spinel/test/bundle_classd_12.rb.expected +19 -0
  230. data/vendor/spinel/test/bundle_classd_13.rb +246 -0
  231. data/vendor/spinel/test/bundle_classd_13.rb.expected +20 -0
  232. data/vendor/spinel/test/bundle_classd_14.rb +72 -0
  233. data/vendor/spinel/test/bundle_classd_14.rb.expected +10 -0
  234. data/vendor/spinel/test/bundle_classd_17.rb +206 -0
  235. data/vendor/spinel/test/bundle_classd_17.rb.expected +17 -0
  236. data/vendor/spinel/test/bundle_classd_18.rb +89 -0
  237. data/vendor/spinel/test/bundle_classd_18.rb.expected +3 -0
  238. data/vendor/spinel/test/bundle_classd_19.rb +134 -0
  239. data/vendor/spinel/test/bundle_classd_19.rb.expected +23 -0
  240. data/vendor/spinel/test/bundle_classd_26.rb +103 -0
  241. data/vendor/spinel/test/bundle_classd_26.rb.expected +7 -0
  242. data/vendor/spinel/test/bundle_classd_27.rb +241 -0
  243. data/vendor/spinel/test/bundle_classd_27.rb.expected +15 -0
  244. data/vendor/spinel/test/bundle_classd_28.rb +80 -0
  245. data/vendor/spinel/test/bundle_classd_28.rb.expected +10 -0
  246. data/vendor/spinel/test/bundle_classd_32.rb +63 -0
  247. data/vendor/spinel/test/bundle_classd_32.rb.expected +7 -0
  248. data/vendor/spinel/test/bundle_classd_33.rb +117 -0
  249. data/vendor/spinel/test/bundle_classd_33.rb.expected +17 -0
  250. data/vendor/spinel/test/bundle_classd_35.rb +90 -0
  251. data/vendor/spinel/test/bundle_classd_35.rb.expected +5 -0
  252. data/vendor/spinel/test/bundle_classd_36.rb +97 -0
  253. data/vendor/spinel/test/bundle_classd_36.rb.expected +5 -0
  254. data/vendor/spinel/test/bundle_classd_38.rb +50 -0
  255. data/vendor/spinel/test/bundle_classd_38.rb.expected +7 -0
  256. data/vendor/spinel/test/bundle_classd_39.rb +76 -0
  257. data/vendor/spinel/test/bundle_classd_39.rb.expected +15 -0
  258. data/vendor/spinel/test/bundle_classd_43.rb +84 -0
  259. data/vendor/spinel/test/bundle_classd_43.rb.expected +7 -0
  260. data/vendor/spinel/test/bundle_classd_45.rb +115 -0
  261. data/vendor/spinel/test/bundle_classd_45.rb.expected +7 -0
  262. data/vendor/spinel/test/bundle_classd_46.rb +72 -0
  263. data/vendor/spinel/test/bundle_classd_46.rb.expected +5 -0
  264. data/vendor/spinel/test/bundle_classd_48.rb +85 -0
  265. data/vendor/spinel/test/bundle_classd_49.rb +153 -0
  266. data/vendor/spinel/test/bundle_classd_50.rb +200 -0
  267. data/vendor/spinel/test/bundle_classd_50.rb.expected +10 -0
  268. data/vendor/spinel/test/bundle_classd_51.rb +191 -0
  269. data/vendor/spinel/test/bundle_classd_51.rb.expected +12 -0
  270. data/vendor/spinel/test/bundle_file_a.rb +119 -0
  271. data/vendor/spinel/test/bundle_file_a.rb.expected +23 -0
  272. data/vendor/spinel/test/bundle_hash_b1.rb +99 -0
  273. data/vendor/spinel/test/bundle_hash_b2.rb +107 -0
  274. data/vendor/spinel/test/bundle_int_a.rb +104 -0
  275. data/vendor/spinel/test/bundle_int_a.rb.expected +23 -0
  276. data/vendor/spinel/test/bundle_misc_c_08.rb +170 -0
  277. data/vendor/spinel/test/bundle_misc_c_08.rb.expected +33 -0
  278. data/vendor/spinel/test/bundle_misc_c_09.rb +72 -0
  279. data/vendor/spinel/test/bundle_misc_c_09.rb.expected +6 -0
  280. data/vendor/spinel/test/bundle_misc_c_10.rb +73 -0
  281. data/vendor/spinel/test/bundle_misc_c_10.rb.expected +8 -0
  282. data/vendor/spinel/test/bundle_misc_c_17.rb +124 -0
  283. data/vendor/spinel/test/bundle_misc_c_17.rb.expected +18 -0
  284. data/vendor/spinel/test/bundle_misc_c_23.rb +97 -0
  285. data/vendor/spinel/test/bundle_misc_c_23.rb.expected +10 -0
  286. data/vendor/spinel/test/bundle_misc_c_26.rb +107 -0
  287. data/vendor/spinel/test/bundle_misc_c_26.rb.expected +24 -0
  288. data/vendor/spinel/test/bundle_misc_c_30.rb +94 -0
  289. data/vendor/spinel/test/bundle_misc_c_31.rb +89 -0
  290. data/vendor/spinel/test/bundle_misc_c_31.rb.expected +14 -0
  291. data/vendor/spinel/test/bundle_misc_c_33.rb +163 -0
  292. data/vendor/spinel/test/bundle_misc_c_33.rb.expected +53 -0
  293. data/vendor/spinel/test/bundle_misc_c_34.rb +106 -0
  294. data/vendor/spinel/test/bundle_misc_c_34.rb.expected +11 -0
  295. data/vendor/spinel/test/bundle_misc_c_35.rb +143 -0
  296. data/vendor/spinel/test/bundle_misc_c_35.rb.expected +44 -0
  297. data/vendor/spinel/test/bundle_misc_c_36.rb +165 -0
  298. data/vendor/spinel/test/bundle_misc_c_36.rb.expected +59 -0
  299. data/vendor/spinel/test/bundle_misc_c_37.rb +119 -0
  300. data/vendor/spinel/test/bundle_misc_c_37.rb.expected +22 -0
  301. data/vendor/spinel/test/bundle_poly_b1.rb +44 -0
  302. data/vendor/spinel/test/bundle_poly_b1.rb.expected +7 -0
  303. data/vendor/spinel/test/bundle_string_c.rb +250 -0
  304. data/vendor/spinel/test/bundle_string_c.rb.expected +51 -0
  305. data/vendor/spinel/test/bundle_sym.rb +2 -4
  306. data/vendor/spinel/test/bundle_tiny_array.rb +160 -0
  307. data/vendor/spinel/test/bundle_tiny_array.rb.expected +53 -0
  308. data/vendor/spinel/test/bundle_tiny_misc.rb +187 -0
  309. data/vendor/spinel/test/bundle_tiny_misc.rb.expected +59 -0
  310. data/vendor/spinel/test/bundle_tiny_num.rb +87 -0
  311. data/vendor/spinel/test/bundle_tiny_num.rb.expected +27 -0
  312. data/vendor/spinel/test/bundle_tiny_string.rb +226 -0
  313. data/vendor/spinel/test/bundle_tiny_string.rb.expected +78 -0
  314. data/vendor/spinel/test/captured_block_call_value.rb +45 -0
  315. data/vendor/spinel/test/captured_block_call_value.rb.expected +5 -0
  316. data/vendor/spinel/test/case_fallthrough_returns_nil.expected +5 -0
  317. data/vendor/spinel/test/case_fallthrough_returns_nil.rb +8 -0
  318. data/vendor/spinel/test/case_in_no_match_error.rb +54 -0
  319. data/vendor/spinel/test/case_in_no_match_error.rb.expected +6 -0
  320. data/vendor/spinel/test/case_local_in_lambda.rb +57 -0
  321. data/vendor/spinel/test/case_local_in_lambda.rb.expected +5 -0
  322. data/vendor/spinel/test/case_when_class.rb +28 -66
  323. data/vendor/spinel/test/case_when_class.rb.expected +5 -5
  324. data/vendor/spinel/test/case_when_class_scalar_predicate.rb +53 -0
  325. data/vendor/spinel/test/case_when_class_scalar_predicate.rb.expected +3 -0
  326. data/vendor/spinel/test/case_when_poly_scrutinee_bound_to_local.rb +36 -0
  327. data/vendor/spinel/test/case_when_poly_scrutinee_bound_to_local.rb.expected +5 -0
  328. data/vendor/spinel/test/case_when_qualified_class.rb +47 -0
  329. data/vendor/spinel/test/case_when_qualified_class.rb.expected +2 -0
  330. data/vendor/spinel/test/case_when_regex.rb +23 -0
  331. data/vendor/spinel/test/case_when_regex.rb.expected +3 -0
  332. data/vendor/spinel/test/chained_aset_on_poly_hash_recv.rb +19 -0
  333. data/vendor/spinel/test/chained_aset_on_poly_hash_recv.rb.expected +1 -0
  334. data/vendor/spinel/test/clamp_string_and_range.rb +14 -0
  335. data/vendor/spinel/test/clamp_string_and_range.rb.expected +8 -0
  336. data/vendor/spinel/test/class_const_as_value.rb +58 -0
  337. data/vendor/spinel/test/class_const_as_value.rb.expected +4 -0
  338. data/vendor/spinel/test/class_hierarchy.rb +41 -0
  339. data/vendor/spinel/test/class_hierarchy.rb.expected +10 -0
  340. data/vendor/spinel/test/class_hierarchy_builtin.rb +43 -0
  341. data/vendor/spinel/test/class_hierarchy_builtin.rb.expected +14 -0
  342. data/vendor/spinel/test/class_hierarchy_module.rb +55 -0
  343. data/vendor/spinel/test/class_hierarchy_module.rb.expected +6 -0
  344. data/vendor/spinel/test/class_in_collection.rb +22 -0
  345. data/vendor/spinel/test/class_in_collection.rb.expected +10 -0
  346. data/vendor/spinel/test/class_ivar_in_class_method.rb +66 -0
  347. data/vendor/spinel/test/class_ivar_in_class_method.rb.expected +11 -0
  348. data/vendor/spinel/test/class_lt_builtin_parent.rb +13 -0
  349. data/vendor/spinel/test/class_name_in_cmeth.rb +30 -0
  350. data/vendor/spinel/test/class_name_in_cmeth.rb.expected +3 -0
  351. data/vendor/spinel/test/class_of_primitive.rb +14 -0
  352. data/vendor/spinel/test/class_of_primitive.rb.expected +9 -0
  353. data/vendor/spinel/test/class_phase2_name_eq.rb +61 -0
  354. data/vendor/spinel/test/class_phase2_name_eq.rb.expected +15 -0
  355. data/vendor/spinel/test/class_reopen_replaces_methods.rb +40 -0
  356. data/vendor/spinel/test/class_reopen_replaces_methods.rb.expected +3 -0
  357. data/vendor/spinel/test/class_singleton_def.rb +27 -0
  358. data/vendor/spinel/test/class_singleton_def.rb.expected +4 -0
  359. data/vendor/spinel/test/class_superclass.rb +15 -0
  360. data/vendor/spinel/test/class_superclass.rb.expected +3 -0
  361. data/vendor/spinel/test/class_value_builtin_and_nested.rb +36 -0
  362. data/vendor/spinel/test/cls_method_specialization_boxer.expected +2 -0
  363. data/vendor/spinel/test/cls_method_specialization_boxer.rb +47 -0
  364. data/vendor/spinel/test/cmeth_name_collision_with_kernel.rb +56 -0
  365. data/vendor/spinel/test/cmeth_name_collision_with_kernel.rb.expected +5 -0
  366. data/vendor/spinel/test/cmeth_return_nil_or_obj_via_intermediate.rb +89 -0
  367. data/vendor/spinel/test/cmeth_return_nil_or_obj_via_intermediate.rb.expected +6 -0
  368. data/vendor/spinel/test/comparable_between_user_type.rb +26 -0
  369. data/vendor/spinel/test/comparable_between_user_type.rb.expected +10 -0
  370. data/vendor/spinel/test/compile_time_attribute_singular.rb +12 -0
  371. data/vendor/spinel/test/compile_time_attribute_singular.rb.expected +2 -0
  372. data/vendor/spinel/test/compile_time_attribute_wrapped_record.rb +72 -0
  373. data/vendor/spinel/test/compile_time_attribute_wrapped_record.rb.expected +2 -0
  374. data/vendor/spinel/test/compile_time_attributes.rb +14 -0
  375. data/vendor/spinel/test/compile_time_attributes.rb.expected +3 -0
  376. data/vendor/spinel/test/compile_time_define_method_direct.rb +8 -0
  377. data/vendor/spinel/test/compile_time_define_method_direct.rb.expected +2 -0
  378. data/vendor/spinel/test/compile_time_define_method_int.rb +9 -0
  379. data/vendor/spinel/test/compile_time_define_method_int.rb.expected +2 -0
  380. data/vendor/spinel/test/compile_time_define_method_predicates.rb +18 -0
  381. data/vendor/spinel/test/compile_time_define_method_predicates.rb.expected +4 -0
  382. data/vendor/spinel/test/compile_time_define_method_strings.rb +9 -0
  383. data/vendor/spinel/test/compile_time_define_method_strings.rb.expected +2 -0
  384. data/vendor/spinel/test/complex_polar.rb +37 -0
  385. data/vendor/spinel/test/complex_polar.rb.expected +10 -0
  386. data/vendor/spinel/test/concat_chain_operand_gc_root.rb +16 -0
  387. data/vendor/spinel/test/concat_chain_operand_gc_root.rb.expected +4 -0
  388. data/vendor/spinel/test/concat_print_poly_unbox.rb +115 -0
  389. data/vendor/spinel/test/concat_print_poly_unbox.rb.expected +9 -0
  390. data/vendor/spinel/test/const_aliased_class_reopen_include.rb +33 -0
  391. data/vendor/spinel/test/const_aliased_class_reopen_include.rb.expected +2 -0
  392. data/vendor/spinel/test/const_compound_assign.rb +34 -0
  393. data/vendor/spinel/test/const_path_compound_assign.rb +15 -0
  394. data/vendor/spinel/test/const_path_deep_nesting.rb +30 -0
  395. data/vendor/spinel/test/const_path_deep_nesting.rb.expected +2 -0
  396. data/vendor/spinel/test/const_path_unresolved_require.rb +11 -0
  397. data/vendor/spinel/test/const_path_unresolved_require.rb.expected +2 -0
  398. data/vendor/spinel/test/const_self_ref_raises_name_error.rb +44 -0
  399. data/vendor/spinel/test/const_self_ref_raises_name_error.rb.expected +3 -0
  400. data/vendor/spinel/test/crashers_536.rb +32 -0
  401. data/vendor/spinel/test/crashers_536.rb.expected +4 -0
  402. data/vendor/spinel/test/cvar_method_first_assign.rb +6 -0
  403. data/vendor/spinel/test/data_define.rb +27 -0
  404. data/vendor/spinel/test/data_define.rb.expected +5 -0
  405. data/vendor/spinel/test/default_nil_param_with_str_return.rb +30 -0
  406. data/vendor/spinel/test/default_nil_param_with_str_return.rb.expected +4 -0
  407. data/vendor/spinel/test/defined_globals_methods.rb +15 -0
  408. data/vendor/spinel/test/defined_globals_methods.rb.expected +5 -0
  409. data/vendor/spinel/test/defined_op.rb +17 -0
  410. data/vendor/spinel/test/defined_op.rb.expected +8 -0
  411. data/vendor/spinel/test/destructure_typed_ptr_array_return.rb +30 -0
  412. data/vendor/spinel/test/destructure_typed_ptr_array_return.rb.expected +2 -0
  413. data/vendor/spinel/test/dir_exist.rb +2 -0
  414. data/vendor/spinel/test/dir_methods.rb +15 -0
  415. data/vendor/spinel/test/double_splat_kwargs.rb +26 -0
  416. data/vendor/spinel/test/double_splat_kwargs.rb.expected +6 -0
  417. data/vendor/spinel/test/dynamic_class_new.rb +44 -0
  418. data/vendor/spinel/test/dynamic_class_new.rb.expected +4 -0
  419. data/vendor/spinel/test/dynamic_is_a.rb +41 -0
  420. data/vendor/spinel/test/dynamic_is_a.rb.expected +5 -0
  421. data/vendor/spinel/test/each_cons_block_destructure.rb +10 -0
  422. data/vendor/spinel/test/each_cons_with_index_map_no_double_gets.rb +21 -0
  423. data/vendor/spinel/test/each_cons_with_index_map_no_double_gets.rb.expected +3 -0
  424. data/vendor/spinel/test/each_entry_reverse_each.rb +8 -0
  425. data/vendor/spinel/test/each_slice_map.rb +7 -0
  426. data/vendor/spinel/test/empty_array_local_arg_widen.rb +57 -0
  427. data/vendor/spinel/test/empty_array_local_arg_widen.rb.expected +2 -0
  428. data/vendor/spinel/test/empty_array_loop_intermediate_push.rb +103 -0
  429. data/vendor/spinel/test/empty_array_loop_intermediate_push.rb.expected +5 -0
  430. data/vendor/spinel/test/empty_array_push_container_elem.rb +35 -0
  431. data/vendor/spinel/test/empty_array_push_ptr.rb +26 -0
  432. data/vendor/spinel/test/empty_array_push_ptr.rb.expected +3 -0
  433. data/vendor/spinel/test/empty_hash_arg_param_widen.rb +44 -0
  434. data/vendor/spinel/test/empty_hash_arg_param_widen.rb.expected +6 -0
  435. data/vendor/spinel/test/empty_hash_int_int_no_widen.rb +46 -0
  436. data/vendor/spinel/test/empty_hash_int_int_no_widen.rb.expected +4 -0
  437. data/vendor/spinel/test/empty_hash_int_int_promote.rb +32 -0
  438. data/vendor/spinel/test/empty_hash_int_int_promote.rb.expected +8 -0
  439. data/vendor/spinel/test/empty_int_array_min_max.rb +15 -0
  440. data/vendor/spinel/test/empty_int_array_min_max.rb.expected +4 -0
  441. data/vendor/spinel/test/empty_method_returns_nil.rb +8 -0
  442. data/vendor/spinel/test/empty_method_returns_nil.rb.expected +3 -0
  443. data/vendor/spinel/test/empty_symbol_literal.rb +23 -0
  444. data/vendor/spinel/test/empty_symbol_literal.rb.expected +9 -0
  445. data/vendor/spinel/test/eq_string_vs_int.rb +20 -0
  446. data/vendor/spinel/test/eq_string_vs_int.rb.expected +9 -0
  447. data/vendor/spinel/test/exception_backtrace_empty.rb +12 -0
  448. data/vendor/spinel/test/exception_backtrace_empty.rb.expected +4 -0
  449. data/vendor/spinel/test/exception_class_new.rb +46 -0
  450. data/vendor/spinel/test/exception_class_new.rb.expected +10 -0
  451. data/vendor/spinel/test/exception_namespaced_default_message.rb +28 -0
  452. data/vendor/spinel/test/exception_namespaced_default_message.rb.expected +4 -0
  453. data/vendor/spinel/test/exception_object_methods.rb +3 -2
  454. data/vendor/spinel/test/exception_object_methods.rb.expected +1 -1
  455. data/vendor/spinel/test/exception_super_interpolated_message.rb +41 -0
  456. data/vendor/spinel/test/exception_super_interpolated_message.rb.expected +5 -0
  457. data/vendor/spinel/test/exception_user_subclass.rb +35 -0
  458. data/vendor/spinel/test/exception_user_subclass.rb.expected +10 -0
  459. data/vendor/spinel/test/extend_module.rb +30 -0
  460. data/vendor/spinel/test/extend_module.rb.expected +4 -0
  461. data/vendor/spinel/test/ffi_array_specs_decl.rb +25 -0
  462. data/vendor/spinel/test/ffi_int_arg_poly_value.rb +27 -0
  463. data/vendor/spinel/test/ffi_ptr_array.rb +35 -0
  464. data/vendor/spinel/test/ffi_ptr_int_literal.rb +31 -0
  465. data/vendor/spinel/test/ffi_str_arg_poly_value.rb +26 -0
  466. data/vendor/spinel/test/fiber_capture_no_leak_to_sibling.rb +27 -0
  467. data/vendor/spinel/test/fiber_capture_no_leak_to_sibling.rb.expected +1 -0
  468. data/vendor/spinel/test/fiber_gc_root_isolation.rb +76 -0
  469. data/vendor/spinel/test/file_dir_methods.rb +11 -0
  470. data/vendor/spinel/test/file_dir_methods.rb.expected +8 -0
  471. data/vendor/spinel/test/file_expand_path.rb +10 -0
  472. data/vendor/spinel/test/file_expand_path.rb.expected +8 -0
  473. data/vendor/spinel/test/file_mtime.rb +10 -0
  474. data/vendor/spinel/test/file_mtime.rb.expected +2 -0
  475. data/vendor/spinel/test/file_read_count.rb +19 -0
  476. data/vendor/spinel/test/file_read_count.rb.expected +5 -0
  477. data/vendor/spinel/test/file_readlines.rb +15 -0
  478. data/vendor/spinel/test/file_readlines.rb.expected +4 -0
  479. data/vendor/spinel/test/file_write_at_tail_position.rb +16 -0
  480. data/vendor/spinel/test/float_array_first_last_nil.rb +20 -0
  481. data/vendor/spinel/test/float_array_first_last_nil.rb.expected +8 -0
  482. data/vendor/spinel/test/float_divmod_zerodiv.rb +57 -0
  483. data/vendor/spinel/test/float_divmod_zerodiv.rb.expected +7 -0
  484. data/vendor/spinel/test/float_limit_constants.rb +14 -0
  485. data/vendor/spinel/test/float_round_ndigits_return_type.rb +24 -0
  486. data/vendor/spinel/test/float_round_ndigits_return_type.rb.expected +14 -0
  487. data/vendor/spinel/test/float_round_value_based_type.rb +17 -0
  488. data/vendor/spinel/test/float_round_value_based_type.rb.expected +14 -0
  489. data/vendor/spinel/test/float_round_zero.rb +18 -0
  490. data/vendor/spinel/test/float_round_zero.rb.expected +11 -0
  491. data/vendor/spinel/test/float_spaceship.rb +13 -0
  492. data/vendor/spinel/test/float_step_block.rb +16 -0
  493. data/vendor/spinel/test/float_step_block.rb.expected +3 -0
  494. data/vendor/spinel/test/float_step_to_a.rb +20 -0
  495. data/vendor/spinel/test/float_step_to_a.rb.expected +10 -0
  496. data/vendor/spinel/test/gc_grandparent_ivar_scan.rb +28 -0
  497. data/vendor/spinel/test/gc_grandparent_ivar_scan.rb.expected +2 -0
  498. data/vendor/spinel/test/gc_poly_local_int.rb +11 -0
  499. data/vendor/spinel/test/gc_poly_local_int.rb.expected +1 -0
  500. data/vendor/spinel/test/gc_poly_local_root.rb +19 -0
  501. data/vendor/spinel/test/gc_poly_local_root.rb.expected +1 -0
  502. data/vendor/spinel/test/gc_poly_local_setjmp.rb +16 -0
  503. data/vendor/spinel/test/gc_poly_local_setjmp.rb.expected +1 -0
  504. data/vendor/spinel/test/gc_root_call_args.rb +35 -0
  505. data/vendor/spinel/test/gc_root_call_args.rb.expected +3 -0
  506. data/vendor/spinel/test/gc_root_heap_param.rb +21 -0
  507. data/vendor/spinel/test/gc_root_heap_param.rb.expected +1 -0
  508. data/vendor/spinel/test/global_multi_assign.rb +28 -0
  509. data/vendor/spinel/test/global_var_punct.rb +9 -0
  510. data/vendor/spinel/test/gsub_sub_block.rb +14 -0
  511. data/vendor/spinel/test/gsub_sub_block.rb.expected +7 -0
  512. data/vendor/spinel/test/gsub_with_hash_replacement.rb +19 -0
  513. data/vendor/spinel/test/gsub_with_hash_replacement.rb.expected +3 -0
  514. data/vendor/spinel/test/hangs_548.rb +66 -0
  515. data/vendor/spinel/test/hangs_548.rb.expected +4 -0
  516. data/vendor/spinel/test/hash_block_methods_variants.rb +15 -0
  517. data/vendor/spinel/test/hash_block_methods_variants.rb.expected +12 -0
  518. data/vendor/spinel/test/hash_clear.rb +43 -0
  519. data/vendor/spinel/test/hash_clear.rb.expected +12 -0
  520. data/vendor/spinel/test/hash_compound_assign_more.rb +21 -0
  521. data/vendor/spinel/test/hash_delete_if_select_reject_bang.rb +19 -0
  522. data/vendor/spinel/test/hash_delete_if_select_reject_bang.rb.expected +4 -0
  523. data/vendor/spinel/test/hash_delete_returns_value.rb +21 -0
  524. data/vendor/spinel/test/hash_delete_returns_value.rb.expected +8 -0
  525. data/vendor/spinel/test/hash_dispatch_missing_cells.rb +24 -0
  526. data/vendor/spinel/test/hash_dispatch_missing_cells.rb.expected +8 -0
  527. data/vendor/spinel/test/hash_each_key_value.rb +85 -0
  528. data/vendor/spinel/test/hash_each_key_value.rb.expected +72 -0
  529. data/vendor/spinel/test/hash_each_with_object.rb +16 -0
  530. data/vendor/spinel/test/hash_each_with_object.rb.expected +2 -0
  531. data/vendor/spinel/test/hash_fetch_block.rb +31 -0
  532. data/vendor/spinel/test/hash_fetch_block.rb.expected +13 -0
  533. data/vendor/spinel/test/hash_fetch_block_key.rb +9 -0
  534. data/vendor/spinel/test/hash_fetch_block_key.rb.expected +4 -0
  535. data/vendor/spinel/test/hash_fetch_block_key_type.rb +37 -0
  536. data/vendor/spinel/test/hash_fetch_block_key_type.rb.expected +7 -0
  537. data/vendor/spinel/test/hash_fetch_keyerror.rb +23 -0
  538. data/vendor/spinel/test/hash_flatten_rassoc_compact.rb +33 -0
  539. data/vendor/spinel/test/hash_float_value_preserved.rb +27 -0
  540. data/vendor/spinel/test/hash_float_value_preserved.rb.expected +6 -0
  541. data/vendor/spinel/test/hash_index_assign_expression.rb +21 -0
  542. data/vendor/spinel/test/hash_inspect_poly.rb +13 -0
  543. data/vendor/spinel/test/hash_inspect_poly.rb.expected +6 -0
  544. data/vendor/spinel/test/hash_int_and_or_assign.rb +30 -0
  545. data/vendor/spinel/test/hash_int_and_or_assign.rb.expected +8 -0
  546. data/vendor/spinel/test/hash_invert.rb +16 -0
  547. data/vendor/spinel/test/hash_invert.rb.expected +4 -0
  548. data/vendor/spinel/test/hash_key_via_intermediate_local.rb +32 -0
  549. data/vendor/spinel/test/hash_map_block.rb +6 -0
  550. data/vendor/spinel/test/hash_merge_with_block.rb +10 -0
  551. data/vendor/spinel/test/hash_new_default_block_assign.rb +16 -0
  552. data/vendor/spinel/test/hash_param_each_kv_inference.rb +67 -0
  553. data/vendor/spinel/test/hash_param_each_kv_inference.rb.expected +3 -0
  554. data/vendor/spinel/test/hash_param_each_kv_weak_default.rb +75 -0
  555. data/vendor/spinel/test/hash_param_each_kv_weak_default.rb.expected +6 -0
  556. data/vendor/spinel/test/hash_param_widen_from_callee_slot.rb +43 -0
  557. data/vendor/spinel/test/hash_replace_variants.rb +17 -0
  558. data/vendor/spinel/test/hash_replace_variants.rb.expected +6 -0
  559. data/vendor/spinel/test/hash_temp_gc_root.rb +26 -0
  560. data/vendor/spinel/test/hash_transform_keys_cross.rb +12 -0
  561. data/vendor/spinel/test/hash_transform_keys_cross.rb.expected +5 -0
  562. data/vendor/spinel/test/hash_value_p.rb +19 -0
  563. data/vendor/spinel/test/hash_value_p.rb.expected +7 -0
  564. data/vendor/spinel/test/hash_value_split_widen.rb +22 -0
  565. data/vendor/spinel/test/hash_value_split_widen.rb.expected +2 -0
  566. data/vendor/spinel/test/hash_values_at_fetch_values.rb +23 -0
  567. data/vendor/spinel/test/hash_variant_call_boundary_sym.rb +27 -0
  568. data/vendor/spinel/test/hash_variant_call_boundary_sym.rb.expected +2 -0
  569. data/vendor/spinel/test/heterogeneous_tuple_return.rb +32 -0
  570. data/vendor/spinel/test/heterogeneous_tuple_return.rb.expected +7 -0
  571. data/vendor/spinel/test/i1006.rb +21 -0
  572. data/vendor/spinel/test/i1006.rb.expected +7 -0
  573. data/vendor/spinel/test/i1007.rb +25 -0
  574. data/vendor/spinel/test/i1007.rb.expected +4 -0
  575. data/vendor/spinel/test/i1009.rb +9 -0
  576. data/vendor/spinel/test/i1009.rb.expected +4 -0
  577. data/vendor/spinel/test/i1011.rb +12 -0
  578. data/vendor/spinel/test/i1011.rb.expected +2 -0
  579. data/vendor/spinel/test/i1015.rb +11 -0
  580. data/vendor/spinel/test/i1015.rb.expected +2 -0
  581. data/vendor/spinel/test/i1017.rb +37 -0
  582. data/vendor/spinel/test/i1017.rb.expected +1 -0
  583. data/vendor/spinel/test/i1020.rb +31 -0
  584. data/vendor/spinel/test/i1020.rb.expected +2 -0
  585. data/vendor/spinel/test/i1021.rb +12 -0
  586. data/vendor/spinel/test/i1021.rb.expected +9 -0
  587. data/vendor/spinel/test/i1027.rb +28 -0
  588. data/vendor/spinel/test/i1027.rb.expected +1 -0
  589. data/vendor/spinel/test/i1031.rb +64 -0
  590. data/vendor/spinel/test/i1031.rb.expected +4 -0
  591. data/vendor/spinel/test/i1035.rb +18 -0
  592. data/vendor/spinel/test/i1035.rb.expected +3 -0
  593. data/vendor/spinel/test/i1038.rb +7 -0
  594. data/vendor/spinel/test/i1038.rb.expected +4 -0
  595. data/vendor/spinel/test/i1041.rb +31 -0
  596. data/vendor/spinel/test/i1041.rb.expected +4 -0
  597. data/vendor/spinel/test/i709.rb +41 -0
  598. data/vendor/spinel/test/i709.rb.expected +3 -0
  599. data/vendor/spinel/test/i898.rb +23 -0
  600. data/vendor/spinel/test/i898.rb.expected +7 -0
  601. data/vendor/spinel/test/i912.rb +15 -0
  602. data/vendor/spinel/test/i912.rb.expected +8 -0
  603. data/vendor/spinel/test/i916.rb +14 -0
  604. data/vendor/spinel/test/i916.rb.expected +4 -0
  605. data/vendor/spinel/test/i918.rb +45 -0
  606. data/vendor/spinel/test/i918.rb.expected +13 -0
  607. data/vendor/spinel/test/i919.rb +12 -0
  608. data/vendor/spinel/test/i919.rb.expected +5 -0
  609. data/vendor/spinel/test/i920.rb +9 -0
  610. data/vendor/spinel/test/i920.rb.expected +4 -0
  611. data/vendor/spinel/test/i960.rb +13 -0
  612. data/vendor/spinel/test/i960.rb.expected +5 -0
  613. data/vendor/spinel/test/i972.rb +38 -0
  614. data/vendor/spinel/test/i972.rb.expected +11 -0
  615. data/vendor/spinel/test/i974.rb +23 -0
  616. data/vendor/spinel/test/i974.rb.expected +19 -0
  617. data/vendor/spinel/test/i994.rb +18 -0
  618. data/vendor/spinel/test/i994.rb.expected +9 -0
  619. data/vendor/spinel/test/i997.rb +20 -0
  620. data/vendor/spinel/test/i997.rb.expected +6 -0
  621. data/vendor/spinel/test/if_expr_multi_stmt_body.rb +78 -0
  622. data/vendor/spinel/test/if_expr_multi_stmt_body.rb.expected +13 -0
  623. data/vendor/spinel/test/implicit_alloc_site_cache_helper_specialization.rb +112 -0
  624. data/vendor/spinel/test/implicit_alloc_site_cache_helper_specialization.rb.expected +6 -0
  625. data/vendor/spinel/test/implicit_alloc_site_escape_layout.rb +30 -0
  626. data/vendor/spinel/test/implicit_alloc_site_escape_layout.rb.expected +2 -0
  627. data/vendor/spinel/test/implicit_alloc_site_escape_param.rb +27 -0
  628. data/vendor/spinel/test/implicit_alloc_site_escape_param.rb.expected +2 -0
  629. data/vendor/spinel/test/implicit_alloc_site_escape_scoped_locals.rb +44 -0
  630. data/vendor/spinel/test/implicit_alloc_site_escape_scoped_locals.rb.expected +2 -0
  631. data/vendor/spinel/test/implicit_alloc_site_holder_poly_array.rb +23 -0
  632. data/vendor/spinel/test/implicit_alloc_site_holder_poly_array.rb.expected +3 -0
  633. data/vendor/spinel/test/implicit_alloc_site_holder_shared_method.rb +23 -0
  634. data/vendor/spinel/test/implicit_alloc_site_holder_shared_method.rb.expected +3 -0
  635. data/vendor/spinel/test/implicit_alloc_site_holder_specialization.rb +19 -0
  636. data/vendor/spinel/test/implicit_alloc_site_holder_specialization.rb.expected +3 -0
  637. data/vendor/spinel/test/implicit_alloc_site_ivar_helper_return_specialization.rb +46 -0
  638. data/vendor/spinel/test/implicit_alloc_site_ivar_helper_return_specialization.rb.expected +3 -0
  639. data/vendor/spinel/test/implicit_alloc_site_store_specialization.rb +28 -0
  640. data/vendor/spinel/test/implicit_alloc_site_store_specialization.rb.expected +4 -0
  641. data/vendor/spinel/test/implicit_new_local_method_scope.rb +65 -0
  642. data/vendor/spinel/test/implicit_new_local_method_scope.rb.expected +2 -0
  643. data/vendor/spinel/test/include_chain_const_lookup.rb +48 -0
  644. data/vendor/spinel/test/include_chain_const_lookup.rb.expected +3 -0
  645. data/vendor/spinel/test/include_chain_module_nested_const.rb +62 -0
  646. data/vendor/spinel/test/include_chain_module_nested_const.rb.expected +2 -0
  647. data/vendor/spinel/test/include_constant_path.rb +98 -0
  648. data/vendor/spinel/test/include_constant_path.rb.expected +5 -0
  649. data/vendor/spinel/test/include_nested_module.rb +99 -0
  650. data/vendor/spinel/test/include_nested_module.rb.expected +3 -0
  651. data/vendor/spinel/test/include_skips_module_class_methods.rb +27 -0
  652. data/vendor/spinel/test/include_skips_module_class_methods.rb.expected +3 -0
  653. data/vendor/spinel/test/inherited_factory_poly_param_narrow_attr.rb +25 -0
  654. data/vendor/spinel/test/inherited_kwarg_nil_default_string_ivar.rb +61 -0
  655. data/vendor/spinel/test/inherited_kwarg_nil_default_string_ivar.rb.expected +2 -0
  656. data/vendor/spinel/test/inherited_method_kwarg_block_self.rb +67 -0
  657. data/vendor/spinel/test/inherited_method_kwarg_block_self.rb.expected +2 -0
  658. data/vendor/spinel/test/initialize_override_param_inference.rb +68 -0
  659. data/vendor/spinel/test/inject_symbol_op.rb +11 -0
  660. data/vendor/spinel/test/inject_symbol_op.rb.expected +7 -0
  661. data/vendor/spinel/test/inject_symbol_to_proc.rb +41 -0
  662. data/vendor/spinel/test/inject_symbol_to_proc.rb.expected +12 -0
  663. data/vendor/spinel/test/inlined_yield_block_param_type.rb +32 -0
  664. data/vendor/spinel/test/inlined_yield_block_param_type.rb.expected +2 -0
  665. data/vendor/spinel/test/inlined_yield_self_locals.rb +49 -0
  666. data/vendor/spinel/test/inlined_yield_self_locals.rb.expected +5 -0
  667. data/vendor/spinel/test/instance_eval_block_params.rb +93 -0
  668. data/vendor/spinel/test/instance_eval_block_params.rb.expected +10 -0
  669. data/vendor/spinel/test/instance_eval_trampoline.rb +17 -0
  670. data/vendor/spinel/test/instance_eval_trampoline.rb.expected +1 -0
  671. data/vendor/spinel/test/instance_exec_capture.rb +48 -0
  672. data/vendor/spinel/test/instance_exec_capture.rb.expected +7 -0
  673. data/vendor/spinel/test/instance_exec_direct.rb +72 -0
  674. data/vendor/spinel/test/instance_exec_direct.rb.expected +5 -0
  675. data/vendor/spinel/test/instance_exec_override.rb +69 -0
  676. data/vendor/spinel/test/instance_exec_override.rb.expected +4 -0
  677. data/vendor/spinel/test/instance_exec_parity.rb +44 -0
  678. data/vendor/spinel/test/instance_exec_parity.rb.expected +6 -0
  679. data/vendor/spinel/test/instance_exec_return.rb +54 -0
  680. data/vendor/spinel/test/instance_exec_return.rb.expected +7 -0
  681. data/vendor/spinel/test/instance_exec_trampoline.rb +101 -0
  682. data/vendor/spinel/test/instance_exec_trampoline.rb.expected +6 -0
  683. data/vendor/spinel/test/instance_exec_valuetype.rb +48 -0
  684. data/vendor/spinel/test/instance_exec_valuetype.rb.expected +7 -0
  685. data/vendor/spinel/test/instance_methods_false.rb +41 -0
  686. data/vendor/spinel/test/instance_methods_false.rb.expected +14 -0
  687. data/vendor/spinel/test/int_array_bracket_assign_expr.rb +30 -0
  688. data/vendor/spinel/test/int_array_bracket_assign_expr.rb.expected +8 -0
  689. data/vendor/spinel/test/int_array_tally.rb +14 -0
  690. data/vendor/spinel/test/int_array_tally.rb.expected +8 -0
  691. data/vendor/spinel/test/int_array_uniq_bang.rb +15 -0
  692. data/vendor/spinel/test/int_array_uniq_bang.rb.expected +3 -0
  693. data/vendor/spinel/test/int_ceil_floor_round.rb +11 -0
  694. data/vendor/spinel/test/int_ceil_floor_round.rb.expected +8 -0
  695. data/vendor/spinel/test/int_ceil_floor_round_truncate_ndigits.rb +17 -0
  696. data/vendor/spinel/test/int_ceil_floor_round_truncate_ndigits.rb.expected +12 -0
  697. data/vendor/spinel/test/int_hash_missing_nil_check.rb +27 -0
  698. data/vendor/spinel/test/int_hash_missing_nil_check.rb.expected +14 -0
  699. data/vendor/spinel/test/int_int_hash_literal.rb +13 -0
  700. data/vendor/spinel/test/int_int_hash_literal.rb.expected +7 -0
  701. data/vendor/spinel/test/int_int_hash_missing_key_nil.rb +10 -0
  702. data/vendor/spinel/test/int_int_hash_missing_key_nil.rb.expected +8 -0
  703. data/vendor/spinel/test/int_opt_p_inspect.rb +8 -0
  704. data/vendor/spinel/test/int_opt_p_inspect.rb.expected +5 -0
  705. data/vendor/spinel/test/int_overflow_promote.rb +41 -0
  706. data/vendor/spinel/test/int_overflow_raises.rb +34 -0
  707. data/vendor/spinel/test/int_overflow_raises.rb.expected +6 -0
  708. data/vendor/spinel/test/int_round_large_precision.rb +11 -0
  709. data/vendor/spinel/test/int_round_large_precision.rb.expected +8 -0
  710. data/vendor/spinel/test/integer_argument_error.rb +116 -0
  711. data/vendor/spinel/test/integer_argument_error.rb.expected +36 -0
  712. data/vendor/spinel/test/integer_extra_methods.rb +12 -0
  713. data/vendor/spinel/test/integer_extra_methods.rb.expected +8 -0
  714. data/vendor/spinel/test/integer_pow_negative_exponent.rb +17 -0
  715. data/vendor/spinel/test/integer_pow_negative_exponent.rb.expected +6 -0
  716. data/vendor/spinel/test/integer_times_as_expr.rb +9 -0
  717. data/vendor/spinel/test/integer_times_as_expr.rb.expected +3 -0
  718. data/vendor/spinel/test/interp_array_container.rb +41 -0
  719. data/vendor/spinel/test/interp_array_container.rb.expected +8 -0
  720. data/vendor/spinel/test/interpolation_multistmt.rb +15 -0
  721. data/vendor/spinel/test/interpolation_multistmt.rb.expected +3 -0
  722. data/vendor/spinel/test/interpolation_symbol_name.rb +23 -0
  723. data/vendor/spinel/test/interpolation_symbol_name.rb.expected +6 -0
  724. data/vendor/spinel/test/introspection_constant_path.rb +60 -0
  725. data/vendor/spinel/test/introspection_constant_path.rb.expected +13 -0
  726. data/vendor/spinel/test/io_pipe.rb +6 -0
  727. data/vendor/spinel/test/io_pipe.rb.expected +3 -0
  728. data/vendor/spinel/test/is_a_descendant_runtime.rb +59 -0
  729. data/vendor/spinel/test/is_a_descendant_runtime.rb.expected +11 -0
  730. data/vendor/spinel/test/is_a_included_module.rb +28 -0
  731. data/vendor/spinel/test/is_a_included_module.rb.expected +9 -0
  732. data/vendor/spinel/test/ivar_assign_and_expr.rb +22 -0
  733. data/vendor/spinel/test/ivar_assign_and_expr.rb.expected +3 -0
  734. data/vendor/spinel/test/ivar_lazy_init_push_via_inherited_getter.rb +67 -0
  735. data/vendor/spinel/test/ivar_lazy_init_push_via_inherited_getter.rb.expected +6 -0
  736. data/vendor/spinel/test/json_generate_basic.rb +16 -0
  737. data/vendor/spinel/test/json_generate_basic.rb.expected +12 -0
  738. data/vendor/spinel/test/json_generate_struct_record.rb +9 -0
  739. data/vendor/spinel/test/json_generate_struct_record.rb.expected +4 -0
  740. data/vendor/spinel/test/kernel_complex.rb +9 -0
  741. data/vendor/spinel/test/kernel_dir.rb +9 -0
  742. data/vendor/spinel/test/kernel_dir.rb.expected +2 -0
  743. data/vendor/spinel/test/kernel_float_strict.rb +16 -0
  744. data/vendor/spinel/test/kernel_float_strict.rb.expected +6 -0
  745. data/vendor/spinel/test/kernel_integer_with_base.rb +16 -0
  746. data/vendor/spinel/test/kernel_integer_with_base.rb.expected +6 -0
  747. data/vendor/spinel/test/kwarg_back_propagate_callee_type.rb +26 -0
  748. data/vendor/spinel/test/kwarg_back_propagate_callee_type.rb.expected +1 -0
  749. data/vendor/spinel/test/kwargs_double_splat.rb +15 -0
  750. data/vendor/spinel/test/kwargs_double_splat.rb.expected +2 -0
  751. data/vendor/spinel/test/kwargs_merge_default_hash.rb +51 -0
  752. data/vendor/spinel/test/lambda_arrow_string_return.rb +8 -0
  753. data/vendor/spinel/test/lambda_arrow_string_return.rb.expected +2 -0
  754. data/vendor/spinel/test/lambda_inference_chain.rb +35 -0
  755. data/vendor/spinel/test/lambda_inference_chain.rb.expected +5 -0
  756. data/vendor/spinel/test/local_empty_array_push_promotion_propagates.rb +61 -0
  757. data/vendor/spinel/test/local_empty_array_push_promotion_propagates.rb.expected +3 -0
  758. data/vendor/spinel/test/logical_pointer_guard.rb +20 -0
  759. data/vendor/spinel/test/logical_pointer_guard.rb.expected +2 -0
  760. data/vendor/spinel/test/logical_value_return.rb +15 -0
  761. data/vendor/spinel/test/lv_widen_from_callee_arg_slot.rb +56 -0
  762. data/vendor/spinel/test/lv_widen_from_callee_arg_slot.rb.expected +1 -0
  763. data/vendor/spinel/test/map_block_string_new_join.rb +33 -0
  764. data/vendor/spinel/test/map_block_string_new_join.rb.expected +3 -0
  765. data/vendor/spinel/test/map_obj_array_poly_field.rb +28 -0
  766. data/vendor/spinel/test/map_recv_emitted_once.rb +44 -0
  767. data/vendor/spinel/test/map_recv_emitted_once.rb.expected +7 -0
  768. data/vendor/spinel/test/map_to_heap_objects.rb +16 -0
  769. data/vendor/spinel/test/map_to_heap_objects.rb.expected +4 -0
  770. data/vendor/spinel/test/map_to_value_type_objects.rb +26 -0
  771. data/vendor/spinel/test/map_to_value_type_objects.rb.expected +7 -0
  772. data/vendor/spinel/test/masgn_classvar_target.rb +13 -0
  773. data/vendor/spinel/test/masgn_constpath_target.rb +11 -0
  774. data/vendor/spinel/test/method_level_rescue_return.rb +42 -0
  775. data/vendor/spinel/test/method_level_rescue_return.rb.expected +6 -0
  776. data/vendor/spinel/test/method_level_rescue_return_string.rb +39 -0
  777. data/vendor/spinel/test/method_level_rescue_return_string.rb.expected +6 -0
  778. data/vendor/spinel/test/method_return_mixed_int_string.rb +65 -0
  779. data/vendor/spinel/test/method_return_mixed_int_string.rb.expected +10 -0
  780. data/vendor/spinel/test/middle_splat_param.rb +14 -0
  781. data/vendor/spinel/test/middle_splat_param.rb.expected +4 -0
  782. data/vendor/spinel/test/missing_positional_arg_warns.rb +23 -0
  783. data/vendor/spinel/test/missing_positional_arg_warns.rb.expected +2 -0
  784. data/vendor/spinel/test/module_accessor_delete_chain.rb +32 -0
  785. data/vendor/spinel/test/module_accessor_delete_chain.rb.expected +1 -0
  786. data/vendor/spinel/test/module_accessor_nil_check.rb +40 -0
  787. data/vendor/spinel/test/module_accessor_nil_check.rb.expected +2 -0
  788. data/vendor/spinel/test/module_body_string_const_interp.rb +40 -0
  789. data/vendor/spinel/test/module_body_string_const_interp.rb.expected +5 -0
  790. data/vendor/spinel/test/module_class_body_side_effects.rb +32 -0
  791. data/vendor/spinel/test/module_class_body_side_effects.rb.expected +7 -0
  792. data/vendor/spinel/test/module_class_reflection.rb +27 -0
  793. data/vendor/spinel/test/module_class_reflection.rb.expected +14 -0
  794. data/vendor/spinel/test/module_const_defined.rb +13 -0
  795. data/vendor/spinel/test/module_const_defined.rb.expected +4 -0
  796. data/vendor/spinel/test/module_cvars.rb +32 -0
  797. data/vendor/spinel/test/module_cvars.rb.expected +4 -0
  798. data/vendor/spinel/test/module_def_self_ivar.rb +14 -0
  799. data/vendor/spinel/test/module_def_self_ivar.rb.expected +2 -0
  800. data/vendor/spinel/test/module_function_dead_method_dce.rb +29 -0
  801. data/vendor/spinel/test/module_function_dead_method_dce.rb.expected +2 -0
  802. data/vendor/spinel/test/module_function_namespace.rb.expected +3 -0
  803. data/vendor/spinel/test/module_function_str_method_param.rb +14 -0
  804. data/vendor/spinel/test/module_function_str_method_param.rb.expected +1 -0
  805. data/vendor/spinel/test/module_ivar_empty_hash_reset.rb +64 -0
  806. data/vendor/spinel/test/module_ivar_empty_hash_reset.rb.expected +7 -0
  807. data/vendor/spinel/test/module_ivar_nil_poly.rb +17 -0
  808. data/vendor/spinel/test/module_ivar_op_write.rb +53 -0
  809. data/vendor/spinel/test/module_ivar_op_write.rb.expected +3 -0
  810. data/vendor/spinel/test/module_reopen_after_include_ivar.rb +33 -0
  811. data/vendor/spinel/test/module_reopen_after_include_ivar.rb.expected +2 -0
  812. data/vendor/spinel/test/module_reopen_after_include_namespaced.rb +33 -0
  813. data/vendor/spinel/test/module_reopen_after_include_namespaced.rb.expected +1 -0
  814. data/vendor/spinel/test/module_reopen_cmeth_redefine.rb +26 -0
  815. data/vendor/spinel/test/module_reopen_cmeth_redefine.rb.expected +2 -0
  816. data/vendor/spinel/test/module_singleton_attr_accessor.rb +41 -0
  817. data/vendor/spinel/test/module_singleton_attr_accessor.rb.expected +4 -0
  818. data/vendor/spinel/test/module_singleton_new_user_method.rb +23 -0
  819. data/vendor/spinel/test/module_singleton_new_user_method.rb.expected +2 -0
  820. data/vendor/spinel/test/module_singleton_self_accessor.rb +31 -0
  821. data/vendor/spinel/test/module_singleton_self_accessor.rb.expected +4 -0
  822. data/vendor/spinel/test/more_truthies_nil_compare.rb +31 -0
  823. data/vendor/spinel/test/more_truthies_nil_compare.rb.expected +9 -0
  824. data/vendor/spinel/test/multi_arg_lambda.rb +31 -0
  825. data/vendor/spinel/test/multi_arg_lambda.rb.expected +6 -0
  826. data/vendor/spinel/test/multi_assign_ivar_type.rb +21 -0
  827. data/vendor/spinel/test/multi_assign_ivar_type.rb.expected +3 -0
  828. data/vendor/spinel/test/multi_write_call_rhs_as_expr.rb +31 -0
  829. data/vendor/spinel/test/multi_write_call_rhs_as_expr.rb.expected +10 -0
  830. data/vendor/spinel/test/multi_write_scalar_rhs.rb +12 -0
  831. data/vendor/spinel/test/multi_write_scalar_rhs.rb.expected +5 -0
  832. data/vendor/spinel/test/mutable_str_alias_uaf.rb +62 -0
  833. data/vendor/spinel/test/mutable_str_alias_uaf.rb.expected +4 -0
  834. data/vendor/spinel/test/mutable_str_force_encoding_return.rb +21 -0
  835. data/vendor/spinel/test/mutable_str_force_encoding_return.rb.expected +1 -0
  836. data/vendor/spinel/test/mutable_str_into_str_array.rb +46 -0
  837. data/vendor/spinel/test/mutable_str_into_str_array.rb.expected +7 -0
  838. data/vendor/spinel/test/mutex_synchronize_block.rb +29 -0
  839. data/vendor/spinel/test/mutex_synchronize_block.rb.expected +6 -0
  840. data/vendor/spinel/test/narrow_and_predicate.rb +23 -0
  841. data/vendor/spinel/test/narrow_and_predicate.rb.expected +1 -0
  842. data/vendor/spinel/test/negative_range_slice.rb +32 -0
  843. data/vendor/spinel/test/negative_range_slice.rb.expected +12 -0
  844. data/vendor/spinel/test/nested_array_eq.rb +22 -0
  845. data/vendor/spinel/test/nested_array_eq.rb.expected +12 -0
  846. data/vendor/spinel/test/nested_yield_with_block_param.rb +33 -0
  847. data/vendor/spinel/test/nil_default_param_widens_to_poly.rb +26 -0
  848. data/vendor/spinel/test/nil_default_param_widens_to_poly.rb.expected +4 -0
  849. data/vendor/spinel/test/nil_guard_narrows_int_or_nil.rb +40 -0
  850. data/vendor/spinel/test/nil_guard_narrows_int_or_nil.rb.expected +5 -0
  851. data/vendor/spinel/test/not_match_op.rb +8 -0
  852. data/vendor/spinel/test/not_match_op.rb.expected +4 -0
  853. data/vendor/spinel/test/nullable_hash_recv_bracket.expected +3 -0
  854. data/vendor/spinel/test/nullable_hash_recv_bracket.rb +33 -0
  855. data/vendor/spinel/test/nullable_obj_nil_compare.rb +92 -0
  856. data/vendor/spinel/test/nullable_obj_nil_compare.rb.expected +8 -0
  857. data/vendor/spinel/test/nullable_poly_hash_arg_unbox.rb +85 -0
  858. data/vendor/spinel/test/nullable_poly_hash_arg_unbox.rb.expected +4 -0
  859. data/vendor/spinel/test/numeric_coerce.rb +24 -0
  860. data/vendor/spinel/test/numeric_coerce.rb.expected +9 -0
  861. data/vendor/spinel/test/obj_class_dispatch.rb +69 -0
  862. data/vendor/spinel/test/obj_class_dispatch.rb.expected +4 -0
  863. data/vendor/spinel/test/object_id_basic.rb +19 -0
  864. data/vendor/spinel/test/object_id_basic.rb.expected +8 -0
  865. data/vendor/spinel/test/object_method_gaps_callee_clone_itself.rb +23 -0
  866. data/vendor/spinel/test/object_method_gaps_callee_clone_itself.rb.expected +7 -0
  867. data/vendor/spinel/test/op_assign_user_operator.rb.expected +2 -0
  868. data/vendor/spinel/test/op_method_bitwise.rb +30 -0
  869. data/vendor/spinel/test/op_method_bitwise.rb.expected +8 -0
  870. data/vendor/spinel/test/op_method_shift.rb +30 -0
  871. data/vendor/spinel/test/op_method_shift.rb.expected +3 -0
  872. data/vendor/spinel/test/open_class_array_builtin.rb +11 -0
  873. data/vendor/spinel/test/open_class_array_builtin.rb.expected +1 -0
  874. data/vendor/spinel/test/open_class_bool_builtin.rb +28 -0
  875. data/vendor/spinel/test/open_class_bool_builtin.rb.expected +6 -0
  876. data/vendor/spinel/test/open_class_default_arg.rb +47 -0
  877. data/vendor/spinel/test/open_class_default_arg.rb.expected +9 -0
  878. data/vendor/spinel/test/open_class_object.rb +26 -0
  879. data/vendor/spinel/test/open_class_object.rb.expected +5 -0
  880. data/vendor/spinel/test/pack_utf8.rb +12 -0
  881. data/vendor/spinel/test/pack_utf8.rb.expected +10 -0
  882. data/vendor/spinel/test/param_body_array_hash_inference.rb +58 -0
  883. data/vendor/spinel/test/param_body_array_hash_inference.rb.expected +3 -0
  884. data/vendor/spinel/test/param_body_hash_inference.rb +72 -0
  885. data/vendor/spinel/test/param_body_hash_inference.rb.expected +6 -0
  886. data/vendor/spinel/test/param_include_body_widen.rb +32 -0
  887. data/vendor/spinel/test/param_include_body_widen.rb.expected +5 -0
  888. data/vendor/spinel/test/param_lengthlike_body_widen.rb +53 -0
  889. data/vendor/spinel/test/param_lengthlike_body_widen.rb.expected +6 -0
  890. data/vendor/spinel/test/param_ptr_from_callee_slot.rb +42 -0
  891. data/vendor/spinel/test/param_ptr_from_callee_slot.rb.expected +6 -0
  892. data/vendor/spinel/test/parser_long_ast_record.rb +132 -0
  893. data/vendor/spinel/test/parser_long_ast_record.rb.expected +3 -0
  894. data/vendor/spinel/test/pattern_match_as_and_guard.rb +33 -0
  895. data/vendor/spinel/test/pattern_match_as_and_guard.rb.expected +4 -0
  896. data/vendor/spinel/test/pattern_match_splat_rest.rb +15 -0
  897. data/vendor/spinel/test/pattern_match_splat_rest.rb.expected +4 -0
  898. data/vendor/spinel/test/pattern_pin.rb +105 -0
  899. data/vendor/spinel/test/pattern_pin.rb.expected +11 -0
  900. data/vendor/spinel/test/poly_arg_int_keyed_hash.rb +42 -0
  901. data/vendor/spinel/test/poly_arg_int_keyed_hash.rb.expected +5 -0
  902. data/vendor/spinel/test/poly_arg_to_typed_method_param.rb +28 -0
  903. data/vendor/spinel/test/poly_arg_to_typed_method_param.rb.expected +4 -0
  904. data/vendor/spinel/test/poly_array_assoc.rb +9 -0
  905. data/vendor/spinel/test/poly_array_compact.rb +15 -0
  906. data/vendor/spinel/test/poly_array_compact.rb.expected +3 -0
  907. data/vendor/spinel/test/poly_array_compact_bang.rb +17 -0
  908. data/vendor/spinel/test/poly_array_compact_bang.rb.expected +3 -0
  909. data/vendor/spinel/test/poly_array_dup.rb +52 -0
  910. data/vendor/spinel/test/poly_array_dup.rb.expected +5 -0
  911. data/vendor/spinel/test/poly_array_flatten.rb +17 -0
  912. data/vendor/spinel/test/poly_array_flatten.rb.expected +4 -0
  913. data/vendor/spinel/test/poly_attr_reader_box_dispatch.expected +1 -0
  914. data/vendor/spinel/test/poly_attr_reader_box_dispatch.rb +35 -0
  915. data/vendor/spinel/test/poly_attr_writer.rb +95 -0
  916. data/vendor/spinel/test/poly_attr_writer.rb.expected +5 -0
  917. data/vendor/spinel/test/poly_class_dispatch.rb +11 -0
  918. data/vendor/spinel/test/poly_class_dispatch.rb.expected +7 -0
  919. data/vendor/spinel/test/poly_dispatch_inherited_attr_reader.rb +20 -0
  920. data/vendor/spinel/test/poly_dispatch_inherited_attr_reader.rb.expected +2 -0
  921. data/vendor/spinel/test/poly_dispatch_inherited_method.rb +19 -0
  922. data/vendor/spinel/test/poly_dispatch_inherited_method.rb.expected +2 -0
  923. data/vendor/spinel/test/poly_dispatch_multilevel_inheritance.rb +28 -0
  924. data/vendor/spinel/test/poly_dispatch_multilevel_inheritance.rb.expected +4 -0
  925. data/vendor/spinel/test/poly_dispatch_narrow_return_type.rb +53 -0
  926. data/vendor/spinel/test/poly_dispatch_narrow_return_type.rb.expected +3 -0
  927. data/vendor/spinel/test/poly_each_receiver_gc_root.rb +40 -0
  928. data/vendor/spinel/test/poly_each_receiver_gc_root.rb.expected +1 -0
  929. data/vendor/spinel/test/poly_gsub_str_arm.rb +24 -0
  930. data/vendor/spinel/test/poly_gsub_str_arm.rb.expected +1 -0
  931. data/vendor/spinel/test/poly_gsub_str_str_hash.rb +37 -0
  932. data/vendor/spinel/test/poly_gsub_str_str_hash.rb.expected +1 -0
  933. data/vendor/spinel/test/poly_idx_int_nullable.rb +14 -0
  934. data/vendor/spinel/test/poly_idx_int_nullable.rb.expected +1 -0
  935. data/vendor/spinel/test/poly_length_string_tag.rb +20 -0
  936. data/vendor/spinel/test/poly_length_string_tag.rb.expected +3 -0
  937. data/vendor/spinel/test/poly_local_is_a_string_narrow_in_non_tail_if.rb +39 -0
  938. data/vendor/spinel/test/poly_local_is_a_string_narrow_in_non_tail_if.rb.expected +1 -0
  939. data/vendor/spinel/test/poly_local_is_a_string_narrow_in_tail_if.rb +23 -0
  940. data/vendor/spinel/test/poly_local_is_a_string_narrow_in_tail_if.rb.expected +1 -0
  941. data/vendor/spinel/test/poly_local_is_a_string_narrow_in_y1_inline.rb +27 -0
  942. data/vendor/spinel/test/poly_local_is_a_string_narrow_in_y1_inline.rb.expected +1 -0
  943. data/vendor/spinel/test/poly_poly_hash_each_value.rb +34 -0
  944. data/vendor/spinel/test/poly_poly_hash_each_value.rb.expected +11 -0
  945. data/vendor/spinel/test/poly_poly_hash_fetch_dup.rb +26 -0
  946. data/vendor/spinel/test/poly_poly_hash_fetch_dup.rb.expected +2 -0
  947. data/vendor/spinel/test/poly_recv_aset_poly_key.rb +30 -0
  948. data/vendor/spinel/test/poly_recv_aset_poly_key.rb.expected +1 -0
  949. data/vendor/spinel/test/poly_recv_dispatch_narrow_module.rb +37 -0
  950. data/vendor/spinel/test/poly_recv_dispatch_narrow_module.rb.expected +3 -0
  951. data/vendor/spinel/test/poly_recv_fetch_arm_incompat.rb +37 -0
  952. data/vendor/spinel/test/poly_recv_fetch_arm_incompat.rb.expected +1 -0
  953. data/vendor/spinel/test/poly_recv_fetch_hash_arms.rb +31 -0
  954. data/vendor/spinel/test/poly_recv_fetch_hash_arms.rb.expected +2 -0
  955. data/vendor/spinel/test/poly_recv_hash_dup.rb +48 -0
  956. data/vendor/spinel/test/poly_recv_hash_each_dispatch.rb +40 -0
  957. data/vendor/spinel/test/poly_recv_hash_each_dispatch.rb.expected +5 -0
  958. data/vendor/spinel/test/poly_recv_map_join.rb +31 -0
  959. data/vendor/spinel/test/poly_recv_map_join.rb.expected +5 -0
  960. data/vendor/spinel/test/poly_recv_range_sym_hash_dispatch.rb +55 -0
  961. data/vendor/spinel/test/poly_recv_range_sym_hash_dispatch.rb.expected +9 -0
  962. data/vendor/spinel/test/poly_recv_two_arg_slice.rb +27 -0
  963. data/vendor/spinel/test/poly_value_into_str_str_hash.rb +44 -0
  964. data/vendor/spinel/test/poly_value_into_str_str_hash.rb.expected +2 -0
  965. data/vendor/spinel/test/prepend_super_basic.rb +18 -0
  966. data/vendor/spinel/test/prepend_super_basic.rb.expected +1 -0
  967. data/vendor/spinel/test/prepend_super_chain.rb +23 -0
  968. data/vendor/spinel/test/prepend_super_chain.rb.expected +1 -0
  969. data/vendor/spinel/test/prepend_super_forward.rb +17 -0
  970. data/vendor/spinel/test/prepend_super_forward.rb.expected +1 -0
  971. data/vendor/spinel/test/prepend_super_multi.rb +25 -0
  972. data/vendor/spinel/test/prepend_super_multi.rb.expected +1 -0
  973. data/vendor/spinel/test/prepend_super_subclass.rb +21 -0
  974. data/vendor/spinel/test/prepend_super_subclass.rb.expected +1 -0
  975. data/vendor/spinel/test/primitive_eq_user_class.rb +84 -0
  976. data/vendor/spinel/test/primitive_eq_user_class.rb.expected +49 -0
  977. data/vendor/spinel/test/proc_anon_dot_call.rb +50 -0
  978. data/vendor/spinel/test/proc_anon_dot_call.rb.expected +5 -0
  979. data/vendor/spinel/test/proc_arg_object.rb +17 -0
  980. data/vendor/spinel/test/proc_arg_object.rb.expected +1 -0
  981. data/vendor/spinel/test/proc_call_value_return.rb +20 -0
  982. data/vendor/spinel/test/proc_call_value_return.rb.expected +5 -0
  983. data/vendor/spinel/test/proc_introspection.rb +11 -0
  984. data/vendor/spinel/test/proc_introspection.rb.expected +6 -0
  985. data/vendor/spinel/test/process_pid.rb +5 -0
  986. data/vendor/spinel/test/process_pid.rb.expected +2 -0
  987. data/vendor/spinel/test/ptr_array_to_poly_array_at_call.rb +39 -0
  988. data/vendor/spinel/test/ptr_array_to_poly_array_at_call.rb.expected +2 -0
  989. data/vendor/spinel/test/public_send.rb +12 -0
  990. data/vendor/spinel/test/public_send.rb.expected +4 -0
  991. data/vendor/spinel/test/puts_class_value.rb +10 -0
  992. data/vendor/spinel/test/puts_class_value.rb.expected +2 -0
  993. data/vendor/spinel/test/puts_nil.rb +7 -0
  994. data/vendor/spinel/test/raise_reraise_in_def.rb +44 -0
  995. data/vendor/spinel/test/raise_unless_isa_narrow.rb +72 -0
  996. data/vendor/spinel/test/raise_unless_isa_narrow.rb.expected +3 -0
  997. data/vendor/spinel/test/range_and_array_range_args.rb +43 -0
  998. data/vendor/spinel/test/range_and_array_range_args.rb.expected +18 -0
  999. data/vendor/spinel/test/range_begin_end_polyarr_include_delete_bang.rb +21 -0
  1000. data/vendor/spinel/test/range_bsearch.rb +11 -0
  1001. data/vendor/spinel/test/range_bsearch.rb.expected +8 -0
  1002. data/vendor/spinel/test/range_case_eq_type_mismatch.rb +10 -0
  1003. data/vendor/spinel/test/range_case_eq_type_mismatch.rb.expected +7 -0
  1004. data/vendor/spinel/test/range_each_fusion.rb +109 -0
  1005. data/vendor/spinel/test/range_each_fusion.rb.expected +16 -0
  1006. data/vendor/spinel/test/range_each_no_block.rb +5 -0
  1007. data/vendor/spinel/test/range_equality.rb +9 -0
  1008. data/vendor/spinel/test/range_exclude_end_size.rb +24 -0
  1009. data/vendor/spinel/test/range_exclude_end_size.rb.expected +18 -0
  1010. data/vendor/spinel/test/range_exclusive_nonliteral.rb +34 -0
  1011. data/vendor/spinel/test/range_overlap_array_flatten_depth.rb +14 -0
  1012. data/vendor/spinel/test/range_overlap_array_flatten_depth.rb.expected +8 -0
  1013. data/vendor/spinel/test/range_step.rb +15 -0
  1014. data/vendor/spinel/test/range_step.rb.expected +5 -0
  1015. data/vendor/spinel/test/range_string_cover.rb +10 -0
  1016. data/vendor/spinel/test/rational_complex_literals.rb +27 -0
  1017. data/vendor/spinel/test/rational_complex_literals.rb.expected +12 -0
  1018. data/vendor/spinel/test/rbs/README.md +45 -0
  1019. data/vendor/spinel/test/rbs/arrays.rbs +10 -0
  1020. data/vendor/spinel/test/rbs/arrays.seed.expected +7 -0
  1021. data/vendor/spinel/test/rbs/dropped_members.rbs +13 -0
  1022. data/vendor/spinel/test/rbs/dropped_members.seed.expected +2 -0
  1023. data/vendor/spinel/test/rbs/dropped_sigs.rbs +14 -0
  1024. data/vendor/spinel/test/rbs/dropped_sigs.seed.expected +2 -0
  1025. data/vendor/spinel/test/rbs/dropped_types.rbs +14 -0
  1026. data/vendor/spinel/test/rbs/dropped_types.seed.expected +2 -0
  1027. data/vendor/spinel/test/rbs/hashes.rbs +11 -0
  1028. data/vendor/spinel/test/rbs/hashes.seed.expected +7 -0
  1029. data/vendor/spinel/test/rbs/ivars.rbs +8 -0
  1030. data/vendor/spinel/test/rbs/ivars.seed.expected +5 -0
  1031. data/vendor/spinel/test/rbs/members.rbs +7 -0
  1032. data/vendor/spinel/test/rbs/members.seed.expected +5 -0
  1033. data/vendor/spinel/test/rbs/nominal.rbs +7 -0
  1034. data/vendor/spinel/test/rbs/nominal.seed.expected +3 -0
  1035. data/vendor/spinel/test/rbs/nullable.rbs +8 -0
  1036. data/vendor/spinel/test/rbs/nullable.seed.expected +5 -0
  1037. data/vendor/spinel/test/rbs/overloads.rbs +13 -0
  1038. data/vendor/spinel/test/rbs/overloads.seed.expected +3 -0
  1039. data/vendor/spinel/test/rbs/scalars.rbs +12 -0
  1040. data/vendor/spinel/test/rbs/scalars.seed.expected +9 -0
  1041. data/vendor/spinel/test/rbs/union_poly.rbs +9 -0
  1042. data/vendor/spinel/test/rbs/union_poly.seed.expected +4 -0
  1043. data/vendor/spinel/test/rbs/unqualified.rbs +13 -0
  1044. data/vendor/spinel/test/rbs/unqualified.seed.expected +4 -0
  1045. data/vendor/spinel/test/recursive_block_forward_value.rb +19 -0
  1046. data/vendor/spinel/test/recursive_block_forward_value.rb.expected +3 -0
  1047. data/vendor/spinel/test/recursive_implicit_yield_value.rb +35 -0
  1048. data/vendor/spinel/test/recursive_implicit_yield_value.rb.expected +7 -0
  1049. data/vendor/spinel/test/regex.rb +7 -0
  1050. data/vendor/spinel/test/regex.rb.expected +2 -0
  1051. data/vendor/spinel/test/regex_backref_oob_nil.rb +13 -0
  1052. data/vendor/spinel/test/regex_backref_oob_nil.rb.expected +5 -0
  1053. data/vendor/spinel/test/regex_const_freeze.rb +17 -0
  1054. data/vendor/spinel/test/regex_const_freeze.rb.expected +3 -0
  1055. data/vendor/spinel/test/regex_hex_shorthand.rb +13 -0
  1056. data/vendor/spinel/test/regex_hex_shorthand.rb.expected +12 -0
  1057. data/vendor/spinel/test/regex_match_nonstring.rb +18 -0
  1058. data/vendor/spinel/test/regex_match_nonstring.rb.expected +3 -0
  1059. data/vendor/spinel/test/regex_namespaced_const.rb +17 -0
  1060. data/vendor/spinel/test/regex_namespaced_const.rb.expected +4 -0
  1061. data/vendor/spinel/test/regex_negated_shorthand_toplevel.rb +9 -0
  1062. data/vendor/spinel/test/regex_negated_shorthand_toplevel.rb.expected +13 -0
  1063. data/vendor/spinel/test/regex_sub_backref.rb +11 -0
  1064. data/vendor/spinel/test/regex_unanchored_class_seq.rb +13 -0
  1065. data/vendor/spinel/test/regex_unanchored_class_seq.rb.expected +6 -0
  1066. data/vendor/spinel/test/regexp_capture_ivar_hash.rb +48 -0
  1067. data/vendor/spinel/test/regexp_capture_ivar_hash.rb.expected +7 -0
  1068. data/vendor/spinel/test/regexp_error_catchable.rb +14 -0
  1069. data/vendor/spinel/test/regexp_error_catchable.rb.expected +3 -0
  1070. data/vendor/spinel/test/regexp_gsub_zero_width.rb +27 -0
  1071. data/vendor/spinel/test/regexp_gsub_zero_width.rb.expected +12 -0
  1072. data/vendor/spinel/test/regexp_last_match_capture.rb +21 -0
  1073. data/vendor/spinel/test/regexp_last_match_capture.rb.expected +7 -0
  1074. data/vendor/spinel/test/regexp_line_anchors.rb +34 -0
  1075. data/vendor/spinel/test/regexp_line_anchors.rb.expected +15 -0
  1076. data/vendor/spinel/test/regexp_local_if_else_union.rb +36 -0
  1077. data/vendor/spinel/test/regexp_local_if_else_union.rb.expected +5 -0
  1078. data/vendor/spinel/test/regexp_match_data.rb +17 -0
  1079. data/vendor/spinel/test/regexp_match_data.rb.expected +8 -0
  1080. data/vendor/spinel/test/regexp_match_p_position.rb +7 -0
  1081. data/vendor/spinel/test/regexp_match_p_position.rb.expected +4 -0
  1082. data/vendor/spinel/test/regexp_new_literal.rb +72 -0
  1083. data/vendor/spinel/test/regexp_new_literal.rb.expected +24 -0
  1084. data/vendor/spinel/test/regexp_source_options.rb +10 -0
  1085. data/vendor/spinel/test/regexp_source_options.rb.expected +5 -0
  1086. data/vendor/spinel/test/regexp_sub_gsub_length.rb +17 -0
  1087. data/vendor/spinel/test/regexp_sub_gsub_length.rb.expected +10 -0
  1088. data/vendor/spinel/test/regexp_sub_gsub_realloc.rb +25 -0
  1089. data/vendor/spinel/test/regexp_sub_gsub_realloc.rb.expected +6 -0
  1090. data/vendor/spinel/test/regexp_threeq.rb +15 -0
  1091. data/vendor/spinel/test/regexp_threeq.rb.expected +4 -0
  1092. data/vendor/spinel/test/require_first_line.rb +7 -0
  1093. data/vendor/spinel/test/require_first_line.rb.expected +1 -0
  1094. data/vendor/spinel/test/require_modifier.rb +12 -0
  1095. data/vendor/spinel/test/require_same_line.rb +5 -0
  1096. data/vendor/spinel/test/require_same_line.rb.expected +2 -0
  1097. data/vendor/spinel/test/require_unsupported_lib.rb +8 -0
  1098. data/vendor/spinel/test/require_unsupported_lib.rb.expected +2 -0
  1099. data/vendor/spinel/test/rescue_bind_in_fiber_body.rb +24 -0
  1100. data/vendor/spinel/test/rescue_bind_in_fiber_body.rb.expected +6 -0
  1101. data/vendor/spinel/test/rescue_bind_in_lambda_proc_body.rb +16 -0
  1102. data/vendor/spinel/test/rescue_bind_in_lambda_proc_body.rb.expected +4 -0
  1103. data/vendor/spinel/test/rescue_walks_class_hierarchy.rb +37 -0
  1104. data/vendor/spinel/test/rescue_walks_class_hierarchy.rb.expected +7 -0
  1105. data/vendor/spinel/test/rescue_widens_nil_local_to_exception.expected +1 -0
  1106. data/vendor/spinel/test/rescue_widens_nil_local_to_exception.rb +11 -0
  1107. data/vendor/spinel/test/respond_to_def_self_method.rb +14 -0
  1108. data/vendor/spinel/test/respond_to_module_singleton_accessor.rb +10 -0
  1109. data/vendor/spinel/test/respond_to_module_singleton_accessor.rb.expected +3 -0
  1110. data/vendor/spinel/test/respond_to_primitive.rb +9 -0
  1111. data/vendor/spinel/test/respond_to_primitive.rb.expected +5 -0
  1112. data/vendor/spinel/test/return_unify_int_vs_pointer.rb +37 -0
  1113. data/vendor/spinel/test/return_unify_int_vs_pointer.rb.expected +4 -0
  1114. data/vendor/spinel/test/return_unify_isa_early_return.rb +29 -0
  1115. data/vendor/spinel/test/return_unify_isa_early_return.rb.expected +2 -0
  1116. data/vendor/spinel/test/rightward_assign_destructure.rb +27 -0
  1117. data/vendor/spinel/test/rightward_assign_destructure.rb.expected +8 -0
  1118. data/vendor/spinel/test/safe_navigation_returns_nil.expected +6 -0
  1119. data/vendor/spinel/test/safe_navigation_returns_nil.rb +28 -0
  1120. data/vendor/spinel/test/same_named_attr_no_widen.rb +24 -0
  1121. data/vendor/spinel/test/same_named_attr_no_widen.rb.expected +1 -0
  1122. data/vendor/spinel/test/scan_locals_def_boundary.rb +58 -0
  1123. data/vendor/spinel/test/scan_locals_def_boundary.rb.expected +2 -0
  1124. data/vendor/spinel/test/scan_walkers_def_boundary.rb +52 -0
  1125. data/vendor/spinel/test/scan_walkers_def_boundary.rb.expected +4 -0
  1126. data/vendor/spinel/test/self_class_divergent_returns.rb +61 -0
  1127. data/vendor/spinel/test/self_class_divergent_returns.rb.expected +3 -0
  1128. data/vendor/spinel/test/self_class_new_construction.rb +16 -0
  1129. data/vendor/spinel/test/self_class_new_construction.rb.expected +2 -0
  1130. data/vendor/spinel/test/self_operator_subclass_dispatch.rb +40 -0
  1131. data/vendor/spinel/test/self_operator_subclass_dispatch.rb.expected +2 -0
  1132. data/vendor/spinel/test/signal_trap_stub.rb +30 -0
  1133. data/vendor/spinel/test/signal_trap_stub.rb.expected +3 -0
  1134. data/vendor/spinel/test/sort_min_max_block_comparator.rb +18 -0
  1135. data/vendor/spinel/test/sort_min_max_block_comparator.rb.expected +7 -0
  1136. data/vendor/spinel/test/source_encoding.rb +19 -0
  1137. data/vendor/spinel/test/source_encoding.rb.expected +9 -0
  1138. data/vendor/spinel/test/sp_crypto_basic.rb +45 -0
  1139. data/vendor/spinel/test/sp_crypto_basic.rb.expected +6 -0
  1140. data/vendor/spinel/test/sp_crypto_sha1.rb +18 -0
  1141. data/vendor/spinel/test/sp_crypto_sha1.rb.expected +3 -0
  1142. data/vendor/spinel/test/sp_net_basic.rb +17 -0
  1143. data/vendor/spinel/test/sp_net_basic.rb.expected +2 -0
  1144. data/vendor/spinel/test/splat_heterogeneous_call.rb +14 -0
  1145. data/vendor/spinel/test/splat_params_instance.rb +21 -0
  1146. data/vendor/spinel/test/splat_params_instance.rb.expected +8 -0
  1147. data/vendor/spinel/test/sprintf_long_interp.rb +23 -0
  1148. data/vendor/spinel/test/sprintf_long_interp.rb.expected +3 -0
  1149. data/vendor/spinel/test/stdlib_class_in_ivar.rb +85 -0
  1150. data/vendor/spinel/test/stdlib_class_in_ivar.rb.expected +7 -0
  1151. data/vendor/spinel/test/str_array_max_min_by.rb +6 -0
  1152. data/vendor/spinel/test/str_array_sort_by.rb +11 -0
  1153. data/vendor/spinel/test/str_array_sort_by.rb.expected +5 -0
  1154. data/vendor/spinel/test/str_chomp_paragraph_mode.rb +16 -0
  1155. data/vendor/spinel/test/str_chomp_paragraph_mode.rb.expected +9 -0
  1156. data/vendor/spinel/test/str_count_multi_arg.rb +11 -0
  1157. data/vendor/spinel/test/str_count_multi_arg.rb.expected +4 -0
  1158. data/vendor/spinel/test/str_delete_count_negation.rb +16 -0
  1159. data/vendor/spinel/test/str_delete_count_negation.rb.expected +6 -0
  1160. data/vendor/spinel/test/str_force_encoding_return.rb +26 -0
  1161. data/vendor/spinel/test/str_force_encoding_return.rb.expected +3 -0
  1162. data/vendor/spinel/test/str_int_hash_missing_key_nil.rb +16 -0
  1163. data/vendor/spinel/test/str_int_hash_missing_key_nil.rb.expected +10 -0
  1164. data/vendor/spinel/test/str_match_position.rb +15 -0
  1165. data/vendor/spinel/test/str_match_position.rb.expected +8 -0
  1166. data/vendor/spinel/test/str_method_nil_arg_no_segv.rb +47 -0
  1167. data/vendor/spinel/test/str_method_nil_arg_no_segv.rb.expected +7 -0
  1168. data/vendor/spinel/test/str_oct_prefix.rb +11 -0
  1169. data/vendor/spinel/test/str_oct_prefix.rb.expected +5 -0
  1170. data/vendor/spinel/test/str_ord_empty.rb +11 -0
  1171. data/vendor/spinel/test/str_ord_empty.rb.expected +3 -0
  1172. data/vendor/spinel/test/str_poly_hash_dup_widen.rb +21 -0
  1173. data/vendor/spinel/test/str_poly_hash_dup_widen.rb.expected +4 -0
  1174. data/vendor/spinel/test/str_poly_hash_dup_widen_from_int.rb +17 -0
  1175. data/vendor/spinel/test/str_poly_hash_dup_widen_from_int.rb.expected +3 -0
  1176. data/vendor/spinel/test/str_rindex_position.rb +23 -0
  1177. data/vendor/spinel/test/str_rindex_position.rb.expected +17 -0
  1178. data/vendor/spinel/test/str_split_space_whitespace.rb +10 -0
  1179. data/vendor/spinel/test/str_split_space_whitespace.rb.expected +4 -0
  1180. data/vendor/spinel/test/str_split_trailing_empty.rb +12 -0
  1181. data/vendor/spinel/test/str_split_trailing_empty.rb.expected +7 -0
  1182. data/vendor/spinel/test/str_squeeze_arg.rb +18 -0
  1183. data/vendor/spinel/test/str_squeeze_arg.rb.expected +7 -0
  1184. data/vendor/spinel/test/str_str_hash_nullable_bracket_assign.rb +39 -0
  1185. data/vendor/spinel/test/str_str_hash_nullable_bracket_assign.rb.expected +5 -0
  1186. data/vendor/spinel/test/str_to_i_overflow.rb +32 -0
  1187. data/vendor/spinel/test/str_to_i_overflow.rb.expected +6 -0
  1188. data/vendor/spinel/test/str_tr_negation.rb +10 -0
  1189. data/vendor/spinel/test/str_tr_negation.rb.expected +4 -0
  1190. data/vendor/spinel/test/string_byteslice.rb +9 -0
  1191. data/vendor/spinel/test/string_byteslice.rb.expected +6 -0
  1192. data/vendor/spinel/test/string_casecmp.rb +9 -0
  1193. data/vendor/spinel/test/string_casecmp.rb.expected +7 -0
  1194. data/vendor/spinel/test/string_charset_range_notation.rb +9 -0
  1195. data/vendor/spinel/test/string_charset_range_notation.rb.expected +5 -0
  1196. data/vendor/spinel/test/string_chomp_bang_mutates.rb +20 -0
  1197. data/vendor/spinel/test/string_chomp_bang_mutates.rb.expected +4 -0
  1198. data/vendor/spinel/test/string_crypt.rb +22 -0
  1199. data/vendor/spinel/test/string_crypt.rb.expected +7 -0
  1200. data/vendor/spinel/test/string_delete_prefix_suffix_bang.rb +29 -0
  1201. data/vendor/spinel/test/string_delete_prefix_suffix_bang.rb.expected +5 -0
  1202. data/vendor/spinel/test/string_each_char_returns_self.rb +15 -0
  1203. data/vendor/spinel/test/string_each_line_expr.rb +25 -0
  1204. data/vendor/spinel/test/string_each_line_expr.rb.expected +19 -0
  1205. data/vendor/spinel/test/string_each_line_keeps_newline.rb +15 -0
  1206. data/vendor/spinel/test/string_encoding.rb +8 -0
  1207. data/vendor/spinel/test/string_encoding.rb.expected +5 -0
  1208. data/vendor/spinel/test/string_enum_block_returns_self.rb +27 -0
  1209. data/vendor/spinel/test/string_enum_block_returns_self.rb.expected +11 -0
  1210. data/vendor/spinel/test/string_format_polyarr.rb +6 -0
  1211. data/vendor/spinel/test/string_frozen_mutation_raises.rb +27 -0
  1212. data/vendor/spinel/test/string_frozen_mutation_raises.rb.expected +4 -0
  1213. data/vendor/spinel/test/string_gsub_block.rb +8 -0
  1214. data/vendor/spinel/test/string_index_nil_typeerror.rb +9 -0
  1215. data/vendor/spinel/test/string_index_nil_typeerror.rb.expected +1 -0
  1216. data/vendor/spinel/test/string_index_regex.rb +9 -0
  1217. data/vendor/spinel/test/string_index_regex.rb.expected +6 -0
  1218. data/vendor/spinel/test/string_lshift_int_codepoint.rb +9 -0
  1219. data/vendor/spinel/test/string_lshift_int_codepoint.rb.expected +2 -0
  1220. data/vendor/spinel/test/string_multiply_overflow.rb +14 -0
  1221. data/vendor/spinel/test/string_multiply_overflow.rb.expected +4 -0
  1222. data/vendor/spinel/test/string_nil_arg_typeerror.rb +38 -0
  1223. data/vendor/spinel/test/string_nil_arg_typeerror.rb.expected +5 -0
  1224. data/vendor/spinel/test/string_param_inference_from_body.rb +40 -0
  1225. data/vendor/spinel/test/string_param_inference_from_body.rb.expected +3 -0
  1226. data/vendor/spinel/test/string_partition_regex_and_puts.rb +8 -0
  1227. data/vendor/spinel/test/string_partition_regex_and_puts.rb.expected +6 -0
  1228. data/vendor/spinel/test/string_range_to_a.rb +10 -0
  1229. data/vendor/spinel/test/string_range_to_a.rb.expected +4 -0
  1230. data/vendor/spinel/test/string_replace_prepend_insert_clear.rb +34 -0
  1231. data/vendor/spinel/test/string_rpartition_regex.rb +10 -0
  1232. data/vendor/spinel/test/string_scanner_basic.rb +35 -0
  1233. data/vendor/spinel/test/string_scanner_check_unscan.rb +24 -0
  1234. data/vendor/spinel/test/string_scanner_getch_utf8.rb +26 -0
  1235. data/vendor/spinel/test/string_scanner_nullable.rb +75 -0
  1236. data/vendor/spinel/test/string_scanner_nullable.rb.expected +37 -0
  1237. data/vendor/spinel/test/string_scanner_scan_to_local.rb +11 -0
  1238. data/vendor/spinel/test/string_scanner_unscan_error.rb +54 -0
  1239. data/vendor/spinel/test/string_scrub.rb +7 -0
  1240. data/vendor/spinel/test/string_slice_range.rb +18 -0
  1241. data/vendor/spinel/test/string_split_capture.rb +8 -0
  1242. data/vendor/spinel/test/string_split_inline_arg_gc_root.rb +77 -0
  1243. data/vendor/spinel/test/string_split_inline_arg_gc_root.rb.expected +1 -0
  1244. data/vendor/spinel/test/string_squeeze_no_trailing_nul.rb +12 -0
  1245. data/vendor/spinel/test/string_sub_hash_replacement.rb +8 -0
  1246. data/vendor/spinel/test/string_to_i_base.rb +9 -0
  1247. data/vendor/spinel/test/string_to_i_base.rb.expected +6 -0
  1248. data/vendor/spinel/test/string_to_i_base_zero.rb +9 -0
  1249. data/vendor/spinel/test/string_to_i_overflow_raises.rb +30 -0
  1250. data/vendor/spinel/test/string_to_i_overflow_raises.rb.expected +6 -0
  1251. data/vendor/spinel/test/string_undump.rb +9 -0
  1252. data/vendor/spinel/test/string_upto.rb +20 -0
  1253. data/vendor/spinel/test/string_with_embedded_nul.rb +46 -0
  1254. data/vendor/spinel/test/string_with_embedded_nul.rb.expected +16 -0
  1255. data/vendor/spinel/test/stringio_new_mode.rb +21 -0
  1256. data/vendor/spinel/test/stringio_new_mode.rb.expected +5 -0
  1257. data/vendor/spinel/test/stringio_new_nil.rb +14 -0
  1258. data/vendor/spinel/test/stringio_new_nil.rb.expected +2 -0
  1259. data/vendor/spinel/test/stringio_open.rb +30 -0
  1260. data/vendor/spinel/test/stringio_open.rb.expected +10 -0
  1261. data/vendor/spinel/test/stringio_putc_fsync.rb +24 -0
  1262. data/vendor/spinel/test/stringio_putc_fsync.rb.expected +3 -0
  1263. data/vendor/spinel/test/strip_nul.rb +13 -0
  1264. data/vendor/spinel/test/strip_nul.rb.expected +8 -0
  1265. data/vendor/spinel/test/struct_dig.rb +16 -0
  1266. data/vendor/spinel/test/struct_int_member_unboxed.rb +14 -0
  1267. data/vendor/spinel/test/struct_int_member_unboxed.rb.expected +6 -0
  1268. data/vendor/spinel/test/struct_member_name_no_global_merge.rb +28 -0
  1269. data/vendor/spinel/test/struct_member_name_no_global_merge.rb.expected +3 -0
  1270. data/vendor/spinel/test/struct_to_a_values.rb +12 -0
  1271. data/vendor/spinel/test/struct_to_a_values.rb.expected +6 -0
  1272. data/vendor/spinel/test/struct_to_h.rb +8 -0
  1273. data/vendor/spinel/test/struct_to_h.rb.expected +4 -0
  1274. data/vendor/spinel/test/struct_to_h_block.rb +30 -0
  1275. data/vendor/spinel/test/super_in_non_initialize.rb +44 -0
  1276. data/vendor/spinel/test/super_in_non_initialize.rb.expected +3 -0
  1277. data/vendor/spinel/test/super_with_args.rb +25 -0
  1278. data/vendor/spinel/test/super_with_args.rb.expected +4 -0
  1279. data/vendor/spinel/test/sym_hash_cross_variant_merge.rb +40 -0
  1280. data/vendor/spinel/test/sym_hash_cross_variant_merge.rb.expected +3 -0
  1281. data/vendor/spinel/test/sym_int_hash_merge.rb +52 -0
  1282. data/vendor/spinel/test/sym_int_hash_merge.rb.expected +11 -0
  1283. data/vendor/spinel/test/sym_int_hash_missing_key_nil.rb +15 -0
  1284. data/vendor/spinel/test/sym_int_hash_missing_key_nil.rb.expected +11 -0
  1285. data/vendor/spinel/test/sym_poly_hash_merge_sym_int.rb +18 -0
  1286. data/vendor/spinel/test/sym_poly_hash_merge_sym_int.rb.expected +4 -0
  1287. data/vendor/spinel/test/symbol_id2name_ruby_desc_minmax.rb +13 -0
  1288. data/vendor/spinel/test/symbol_id2name_ruby_desc_minmax.rb.expected +7 -0
  1289. data/vendor/spinel/test/symbol_to_proc_after_positional.rb +18 -0
  1290. data/vendor/spinel/test/symbol_to_proc_after_positional.rb.expected +5 -0
  1291. data/vendor/spinel/test/symbol_upcase_interpolation.rb +24 -0
  1292. data/vendor/spinel/test/symbol_upcase_interpolation.rb.expected +5 -0
  1293. data/vendor/spinel/test/system_argument_list.rb +15 -0
  1294. data/vendor/spinel/test/system_argument_list.rb.expected +7 -0
  1295. data/vendor/spinel/test/thread_new_value.rb +5 -0
  1296. data/vendor/spinel/test/thread_new_value.rb.expected +2 -0
  1297. data/vendor/spinel/test/time_localtime_gmtime.rb +19 -0
  1298. data/vendor/spinel/test/time_mktime_local_utc.rb +24 -0
  1299. data/vendor/spinel/test/time_mktime_local_utc.rb.expected +13 -0
  1300. data/vendor/spinel/test/time_strftime_long_format.rb +21 -0
  1301. data/vendor/spinel/test/time_strftime_long_format.rb.expected +5 -0
  1302. data/vendor/spinel/test/time_subsec_utc.rb +19 -0
  1303. data/vendor/spinel/test/time_subsec_utc.rb.expected +9 -0
  1304. data/vendor/spinel/test/time_to_s.rb +5 -0
  1305. data/vendor/spinel/test/toplevel_include_module_function.rb +101 -0
  1306. data/vendor/spinel/test/toplevel_include_module_function.rb.expected +8 -0
  1307. data/vendor/spinel/test/toplevel_kwrest_block_param.rb +19 -0
  1308. data/vendor/spinel/test/toplevel_kwrest_block_param.rb.expected +2 -0
  1309. data/vendor/spinel/test/tuple_inspect.rb +9 -0
  1310. data/vendor/spinel/test/tuple_inspect.rb.expected +4 -0
  1311. data/vendor/spinel/test/typed_array_empty_returns_nil.rb +19 -0
  1312. data/vendor/spinel/test/typed_array_empty_returns_nil.rb.expected +8 -0
  1313. data/vendor/spinel/test/typed_array_include_cross_type.rb +6 -0
  1314. data/vendor/spinel/test/typed_hash_caller_no_body_widen.rb +46 -0
  1315. data/vendor/spinel/test/typed_hash_caller_no_body_widen.rb.expected +1 -0
  1316. data/vendor/spinel/test/typed_hash_inspect_and_each_value.rb +12 -0
  1317. data/vendor/spinel/test/typed_hash_inspect_and_each_value.rb.expected +6 -0
  1318. data/vendor/spinel/test/typed_hash_missing_returns_nil.rb +17 -0
  1319. data/vendor/spinel/test/typed_hash_missing_returns_nil.rb.expected +4 -0
  1320. data/vendor/spinel/test/unary_operator_methods.rb +19 -0
  1321. data/vendor/spinel/test/unary_operator_methods.rb.expected +3 -0
  1322. data/vendor/spinel/test/undef_method_raises.rb +34 -0
  1323. data/vendor/spinel/test/undef_method_raises.rb.expected +3 -0
  1324. data/vendor/spinel/test/uninit_const_raises_nameerror.rb +18 -0
  1325. data/vendor/spinel/test/uninit_ivar_returns_nil.rb +23 -0
  1326. data/vendor/spinel/test/uninit_ivar_returns_nil.rb.expected +4 -0
  1327. data/vendor/spinel/test/unless_return_path_hash.rb +39 -0
  1328. data/vendor/spinel/test/unless_return_path_hash.rb.expected +6 -0
  1329. data/vendor/spinel/test/user_class_aset_dispatch.rb +75 -0
  1330. data/vendor/spinel/test/user_class_aset_dispatch.rb.expected +3 -0
  1331. data/vendor/spinel/test/value_type_tuple_gc_scan.rb +28 -0
  1332. data/vendor/spinel/test/value_type_tuple_gc_scan.rb.expected +2 -0
  1333. data/vendor/spinel/test/values_at.rb +19 -0
  1334. data/vendor/spinel/test/values_at.rb.expected +4 -0
  1335. data/vendor/spinel/test/volatile_strip_skips_coerced_arg.expected +1 -0
  1336. data/vendor/spinel/test/volatile_strip_skips_coerced_arg.rb +30 -0
  1337. data/vendor/spinel/test/warn_kernel.rb +13 -0
  1338. data/vendor/spinel/test/warn_kernel.rb.expected +3 -0
  1339. data/vendor/spinel/test/warn_unresolved_call_context.rb +20 -0
  1340. data/vendor/spinel/test/when_splat_rational_complex.rb +30 -0
  1341. data/vendor/spinel/test/when_splat_rational_complex.rb.expected +6 -0
  1342. data/vendor/spinel/test/yield_call_assigned_return.rb +41 -0
  1343. data/vendor/spinel/test/yield_call_assigned_return.rb.expected +6 -0
  1344. data/vendor/spinel/test/yield_typed_hash_block_local.rb +41 -0
  1345. data/vendor/spinel/test/yield_typed_hash_block_local.rb.expected +2 -0
  1346. data/vendor/spinel/test/yield_value_return.rb +52 -0
  1347. data/vendor/spinel/test/yield_value_return.rb.expected +8 -0
  1348. data/vendor/spinel/test/yield_value_returns_from_method.expected +6 -0
  1349. data/vendor/spinel/test/yield_value_returns_from_method.rb +27 -0
  1350. data/vendor/spinel/tools/fast-bootstrap +74 -0
  1351. metadata +1326 -279
  1352. data/vendor/spinel/test/alias_method.rb +0 -28
  1353. data/vendor/spinel/test/alias_method.rb.expected +0 -5
  1354. data/vendor/spinel/test/and_node_poly_operand.rb +0 -24
  1355. data/vendor/spinel/test/array_3d_nested.rb +0 -48
  1356. data/vendor/spinel/test/array_clear_typed.rb +0 -40
  1357. data/vendor/spinel/test/array_clear_typed.rb.expected +0 -7
  1358. data/vendor/spinel/test/array_concat.rb +0 -18
  1359. data/vendor/spinel/test/array_concat.rb.expected +0 -2
  1360. data/vendor/spinel/test/array_each_with_object.rb +0 -17
  1361. data/vendor/spinel/test/array_flat_map.rb +0 -16
  1362. data/vendor/spinel/test/array_flat_map.rb.expected +0 -2
  1363. data/vendor/spinel/test/array_keyed_hash_int_array_eql.rb +0 -29
  1364. data/vendor/spinel/test/array_keyed_hash_int_array_eql.rb.expected +0 -6
  1365. data/vendor/spinel/test/array_method_name_clash.rb +0 -15
  1366. data/vendor/spinel/test/array_new_block_typed_container.rb +0 -37
  1367. data/vendor/spinel/test/array_new_block_typed_container.rb.expected +0 -7
  1368. data/vendor/spinel/test/array_new_empty_inner_deferred.rb +0 -47
  1369. data/vendor/spinel/test/array_new_empty_inner_deferred.rb.expected +0 -8
  1370. data/vendor/spinel/test/array_partition_gc.rb +0 -20
  1371. data/vendor/spinel/test/array_partition_gc.rb.expected +0 -3
  1372. data/vendor/spinel/test/array_plus.rb +0 -18
  1373. data/vendor/spinel/test/array_plus.rb.expected +0 -2
  1374. data/vendor/spinel/test/array_rotate_bang.rb +0 -54
  1375. data/vendor/spinel/test/array_rotate_bang.rb.expected +0 -34
  1376. data/vendor/spinel/test/array_shuffle_ptr.rb +0 -11
  1377. data/vendor/spinel/test/attr_writer_poly_box.rb +0 -24
  1378. data/vendor/spinel/test/attr_writer_poly_no_double_eval.rb +0 -29
  1379. data/vendor/spinel/test/attr_writer_poly_no_double_eval.rb.expected +0 -3
  1380. data/vendor/spinel/test/attr_writer_returns_rhs.rb +0 -23
  1381. data/vendor/spinel/test/auto_unbox_keeps_int_slot.rb +0 -38
  1382. data/vendor/spinel/test/auto_unbox_keeps_int_slot.rb.expected +0 -3
  1383. data/vendor/spinel/test/auto_unbox_poly_to_int_local.rb +0 -36
  1384. data/vendor/spinel/test/auto_unbox_poly_to_int_local.rb.expected +0 -2
  1385. data/vendor/spinel/test/bare_return_in_cls_method.rb +0 -46
  1386. data/vendor/spinel/test/bare_return_in_cls_method.rb.expected +0 -2
  1387. data/vendor/spinel/test/bare_return_in_poly_method.rb +0 -26
  1388. data/vendor/spinel/test/bare_return_in_poly_method.rb.expected +0 -2
  1389. data/vendor/spinel/test/bound_method_array.rb +0 -22
  1390. data/vendor/spinel/test/bound_method_array.rb.expected +0 -4
  1391. data/vendor/spinel/test/bound_method_basic.rb +0 -27
  1392. data/vendor/spinel/test/bound_method_basic.rb.expected +0 -2
  1393. data/vendor/spinel/test/call_op_write.rb +0 -51
  1394. data/vendor/spinel/test/call_op_write.rb.expected +0 -11
  1395. data/vendor/spinel/test/chained_attr_setter.rb +0 -55
  1396. data/vendor/spinel/test/chained_ivar_op_assign_emits_inner_write.rb +0 -28
  1397. data/vendor/spinel/test/chained_ivar_op_assign_emits_inner_write.rb.expected +0 -3
  1398. data/vendor/spinel/test/chained_ivar_write_split.rb +0 -26
  1399. data/vendor/spinel/test/chained_ivar_write_split.rb.expected +0 -3
  1400. data/vendor/spinel/test/chained_ivar_write_subclass.rb +0 -26
  1401. data/vendor/spinel/test/chained_ivar_write_subclass.rb.expected +0 -2
  1402. data/vendor/spinel/test/chained_or_assign_collection_array_nested.rb +0 -32
  1403. data/vendor/spinel/test/chained_or_assign_collection_array_nested.rb.expected +0 -3
  1404. data/vendor/spinel/test/chained_or_assign_collection_element.rb +0 -29
  1405. data/vendor/spinel/test/chained_or_assign_collection_element.rb.expected +0 -2
  1406. data/vendor/spinel/test/chained_or_assign_collection_poly_array.rb +0 -25
  1407. data/vendor/spinel/test/chained_or_assign_collection_poly_array.rb.expected +0 -3
  1408. data/vendor/spinel/test/chained_or_assign_collection_str_keys.rb +0 -24
  1409. data/vendor/spinel/test/chained_or_assign_collection_str_keys.rb.expected +0 -3
  1410. data/vendor/spinel/test/class_constant_sym_array.rb +0 -19
  1411. data/vendor/spinel/test/class_var_write.rb +0 -19
  1412. data/vendor/spinel/test/cls_ivar_type_parent_defer.rb +0 -32
  1413. data/vendor/spinel/test/cls_method_object.rb +0 -31
  1414. data/vendor/spinel/test/cls_method_object.rb.expected +0 -2
  1415. data/vendor/spinel/test/comparable.rb +0 -21
  1416. data/vendor/spinel/test/comparable.rb.expected +0 -5
  1417. data/vendor/spinel/test/default_argv_narrows_from_string_call_site.rb +0 -32
  1418. data/vendor/spinel/test/each_poly_recv_block_auto_splat.rb +0 -43
  1419. data/vendor/spinel/test/each_poly_recv_block_auto_splat.rb.expected +0 -2
  1420. data/vendor/spinel/test/each_with_object_seed_shadow.rb +0 -14
  1421. data/vendor/spinel/test/each_with_object_seed_shadow.rb.expected +0 -4
  1422. data/vendor/spinel/test/empty_hash_ivar_string_value.rb +0 -51
  1423. data/vendor/spinel/test/empty_hash_ivar_string_value.rb.expected +0 -4
  1424. data/vendor/spinel/test/ensure_raise_overrides_body_raise.rb +0 -19
  1425. data/vendor/spinel/test/ensure_raise_overrides_body_raise.rb.expected +0 -1
  1426. data/vendor/spinel/test/ensure_runs_on_raise.rb +0 -32
  1427. data/vendor/spinel/test/ensure_runs_on_raise.rb.expected +0 -3
  1428. data/vendor/spinel/test/ensure_runs_on_raise_nested.rb +0 -38
  1429. data/vendor/spinel/test/ensure_runs_on_raise_nested.rb.expected +0 -4
  1430. data/vendor/spinel/test/ensure_runs_on_return_from_rescue.rb +0 -26
  1431. data/vendor/spinel/test/ensure_runs_on_return_from_rescue.rb.expected +0 -2
  1432. data/vendor/spinel/test/ensure_runs_on_return_nested.rb +0 -37
  1433. data/vendor/spinel/test/ensure_runs_on_return_nested.rb.expected +0 -3
  1434. data/vendor/spinel/test/enumerable.rb +0 -19
  1435. data/vendor/spinel/test/enumerable.rb.expected +0 -2
  1436. data/vendor/spinel/test/exceptions.rb +0 -34
  1437. data/vendor/spinel/test/exceptions.rb.expected +0 -4
  1438. data/vendor/spinel/test/fiber_ivar_persists_across_yield.rb +0 -22
  1439. data/vendor/spinel/test/fiber_ivar_persists_across_yield.rb.expected +0 -3
  1440. data/vendor/spinel/test/fiber_yield_across_method_call.rb +0 -28
  1441. data/vendor/spinel/test/fiber_yield_across_method_call.rb.expected +0 -3
  1442. data/vendor/spinel/test/file_class_methods_int_recv.rb +0 -20
  1443. data/vendor/spinel/test/file_class_methods_int_recv.rb.expected +0 -3
  1444. data/vendor/spinel/test/float_array_sort.rb +0 -18
  1445. data/vendor/spinel/test/float_array_sort.rb.expected +0 -8
  1446. data/vendor/spinel/test/forward_call_class_method_inherited_init.rb +0 -36
  1447. data/vendor/spinel/test/forward_call_class_method_inherited_init.rb.expected +0 -1
  1448. data/vendor/spinel/test/forward_call_class_method_inherited_init_int_array.rb +0 -32
  1449. data/vendor/spinel/test/forward_call_class_method_inherited_init_obj.rb +0 -41
  1450. data/vendor/spinel/test/forward_call_class_method_inherited_init_obj.rb.expected +0 -1
  1451. data/vendor/spinel/test/forward_call_class_method_int_array.rb +0 -26
  1452. data/vendor/spinel/test/forward_call_class_method_int_array.rb.expected +0 -1
  1453. data/vendor/spinel/test/forward_call_class_method_nested.rb +0 -35
  1454. data/vendor/spinel/test/forward_call_class_method_obj.rb +0 -36
  1455. data/vendor/spinel/test/forward_call_class_method_obj.rb.expected +0 -1
  1456. data/vendor/spinel/test/forward_call_param_type_int_array.rb +0 -34
  1457. data/vendor/spinel/test/forward_call_param_type_int_array.rb.expected +0 -1
  1458. data/vendor/spinel/test/forward_call_param_type_obj.rb +0 -38
  1459. data/vendor/spinel/test/forward_call_param_type_obj.rb.expected +0 -1
  1460. data/vendor/spinel/test/gc_save_double_in_new.rb +0 -13
  1461. data/vendor/spinel/test/gc_save_double_in_new.rb.expected +0 -1
  1462. data/vendor/spinel/test/gc_scan_skip_inherited_ivar.rb +0 -54
  1463. data/vendor/spinel/test/gc_scan_skip_inherited_ivar.rb.expected +0 -2
  1464. data/vendor/spinel/test/heterogeneous_dispatch_int_return_narrow.rb +0 -46
  1465. data/vendor/spinel/test/heterogeneous_dispatch_int_return_narrow.rb.expected +0 -2
  1466. data/vendor/spinel/test/if_static_false_skips_dead_branch_compile.rb +0 -50
  1467. data/vendor/spinel/test/if_static_false_skips_dead_branch_compile.rb.expected +0 -3
  1468. data/vendor/spinel/test/implicit_self_param_poly.rb +0 -17
  1469. data/vendor/spinel/test/implicit_self_param_poly.rb.expected +0 -1
  1470. data/vendor/spinel/test/inherit.rb +0 -52
  1471. data/vendor/spinel/test/inherit.rb.expected +0 -7
  1472. data/vendor/spinel/test/inherited_method_param_widen.rb +0 -49
  1473. data/vendor/spinel/test/init_locals.rb +0 -43
  1474. data/vendor/spinel/test/init_locals.rb.expected +0 -3
  1475. data/vendor/spinel/test/initialize_param_poly_call_sites.rb +0 -20
  1476. data/vendor/spinel/test/initialize_param_poly_call_sites.rb.expected +0 -2
  1477. data/vendor/spinel/test/initialize_param_through_call.rb +0 -15
  1478. data/vendor/spinel/test/initialize_void_wrapper_gc_save.rb +0 -40
  1479. data/vendor/spinel/test/int_array_replace_expr.rb +0 -23
  1480. data/vendor/spinel/test/int_array_replace_expr.rb.expected +0 -7
  1481. data/vendor/spinel/test/intarray_slice_assign_from_intarray_ptr_array_first.rb +0 -25
  1482. data/vendor/spinel/test/intarray_slice_assign_from_intarray_ptr_array_first.rb.expected +0 -1
  1483. data/vendor/spinel/test/integer_div.rb +0 -21
  1484. data/vendor/spinel/test/integer_div.rb.expected +0 -10
  1485. data/vendor/spinel/test/introspect.rb +0 -35
  1486. data/vendor/spinel/test/introspect.rb.expected +0 -11
  1487. data/vendor/spinel/test/issue176_empty_hash_default.rb +0 -34
  1488. data/vendor/spinel/test/issue176_empty_hash_default.rb.expected +0 -3
  1489. data/vendor/spinel/test/issue204_user_find_fetch.rb +0 -25
  1490. data/vendor/spinel/test/issue204_user_find_fetch.rb.expected +0 -5
  1491. data/vendor/spinel/test/issue207_factory_pattern.rb +0 -35
  1492. data/vendor/spinel/test/issue207_factory_pattern.rb.expected +0 -1
  1493. data/vendor/spinel/test/issue207_full_repro.rb +0 -52
  1494. data/vendor/spinel/test/issue207_full_repro.rb.expected +0 -1
  1495. data/vendor/spinel/test/issue208_inherited_class_method.rb +0 -24
  1496. data/vendor/spinel/test/issue208_inherited_class_method.rb.expected +0 -4
  1497. data/vendor/spinel/test/issue219_aref_chain.rb +0 -36
  1498. data/vendor/spinel/test/issue219_aref_chain.rb.expected +0 -5
  1499. data/vendor/spinel/test/issue235_chain_tail_widening.rb +0 -48
  1500. data/vendor/spinel/test/issue235_chain_tail_widening.rb.expected +0 -5
  1501. data/vendor/spinel/test/ivar_and_write_basic.rb +0 -34
  1502. data/vendor/spinel/test/ivar_and_write_basic.rb.expected +0 -5
  1503. data/vendor/spinel/test/ivar_c_keyword.rb +0 -14
  1504. data/vendor/spinel/test/ivar_c_keyword.rb.expected +0 -1
  1505. data/vendor/spinel/test/ivar_float_array.rb +0 -59
  1506. data/vendor/spinel/test/ivar_float_array.rb.expected +0 -11
  1507. data/vendor/spinel/test/ivar_op_assign_bitwise_and_shift.rb +0 -22
  1508. data/vendor/spinel/test/ivar_op_assign_bitwise_and_shift.rb.expected +0 -6
  1509. data/vendor/spinel/test/ivar_or_write_basic.rb +0 -57
  1510. data/vendor/spinel/test/ivar_or_write_basic.rb.expected +0 -9
  1511. data/vendor/spinel/test/ivar_writer_heterogeneity.rb +0 -34
  1512. data/vendor/spinel/test/ivar_writer_heterogeneity.rb.expected +0 -2
  1513. data/vendor/spinel/test/known_constants.rb +0 -25
  1514. data/vendor/spinel/test/known_constants.rb.expected +0 -4
  1515. data/vendor/spinel/test/kw_nil_default_string_call.rb +0 -6
  1516. data/vendor/spinel/test/kw_nil_default_string_call.rb.expected +0 -2
  1517. data/vendor/spinel/test/map_array_block_result.rb +0 -29
  1518. data/vendor/spinel/test/map_array_block_result.rb.expected +0 -2
  1519. data/vendor/spinel/test/map_block_returns_nested_array.rb +0 -53
  1520. data/vendor/spinel/test/map_empty_block.rb +0 -26
  1521. data/vendor/spinel/test/map_empty_block.rb.expected +0 -6
  1522. data/vendor/spinel/test/map_range_recv_array_block.rb +0 -31
  1523. data/vendor/spinel/test/method_param_unify_to_poly.rb +0 -40
  1524. data/vendor/spinel/test/method_param_unify_to_poly.rb.expected +0 -2
  1525. data/vendor/spinel/test/multi_arg_block_call.rb +0 -56
  1526. data/vendor/spinel/test/multi_arg_block_call.rb.expected +0 -5
  1527. data/vendor/spinel/test/multi_assign_ivar.rb +0 -33
  1528. data/vendor/spinel/test/multi_assign_ivar.rb.expected +0 -1
  1529. data/vendor/spinel/test/multi_write_const.rb +0 -36
  1530. data/vendor/spinel/test/multi_write_const.rb.expected +0 -9
  1531. data/vendor/spinel/test/multi_write_from_poly_recv.rb +0 -34
  1532. data/vendor/spinel/test/multi_write_ivar.rb +0 -34
  1533. data/vendor/spinel/test/multi_write_ivar.rb.expected +0 -3
  1534. data/vendor/spinel/test/multi_write_ivar_widening.rb +0 -10
  1535. data/vendor/spinel/test/multi_write_ivar_widening.rb.expected +0 -2
  1536. data/vendor/spinel/test/multi_write_map_block.rb +0 -31
  1537. data/vendor/spinel/test/multi_write_map_block.rb.expected +0 -7
  1538. data/vendor/spinel/test/multi_write_setter.rb +0 -28
  1539. data/vendor/spinel/test/multi_write_setter.rb.expected +0 -3
  1540. data/vendor/spinel/test/multi_write_typed_array_dispatch.rb +0 -44
  1541. data/vendor/spinel/test/multi_write_typed_array_dispatch.rb.expected +0 -3
  1542. data/vendor/spinel/test/nested_class_in_class.rb +0 -38
  1543. data/vendor/spinel/test/nested_class_in_class.rb.expected +0 -3
  1544. data/vendor/spinel/test/no_attr_write_shortcut_complex.rb +0 -33
  1545. data/vendor/spinel/test/no_attr_write_shortcut_complex.rb.expected +0 -3
  1546. data/vendor/spinel/test/no_attr_write_shortcut_multi.rb +0 -38
  1547. data/vendor/spinel/test/no_attr_write_shortcut_multi.rb.expected +0 -4
  1548. data/vendor/spinel/test/no_keywords_param.rb +0 -15
  1549. data/vendor/spinel/test/no_keywords_param.rb.expected +0 -2
  1550. data/vendor/spinel/test/obj_array.rb +0 -26
  1551. data/vendor/spinel/test/obj_array.rb.expected +0 -6
  1552. data/vendor/spinel/test/obj_class_returns_string.rb +0 -24
  1553. data/vendor/spinel/test/obj_class_returns_string.rb.expected +0 -2
  1554. data/vendor/spinel/test/optional_string_param_method.rb +0 -11
  1555. data/vendor/spinel/test/optional_string_param_method.rb.expected +0 -3
  1556. data/vendor/spinel/test/poly_array_literal_callnode_typed_elem.rb +0 -37
  1557. data/vendor/spinel/test/poly_array_literal_callnode_typed_elem.rb.expected +0 -9
  1558. data/vendor/spinel/test/poly_array_slot_literal.rb +0 -41
  1559. data/vendor/spinel/test/poly_array_slot_sized_default.rb +0 -33
  1560. data/vendor/spinel/test/poly_box_int_array.rb +0 -13
  1561. data/vendor/spinel/test/poly_box_int_array.rb.expected +0 -2
  1562. data/vendor/spinel/test/poly_dispatch_args_ret.rb +0 -45
  1563. data/vendor/spinel/test/poly_dispatch_args_ret.rb.expected +0 -4
  1564. data/vendor/spinel/test/poly_dispatch_arity_padding.rb +0 -36
  1565. data/vendor/spinel/test/poly_dispatch_arity_truncate.rb +0 -45
  1566. data/vendor/spinel/test/poly_hash_literal_from_ivar.rb +0 -47
  1567. data/vendor/spinel/test/poly_hash_literal_from_ivar.rb.expected +0 -5
  1568. data/vendor/spinel/test/poly_int_arith_auto_unify.rb +0 -26
  1569. data/vendor/spinel/test/poly_int_arith_auto_unify.rb.expected +0 -4
  1570. data/vendor/spinel/test/poly_int_bit_index.rb +0 -50
  1571. data/vendor/spinel/test/poly_int_bit_index.rb.expected +0 -14
  1572. data/vendor/spinel/test/poly_or_returns_value.rb +0 -13
  1573. data/vendor/spinel/test/poly_or_returns_value.rb.expected +0 -4
  1574. data/vendor/spinel/test/poly_recv_bracket_assign.rb +0 -31
  1575. data/vendor/spinel/test/poly_recv_bracket_assign.rb.expected +0 -1
  1576. data/vendor/spinel/test/poly_recv_each.rb +0 -35
  1577. data/vendor/spinel/test/poly_recv_each.rb.expected +0 -7
  1578. data/vendor/spinel/test/poly_shl_array_push_dispatch.rb +0 -35
  1579. data/vendor/spinel/test/poly_shl_array_push_dispatch.rb.expected +0 -5
  1580. data/vendor/spinel/test/proc_hash_value.rb +0 -27
  1581. data/vendor/spinel/test/regexp.rb +0 -34
  1582. data/vendor/spinel/test/regexp.rb.expected +0 -12
  1583. data/vendor/spinel/test/reopen_class.rb +0 -33
  1584. data/vendor/spinel/test/reopen_class.rb.expected +0 -5
  1585. data/vendor/spinel/test/runtime_widen_int_to_poly_array.rb +0 -27
  1586. data/vendor/spinel/test/runtime_widen_int_to_poly_array.rb.expected +0 -3
  1587. data/vendor/spinel/test/sample_user_method.rb +0 -20
  1588. data/vendor/spinel/test/scan_ivars_inherited_slot.rb +0 -32
  1589. data/vendor/spinel/test/scan_new_calls_class_scope.rb +0 -35
  1590. data/vendor/spinel/test/send.rb +0 -15
  1591. data/vendor/spinel/test/send.rb.expected +0 -2
  1592. data/vendor/spinel/test/stdlib.rb +0 -28
  1593. data/vendor/spinel/test/stdlib.rb.expected +0 -5
  1594. data/vendor/spinel/test/strip_nullable_int_cast.rb +0 -24
  1595. data/vendor/spinel/test/super_arg_cross_class_cast.rb +0 -39
  1596. data/vendor/spinel/test/super_arg_cross_class_cast.rb.expected +0 -2
  1597. data/vendor/spinel/test/super_forwarding.rb +0 -25
  1598. data/vendor/spinel/test/super_forwarding.rb.expected +0 -3
  1599. data/vendor/spinel/test/sym_case.rb +0 -15
  1600. data/vendor/spinel/test/sym_case.rb.expected +0 -4
  1601. data/vendor/spinel/test/symbol_ivar_reassign.rb +0 -25
  1602. data/vendor/spinel/test/symbol_ivar_reassign.rb.expected +0 -2
  1603. data/vendor/spinel/test/toplevel_ivar_array.rb +0 -19
  1604. data/vendor/spinel/test/toplevel_ivar_array.rb.expected +0 -4
  1605. data/vendor/spinel/test/unbox_poly_index_in_dispatch.rb +0 -20
  1606. data/vendor/spinel/test/unbox_poly_index_in_dispatch.rb.expected +0 -1
  1607. data/vendor/spinel/test/undef.rb +0 -33
  1608. data/vendor/spinel/test/undef.rb.expected +0 -2
  1609. data/vendor/spinel/test/user_method_named_like_mutator.rb +0 -30
  1610. data/vendor/spinel/test/user_method_named_like_mutator.rb.expected +0 -2
  1611. data/vendor/spinel/test/value_type_ctor_gc_save.rb +0 -46
  1612. data/vendor/spinel/test/value_type_ctor_gc_save.rb.expected +0 -3
  1613. data/vendor/spinel/test/widen_int_obj_to_poly.rb +0 -29
  1614. /data/vendor/spinel/test/{multi_write_from_poly_recv.rb.expected → bare_arg_in_module_body.rb.expected} +0 -0
  1615. /data/vendor/spinel/test/{default_argv_narrows_from_string_call_site.rb.expected → cvar_method_first_assign.rb.expected} +0 -0
  1616. /data/vendor/spinel/test/{array_each_with_object.rb.expected → empty_array_push_container_elem.rb.expected} +0 -0
  1617. /data/vendor/spinel/test/{class_var_write.rb.expected → ffi_array_specs_decl.rb.expected} +0 -0
  1618. /data/vendor/spinel/test/{array_method_name_clash.rb.expected → ffi_int_arg_poly_value.rb.expected} +0 -0
  1619. /data/vendor/spinel/test/{initialize_void_wrapper_gc_save.rb.expected → ffi_ptr_array.rb.expected} +0 -0
  1620. /data/vendor/spinel/test/{inherited_method_param_widen.rb.expected → ffi_ptr_int_literal.rb.expected} +0 -0
  1621. /data/vendor/spinel/test/{forward_call_class_method_nested.rb.expected → ffi_str_arg_poly_value.rb.expected} +0 -0
  1622. /data/vendor/spinel/test/{proc_hash_value.rb.expected → file_write_at_tail_position.rb.expected} +0 -0
  1623. /data/vendor/spinel/test/{array_shuffle_ptr.rb.expected → hash_key_via_intermediate_local.rb.expected} +0 -0
  1624. /data/vendor/spinel/test/{forward_call_class_method_inherited_init_int_array.rb.expected → hash_param_widen_from_callee_slot.rb.expected} +0 -0
  1625. /data/vendor/spinel/test/{strip_nullable_int_cast.rb.expected → hash_temp_gc_root.rb.expected} +0 -0
  1626. /data/vendor/spinel/test/{scan_new_calls_class_scope.rb.expected → poly_recv_hash_dup.rb.expected} +0 -0
  1627. /data/vendor/spinel/test/{class_constant_sym_array.rb.expected → respond_to_def_self_method.rb.expected} +0 -0
  1628. /data/vendor/spinel/test/{initialize_param_through_call.rb.expected → warn_unresolved_call_context.rb.expected} +0 -0
@@ -2,17 +2,12 @@
2
2
  #ifndef SP_RUNTIME_H
3
3
  #define SP_RUNTIME_H
4
4
 
5
- #ifdef __APPLE__
6
- /* getcontext/makecontext/swapcontext are gated behind _XOPEN_SOURCE on
7
- Darwin and marked deprecated since 10.6. _DARWIN_C_SOURCE re-enables
8
- Darwin extensions (MAP_ANON, etc.) that _XOPEN_SOURCE alone would hide.
9
- Suppress the deprecation warning so -Werror builds pass we knowingly
10
- use these APIs because Spinel's Fiber implementation depends on them. */
11
- #define _XOPEN_SOURCE 600
12
- #define _DARWIN_C_SOURCE
13
- #pragma clang diagnostic push
14
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
15
- #endif
5
+ /* Platform feature-test macros (_XOPEN_SOURCE for Darwin's ucontext, etc.)
6
+ live at the top of sp_types.h so every translation unit that includes it --
7
+ the generated TU here, and the standalone libspinel_rt.a units like
8
+ sp_fiber.c that reach <ucontext.h> via sp_fiber.h -> sp_gc.h -> sp_types.h --
9
+ define them before the first system header. Must precede <stdio.h>. */
10
+ #include "sp_types.h"
16
11
 
17
12
  #include <stdio.h>
18
13
  #include <stdlib.h>
@@ -24,8 +19,34 @@
24
19
  #include <stdarg.h>
25
20
  #include <time.h>
26
21
  #include <setjmp.h>
22
+ #include <errno.h>
23
+ #include <sys/stat.h>
24
+ #include <dirent.h>
25
+
26
+ /* Opt-in native backtrace (spinel --debug). In a -g, non-inlined build the
27
+ sp_<method> symbols are present, so sp_raise_cls can snapshot the live C
28
+ stack at raise time and Exception#backtrace / caller format it into a
29
+ Ruby-style backtrace — no per-method shadow frames needed. Off unless the
30
+ generated main() sets sp_bt_enabled (debug builds), so non-debug behaviour
31
+ and cost are unchanged. execinfo is POSIX-ish; absent on Windows. */
32
+ #if !defined(_WIN32)
33
+ #include <execinfo.h>
34
+ #define SP_BT_AVAILABLE 1
35
+ #else
36
+ #define SP_BT_AVAILABLE 0
37
+ #endif
38
+ static int sp_bt_enabled = 0; /* set to 1 by debug-build main() */
39
+ static const char *sp_bt_srcfile = ""; /* toplevel .rb path, set by debug main() */
40
+ #if SP_BT_AVAILABLE
41
+ static void *sp_bt_buf[256]; /* frames captured at the last raise */
42
+ static int sp_bt_n = 0;
43
+ #endif
27
44
  #ifdef _WIN32
28
45
  #include <windows.h>
46
+ #include <process.h>
47
+ #include <direct.h>
48
+ #include <io.h> /* _pipe (POSIX pipe(2) is absent on MinGW) */
49
+ #include <fcntl.h> /* _O_BINARY */
29
50
  /* POSIX compat shims for MinGW */
30
51
  #define mmap(a,l,p,f,fd,off) VirtualAlloc(NULL,(l),MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE)
31
52
  #define munmap(a,l) (VirtualFree((a),0,MEM_RELEASE)?0:-1)
@@ -39,8 +60,9 @@
39
60
  #include <ucontext.h>
40
61
  #include <unistd.h>
41
62
  #include <sys/mman.h>
63
+ #include <sys/wait.h>
42
64
  #endif
43
- #if !defined(__APPLE__) && !defined(_WIN32)
65
+ #if !defined(__APPLE__) && !defined(_WIN32) && !defined(__FreeBSD__)
44
66
  #include <malloc.h>
45
67
  #else
46
68
  /* Darwin's libc has no malloc_trim; make it a no-op so call sites stay portable. */
@@ -49,27 +71,30 @@
49
71
  #ifndef MAP_ANONYMOUS
50
72
  #define MAP_ANONYMOUS MAP_ANON
51
73
  #endif
52
-
53
- typedef int64_t mrb_int;
54
- typedef double mrb_float;
55
- typedef bool mrb_bool;
56
- /* sp_sym is defined per-program in emit_sym_runtime, but poly helpers
57
- below need to reference it by forward declaration. */
58
- typedef mrb_int sp_sym;
59
- static const char *sp_sym_to_s(sp_sym id);
60
- #ifndef TRUE
61
- #define TRUE true
74
+ #ifndef S_ISDIR
75
+ #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
62
76
  #endif
63
- #ifndef FALSE
64
- #define FALSE false
77
+ #ifndef S_ISREG
78
+ #define S_ISREG(m) (((m) & S_IFMT) == S_IFREG)
65
79
  #endif
66
80
 
81
+ /* Core value-type definitions (primitives, leaf structs, GC headers,
82
+ typed arrays, non-poly hashes) live in sp_types.h so libspinel_rt.a
83
+ sources can share them without the function bodies below. */
84
+ #include "sp_types.h"
85
+ /* sp_RbVal, the collector globals/entry points, and the hot inline mark
86
+ helpers; the collector body lives in libspinel_rt.a (lib/sp_gc.c). */
87
+ #include "sp_gc.h"
88
+ /* sp_Fiber + the Fiber API; the bodies live in libspinel_rt.a (lib/sp_fiber.c). */
89
+ #include "sp_fiber.h"
90
+ static const char *sp_sym_to_s(sp_sym id);
91
+
67
92
  /* sp_raise_cls forward decl — defined later in this header (line ~1017).
68
93
  Used by the integer-division helpers below to match CRuby semantics:
69
94
  `a / 0`, `a % 0`, `a.divmod(0)`, `a.ceildiv(0)`, and `a.pow(e, 0)` all
70
95
  raise ZeroDivisionError instead of triggering C undefined behaviour
71
96
  (SIGFPE on x86) or silently returning 0. */
72
- static void sp_raise_cls(const char *cls, const char *msg);
97
+ SP_NORETURN SP_COLD void sp_raise_cls(const char *cls, const char *msg);
73
98
 
74
99
  static inline mrb_int sp_idiv(mrb_int a, mrb_int b) {
75
100
  if (b == 0) sp_raise_cls("ZeroDivisionError", "divided by 0");
@@ -83,65 +108,318 @@ static inline mrb_int sp_imod(mrb_int a, mrb_int b) {
83
108
  if ((r != 0) && ((r ^ b) < 0)) r += b;
84
109
  return r;
85
110
  }
111
+ /* Overflow-checked integer arithmetic (BIGINT.md option β: raise on
112
+ overflow, keep locals at native mrb_int width).
113
+
114
+ Build modes (preprocessor toggles):
115
+ default : overflow raises RangeError
116
+ SP_NO_OVERFLOW_CHECK : bare `+ - *`, silent wrap (UB on signed
117
+ overflow, but matches spinel's prior
118
+ behaviour where the user asks for
119
+ max speed and accepts the risk)
120
+
121
+ The actual overflow check is wrapped through
122
+ `sp_int_*_overflow_p` helpers so non-gcc/clang compilers fall back
123
+ to a portable unsigned-arithmetic implementation. The shape
124
+ mirrors mruby's mrb_int_*_overflow in include/mruby/numeric.h.
125
+
126
+ The outer sp_int_add/sub/mul/neg are GCC statement-expression
127
+ macros (`({ ... })`) rather than functions: an earlier inline-
128
+ function variant produced wrong optcarrot output (checksum
129
+ diverged 59662→4096) even with __attribute__((always_inline)),
130
+ apparently from a UB-assumption interaction between the
131
+ optimizer and the function-call-shaped expression that the
132
+ macro form bypasses by keeping every operand in its surrounding
133
+ expression context. The macros also let the compiler fold
134
+ constant-operand cases at every call site. */
135
+ #ifndef __has_builtin
136
+ # define __has_builtin(x) 0
137
+ #endif
138
+ #if (defined(__GNUC__) && __GNUC__ >= 5) || \
139
+ (__has_builtin(__builtin_add_overflow) && \
140
+ __has_builtin(__builtin_sub_overflow) && \
141
+ __has_builtin(__builtin_mul_overflow))
142
+ # define SP_HAVE_OVERFLOW_BUILTINS 1
143
+ #endif
144
+
145
+ #ifdef SP_HAVE_OVERFLOW_BUILTINS
146
+ static inline mrb_bool sp_int_add_overflow_p(mrb_int a, mrb_int b, mrb_int *r) {
147
+ return __builtin_add_overflow(a, b, r);
148
+ }
149
+ static inline mrb_bool sp_int_sub_overflow_p(mrb_int a, mrb_int b, mrb_int *r) {
150
+ return __builtin_sub_overflow(a, b, r);
151
+ }
152
+ static inline mrb_bool sp_int_mul_overflow_p(mrb_int a, mrb_int b, mrb_int *r) {
153
+ return __builtin_mul_overflow(a, b, r);
154
+ }
155
+ #else
156
+ /* Portable fallback for compilers lacking __builtin_*_overflow.
157
+ mrb_int is pointer-width (intptr_t), so compute in uintptr_t --
158
+ unsigned overflow is well-defined wrap-around in C -- and detect
159
+ signed overflow via the sign-bit XOR trick at the *correct* width
160
+ (the sign bit is mrb_int's top bit: 63 on 64-bit, 31 on 32-bit).
161
+ Bounds use INTPTR_MAX/MIN, not the int64 MRB_INT_* macros, so this
162
+ path is self-contained and width-correct. Mul checks bounds before
163
+ multiplying because a 2x-width intermediate isn't portable. */
164
+ #define SP_INT_OVF_SIGN ((uintptr_t)1 << (sizeof(mrb_int) * 8 - 1))
165
+ static inline mrb_bool sp_int_add_overflow_p(mrb_int a, mrb_int b, mrb_int *r) {
166
+ uintptr_t x = (uintptr_t)a, y = (uintptr_t)b, z = x + y;
167
+ *r = (mrb_int)z;
168
+ return !!(((x ^ z) & (y ^ z)) & SP_INT_OVF_SIGN);
169
+ }
170
+ static inline mrb_bool sp_int_sub_overflow_p(mrb_int a, mrb_int b, mrb_int *r) {
171
+ uintptr_t x = (uintptr_t)a, y = (uintptr_t)b, z = x - y;
172
+ *r = (mrb_int)z;
173
+ return !!(((x ^ z) & (~y ^ z)) & SP_INT_OVF_SIGN);
174
+ }
175
+ static inline mrb_bool sp_int_mul_overflow_p(mrb_int a, mrb_int b, mrb_int *r) {
176
+ if (a > 0 && b > 0 && a > INTPTR_MAX / b) { *r = a * b; return TRUE; }
177
+ if (a < 0 && b > 0 && a < INTPTR_MIN / b) { *r = a * b; return TRUE; }
178
+ if (a > 0 && b < 0 && b < INTPTR_MIN / a) { *r = a * b; return TRUE; }
179
+ if (a < 0 && b < 0 && (a <= INTPTR_MIN || b <= INTPTR_MIN || -a > INTPTR_MAX / -b)) {
180
+ *r = a * b; return TRUE;
181
+ }
182
+ *r = a * b;
183
+ return FALSE;
184
+ }
185
+ #undef SP_INT_OVF_SIGN
186
+ #endif
187
+
188
+ /* Three modes selected by `--int-overflow=raise|wrap|promote` on the
189
+ spinel wrapper, which passes -DSP_INT_OVERFLOW_MODE_{RAISE,WRAP,
190
+ PROMOTE}. WRAP skips the check entirely. PROMOTE still raises at
191
+ this layer -- the actual promotion semantics is implemented in the
192
+ analyzer by rewriting every int local as bigint, so the helpers
193
+ below are only reached on the few residual sites that stay int
194
+ even in promote mode (FFI argument coercion, etc.). */
195
+ #ifdef SP_INT_OVERFLOW_MODE_WRAP
196
+ # define sp_int_add(a, b) ((a) + (b))
197
+ # define sp_int_sub(a, b) ((a) - (b))
198
+ # define sp_int_mul(a, b) ((a) * (b))
199
+ # define sp_int_neg(a) (-(a))
200
+ #else
201
+ # define sp_int_add(a, b) ({ mrb_int _sp_a = (a), _sp_b = (b), _sp_r; \
202
+ if (sp_int_add_overflow_p(_sp_a, _sp_b, &_sp_r)) sp_raise_cls("RangeError", "integer overflow in +"); \
203
+ _sp_r; })
204
+ # define sp_int_sub(a, b) ({ mrb_int _sp_a = (a), _sp_b = (b), _sp_r; \
205
+ if (sp_int_sub_overflow_p(_sp_a, _sp_b, &_sp_r)) sp_raise_cls("RangeError", "integer overflow in -"); \
206
+ _sp_r; })
207
+ # define sp_int_mul(a, b) ({ mrb_int _sp_a = (a), _sp_b = (b), _sp_r; \
208
+ if (sp_int_mul_overflow_p(_sp_a, _sp_b, &_sp_r)) sp_raise_cls("RangeError", "integer overflow in *"); \
209
+ _sp_r; })
210
+ # define sp_int_neg(a) ({ mrb_int _sp_a = (a), _sp_r; \
211
+ if (sp_int_sub_overflow_p((mrb_int)0, _sp_a, &_sp_r)) sp_raise_cls("RangeError", "integer overflow in -@"); \
212
+ _sp_r; })
213
+ #endif
86
214
 
87
- static mrb_int sp_gcd(mrb_int a,mrb_int b){if(a<0)a=-a;if(b<0)b=-b;while(b){mrb_int t=b;b=a%b;a=t;}return a;}
88
- static mrb_int sp_lcm(mrb_int a,mrb_int b){if(a==0||b==0)return 0;mrb_int g=sp_gcd(a,b);if(a<0)a=-a;if(b<0)b=-b;return (a/g)*b;}
89
- static mrb_int sp_powmod(mrb_int base,mrb_int exp,mrb_int mod){if(mod==0)sp_raise_cls("ZeroDivisionError","divided by 0");mrb_int r=1;mrb_int m=mod<0?-mod:mod;if(m==1){r=0;}else{base=base%m;if(base<0)base+=m;while(exp>0){if(exp%2==1)r=r*base%m;exp=exp/2;base=base*base%m;}}if(mod<0&&r>0)r-=m;return r;}
90
- static mrb_int sp_ceildiv(mrb_int a,mrb_int b){if(b==0)sp_raise_cls("ZeroDivisionError","divided by 0");if(b==-1)return -a;mrb_int q=a/b;if(a%b!=0&&((a^b)>=0))q++;return q;}
91
- static mrb_int sp_int_clamp(mrb_int v,mrb_int lo,mrb_int hi){return v<lo?lo:v>hi?hi:v;}
92
- /* Integer square root via Newton's method — exact for the full mrb_int
93
- range (no double-precision rounding loss for n > 2^53). CRuby raises
94
- on negative input; we mirror Spinel's other arithmetic helpers and
95
- return 0 to avoid an exception path. */
96
- static mrb_int sp_int_sqrt(mrb_int n){if(n<0)return 0;if(n<2)return n;mrb_int x=n,y=(x+1)/2;while(y<x){x=y;y=(x+n/x)/2;}return x;}
215
+ /* sp_gcd / sp_lcm / sp_powmod / sp_ceildiv / sp_int_clamp / sp_int_sqrt
216
+ now live in libspinel_rt.a (lib/sp_core.c); declared via sp_core.h. */
97
217
  static inline char *sp_str_alloc_raw(size_t total_with_null); /* fwd decl */
98
218
  static const char*sp_int_chr(mrb_int n){char*s=sp_str_alloc_raw(2);s[0]=(char)n;s[1]=0;return s;}
99
- typedef struct{mrb_int first;mrb_int last;}sp_Range;
100
- static sp_Range sp_range_new(mrb_int f,mrb_int l){sp_Range r;r.first=f;r.last=l;return r;}
219
+ /* sp_ipow10 / sp_int_round / sp_int_ceil / sp_int_floor /
220
+ sp_int_truncate / sp_str_oct now live in libspinel_rt.a
221
+ (lib/sp_core.c); declared via sp_core.h. */
222
+ /* Narrow a foreign 64-bit value (time_t / off_t, etc.) to a Ruby
223
+ Integer. On 64-bit mrb_int this is the identity. On 32-bit, a value
224
+ outside the mrb_int range can't be represented; rather than silently
225
+ truncating a clock/size value the program never computed (which
226
+ `int-overflow=wrap` is NOT meant to license -- wrap is about the
227
+ user's own arithmetic), raise RangeError. promote-mode bigint
228
+ promotion of these boundary values is a follow-up. */
229
+ static inline mrb_int sp_i64_to_int(int64_t v){
230
+ #if INTPTR_MAX == INT64_MAX
231
+ return (mrb_int)v;
232
+ #else
233
+ if(v < (int64_t)INTPTR_MIN || v > (int64_t)INTPTR_MAX)
234
+ sp_raise_cls("RangeError","value out of range for 32-bit Integer");
235
+ return (mrb_int)v;
236
+ #endif
237
+ }
238
+
239
+ /* Forward decls for helpers used across this header (and by the
240
+ string->number parsers that now live in libspinel_rt.a). */
241
+ SP_NORETURN SP_COLD void sp_raise_cls(const char *cls, const char *msg);
242
+ const char *sp_sprintf(const char *fmt, ...);
243
+
244
+ /* String -> number parsers now live in libspinel_rt.a (lib/sp_core.c). */
245
+ #include "sp_core.h"
246
+ /* system()/backtick ($?) support now lives in libspinel_rt.a (lib/sp_system.c). */
247
+ #include "sp_system.h"
248
+
249
+ static sp_Range sp_range_new(mrb_int f,mrb_int l,mrb_int e){sp_Range r;r.first=f;r.last=l;r.excl=e;return r;}
250
+ static mrb_bool sp_range_eq(sp_Range a,sp_Range b){return a.first==b.first&&a.last==b.last&&a.excl==b.excl;}
251
+ /* `Range#include?`/`#cover?` on the boxed (SP_TAG_OBJ cls_id
252
+ SP_BUILTIN_RANGE) Range value. The direct sp_Range typed path
253
+ inlines this same check via compile_range_method_expr; poly-recv
254
+ dispatch needs the wrapper so the cls_id arm in
255
+ emit_poly_builtin_dispatch can land on a single C expression. An
256
+ exclusive range stops one short of `last`, so the upper bound is
257
+ `last - excl` (excl is 0 or 1). */
258
+ static mrb_bool sp_range_include(sp_Range *r, mrb_int x){return r->first<=x && x<=r->last-r->excl;}
259
+
260
+ /* ---- Class object ----
261
+ Value-type Class reference: a single class id that indexes into
262
+ the per-program sp_class_names[] table emitted by codegen. Lets
263
+ `c = Foo` produce a runtime value (`(sp_Class){<id>}`) instead of
264
+ a bare C identifier, and `c.to_s` lower to a names-table lookup.
265
+ Other Class methods (`.name`, `.inspect`, `.==`, `.!=`,
266
+ `.superclass`, `.ancestors`, dynamic `is_a?(c)` against a
267
+ variable, etc.) are not yet supported. */
268
+
269
+ /* ---- Complex runtime ---- */
270
+ /* Value-type Cartesian Complex: 16 bytes, passed by value. Used by
271
+ optcarrot's nestopia palette generator; the palette is precomputed
272
+ in the default code path so this is exercised only with
273
+ `--nestopia-palette`. */
274
+ static inline sp_Complex sp_complex_polar(mrb_float m,mrb_float a){sp_Complex c;c.re=m*cos(a);c.im=m*sin(a);return c;}
275
+ static inline sp_Complex sp_complex_add(sp_Complex a,sp_Complex b){sp_Complex c;c.re=a.re+b.re;c.im=a.im+b.im;return c;}
276
+ static inline sp_Complex sp_complex_mul(sp_Complex a,sp_Complex b){sp_Complex c;c.re=a.re*b.re-a.im*b.im;c.im=a.re*b.im+a.im*b.re;return c;}
277
+ static inline sp_Complex sp_complex_conjugate(sp_Complex a){sp_Complex c;c.re=a.re;c.im=-a.im;return c;}
278
+ /* Inspect renders Complex per Ruby: `(re+imi)` or `(re-imi)` for
279
+ negative imaginary. Integer-valued components render without
280
+ decimals; fractional render via %g. Issue #840. */
281
+ static const char *sp_complex_inspect(sp_Complex c) {
282
+ char buf[128];
283
+ int n = 0;
284
+ /* Real part: keep integer-looking values short. */
285
+ if (c.re == (mrb_int)c.re) n += snprintf(buf + n, sizeof(buf) - n, "(%lld", (long long)c.re);
286
+ else n += snprintf(buf + n, sizeof(buf) - n, "(%g", c.re);
287
+ /* Imaginary sign + value. */
288
+ if (c.im < 0) {
289
+ if (c.im == (mrb_int)c.im) n += snprintf(buf + n, sizeof(buf) - n, "-%lldi)", -(long long)c.im);
290
+ else n += snprintf(buf + n, sizeof(buf) - n, "%gi)", c.im);
291
+ } else {
292
+ if (c.im == (mrb_int)c.im) n += snprintf(buf + n, sizeof(buf) - n, "+%lldi)", (long long)c.im);
293
+ else n += snprintf(buf + n, sizeof(buf) - n, "+%gi)", c.im);
294
+ }
295
+ if (n < 0) n = 0;
296
+ char *r = sp_str_alloc_raw(n + 1);
297
+ memcpy(r, buf, n);
298
+ r[n] = 0;
299
+ return r;
300
+ }
301
+
302
+ /* ---- Rational runtime ---- */
303
+ /* Value-type Rational: 16 bytes (two mrb_ints), passed by value.
304
+ Stored in reduced form -- the parser hands us the already-reduced
305
+ numerator/denominator from the literal; Integer#quo / arithmetic
306
+ normalizes via sp_rational_reduce. Issue #841. */
307
+ static inline mrb_int sp_rational_gcd_i(mrb_int a, mrb_int b) {
308
+ if (a < 0) a = -a;
309
+ if (b < 0) b = -b;
310
+ while (b) { mrb_int t = b; b = a % b; a = t; }
311
+ return a;
312
+ }
313
+ static inline sp_Rational sp_rational_new(mrb_int n, mrb_int d) {
314
+ sp_Rational r;
315
+ if (d == 0) { r.num = n; r.den = 0; return r; }
316
+ if (d < 0) { n = -n; d = -d; }
317
+ mrb_int g = sp_rational_gcd_i(n, d);
318
+ if (g <= 0) g = 1;
319
+ r.num = n / g;
320
+ r.den = d / g;
321
+ return r;
322
+ }
323
+ static const char *sp_rational_inspect(sp_Rational r) {
324
+ char buf[64];
325
+ int n = snprintf(buf, sizeof(buf), "(%lld/%lld)", (long long)r.num, (long long)r.den);
326
+ if (n < 0) n = 0;
327
+ char *o = sp_str_alloc_raw(n + 1);
328
+ memcpy(o, buf, n);
329
+ o[n] = 0;
330
+ return o;
331
+ }
101
332
 
102
333
  /* ---- Time runtime ---- */
103
- /* sp_Time keeps Time.now / Time.at as value-typed structs. d78149b's
104
- sub-second precision is preserved by inlining tv_sec + tv_nsec/1e9
105
- at every Time#to_f / Time#- call site (see spinel_codegen.rb). */
106
- typedef struct { int64_t tv_sec; int32_t tv_nsec; } sp_Time;
107
- static inline sp_Time sp_time_now(void) {
108
- struct timespec ts;
109
- clock_gettime(CLOCK_REALTIME, &ts);
110
- return (sp_Time){ ts.tv_sec, (int32_t)ts.tv_nsec };
111
- }
112
- static inline sp_Time sp_time_at_int(mrb_int sec) {
113
- return (sp_Time){ (int64_t)sec, 0 };
114
- }
115
- /* POSIX convention: keep tv_nsec in [0, 1e9). For negative epoch with a
116
- non-integer fractional part, decrement tv_sec and roll the fraction
117
- into the positive nsec range so Time.at(-0.5).to_i returns -1, not 0. */
118
- static inline sp_Time sp_time_at_float(mrb_float epoch) {
119
- int64_t sec = (int64_t)epoch;
120
- mrb_float frac = epoch - (mrb_float)sec;
121
- if (frac < 0.0) {
122
- sec -= 1;
123
- frac += 1.0;
124
- }
125
- return (sp_Time){ sec, (int32_t)(frac * 1e9) };
126
- }
127
-
128
- typedef struct sp_gc_hdr { struct sp_gc_hdr *next; void (*finalize)(void *); void (*scan)(void *); size_t size; unsigned marked : 1; } sp_gc_hdr;
129
- static sp_gc_hdr *sp_gc_heap = NULL; static size_t sp_gc_bytes = 0; static size_t sp_gc_threshold = 256*1024;
334
+ /* sp_Time and the libc-backed accessors / formatters live in
335
+ lib/sp_time.{c,h} (compiled into libspinel_rt.a). What stays here
336
+ are the GC-aware wrappers sp_box_time copies the value onto the
337
+ GC heap, and the *_gc string forwarders allocate a small stack buf,
338
+ call the libspinel_rt format helper, then sp_str_dup_external the
339
+ result into the GC string heap. is_utc distinguishes UTC-coerced
340
+ times from local-zone times; the underlying epoch is the same. */
341
+ #include "sp_time.h"
342
+
343
+
344
+ /* `recycle`: optional sweep hook. If non-NULL, sp_gc_collect calls
345
+ recycle(h) on the unmarked object instead of finalize+free. The
346
+ hook is responsible for deciding whether to keep the storage
347
+ (pool push) or free it. Used by class-instance free-list pools. */
348
+ /* sp_gc_heap / sp_gc_bytes are defined in lib/sp_gc.c (extern via sp_gc.h). */
349
+ static size_t sp_gc_threshold = 256*1024;
350
+
351
+ /* ---- GC verify: opt-in mark-path validation (release-neutral) ------------
352
+ * SPINEL_GC_VERIFY=1 before the collector invokes an object's scan hook,
353
+ * check that the object is a currently-registered heap allocation. If a
354
+ * raw/aliased pointer (e.g. into a string or builder buffer) has been put
355
+ * on the mark path, abort with a diagnostic at that point instead of
356
+ * calling through a bogus scan-function pointer (which otherwise faults at
357
+ * a garbage address and is near-impossible to attribute).
358
+ * Default OFF; with it unset, behavior and codegen are unchanged. */
359
+ /* GC verify state + helpers live in lib/sp_gc.c. */
130
360
 
131
361
  /* ---- String GC ---- */
132
- typedef struct sp_str_hdr { struct sp_str_hdr *next; size_t size; } sp_str_hdr;
133
362
  static sp_str_hdr *sp_str_heap = NULL;
134
363
  #define SPL(s) (&("\xff" s)[1])
135
364
  static const char sp_str_empty_data[] = "\xff";
136
365
  #define sp_str_empty (sp_str_empty_data + 1)
137
366
 
367
+ /* RUBY_PLATFORM string -- host arch + OS. Detected at C compile time
368
+ so cross-builds report the target platform. Issue #890. */
369
+ #if defined(__x86_64__) || defined(_M_X64)
370
+ # define SP_RUBY_ARCH "x86_64"
371
+ #elif defined(__aarch64__) || defined(_M_ARM64)
372
+ # define SP_RUBY_ARCH "aarch64"
373
+ #elif defined(__i386__) || defined(_M_IX86)
374
+ # define SP_RUBY_ARCH "i686"
375
+ #elif defined(__arm__)
376
+ # define SP_RUBY_ARCH "arm"
377
+ #else
378
+ # define SP_RUBY_ARCH "unknown"
379
+ #endif
380
+ #if defined(__linux__)
381
+ # define SP_RUBY_OS "linux"
382
+ #elif defined(__APPLE__)
383
+ # define SP_RUBY_OS "darwin"
384
+ #elif defined(_WIN32) || defined(_WIN64)
385
+ # define SP_RUBY_OS "mingw32"
386
+ #elif defined(__FreeBSD__)
387
+ # define SP_RUBY_OS "freebsd"
388
+ #else
389
+ # define SP_RUBY_OS "unknown"
390
+ #endif
391
+ static const char sp_ruby_platform_data[] = "\xff" SP_RUBY_ARCH "-" SP_RUBY_OS;
392
+ static inline const char *sp_ruby_platform_str(void) { return sp_ruby_platform_data + 1; }
393
+
394
+ /* Process.ppid wrapper. MinGW's unistd.h doesn't expose getppid;
395
+ return 0 there. Issue #893. */
396
+ static inline mrb_int sp_process_ppid(void) {
397
+ #ifdef _WIN32
398
+ return 0;
399
+ #else
400
+ return (mrb_int)getppid();
401
+ #endif
402
+ }
403
+
404
+ /* ---- Encoding runtime ----
405
+ Spinel currently assumes UTF-8 source and string data. Keep Encoding
406
+ as a tiny value type so `__ENCODING__` and `String#encoding` can
407
+ answer Ruby's Encoding-shaped protocol without carrying full
408
+ transcoding state. */
409
+ static inline sp_Encoding sp_encoding_utf8(void){return(sp_Encoding){"UTF-8"};}
410
+ static inline const char*sp_encoding_name(sp_Encoding e){return e.name?e.name:sp_str_empty;}
411
+ static inline const char*sp_encoding_inspect(sp_Encoding e){return sp_sprintf("#<Encoding:%s>",sp_encoding_name(e));}
412
+ static inline mrb_bool sp_encoding_eq(sp_Encoding a,sp_Encoding b){const char*an=sp_encoding_name(a);const char*bn=sp_encoding_name(b);return strcmp(an,bn)==0;}
413
+
138
414
  static char *sp_str_alloc(size_t len) {
139
415
  size_t total = sizeof(sp_str_hdr) + 1 + len + 1;
140
416
  sp_str_hdr *h = (sp_str_hdr *)malloc(total);
417
+ if (!h) sp_oom_die();
141
418
  h->next = sp_str_heap;
142
419
  h->size = total;
420
+ h->len = len;
143
421
  sp_str_heap = h;
144
- /* Don't fold string-heap pressure into sp_gc_bytes (issue #99): the
422
+ /* Don't fold string-heap pressure into sp_gc_bytes : the
145
423
  threshold heuristic in sp_gc_alloc is keyed on heap survivors, and
146
424
  the str-heap mark-sweep that runs alongside (sp_str_sweep,
147
425
  called from sp_gc_collect) doesn't add surviving strings back into
@@ -166,6 +444,23 @@ static inline char *sp_str_alloc_raw(size_t total_with_null) {
166
444
  return sp_str_alloc(total_with_null > 0 ? total_with_null - 1 : 0);
167
445
  }
168
446
 
447
+ static inline size_t sp_str_byte_len(const char *s) {
448
+ if (!s) return 0;
449
+ unsigned char marker = ((const unsigned char *)s)[-1];
450
+ if (marker == 0xfe || marker == 0xfc) {
451
+ return (((const sp_str_hdr *)(s - 1)) - 1)->len;
452
+ }
453
+ return strlen(s);
454
+ }
455
+
456
+ static inline void sp_str_set_len(char *s, size_t len) {
457
+ if (!s) return;
458
+ unsigned char marker = ((unsigned char *)s)[-1];
459
+ if (marker == 0xfe || marker == 0xfc) {
460
+ (((sp_str_hdr *)(s - 1)) - 1)->len = len;
461
+ }
462
+ }
463
+
169
464
  static const char *sp_str_dup_external(const char *s) {
170
465
  if (!s) return NULL;
171
466
  size_t n = strlen(s);
@@ -182,26 +477,167 @@ static inline int sp_utf8_char_len(unsigned char c){if(c<0x80)return 1;if(c<0xC0
182
477
  static inline int sp_utf8_advance(const char*p){int cn=sp_utf8_char_len((unsigned char)*p);int i=1;while(i<cn&&((unsigned char)p[i]&0xC0)==0x80)i++;return i;}
183
478
  static inline int sp_utf8_decode(const char*p,uint32_t*out){unsigned char c=(unsigned char)p[0];if(c<0x80){*out=c;return 1;}if(c<0xC0){*out=c;return 1;}unsigned char c1=(unsigned char)p[1];if((c1&0xC0)!=0x80){*out=c;return 1;}if(c<0xE0){*out=((uint32_t)(c&0x1F)<<6)|(c1&0x3F);return 2;}unsigned char c2=(unsigned char)p[2];if((c2&0xC0)!=0x80){*out=c;return 1;}if(c<0xF0){*out=((uint32_t)(c&0x0F)<<12)|((uint32_t)(c1&0x3F)<<6)|(c2&0x3F);return 3;}unsigned char c3=(unsigned char)p[3];if((c3&0xC0)!=0x80){*out=c;return 1;}*out=((uint32_t)(c&0x07)<<18)|((uint32_t)(c1&0x3F)<<12)|((uint32_t)(c2&0x3F)<<6)|(c3&0x3F);return 4;}
184
479
  static inline int sp_utf8_encode(uint32_t cp,char*out){if(cp<0x80){out[0]=(char)cp;return 1;}if(cp<0x800){out[0]=(char)(0xC0|(cp>>6));out[1]=(char)(0x80|(cp&0x3F));return 2;}if(cp<0x10000){out[0]=(char)(0xE0|(cp>>12));out[1]=(char)(0x80|((cp>>6)&0x3F));out[2]=(char)(0x80|(cp&0x3F));return 3;}out[0]=(char)(0xF0|(cp>>18));out[1]=(char)(0x80|((cp>>12)&0x3F));out[2]=(char)(0x80|((cp>>6)&0x3F));out[3]=(char)(0x80|(cp&0x3F));return 4;}
185
- static mrb_int sp_str_length(const char*s){mrb_int n=0;while(*s){s+=sp_utf8_advance(s);n++;}return n;}
186
- static mrb_int sp_str_ord(const char*s){if(!*s)return 0;uint32_t cp;sp_utf8_decode(s,&cp);return(mrb_int)cp;}
187
- /* NULL-safe string equality. Issue #129: ENV[] returns NULL for unset vars
480
+ /* Issue #882: `"hello" << 33` should append the character with
481
+ that codepoint, not the decimal digits. UTF-8 encode (1..4 bytes)
482
+ and return a NUL-terminated string. */
483
+ static const char *sp_int_codepoint_to_str(mrb_int n) {
484
+ char *s = sp_str_alloc_raw(5);
485
+ if (n < 0 || n > 0x10FFFF) { s[0] = 0; return s; }
486
+ int len = sp_utf8_encode((uint32_t)n, s);
487
+ s[len] = 0;
488
+ return s;
489
+ }
490
+ /* Direct-mapped pointer-keyed cache for (byte_len, char_len). Populated lazily
491
+ by sp_str_length / sp_utf8_byte_offset; the same entries unlock both calls
492
+ so iterating a single string with `s.length` + `s[i]` walks UTF-8 once
493
+ instead of per access. ASCII strings (char_len == byte_len) take an O(1)
494
+ byte_offset fast path. Cleared by sp_str_sweep so freed-and-reused heap
495
+ addresses can't leak stale entries. Skipped for sp_String wrappers (marker
496
+ 0xfd) whose buffers can move on realloc. */
497
+ #define SP_STR_LCACHE_BITS 5
498
+ #define SP_STR_LCACHE_SIZE (1u << SP_STR_LCACHE_BITS)
499
+ static struct sp_str_lcache_entry {
500
+ const char *s;
501
+ size_t byte_len;
502
+ mrb_int char_len;
503
+ } sp_str_lcache[SP_STR_LCACHE_SIZE];
504
+
505
+ static inline unsigned sp_str_lcache_hash(const char *s) {
506
+ uintptr_t k = (uintptr_t)s;
507
+ return (unsigned)((k ^ (k >> 4) ^ (k >> 12)) & (SP_STR_LCACHE_SIZE - 1));
508
+ }
509
+
510
+ static inline void sp_str_lcache_clear(void) {
511
+ for (unsigned i = 0; i < SP_STR_LCACHE_SIZE; i++) sp_str_lcache[i].s = NULL;
512
+ }
513
+
514
+ /* Count UTF-8 code points in s[0..bl). The 8-byte ASCII-detect prologue skips
515
+ bytes 8 at a time while the high bit stays clear, so pure-ASCII strings (the
516
+ common case in the JSON / CSV / template benchmarks) run vastly faster than
517
+ the per-byte advance loop they used to fall through. */
518
+ static mrb_int sp_str_count_chars(const char *s, size_t bl) {
519
+ const char *p = s, *end = s + bl;
520
+ mrb_int n = 0;
521
+ while (p + 8 <= end) {
522
+ uint64_t w;
523
+ memcpy(&w, p, sizeof(w));
524
+ if (w & 0x8080808080808080ULL) break;
525
+ p += 8; n += 8;
526
+ }
527
+ while (p < end) {
528
+ if ((unsigned char)*p < 0x80) { p++; n++; }
529
+ else { p += sp_utf8_advance(p); n++; }
530
+ }
531
+ return n;
532
+ }
533
+
534
+ /* True when `s` carries one of spinel's own string markers in the
535
+ preceding byte (0xfe / 0xfc heap, 0xff rodata literal). FFI returns
536
+ a bare `const char *` whose preceding byte is whatever C variable
537
+ sits before the buffer in memory — using the pointer as a cache
538
+ key without this gate aliased subsequent FFI calls into the prior
539
+ call's cached length (#611). 0xfd (sp_String wrapper) is excluded
540
+ too because its buffer can move on append. */
541
+ static inline int sp_str_cacheable(const char *s) {
542
+ unsigned char m = ((const unsigned char *)s)[-1];
543
+ return m == 0xfe || m == 0xfc || m == 0xff;
544
+ }
545
+
546
+ static mrb_int sp_str_length(const char*s){
547
+ if (!s) return 0;
548
+ if (!sp_str_cacheable(s)) return sp_str_count_chars(s, sp_str_byte_len(s));
549
+ unsigned h = sp_str_lcache_hash(s);
550
+ if (sp_str_lcache[h].s == s) return sp_str_lcache[h].char_len;
551
+ size_t bl = sp_str_byte_len(s);
552
+ mrb_int n = sp_str_count_chars(s, bl);
553
+ sp_str_lcache[h].s = s;
554
+ sp_str_lcache[h].byte_len = bl;
555
+ sp_str_lcache[h].char_len = n;
556
+ return n;
557
+ }
558
+ static mrb_int sp_str_ord(const char*s){if(!s)sp_raise_cls("ArgumentError","empty string");unsigned char m=((const unsigned char*)s)[-1];size_t blen;if(m==0xfe||m==0xfc){blen=(((const sp_str_hdr*)(s-1))-1)->len;if(blen==0)sp_raise_cls("ArgumentError","empty string");}else{blen=strlen(s);if(blen==0)sp_raise_cls("ArgumentError","empty string");}uint32_t cp;sp_utf8_decode(s,&cp);return(mrb_int)cp;}
559
+ /* NULL-safe string equality. ENV[] returns NULL for unset vars
188
560
  (the dispatch is `sp_str_dup_external(getenv(...))`, which propagates
189
561
  NULL), so emitted strcmp(...) on the result of `ENV["X"] == "1"` would
190
562
  dereference NULL on either side. nil-vs-string equality is false in
191
563
  Ruby; nil == nil is true, so falling back to pointer equality on the
192
564
  NULL path covers both. */
193
565
  static inline int sp_str_eq(const char*a,const char*b){if(!a||!b)return a==b;return strcmp(a,b)==0;}
194
- static size_t sp_utf8_byte_offset(const char*s,mrb_int char_idx){const char*p=s;while(char_idx>0&&*p){p+=sp_utf8_advance(p);char_idx--;}return(size_t)(p-s);}
195
- static uint32_t*sp_utf8_decode_all(const char*s,size_t*out_n){size_t cap=8,n=0;uint32_t*cps=(uint32_t*)malloc(cap*sizeof(uint32_t));const char*p=s;while(*p){if(n>=cap){cap*=2;cps=(uint32_t*)realloc(cps,cap*sizeof(uint32_t));}uint32_t cp;p+=sp_utf8_decode(p,&cp);cps[n++]=cp;}*out_n=n;return cps;}
196
- static int sp_utf8_set_has(const uint32_t*cps,size_t n,uint32_t cp){for(size_t i=0;i<n;i++)if(cps[i]==cp)return 1;return 0;}
566
+ static size_t sp_utf8_byte_offset(const char*s,mrb_int char_idx){
567
+ if (!s || char_idx <= 0) return 0;
568
+ if (sp_str_cacheable(s)) {
569
+ unsigned h = sp_str_lcache_hash(s);
570
+ if (sp_str_lcache[h].s == s
571
+ && (size_t)sp_str_lcache[h].char_len == sp_str_lcache[h].byte_len) {
572
+ size_t off = (size_t)char_idx;
573
+ return off > sp_str_lcache[h].byte_len ? sp_str_lcache[h].byte_len : off;
574
+ }
575
+ }
576
+ /* Walk char_idx code points or stop at byte_len, whichever comes first.
577
+ Bounding on byte_len (instead of "*p != 0") keeps the walk correct
578
+ past an embedded NUL byte -- a heap string with the 0xfe/0xfc marker
579
+ carries its real length in the sp_str_hdr, so a NUL byte inside the
580
+ payload no longer terminates the walk prematurely. For an external
581
+ 0xff literal sp_str_byte_len falls back to strlen which legitimately
582
+ stops at NUL, matching the prior behaviour. */
583
+ size_t blen = sp_str_byte_len(s);
584
+ const char *p = s;
585
+ const char *end = s + blen;
586
+ while (char_idx > 0 && p < end) {
587
+ p += sp_utf8_advance(p);
588
+ char_idx--;
589
+ }
590
+ if (p > end) p = end;
591
+ return (size_t)(p - s);
592
+ }
593
+ /* Issue #762: check malloc/realloc returns. On OOM, return an empty
594
+ array rather than dereferencing NULL. */
595
+ static uint32_t*sp_utf8_decode_all(const char*s,size_t*out_n){size_t cap=8,n=0;uint32_t*cps=(uint32_t*)malloc(cap*sizeof(uint32_t));if(!cps){*out_n=0;return NULL;}const char*p=s;while(s&&*p){if(n>=cap){size_t nc=cap*2;uint32_t*nx=(uint32_t*)realloc(cps,nc*sizeof(uint32_t));if(!nx){free(cps);*out_n=0;return NULL;}cps=nx;cap=nc;}uint32_t cp;p+=sp_utf8_decode(p,&cp);cps[n++]=cp;}*out_n=n;return cps;}
197
596
 
198
- static inline void sp_mark_string(const char *s) {
199
- if (!s) return;
200
- if ((unsigned char)s[-1] == 0xfe) {
201
- ((char *)s)[-1] = (char)0xfc;
597
+ /* Issue #858: expand `a-z` range notation in a String#delete /
598
+ String#tr / String#count character set. `^abc` negation is
599
+ NOT handled (separate v1 scope). Result is a malloc'd flat
600
+ codepoint array caller frees. */
601
+ static uint32_t*sp_utf8_decode_charset(const char*s,size_t*out_n){
602
+ size_t cap=16,n=0;
603
+ uint32_t*cps=(uint32_t*)malloc(cap*sizeof(uint32_t));
604
+ if(!cps){*out_n=0;return NULL;}
605
+ const char*p=s;
606
+ uint32_t prev=0; int has_prev=0;
607
+ while(s&&*p){
608
+ uint32_t cp;
609
+ int len=sp_utf8_decode(p,&cp);
610
+ p+=len;
611
+ /* Detect range: prev '-' next (but leading or trailing '-'
612
+ is literal). When current char is '-' and there's a next
613
+ non-'-' char and we have a prev, expand. */
614
+ if(cp=='-' && has_prev && *p){
615
+ uint32_t hi;
616
+ int hi_len=sp_utf8_decode(p,&hi);
617
+ p+=hi_len;
618
+ if(hi>=prev){
619
+ /* Drop the prev we already wrote, re-emit the whole range. */
620
+ n--; /* undo prev */
621
+ for(uint32_t c=prev;c<=hi;c++){
622
+ if(n>=cap){cap*=2;cps=(uint32_t*)realloc(cps,cap*sizeof(uint32_t));}
623
+ cps[n++]=c;
624
+ }
625
+ has_prev=0;
626
+ continue;
627
+ }
628
+ /* Bad range (hi<prev): fall through, push '-' literally. */
629
+ cp='-';
630
+ }
631
+ if(n>=cap){cap*=2;cps=(uint32_t*)realloc(cps,cap*sizeof(uint32_t));}
632
+ cps[n++]=cp;
633
+ prev=cp; has_prev=1;
202
634
  }
635
+ *out_n=n;
636
+ return cps;
203
637
  }
638
+ static int sp_utf8_set_has(const uint32_t*cps,size_t n,uint32_t cp){for(size_t i=0;i<n;i++)if(cps[i]==cp)return 1;return 0;}
204
639
 
640
+ /* sp_mark_string is an inline helper in sp_gc.h. */
205
641
  static void sp_str_sweep(void) {
206
642
  sp_str_hdr **pp = &sp_str_heap;
207
643
  while (*pp) {
@@ -217,10 +653,55 @@ static void sp_str_sweep(void) {
217
653
  free(h);
218
654
  }
219
655
  }
656
+ sp_str_lcache_clear();
657
+ }
658
+
659
+ /* GC-aware Time trampolines. The libspinel_rt format helpers write
660
+ into a local buffer; we sp_str_dup_external the result so the GC
661
+ tracks the lifetime. strftime returns 0 -- never overruns the buffer
662
+ -- when the formatted result would exceed it, which we surface as "".
663
+ The 4 KB buffer covers any realistic format (CRuby's built-ins are
664
+ ~25 bytes; this leaves room for long literal text or wide fields). A
665
+ pathological field width (`"%1000000000F"`, which CRuby rejects with
666
+ ERANGE) does not fit and yields "" -- a graceful empty string rather
667
+ than a crash. */
668
+ static const char *sp_time_strftime(sp_Time t, const char *fmt) {
669
+ char buf[4096];
670
+ size_t n = sp_time_strftime_to(t, fmt, buf, sizeof(buf));
671
+ if (n == 0) return SPL("");
672
+ return sp_str_dup_external(buf);
673
+ }
674
+ static const char *sp_time_iso8601(sp_Time t) {
675
+ char buf[64];
676
+ size_t n = sp_time_iso8601_to(t, buf, sizeof(buf));
677
+ if (n == 0) return SPL("");
678
+ return sp_str_dup_external(buf);
679
+ }
680
+ static const char *sp_time_zone(sp_Time t) {
681
+ char buf[8];
682
+ sp_time_zone_to(t, buf, sizeof(buf));
683
+ return sp_str_dup_external(buf);
684
+ }
685
+ /* Scalar Time inspect. CRuby form: local "YYYY-MM-DD HH:MM:SS +0900",
686
+ UTC "YYYY-MM-DD HH:MM:SS UTC". The poly-box path keeps its own
687
+ sp_Time_inspect; this value-taking variant is for the scalar
688
+ p/puts/to_s codegen path. */
689
+ static const char *sp_time_inspect_v(sp_Time t) {
690
+ char buf[40];
691
+ sp_time_inspect_to(t, buf, sizeof(buf));
692
+ return sp_str_dup_external(buf);
220
693
  }
221
694
 
222
- #define SP_GC_STACK_MAX 65536
223
- static void **sp_gc_roots[SP_GC_STACK_MAX]; static int sp_gc_nroots = 0;
695
+ /* SP_GC_STACK_MAX, sp_gc_roots, sp_gc_nroots come from sp_gc.h / lib/sp_gc.c. */
696
+ /* Cooperative-fiber GC root storage (issue #636).
697
+ sp_gc_roots[] holds the CURRENT fiber's active roots. When a fiber
698
+ yields, its roots get copied out to the fiber's saved_roots buffer
699
+ and the resuming fiber's saved_roots are copied back in — so the
700
+ per-fiber stacks never clobber each other through interleaved
701
+ pushes the way they did when a single global stack was shared by
702
+ every fiber. sp_gc_mark_all calls the hook below (installed once
703
+ the Fiber section's setup runs) to walk every live fiber's
704
+ saved_roots in addition to the current view. */
224
705
  /* GC root tracking. SP_GC_ROOT registers a stack-resident root with a
225
706
  cleanup-attribute sentinel so it's auto-popped when its declaring
226
707
  scope ends — matches the variable's actual lifetime, including for
@@ -239,24 +720,116 @@ static inline void _sp_gc_root_pop(int *added) { if (*added) sp_gc_nroots--; }
239
720
  #define _SP_GC_CONCAT(a,b) _SP_GC_CONCAT2(a,b)
240
721
  #define SP_GC_SAVE() int __attribute__((cleanup(sp_gc_cleanup))) _gc_saved = sp_gc_nroots
241
722
  #define SP_GC_ROOT(v) int __attribute__((cleanup(_sp_gc_root_pop))) _SP_GC_CONCAT(_sp_gcr_, __COUNTER__) = _sp_gc_root_push((void**)&(v))
723
+ /* Root a poly (sp_RbVal) local/global. The root stack stores void**
724
+ and marks *slot as a single GC pointer, but an sp_RbVal carries its
725
+ object pointer inside a union (offset != 0) and only when its tag is
726
+ STR/OBJ -- a blind *slot deref would read the tag word as a pointer.
727
+ So we tag the stored slot with the low bit (sp_RbVal addresses are
728
+ >=4-byte aligned, so the bit is free) and the mark walker, seeing the
729
+ tag, routes the entry through sp_mark_rbval instead. Same auto-pop
730
+ cleanup as SP_GC_ROOT, so SP_GC_SAVE/RESTORE and the fiber root
731
+ save/restore (which treat entries as opaque) need no changes. */
732
+ #define SP_GC_ROOT_RBVAL(v) int __attribute__((cleanup(_sp_gc_root_pop))) _SP_GC_CONCAT(_sp_gcr_, __COUNTER__) = _sp_gc_root_push((void**)((uintptr_t)&(v) | (uintptr_t)1))
242
733
  #define SP_GC_RESTORE() sp_gc_nroots = _gc_saved
243
734
  #define SP_GC_MARK_STACK_MAX (1024*64)
244
- static void**sp_gc_mark_stack=NULL;static int sp_gc_mark_top=0;
245
- static void sp_gc_mark(void*obj){if(!obj)return;unsigned char pm=((unsigned char*)obj)[-1];if(pm==0xfe){((char*)obj)[-1]=(char)0xfc;return;}if(pm==0xfc||pm==0xff)return;sp_gc_hdr*h=(sp_gc_hdr*)((char*)obj-sizeof(sp_gc_hdr));if(h->marked)return;h->marked=1;if(h->scan){if(sp_gc_mark_stack&&sp_gc_mark_top<SP_GC_MARK_STACK_MAX){sp_gc_mark_stack[sp_gc_mark_top++]=obj;}else{h->scan(obj);}}}
246
- /* Forward decl: defined alongside the regex globals it marks. */
247
- static void sp_re_mark_globals(void);
248
- static void sp_gc_mark_all(void){if(!sp_gc_mark_stack)sp_gc_mark_stack=(void**)malloc(sizeof(void*)*SP_GC_MARK_STACK_MAX);sp_gc_mark_top=0;for(int i=0;i<sp_gc_nroots;i++){void*obj=*sp_gc_roots[i];if(obj)sp_gc_mark(obj);}sp_re_mark_globals();while(sp_gc_mark_top>0){void*obj=sp_gc_mark_stack[--sp_gc_mark_top];sp_gc_hdr*h=(sp_gc_hdr*)((char*)obj-sizeof(sp_gc_hdr));if(h->scan)h->scan(obj);}}
735
+ /* sp_gc_mark / sp_gc_mark_all + the suspended-fiber hook live in
736
+ lib/sp_gc.c (declared in sp_gc.h). sp_re_mark_globals is defined below
737
+ (with the regex globals it marks) and carries external linkage so the
738
+ collector body can reach it. */
249
739
  static void sp_gc_cleanup(int*p){sp_gc_nroots=*p;}
250
740
  #define SP_GC_NBUCKETS 32
251
741
  static sp_gc_hdr*sp_gc_buckets[SP_GC_NBUCKETS];
252
742
  static inline int sp_gc_bucket(size_t sz){int b=(int)(sz/16);return b<SP_GC_NBUCKETS?b:SP_GC_NBUCKETS-1;}
253
- static int sp_gc_cycle=0;
254
- static sp_gc_hdr*sp_gc_old_heap=NULL;static size_t sp_gc_old_bytes=0;
255
- #define SP_GC_FULL_INTERVAL 8
256
- static void sp_gc_collect(void){int full=(sp_gc_cycle%SP_GC_FULL_INTERVAL==0);sp_gc_cycle++;sp_gc_hdr*hh=sp_gc_old_heap;while(hh){hh->marked=0;hh=hh->next;}sp_gc_mark_all();if(full){sp_gc_hdr**pp=&sp_gc_old_heap;sp_gc_old_bytes=0;while(*pp){sp_gc_hdr*h=*pp;if(!h->marked){*pp=h->next;if(h->finalize)h->finalize((char*)h+sizeof(sp_gc_hdr));free(h);}else{h->marked=1;sp_gc_old_bytes+=h->size;pp=&h->next;}}}else{hh=sp_gc_old_heap;while(hh){hh->marked=1;hh=hh->next;}}sp_gc_hdr**pp=&sp_gc_heap;sp_gc_bytes=sp_gc_old_bytes;while(*pp){sp_gc_hdr*h=*pp;if(!h->marked){*pp=h->next;if(h->finalize)h->finalize((char*)h+sizeof(sp_gc_hdr));free(h);}else{h->marked=1;*pp=h->next;h->next=sp_gc_old_heap;sp_gc_old_heap=h;sp_gc_old_bytes+=h->size;sp_gc_bytes+=h->size;}}sp_str_sweep();if(full)malloc_trim(0);}
743
+ /* sp_gc_cycle / sp_gc_old_bytes are in lib/sp_gc.c (extern via sp_gc.h);
744
+ sp_gc_old_heap is collector-private to lib/sp_gc.c. */
745
+
746
+ /* GC verify support + sp_gc_collect live in lib/sp_gc.c. */
257
747
  static size_t sp_gc_threshold_init=256*1024;
258
- void*sp_gc_alloc(size_t sz,void(*fin)(void*),void(*scn)(void*)){if(sp_gc_bytes>sp_gc_threshold){size_t before=sp_gc_bytes;sp_gc_collect();size_t freed=before-sp_gc_bytes;if(freed<before/4){sp_gc_threshold=before*2;}else if(sp_gc_bytes>0){sp_gc_threshold=sp_gc_bytes*4;if(sp_gc_threshold<sp_gc_threshold_init)sp_gc_threshold=sp_gc_threshold_init;}else{sp_gc_threshold=sp_gc_threshold_init;}}size_t need=sizeof(sp_gc_hdr)+sz;sp_gc_hdr*h=(sp_gc_hdr*)calloc(1,need);h->finalize=fin;h->scan=scn;h->size=need;h->marked=0;h->next=sp_gc_heap;sp_gc_heap=h;sp_gc_bytes+=need;return(char*)h+sizeof(sp_gc_hdr);}
259
- void*sp_gc_alloc_nogc(size_t sz,void(*fin)(void*),void(*scn)(void*)){size_t need=sizeof(sp_gc_hdr)+sz;sp_gc_hdr*h=(sp_gc_hdr*)calloc(1,need);h->finalize=fin;h->scan=scn;h->size=need;h->marked=0;h->next=sp_gc_heap;sp_gc_heap=h;sp_gc_bytes+=need;return(char*)h+sizeof(sp_gc_hdr);}
748
+ /* sp_oom_die + the SPINEL_MAX_HEAP_MB governor (sp_gc_enforce_mem_limit)
749
+ live in lib/sp_gc.c. */
750
+ void*sp_gc_alloc(size_t sz,void(*fin)(void*),void(*scn)(void*)){if(sp_gc_bytes>sp_gc_threshold){size_t before=sp_gc_bytes;sp_gc_collect();size_t freed=before-sp_gc_bytes;if(freed<before/4){sp_gc_threshold=before*2;}else if(sp_gc_bytes>0){sp_gc_threshold=sp_gc_bytes*4;if(sp_gc_threshold<sp_gc_threshold_init)sp_gc_threshold=sp_gc_threshold_init;}else{sp_gc_threshold=sp_gc_threshold_init;}sp_gc_enforce_mem_limit();}size_t need=sizeof(sp_gc_hdr)+sz;sp_gc_hdr*h=(sp_gc_hdr*)calloc(1,need);if(!h)sp_oom_die();h->finalize=fin;h->scan=scn;h->size=need;h->marked=0;h->next=sp_gc_heap;sp_gc_heap=h;sp_gc_bytes+=need;return(char*)h+sizeof(sp_gc_hdr);}
751
+ void*sp_gc_alloc_nogc(size_t sz,void(*fin)(void*),void(*scn)(void*)){size_t need=sizeof(sp_gc_hdr)+sz;sp_gc_hdr*h=(sp_gc_hdr*)calloc(1,need);if(!h)sp_oom_die();h->finalize=fin;h->scan=scn;h->size=need;h->marked=0;h->next=sp_gc_heap;sp_gc_heap=h;sp_gc_bytes+=need;return(char*)h+sizeof(sp_gc_hdr);}
752
+ /* GC-header frozen bit — used for containers whose mutators are NOT
753
+ on a hot path (hashes), so the extra cache line vs. a struct field
754
+ doesn't matter. Arrays co-locate `frozen` in the struct instead
755
+ (see sp_IntArray); strings use the 0xff marker / wrapper bit. */
756
+ static inline mrb_bool sp_gc_is_frozen(void *p) { if (!p) return FALSE; return ((sp_gc_hdr *)((char *)p - sizeof(sp_gc_hdr)))->frozen; }
757
+ static inline void *sp_gc_freeze(void *p) { if (p) ((sp_gc_hdr *)((char *)p - sizeof(sp_gc_hdr)))->frozen = 1; return p; }
758
+ static void __attribute__((noinline,cold)) sp_raise_frozen_hash(void){sp_raise_cls("FrozenError","can't modify frozen Hash");}
759
+ /* Pool-aware alloc. The recycle hook is stored in the gc_hdr; sweep
760
+ calls it on unmarked objects instead of finalize+free. The hook
761
+ decides whether to push the storage onto a per-class free-list or
762
+ actually free it. */
763
+ static void *sp_gc_alloc_pool(size_t sz, void(*scn)(void*), void(*recycle)(sp_gc_hdr*)) {
764
+ void *p = sp_gc_alloc(sz, NULL, scn);
765
+ sp_gc_hdr *h = (sp_gc_hdr *)((char *)p - sizeof(sp_gc_hdr));
766
+ h->recycle = recycle;
767
+ return p;
768
+ }
769
+ /* Re-link a previously-pooled slot back into sp_gc_heap so the next
770
+ GC cycle visits it. Called by class _new functions when reusing
771
+ a pooled instance. The storage was kept alive by the pool
772
+ free-list since the last sweep unlinked it from sp_gc_heap. */
773
+ static void sp_gc_pool_relink(sp_gc_hdr *h) {
774
+ h->marked = 0;
775
+ h->next = sp_gc_heap;
776
+ sp_gc_heap = h;
777
+ sp_gc_bytes += h->size;
778
+ }
779
+
780
+ /* Per-class free-list pool boilerplate. SP_POOL_DEFINE(CLS) goes at
781
+ file scope, near the class _new function. SP_POOL_NEW(CLS, scan)
782
+ replaces the body of an `sp_gc_alloc(sizeof(sp_CLS), NULL, scan)`
783
+ call, popping from the per-class free-list if non-empty.
784
+ Default cap can be overridden at runtime via SP_POOL_MAX envvar
785
+ (uniform across classes). SP_POOL_REPORT=1 dumps per-class stats
786
+ at exit. */
787
+ #define SP_POOL_DEFAULT_MAX 1048576L
788
+ #define SP_POOL_DEFINE(CLS) \
789
+ static sp_gc_hdr *sp_##CLS##_pool_head = NULL; \
790
+ static long sp_##CLS##_pool_count = 0; \
791
+ static long sp_##CLS##_pool_max = SP_POOL_DEFAULT_MAX; \
792
+ static long sp_##CLS##_pool_pushes = 0; \
793
+ static long sp_##CLS##_pool_pops = 0; \
794
+ static long sp_##CLS##_pool_freed = 0; \
795
+ static long sp_##CLS##_pool_hwm = 0; \
796
+ __attribute__((constructor)) static void sp_##CLS##_pool_init(void) { \
797
+ const char *m = getenv("SP_POOL_MAX"); \
798
+ if (m && *m) { long v = atol(m); if (v >= 0) sp_##CLS##_pool_max = v; } \
799
+ } \
800
+ static void sp_##CLS##_pool_recycle(sp_gc_hdr *h) { \
801
+ if (sp_##CLS##_pool_count >= sp_##CLS##_pool_max) { \
802
+ free(h); sp_##CLS##_pool_freed++; return; \
803
+ } \
804
+ h->next = sp_##CLS##_pool_head; \
805
+ sp_##CLS##_pool_head = h; \
806
+ sp_##CLS##_pool_count++; \
807
+ sp_##CLS##_pool_pushes++; \
808
+ if (sp_##CLS##_pool_count > sp_##CLS##_pool_hwm) sp_##CLS##_pool_hwm = sp_##CLS##_pool_count; \
809
+ } \
810
+ __attribute__((destructor)) static void sp_##CLS##_pool_report(void) { \
811
+ const char *e = getenv("SP_POOL_REPORT"); \
812
+ if (!e || !e[0] || e[0] == '0') return; \
813
+ fprintf(stderr, #CLS " pool: pops=%ld pushes=%ld over_cap_freed=%ld hwm=%ld retained=%ld cap=%ld\n", \
814
+ sp_##CLS##_pool_pops, sp_##CLS##_pool_pushes, sp_##CLS##_pool_freed, \
815
+ sp_##CLS##_pool_hwm, sp_##CLS##_pool_count, sp_##CLS##_pool_max); \
816
+ }
817
+
818
+ #define SP_POOL_NEW(CLS, SCAN) (__extension__ ({ \
819
+ sp_##CLS *_p; \
820
+ if (sp_##CLS##_pool_head) { \
821
+ sp_gc_hdr *_h = sp_##CLS##_pool_head; \
822
+ sp_##CLS##_pool_head = _h->next; \
823
+ sp_##CLS##_pool_count--; \
824
+ sp_##CLS##_pool_pops++; \
825
+ sp_gc_pool_relink(_h); \
826
+ _h->recycle = sp_##CLS##_pool_recycle; \
827
+ _p = (sp_##CLS *)((char *)_h + sizeof(sp_gc_hdr)); \
828
+ } else { \
829
+ _p = (sp_##CLS *)sp_gc_alloc_pool(sizeof(sp_##CLS), SCAN, sp_##CLS##_pool_recycle); \
830
+ } \
831
+ _p; \
832
+ }))
260
833
 
261
834
  /* `Object.new` — a sentinel object whose only meaningful property is
262
835
  identity. Each call returns a fresh GC-managed allocation, so two
@@ -264,10 +837,27 @@ void*sp_gc_alloc_nogc(size_t sz,void(*fin)(void*),void(*scn)(void*)){size_t need
264
837
  typedef struct sp_Object_s { uint8_t _pad; } sp_Object;
265
838
  static sp_Object *sp_Object_new(void){return(sp_Object*)sp_gc_alloc(sizeof(sp_Object),NULL,NULL);}
266
839
 
267
- typedef struct{mrb_int*data;mrb_int start;mrb_int len;mrb_int cap;}sp_IntArray;
840
+ /* Cold out-of-line raise for frozen-container mutation; keeps the
841
+ hot mutators (push / []=) small enough to stay inlined. */
842
+ static void __attribute__((noinline,cold)) sp_raise_frozen_array(void){sp_raise_cls("FrozenError","can't modify frozen Array");}
843
+
844
+ /* `frozen` rides in the struct (not the GC header) so the hot push /
845
+ []= paths read it from the same cache line as len/cap — no extra
846
+ cache miss vs. the GC-header bit. calloc in sp_gc_alloc zero-inits
847
+ it, so constructors need no change. Issue #918. */
268
848
  static void sp_IntArray_fin(void*p){free(((sp_IntArray*)p)->data);}
269
- static sp_IntArray*sp_IntArray_new(void){sp_IntArray*a=(sp_IntArray*)sp_gc_alloc(sizeof(sp_IntArray),sp_IntArray_fin,NULL);a->cap=16;a->data=(mrb_int*)malloc(sizeof(mrb_int)*a->cap);a->start=0;a->len=0;{sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}return a;}
270
- static sp_IntArray*sp_IntArray_from_range(mrb_int s,mrb_int e){sp_IntArray*a=sp_IntArray_new();mrb_int n=e-s+1;if(n<0)n=0;if(n>a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*a->cap;h->size-=sizeof(mrb_int)*a->cap;a->cap=n;a->data=(mrb_int*)realloc(a->data,sizeof(mrb_int)*a->cap);h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}for(mrb_int i=0;i<n;i++)a->data[i]=s+i;a->len=n;return a;}
849
+ static sp_IntArray*sp_IntArray_new(void){sp_IntArray*a=(sp_IntArray*)sp_gc_alloc(sizeof(sp_IntArray),sp_IntArray_fin,NULL);a->cap=16;a->data=(mrb_int*)malloc(sizeof(mrb_int)*a->cap);if(!a->data)sp_oom_die();a->start=0;a->len=0;{sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}return a;}
850
+ /* Issue #799: clamp e-s+1 against size_t overflow + an arbitrary
851
+ sanity cap (1 << 30 elements; ~8 GB at 8 bytes/elem). Without
852
+ the cap, `(1..MRB_INT_MAX).to_a` overflows the realloc size_t
853
+ to a tiny number, then writes past the allocation. */
854
+ static sp_IntArray*sp_IntArray_from_range(mrb_int s,mrb_int e){sp_IntArray*a=sp_IntArray_new();mrb_int n=e-s+1;if(n<0)n=0;if(n>(mrb_int)(1LL<<30))n=(mrb_int)(1LL<<30);if(n>a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*a->cap;h->size-=sizeof(mrb_int)*a->cap;a->cap=n;a->data=(mrb_int*)realloc(a->data,sizeof(mrb_int)*a->cap);h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}for(mrb_int i=0;i<n;i++)a->data[i]=s+i;a->len=n;return a;}
855
+ /* `(a..b).step(k)` -- emit a, a+k, a+2k, ... up to b. k must be > 0;
856
+ k <= 0 returns empty (matches CRuby's ArgumentError for k <= 0 in
857
+ spirit; we soft-fail instead). Forward-declared since
858
+ sp_IntArray_push is defined below. Issue #731. */
859
+ static void sp_IntArray_push(sp_IntArray*a,mrb_int v);
860
+ static sp_IntArray*sp_IntArray_from_range_step(mrb_int s,mrb_int e,mrb_int k){sp_IntArray*a=sp_IntArray_new();if(k<=0)return a;mrb_int v=s;while(v<=e){sp_IntArray_push(a,v);v+=k;}return a;}
271
861
  static sp_IntArray*sp_IntArray_dup(sp_IntArray*a){sp_IntArray*b=sp_IntArray_new();if(a->len>b->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)b-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*b->cap;h->size-=sizeof(mrb_int)*b->cap;b->cap=a->len;b->data=(mrb_int*)realloc(b->data,sizeof(mrb_int)*b->cap);h->size+=sizeof(mrb_int)*b->cap;sp_gc_bytes+=sizeof(mrb_int)*b->cap;}memcpy(b->data,a->data+a->start,sizeof(mrb_int)*a->len);b->len=a->len;return b;}
272
862
  /* a[start, len] / a[start..end] for IntArray. Negative start counts from
273
863
  * the end. start past the array length yields an empty result; len is
@@ -275,201 +865,389 @@ static sp_IntArray*sp_IntArray_dup(sp_IntArray*a){sp_IntArray*b=sp_IntArray_new(
275
865
  * out-of-bounds start; we return an empty IntArray since this typed
276
866
  * collection has no nullable form. */
277
867
  static sp_IntArray*sp_IntArray_slice(sp_IntArray*a,mrb_int start,mrb_int len){if(start<0)start+=a->len;if(start<0)start=0;sp_IntArray*b=sp_IntArray_new();if(start>=a->len||len<=0)return b;if(start+len>a->len)len=a->len-start;if(len>b->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)b-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*b->cap;h->size-=sizeof(mrb_int)*b->cap;b->cap=len;b->data=(mrb_int*)realloc(b->data,sizeof(mrb_int)*b->cap);h->size+=sizeof(mrb_int)*b->cap;sp_gc_bytes+=sizeof(mrb_int)*b->cap;}memcpy(b->data,a->data+a->start+start,sizeof(mrb_int)*len);b->len=len;return b;}
868
+ /* a[start..end] / a[start...end] with possibly negative endpoints.
869
+ Codegen used to compute (right - left + adj) for the length, which
870
+ silently produced a negative count for `a[1..-2]` and the runtime
871
+ then returned an empty array (issue #496). Normalize end against
872
+ a->len first; the bare _slice already handles negative start. */
873
+ static sp_IntArray*sp_IntArray_slice_range(sp_IntArray*a,mrb_int start,mrb_int end_,mrb_int excl){if(end_<0)end_+=a->len;if(start<0)start+=a->len;mrb_int n=end_-start+(excl?0:1);if(n<0||start<0)n=0;return sp_IntArray_slice(a,start,n);}
278
874
  static void sp_IntArray_replace(sp_IntArray*dst,sp_IntArray*src){dst->len=0;dst->start=0;if(src->len>dst->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)dst-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*dst->cap;h->size-=sizeof(mrb_int)*dst->cap;void*nd=realloc(dst->data,sizeof(mrb_int)*src->len);if(!nd){perror("realloc");exit(1);}dst->data=(mrb_int*)nd;dst->cap=src->len;h->size+=sizeof(mrb_int)*dst->cap;sp_gc_bytes+=sizeof(mrb_int)*dst->cap;}memcpy(dst->data,src->data+src->start,sizeof(mrb_int)*src->len);dst->len=src->len;}
279
- static void __attribute__((noinline)) sp_IntArray_push_grow(sp_IntArray*a){if(a->start>0){memmove(a->data,a->data+a->start,sizeof(mrb_int)*a->len);a->start=0;if(a->len<a->cap)return;}{sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*a->cap;h->size-=sizeof(mrb_int)*a->cap;a->cap=a->cap*2+1;a->data=(mrb_int*)realloc(a->data,sizeof(mrb_int)*a->cap);h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}}
280
- static inline void sp_IntArray_push(sp_IntArray*a,mrb_int v){if(a->start+a->len>=a->cap)sp_IntArray_push_grow(a);a->data[a->start+a->len]=v;a->len++;}
281
- static inline mrb_int sp_IntArray_pop(sp_IntArray*a){return a->data[a->start+--a->len];}
282
- static inline mrb_int sp_IntArray_shift(sp_IntArray*a){mrb_int v=a->data[a->start];a->start++;a->len--;return v;}
875
+ static void __attribute__((noinline)) sp_IntArray_push_grow(sp_IntArray*a){if(a->start>0){memmove(a->data,a->data+a->start,sizeof(mrb_int)*a->len);a->start=0;if(a->len<a->cap)return;}{sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*a->cap;h->size-=sizeof(mrb_int)*a->cap;a->cap=a->cap*2+1;void*nd=realloc(a->data,sizeof(mrb_int)*a->cap);if(!nd)sp_oom_die();a->data=(mrb_int*)nd;h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}}
876
+ static inline void sp_IntArray_push(sp_IntArray*a,mrb_int v){if(a->frozen){sp_raise_frozen_array();return;}if(a->start+a->len>=a->cap)sp_IntArray_push_grow(a);a->data[a->start+a->len]=v;a->len++;}
877
+ /* Issue #826: guard empty arrays. CRuby returns nil; spinel's int slot
878
+ collapses nil to 0. Without the guard, `--a->len` wraps to -1 and
879
+ reads past the buffer start. */
880
+ /* Issue #832: empty pop/shift return SP_INT_NIL (nullable int sentinel)
881
+ to match MRI's nil semantics; callers must treat as int? */
882
+ static inline mrb_int sp_IntArray_pop(sp_IntArray*a){if(!a||a->len<=0)return SP_INT_NIL;if(a->frozen){sp_raise_frozen_array();return SP_INT_NIL;}return a->data[a->start+--a->len];}
883
+ static inline mrb_int sp_IntArray_shift(sp_IntArray*a){if(!a||a->len<=0)return SP_INT_NIL;if(a->frozen){sp_raise_frozen_array();return SP_INT_NIL;}mrb_int v=a->data[a->start];a->start++;a->len--;return v;}
283
884
  static inline mrb_int sp_IntArray_length(sp_IntArray*a){return a->len;}
284
885
  static inline mrb_bool sp_IntArray_empty(sp_IntArray*a){return a->len==0;}
285
- static inline mrb_int sp_IntArray_get(sp_IntArray*a,mrb_int i){if(i<0)i+=a->len;return a->data[a->start+i];}
286
- static void sp_IntArray_set_slow(sp_IntArray*a,mrb_int i,mrb_int v){while(a->start+i>=a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*a->cap;h->size-=sizeof(mrb_int)*a->cap;a->cap=a->cap*2+1;a->data=(mrb_int*)realloc(a->data,sizeof(mrb_int)*a->cap);h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}while(i>=a->len){a->data[a->start+a->len]=0;a->len++;}a->data[a->start+i]=v;}
287
- static inline void sp_IntArray_set(sp_IntArray*a,mrb_int i,mrb_int v){if(i<0)i+=a->len;if(i>=0&&i<a->len){a->data[a->start+i]=v;return;}sp_IntArray_set_slow(a,i,v);}
288
- static void sp_IntArray_reverse_bang(sp_IntArray*a){for(mrb_int i=0,j=a->len-1;i<j;i++,j--){mrb_int t=a->data[a->start+i];a->data[a->start+i]=a->data[a->start+j];a->data[a->start+j]=t;}}
289
- static void sp_IntArray_rotate_bang(sp_IntArray*a,mrb_int n){if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;mrb_int*tmp=(mrb_int*)malloc(sizeof(mrb_int)*a->len);for(mrb_int i=0;i<a->len;i++)tmp[i]=a->data[a->start+(i+n)%a->len];for(mrb_int i=0;i<a->len;i++)a->data[a->start+i]=tmp[i];free(tmp);}
886
+ static inline mrb_int sp_IntArray_get(sp_IntArray*a,mrb_int i){if(!a)return SP_INT_NIL;if(i<0)i+=a->len;if(i<0||i>=a->len)return SP_INT_NIL;return a->data[a->start+i];}
887
+ /* Issue #769: a very-negative i (e.g. `a[-999] = 42` on a 3-elt
888
+ array) leaves i negative after the `i += a->len` adjustment.
889
+ CRuby raises IndexError; spinel no-ops as the safest fallback
890
+ (raising from a typed-array set would need setjmp plumbing
891
+ throughout the call chain). */
892
+ static void sp_IntArray_set_slow(sp_IntArray*a,mrb_int i,mrb_int v){if(i<0)return;while(a->start+i>=a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*a->cap;h->size-=sizeof(mrb_int)*a->cap;a->cap=a->cap*2+1;a->data=(mrb_int*)realloc(a->data,sizeof(mrb_int)*a->cap);h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}while(i>=a->len){a->data[a->start+a->len]=0;a->len++;}a->data[a->start+i]=v;}
893
+ /* Issue #839: an extreme negative index (still negative after
894
+ `i += len`) raises IndexError per MRI. */
895
+ static inline void sp_IntArray_set(sp_IntArray*a,mrb_int i,mrb_int v){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}mrb_int orig=i;if(i<0)i+=a->len;if(i<0)sp_raise_cls("IndexError",sp_sprintf("index %lld too small for array; minimum: %lld",(long long)orig,(long long)-a->len));if(i<a->len){a->data[a->start+i]=v;return;}sp_IntArray_set_slow(a,i,v);}
896
+ static void sp_IntArray_reverse_bang(sp_IntArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}for(mrb_int i=0,j=a->len-1;i<j;i++,j--){mrb_int t=a->data[a->start+i];a->data[a->start+i]=a->data[a->start+j];a->data[a->start+j]=t;}}
897
+ static void sp_IntArray_rotate_bang(sp_IntArray*a,mrb_int n){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;mrb_int*d=a->data+a->start;mrb_int lo=0,hi=n-1;while(lo<hi){mrb_int t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=n;hi=a->len-1;while(lo<hi){mrb_int t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=0;hi=a->len-1;while(lo<hi){mrb_int t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}}
290
898
  static int _sp_int_cmp(const void*a,const void*b){mrb_int va=*(const mrb_int*)a,vb=*(const mrb_int*)b;return(va>vb)-(va<vb);}
291
899
  static sp_IntArray*sp_IntArray_sort(sp_IntArray*a){sp_IntArray*b=sp_IntArray_dup(a);qsort(b->data+b->start,b->len,sizeof(mrb_int),_sp_int_cmp);return b;}
292
- static void sp_IntArray_sort_bang(sp_IntArray*a){qsort(a->data+a->start,a->len,sizeof(mrb_int),_sp_int_cmp);}
293
- static void sp_IntArray_shuffle_bang(sp_IntArray*a){for(mrb_int i=a->len-1;i>0;i--){mrb_int j=(mrb_int)(rand()%(i+1));mrb_int t=a->data[a->start+i];a->data[a->start+i]=a->data[a->start+j];a->data[a->start+j]=t;}}
900
+ static void sp_IntArray_sort_bang(sp_IntArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}qsort(a->data+a->start,a->len,sizeof(mrb_int),_sp_int_cmp);}
901
+ static void sp_IntArray_shuffle_bang(sp_IntArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}for(mrb_int i=a->len-1;i>0;i--){mrb_int j=(mrb_int)(rand()%(i+1));mrb_int t=a->data[a->start+i];a->data[a->start+i]=a->data[a->start+j];a->data[a->start+j]=t;}}
294
902
  static sp_IntArray*sp_IntArray_shuffle(sp_IntArray*a){sp_IntArray*b=sp_IntArray_dup(a);sp_IntArray_shuffle_bang(b);return b;}
295
- static mrb_int sp_IntArray_min(sp_IntArray*a){mrb_int m=a->data[a->start];for(mrb_int i=1;i<a->len;i++)if(a->data[a->start+i]<m)m=a->data[a->start+i];return m;}
296
- static mrb_int sp_IntArray_max(sp_IntArray*a){mrb_int m=a->data[a->start];for(mrb_int i=1;i<a->len;i++)if(a->data[a->start+i]>m)m=a->data[a->start+i];return m;}
297
- static mrb_int sp_IntArray_sum(sp_IntArray*a){mrb_int s=0;for(mrb_int i=0;i<a->len;i++)s+=a->data[a->start+i];return s;}
903
+ /* Array#sample helpers. CRuby returns nil for `[].sample`; in
904
+ spinel's typed-array slots nil collapses to the C zero value
905
+ (0 / 0.0 / NULL / sp_box_nil for poly). Guards rand()%0, which
906
+ raises SIGFPE under -O0 and silently UBs under -O2+. Issue #536. */
907
+ static mrb_int sp_IntArray_sample(sp_IntArray*a){if(a->len<=0)return 0;return a->data[a->start+(mrb_int)(rand()%a->len)];}
908
+ /* Issue #745: guard the initial read on empty arrays. CRuby's [].min /
909
+ .max return nil; spinel's int-typed slot collapses nil to 0. Without
910
+ the guard, the first read is uninitialized memory. */
911
+ /* Issue #832: empty min/max return SP_INT_NIL (caller treats as int?). */
912
+ static mrb_int sp_IntArray_min(sp_IntArray*a){if(!a||a->len<=0)return SP_INT_NIL;mrb_int m=a->data[a->start];for(mrb_int i=1;i<a->len;i++)if(a->data[a->start+i]<m)m=a->data[a->start+i];return m;}
913
+ static mrb_int sp_IntArray_max(sp_IntArray*a){if(!a||a->len<=0)return SP_INT_NIL;mrb_int m=a->data[a->start];for(mrb_int i=1;i<a->len;i++)if(a->data[a->start+i]>m)m=a->data[a->start+i];return m;}
914
+ static mrb_int sp_IntArray_sum(sp_IntArray*a,mrb_int init){mrb_int s=init;for(mrb_int i=0;i<a->len;i++)s+=a->data[a->start+i];return s;}
298
915
  static mrb_bool sp_IntArray_include(sp_IntArray*a,mrb_int v){for(mrb_int i=0;i<a->len;i++)if(a->data[a->start+i]==v)return TRUE;return FALSE;}
299
916
  static mrb_int sp_IntArray_index(sp_IntArray*a,mrb_int v){for(mrb_int i=0;i<a->len;i++)if(a->data[a->start+i]==v)return i;return -1;}
300
917
  static mrb_int sp_IntArray_rindex(sp_IntArray*a,mrb_int v){for(mrb_int i=a->len-1;i>=0;i--)if(a->data[a->start+i]==v)return i;return -1;}
301
- static mrb_int sp_IntArray_delete_at(sp_IntArray*a,mrb_int i){if(i<0)i+=a->len;if(i<0||i>=a->len)return 0;mrb_int v=a->data[a->start+i];for(mrb_int j=i;j<a->len-1;j++)a->data[a->start+j]=a->data[a->start+j+1];a->len--;return v;}
302
- static mrb_int sp_IntArray_delete(sp_IntArray*a,mrb_int v){mrb_int w=0;for(mrb_int i=0;i<a->len;i++){if(a->data[a->start+i]!=v){a->data[a->start+w]=a->data[a->start+i];w++;}}mrb_int d=a->len-w;a->len=w;return d>0?v:0;}
303
- static void sp_IntArray_insert(sp_IntArray*a,mrb_int i,mrb_int v){if(i<0)i+=a->len+1;sp_IntArray_push(a,0);for(mrb_int j=a->len-1;j>i;j--)a->data[a->start+j]=a->data[a->start+j-1];a->data[a->start+i]=v;}
918
+ static mrb_int sp_IntArray_delete_at(sp_IntArray*a,mrb_int i){if(a&&a->frozen){sp_raise_frozen_array();return 0;}if(i<0)i+=a->len;if(i<0||i>=a->len)return 0;mrb_int v=a->data[a->start+i];for(mrb_int j=i;j<a->len-1;j++)a->data[a->start+j]=a->data[a->start+j+1];a->len--;return v;}
919
+ static mrb_int sp_IntArray_delete(sp_IntArray*a,mrb_int v){if(a&&a->frozen){sp_raise_frozen_array();return 0;}mrb_int w=0;for(mrb_int i=0;i<a->len;i++){if(a->data[a->start+i]!=v){a->data[a->start+w]=a->data[a->start+i];w++;}}mrb_int d=a->len-w;a->len=w;return d>0?v:0;}
920
+ /* Issue #788: clamp i so a very-negative index doesn't underflow past
921
+ a->start and write into the array's GC header. */
922
+ static void sp_IntArray_insert(sp_IntArray*a,mrb_int i,mrb_int v){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}if(i<0)i+=a->len+1;if(i<0)i=0;if(i>a->len)i=a->len;sp_IntArray_push(a,0);for(mrb_int j=a->len-1;j>i;j--)a->data[a->start+j]=a->data[a->start+j-1];a->data[a->start+i]=v;}
304
923
  static sp_IntArray*sp_int_digits(mrb_int n,mrb_int base){sp_IntArray*a=sp_IntArray_new();if(base<2)base=10;if(n==0){sp_IntArray_push(a,0);return a;}if(n<0)n=-n;while(n>0){sp_IntArray_push(a,n%base);n/=base;}return a;}
305
924
  static sp_IntArray*sp_IntArray_uniq(sp_IntArray*a){sp_IntArray*b=sp_IntArray_new();for(mrb_int i=0;i<a->len;i++){int found=0;for(mrb_int j=0;j<b->len;j++){if(b->data[b->start+j]==a->data[a->start+i]){found=1;break;}}if(!found)sp_IntArray_push(b,a->data[a->start+i]);}return b;}
306
925
  static sp_IntArray*sp_IntArray_intersect(sp_IntArray*a,sp_IntArray*b){sp_IntArray*r=sp_IntArray_new();for(mrb_int i=0;i<a->len;i++){mrb_int v=a->data[a->start+i];if(sp_IntArray_include(b,v)&&!sp_IntArray_include(r,v))sp_IntArray_push(r,v);}return r;}
307
- static sp_IntArray*sp_IntArray_union(sp_IntArray*a,sp_IntArray*b){sp_IntArray*r=sp_IntArray_new();for(mrb_int i=0;i<a->len;i++){mrb_int v=a->data[a->start+i];if(!sp_IntArray_include(r,v))sp_IntArray_push(r,v);}for(mrb_int i=0;i<b->len;i++){mrb_int v=b->data[b->start+i];if(!sp_IntArray_include(r,v))sp_IntArray_push(r,v);}return r;}
926
+ static sp_IntArray*sp_IntArray_union(sp_IntArray*a,sp_IntArray*b){sp_IntArray*r=sp_IntArray_new();for(mrb_int i=0;i<a->len;i++){mrb_int v=a->data[a->start+i];if(!sp_IntArray_include(r,v))sp_IntArray_push(r,v);}if(b){for(mrb_int i=0;i<b->len;i++){mrb_int v=b->data[b->start+i];if(!sp_IntArray_include(r,v))sp_IntArray_push(r,v);}}return r;}
308
927
  /* Array#- / Array#difference: keep every LHS element NOT in RHS,
309
928
  preserving the LHS's duplicates. CRuby's Array#- is not a set
310
929
  subtraction — `[1,1,2,3] - [3]` is `[1,1,2]`, not `[1,2]`. */
311
930
  static sp_IntArray*sp_IntArray_difference(sp_IntArray*a,sp_IntArray*b){sp_IntArray*r=sp_IntArray_new();for(mrb_int i=0;i<a->len;i++){mrb_int v=a->data[a->start+i];if(!sp_IntArray_include(b,v))sp_IntArray_push(r,v);}return r;}
312
- static void sp_IntArray_unshift(sp_IntArray*a,mrb_int v){if(a->start>0){a->start--;a->data[a->start]=v;a->len++;}else{mrb_int e=a->len+1;if(e>a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*a->cap;h->size-=sizeof(mrb_int)*a->cap;a->cap=a->cap*2+1;a->data=(mrb_int*)realloc(a->data,sizeof(mrb_int)*a->cap);h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}memmove(a->data+1,a->data,sizeof(mrb_int)*a->len);a->data[0]=v;a->len++;}}
931
+ static void sp_IntArray_unshift(sp_IntArray*a,mrb_int v){if(a->frozen){sp_raise_frozen_array();return;}if(a->start>0){a->start--;a->data[a->start]=v;a->len++;}else{mrb_int e=a->len+1;if(e>a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_int)*a->cap;h->size-=sizeof(mrb_int)*a->cap;a->cap=a->cap*2+1;a->data=(mrb_int*)realloc(a->data,sizeof(mrb_int)*a->cap);h->size+=sizeof(mrb_int)*a->cap;sp_gc_bytes+=sizeof(mrb_int)*a->cap;}memmove(a->data+1,a->data,sizeof(mrb_int)*a->len);a->data[0]=v;a->len++;}}
313
932
  static const char*sp_IntArray_join(sp_IntArray*a,const char*sep){size_t sl=strlen(sep),cap=256;char*buf=(char*)malloc(cap);size_t len=0;for(mrb_int i=0;i<a->len;i++){if(i>0){if(len+sl>=cap){cap*=2;buf=(char*)realloc(buf,cap);}memcpy(buf+len,sep,sl);len+=sl;}char tmp[32];int n=snprintf(tmp,32,"%lld",(long long)a->data[a->start+i]);if(len+n>=cap){cap*=2;buf=(char*)realloc(buf,cap);}memcpy(buf+len,tmp,n);len+=n;}buf[len]=0;char*r=sp_str_alloc(len);memcpy(r,buf,len);free(buf);return r;}
314
- static mrb_bool sp_IntArray_eq(sp_IntArray*a,sp_IntArray*b){if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++)if(a->data[a->start+i]!=b->data[b->start+i])return FALSE;return TRUE;}
933
+ static mrb_bool sp_IntArray_eq(sp_IntArray*a,sp_IntArray*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++)if(a->data[a->start+i]!=b->data[b->start+i])return FALSE;return TRUE;}
934
+ /* Array#<=> for IntArray. Lexicographic: per-element compare,
935
+ shorter array sorts before longer if all common elements match
936
+ (matches CRuby `[1,2] <=> [1,2,3] == -1`). Used by `.max` /
937
+ `.min` on `int_array_ptr_array` to dispatch the standard
938
+ sort/compare protocol over arrays of arrays. NULL recv compares
939
+ equal to NULL, lower than any non-NULL (matches the NULL-safe
940
+ pattern other compare helpers use). */
941
+ static mrb_int sp_IntArray_cmp(sp_IntArray*a,sp_IntArray*b){if(!a||!b)return a==b?0:(a?1:-1);mrb_int n=a->len<b->len?a->len:b->len;for(mrb_int i=0;i<n;i++){mrb_int av=a->data[a->start+i],bv=b->data[b->start+i];if(av<bv)return -1;if(av>bv)return 1;}if(a->len<b->len)return -1;if(a->len>b->len)return 1;return 0;}
315
942
 
316
- typedef struct{mrb_float*data;mrb_int len;mrb_int cap;}sp_FloatArray;
317
943
  static void sp_FloatArray_fin(void*p){sp_FloatArray*a=(sp_FloatArray*)p;sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_float)*a->cap;h->size-=sizeof(mrb_float)*a->cap;free(a->data);}
318
- static sp_FloatArray*sp_FloatArray_new(void){sp_FloatArray*a=(sp_FloatArray*)sp_gc_alloc(sizeof(sp_FloatArray),sp_FloatArray_fin,NULL);a->cap=16;a->data=(mrb_float*)malloc(sizeof(mrb_float)*a->cap);a->len=0;{sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));h->size+=sizeof(mrb_float)*a->cap;sp_gc_bytes+=sizeof(mrb_float)*a->cap;}return a;}
319
- static inline void sp_FloatArray_push(sp_FloatArray*a,mrb_float v){if(a->len>=a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_float)*a->cap;h->size-=sizeof(mrb_float)*a->cap;a->cap=a->cap*2+1;a->data=(mrb_float*)realloc(a->data,sizeof(mrb_float)*a->cap);h->size+=sizeof(mrb_float)*a->cap;sp_gc_bytes+=sizeof(mrb_float)*a->cap;}a->data[a->len++]=v;}
944
+ static sp_FloatArray*sp_FloatArray_new(void){sp_FloatArray*a=(sp_FloatArray*)sp_gc_alloc(sizeof(sp_FloatArray),sp_FloatArray_fin,NULL);a->cap=16;a->data=(mrb_float*)malloc(sizeof(mrb_float)*a->cap);if(!a->data)sp_oom_die();a->len=0;{sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));h->size+=sizeof(mrb_float)*a->cap;sp_gc_bytes+=sizeof(mrb_float)*a->cap;}return a;}
945
+ static inline void sp_FloatArray_push(sp_FloatArray*a,mrb_float v){if(a->frozen){sp_raise_frozen_array();return;}if(a->len>=a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_float)*a->cap;h->size-=sizeof(mrb_float)*a->cap;a->cap=a->cap*2+1;a->data=(mrb_float*)realloc(a->data,sizeof(mrb_float)*a->cap);h->size+=sizeof(mrb_float)*a->cap;sp_gc_bytes+=sizeof(mrb_float)*a->cap;}a->data[a->len++]=v;}
946
+ /* Float#step materialised as a FloatArray. Direction follows the
947
+ sign of k; k==0 yields an empty array to avoid an infinite loop. */
948
+ static sp_FloatArray*sp_FloatArray_from_step(mrb_float s,mrb_float e,mrb_float k){sp_FloatArray*a=sp_FloatArray_new();if(k==0.0)return a;mrb_float v=s;if(k>0){while(v<=e){sp_FloatArray_push(a,v);v+=k;}}else{while(v>=e){sp_FloatArray_push(a,v);v+=k;}}return a;}
320
949
  static mrb_float sp_FloatArray_min(sp_FloatArray*a){if(a->len==0)return 0;mrb_float m=a->data[0];for(mrb_int i=1;i<a->len;i++)if(a->data[i]<m)m=a->data[i];return m;}
321
950
  static mrb_float sp_FloatArray_max(sp_FloatArray*a){if(a->len==0)return 0;mrb_float m=a->data[0];for(mrb_int i=1;i<a->len;i++)if(a->data[i]>m)m=a->data[i];return m;}
322
- static mrb_float sp_FloatArray_sum(sp_FloatArray*a){mrb_float s=0;for(mrb_int i=0;i<a->len;i++)s+=a->data[i];return s;}
951
+ static mrb_float sp_FloatArray_sum(sp_FloatArray*a,mrb_float init){mrb_float s=init;for(mrb_int i=0;i<a->len;i++)s+=a->data[i];return s;}
323
952
  static void sp_FloatArray_replace(sp_FloatArray*dst,sp_FloatArray*src){dst->len=0;if(src->len>dst->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)dst-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_float)*dst->cap;h->size-=sizeof(mrb_float)*dst->cap;void*nd=realloc(dst->data,sizeof(mrb_float)*src->len);if(!nd){perror("realloc");exit(1);}dst->data=(mrb_float*)nd;dst->cap=src->len;h->size+=sizeof(mrb_float)*dst->cap;sp_gc_bytes+=sizeof(mrb_float)*dst->cap;}memcpy(dst->data,src->data,sizeof(mrb_float)*src->len);dst->len=src->len;}
324
- static inline mrb_float sp_FloatArray_pop(sp_FloatArray*a){return a->data[--a->len];}
325
- static inline mrb_float sp_FloatArray_shift(sp_FloatArray*a){if(a->len==0)return 0.0;mrb_float v=a->data[0];for(mrb_int i=0;i+1<a->len;i++)a->data[i]=a->data[i+1];a->len--;return v;}
953
+ static inline mrb_float sp_FloatArray_pop(sp_FloatArray*a){if(!a||a->len<=0)return 0.0;if(a->frozen){sp_raise_frozen_array();return 0.0;}return a->data[--a->len];}
954
+ static inline mrb_float sp_FloatArray_shift(sp_FloatArray*a){if(!a||a->len==0)return 0.0;if(a->frozen){sp_raise_frozen_array();return 0.0;}mrb_float v=a->data[0];for(mrb_int i=0;i+1<a->len;i++)a->data[i]=a->data[i+1];a->len--;return v;}
955
+ /* delete_at for float arrays -- the IntArray/StrArray peers existed but
956
+ FloatArray lacked it, so `[1.0].delete_at(0)` raised NoMethodError at runtime
957
+ (surfaced by the tep openai_server embeddings path). FloatArray is 0-based (no
958
+ `start` offset, unlike IntArray). Returns 0.0 on out-of-range (delete_at's nil
959
+ there). (join lives further down, where sp_float_to_s / sp_String are in scope.) */
960
+ static inline mrb_float sp_FloatArray_delete_at(sp_FloatArray*a,mrb_int i){if(!a)return 0.0;if(a->frozen){sp_raise_frozen_array();return 0.0;}if(i<0)i+=a->len;if(i<0||i>=a->len)return 0.0;mrb_float v=a->data[i];for(mrb_int j=i;j+1<a->len;j++)a->data[j]=a->data[j+1];a->len--;return v;}
326
961
  static inline mrb_int sp_FloatArray_length(sp_FloatArray*a){return a->len;}
327
962
  static inline mrb_bool sp_FloatArray_empty(sp_FloatArray*a){return a->len==0;}
328
- static inline mrb_float sp_FloatArray_get(sp_FloatArray*a,mrb_int i){if(i<0)i+=a->len;return a->data[i];}
963
+ static inline mrb_float sp_FloatArray_get(sp_FloatArray*a,mrb_int i){if(!a)return sp_float_nil();if(i<0)i+=a->len;if(i<0||i>=a->len)return sp_float_nil();return a->data[i];}
964
+ /* first/last as float? : nil (sentinel) when empty, else the element.
965
+ `[i]` stays non-nullable (0.0 for OOB) -- only first/last produce nil,
966
+ matching CRuby where Array#first/#last on an empty array return nil. */
967
+ static inline mrb_float sp_FloatArray_first_opt(sp_FloatArray*a){return (!a||a->len<=0)?sp_float_nil():sp_FloatArray_get(a,0);}
968
+ static inline mrb_float sp_FloatArray_last_opt(sp_FloatArray*a){return (!a||a->len<=0)?sp_float_nil():sp_FloatArray_get(a,a->len-1);}
329
969
  /* a[start, len] / a[start..end] for FloatArray. Same negative-start
330
970
  * and length-clamping semantics as sp_IntArray_slice. */
331
971
  static sp_FloatArray*sp_FloatArray_slice(sp_FloatArray*a,mrb_int start,mrb_int len){if(start<0)start+=a->len;if(start<0)start=0;sp_FloatArray*b=sp_FloatArray_new();if(start>=a->len||len<=0)return b;if(start+len>a->len)len=a->len-start;if(len>b->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)b-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(mrb_float)*b->cap;h->size-=sizeof(mrb_float)*b->cap;b->cap=len;b->data=(mrb_float*)realloc(b->data,sizeof(mrb_float)*b->cap);h->size+=sizeof(mrb_float)*b->cap;sp_gc_bytes+=sizeof(mrb_float)*b->cap;}memcpy(b->data,a->data+start,sizeof(mrb_float)*len);b->len=len;return b;}
332
- static inline void sp_FloatArray_set(sp_FloatArray*a,mrb_int i,mrb_float v){if(i<0)i+=a->len;while(i>=a->cap){a->cap=a->cap*2+1;a->data=(mrb_float*)realloc(a->data,sizeof(mrb_float)*a->cap);}while(i>=a->len){a->data[a->len]=0.0;a->len++;}a->data[i]=v;}
333
- static void sp_FloatArray_reverse_bang(sp_FloatArray*a){for(mrb_int i=0,j=a->len-1;i<j;i++,j--){mrb_float t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
334
- static void sp_FloatArray_rotate_bang(sp_FloatArray*a,mrb_int n){if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;mrb_float*tmp=(mrb_float*)malloc(sizeof(mrb_float)*a->len);for(mrb_int i=0;i<a->len;i++)tmp[i]=a->data[(i+n)%a->len];for(mrb_int i=0;i<a->len;i++)a->data[i]=tmp[i];free(tmp);}
972
+ /* See sp_IntArray_slice_range -- same shape, issue #496. */
973
+ static sp_FloatArray*sp_FloatArray_slice_range(sp_FloatArray*a,mrb_int start,mrb_int end_,mrb_int excl){if(end_<0)end_+=a->len;if(start<0)start+=a->len;mrb_int n=end_-start+(excl?0:1);if(n<0||start<0)n=0;return sp_FloatArray_slice(a,start,n);}
974
+ /* Issue #769: no-op for negative index after adjustment. */
975
+ static inline void sp_FloatArray_set(sp_FloatArray*a,mrb_int i,mrb_float v){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}mrb_int orig=i;if(i<0)i+=a->len;if(i<0)sp_raise_cls("IndexError",sp_sprintf("index %lld too small for array; minimum: %lld",(long long)orig,(long long)-a->len));while(i>=a->cap){a->cap=a->cap*2+1;a->data=(mrb_float*)realloc(a->data,sizeof(mrb_float)*a->cap);}while(i>=a->len){a->data[a->len]=0.0;a->len++;}a->data[i]=v;}
976
+ static void sp_FloatArray_reverse_bang(sp_FloatArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}for(mrb_int i=0,j=a->len-1;i<j;i++,j--){mrb_float t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
977
+ static void sp_FloatArray_rotate_bang(sp_FloatArray*a,mrb_int n){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;mrb_float*d=a->data;mrb_int lo=0,hi=n-1;while(lo<hi){mrb_float t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=n;hi=a->len-1;while(lo<hi){mrb_float t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=0;hi=a->len-1;while(lo<hi){mrb_float t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}}
335
978
  static int _sp_float_cmp(const void*a,const void*b){mrb_float va=*(const mrb_float*)a,vb=*(const mrb_float*)b;return(va>vb)-(va<vb);}
336
- static void sp_FloatArray_sort_bang(sp_FloatArray*a){qsort(a->data,a->len,sizeof(mrb_float),_sp_float_cmp);}
337
- static void sp_FloatArray_shuffle_bang(sp_FloatArray*a){for(mrb_int i=a->len-1;i>0;i--){mrb_int j=(mrb_int)(rand()%(i+1));mrb_float t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
979
+ static void sp_FloatArray_sort_bang(sp_FloatArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}qsort(a->data,a->len,sizeof(mrb_float),_sp_float_cmp);}
980
+ static void sp_FloatArray_shuffle_bang(sp_FloatArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}for(mrb_int i=a->len-1;i>0;i--){mrb_int j=(mrb_int)(rand()%(i+1));mrb_float t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
338
981
  static sp_FloatArray*sp_FloatArray_shuffle(sp_FloatArray*a){sp_FloatArray*r=sp_FloatArray_new();sp_FloatArray_replace(r,a);sp_FloatArray_shuffle_bang(r);return r;}
982
+ static mrb_float sp_FloatArray_sample(sp_FloatArray*a){if(a->len<=0)return 0.0;return a->data[(mrb_int)(rand()%a->len)];}
339
983
  /* IEEE 754 == on mrb_float: NaN never matches; +0.0 == -0.0 (diverges from Float#eql?). */
340
984
  static mrb_bool sp_FloatArray_include(sp_FloatArray*a,mrb_float v){for(mrb_int i=0;i<a->len;i++)if(a->data[i]==v)return TRUE;return FALSE;}
341
985
  static sp_FloatArray*sp_FloatArray_intersect(sp_FloatArray*a,sp_FloatArray*b){sp_FloatArray*r=sp_FloatArray_new();for(mrb_int i=0;i<a->len;i++){mrb_float v=a->data[i];if(sp_FloatArray_include(b,v)&&!sp_FloatArray_include(r,v))sp_FloatArray_push(r,v);}return r;}
342
- static sp_FloatArray*sp_FloatArray_union(sp_FloatArray*a,sp_FloatArray*b){sp_FloatArray*r=sp_FloatArray_new();for(mrb_int i=0;i<a->len;i++){mrb_float v=a->data[i];if(!sp_FloatArray_include(r,v))sp_FloatArray_push(r,v);}for(mrb_int i=0;i<b->len;i++){mrb_float v=b->data[i];if(!sp_FloatArray_include(r,v))sp_FloatArray_push(r,v);}return r;}
986
+ static sp_FloatArray*sp_FloatArray_union(sp_FloatArray*a,sp_FloatArray*b){sp_FloatArray*r=sp_FloatArray_new();for(mrb_int i=0;i<a->len;i++){mrb_float v=a->data[i];if(!sp_FloatArray_include(r,v))sp_FloatArray_push(r,v);}if(b){for(mrb_int i=0;i<b->len;i++){mrb_float v=b->data[i];if(!sp_FloatArray_include(r,v))sp_FloatArray_push(r,v);}}return r;}
343
987
  static sp_FloatArray*sp_FloatArray_difference(sp_FloatArray*a,sp_FloatArray*b){sp_FloatArray*r=sp_FloatArray_new();for(mrb_int i=0;i<a->len;i++){mrb_float v=a->data[i];if(!sp_FloatArray_include(b,v))sp_FloatArray_push(r,v);}return r;}
344
988
 
345
989
  /* ---- PtrArray: array of void* pointers ---- */
346
- typedef struct{void**data;mrb_int len;mrb_int cap;void(*scan_elem)(void*);}sp_PtrArray;
347
990
  static void sp_PtrArray_fin(void*p){sp_PtrArray*a=(sp_PtrArray*)p;sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(void*)*a->cap;h->size-=sizeof(void*)*a->cap;free(a->data);}
348
991
  static void sp_PtrArray_gc_scan(void*p){sp_PtrArray*a=(sp_PtrArray*)p;if(!a->scan_elem)return;for(mrb_int i=0;i<a->len;i++){if(a->data[i])a->scan_elem(a->data[i]);}}
349
- static sp_PtrArray*sp_PtrArray_new_scan(void(*scan_elem)(void*)){sp_PtrArray*a=(sp_PtrArray*)sp_gc_alloc(sizeof(sp_PtrArray),sp_PtrArray_fin,scan_elem?sp_PtrArray_gc_scan:NULL);a->cap=16;a->data=(void**)malloc(sizeof(void*)*a->cap);a->len=0;a->scan_elem=scan_elem;{sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));h->size+=sizeof(void*)*a->cap;sp_gc_bytes+=sizeof(void*)*a->cap;}return a;}
992
+ static sp_PtrArray*sp_PtrArray_new_scan(void(*scan_elem)(void*)){sp_PtrArray*a=(sp_PtrArray*)sp_gc_alloc(sizeof(sp_PtrArray),sp_PtrArray_fin,scan_elem?sp_PtrArray_gc_scan:NULL);a->cap=16;a->data=(void**)malloc(sizeof(void*)*a->cap);if(!a->data)sp_oom_die();a->len=0;a->scan_elem=scan_elem;{sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));h->size+=sizeof(void*)*a->cap;sp_gc_bytes+=sizeof(void*)*a->cap;}return a;}
350
993
  static sp_PtrArray*sp_PtrArray_new(void){return sp_PtrArray_new_scan(sp_gc_mark);}
351
- static inline void sp_PtrArray_push(sp_PtrArray*a,void*v){if(a->len>=a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(void*)*a->cap;h->size-=sizeof(void*)*a->cap;a->cap=a->cap*2+1;a->data=(void**)realloc(a->data,sizeof(void*)*a->cap);h->size+=sizeof(void*)*a->cap;sp_gc_bytes+=sizeof(void*)*a->cap;}a->data[a->len++]=v;}
352
- static inline void*sp_PtrArray_get(sp_PtrArray*a,mrb_int i){if(i<0)i+=a->len;return a->data[i];}
353
- static inline void sp_PtrArray_set(sp_PtrArray*a,mrb_int i,void*v){if(i<0)i+=a->len;a->data[i]=v;}
354
- static inline mrb_int sp_PtrArray_length(sp_PtrArray*a){return a->len;}
355
- static inline mrb_bool sp_PtrArray_empty(sp_PtrArray*a){return a->len==0;}
994
+ /* PtrArray for raw external pointers (FFI `:ptr` returns, dlopen
995
+ handles, ...). These don't carry sp_gc_hdr -- the default
996
+ sp_gc_mark element scan would read undefined bytes and likely
997
+ crash at collection time. Skip per-element scanning; the array
998
+ header itself is still GC-tracked. */
999
+ static sp_PtrArray*sp_PtrArray_new_noscan(void){return sp_PtrArray_new_scan(NULL);}
1000
+ static inline void sp_PtrArray_push(sp_PtrArray*a,void*v){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}if(a->len>=a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(void*)*a->cap;h->size-=sizeof(void*)*a->cap;a->cap=a->cap*2+1;void*nd=realloc(a->data,sizeof(void*)*a->cap);if(!nd)sp_oom_die();a->data=(void**)nd;h->size+=sizeof(void*)*a->cap;sp_gc_bytes+=sizeof(void*)*a->cap;}a->data[a->len++]=v;}
1001
+ /* Array#pop on a `<X>_ptr_array`. Returns NULL when empty
1002
+ (matches CRuby's nil for typed-element arrays since the slot
1003
+ can't carry nil). Issue #520: previously the dispatch on
1004
+ `int_array_ptr_array.pop` warned "cannot resolve call to
1005
+ 'pop'" and emitted 0, silently leaving the array intact. */
1006
+ static inline void *sp_PtrArray_pop(sp_PtrArray*a){if(!a||a->len==0)return NULL;return a->data[--a->len];}
1007
+ static inline void*sp_PtrArray_get(sp_PtrArray*a,mrb_int i){if(!a)return NULL;if(i<0)i+=a->len;if(i<0||i>=a->len)return NULL;return a->data[i];}
1008
+ /* Issue #770: bounds-check the final index. CRuby grows the array on
1009
+ out-of-range set; spinel's typed-array slots have a fixed capacity
1010
+ shape per element type, so the safer fallback is to no-op rather
1011
+ than write into adjacent memory. */
1012
+ static inline void sp_PtrArray_set(sp_PtrArray*a,mrb_int i,void*v){if(!a)return;if(i<0)i+=a->len;if(i<0||i>=a->len)return;a->data[i]=v;}
1013
+ static inline mrb_int sp_PtrArray_length(sp_PtrArray*a){if(!a)return 0;return a->len;}
1014
+ static inline mrb_bool sp_PtrArray_empty(sp_PtrArray*a){if(!a)return TRUE;return a->len==0;}
1015
+ /* `Array#delete_at(i)` -- remove and return the element at index i.
1016
+ Negative indices count from the end. Returns NULL when i is out
1017
+ of range (matches CRuby's nil for typed-element arrays). Mirrors
1018
+ sp_IntArray_delete_at / sp_StrArray_delete_at; without this peer
1019
+ user-class arrays can't shrink. */
1020
+ static void*sp_PtrArray_delete_at(sp_PtrArray*a,mrb_int i){if(i<0)i+=a->len;if(i<0||i>=a->len)return NULL;void*v=a->data[i];for(mrb_int j=i;j<a->len-1;j++)a->data[j]=a->data[j+1];a->len--;return v;}
356
1021
  static void sp_PtrArray_reverse_bang(sp_PtrArray*a){for(mrb_int i=0,j=a->len-1;i<j;i++,j--){void*t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
357
- static void sp_PtrArray_rotate_bang(sp_PtrArray*a,mrb_int n){if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;void**tmp=(void**)malloc(sizeof(void*)*a->len);for(mrb_int i=0;i<a->len;i++)tmp[i]=a->data[(i+n)%a->len];for(mrb_int i=0;i<a->len;i++)a->data[i]=tmp[i];free(tmp);}
1022
+ static void sp_PtrArray_rotate_bang(sp_PtrArray*a,mrb_int n){if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;void**d=a->data;mrb_int lo=0,hi=n-1;while(lo<hi){void*t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=n;hi=a->len-1;while(lo<hi){void*t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=0;hi=a->len-1;while(lo<hi){void*t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}}
358
1023
  static sp_PtrArray*sp_PtrArray_dup(sp_PtrArray*a){sp_PtrArray*b=sp_PtrArray_new_scan(a->scan_elem);for(mrb_int i=0;i<a->len;i++)sp_PtrArray_push(b,a->data[i]);return b;}
359
1024
  static sp_PtrArray*sp_PtrArray_slice(sp_PtrArray*a,mrb_int start,mrb_int len){if(start<0)start+=a->len;if(start<0)start=0;sp_PtrArray*b=sp_PtrArray_new_scan(a->scan_elem);if(start>=a->len||len<=0)return b;if(start+len>a->len)len=a->len-start;for(mrb_int i=0;i<len;i++)sp_PtrArray_push(b,a->data[start+i]);return b;}
360
1025
  static void sp_PtrArray_shuffle_bang(sp_PtrArray*a){for(mrb_int i=a->len-1;i>0;i--){mrb_int j=(mrb_int)(rand()%(i+1));void*t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
361
1026
  static sp_PtrArray*sp_PtrArray_shuffle(sp_PtrArray*a){sp_PtrArray*b=sp_PtrArray_dup(a);sp_PtrArray_shuffle_bang(b);return b;}
1027
+ static void *sp_PtrArray_sample(sp_PtrArray*a){if(a->len<=0)return NULL;return a->data[(mrb_int)(rand()%a->len)];}
362
1028
 
363
1029
  /* Small-array optimization: keep the first SP_STRARR_INLINE elements
364
1030
  * inside the struct so empty/short StrArrays skip the data malloc.
365
1031
  * Common idiom "".split(",") and most class-metadata lists stay small.
366
1032
  * data == inline_data is the discriminator for "still on inline storage". */
367
- #define SP_STRARR_INLINE 4
368
- typedef struct{const char**data;mrb_int len;mrb_int cap;const char*inline_data[SP_STRARR_INLINE];}sp_StrArray;
369
1033
  static void sp_StrArray_fin(void*p){sp_StrArray*a=(sp_StrArray*)p;if(a->data!=a->inline_data){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));sp_gc_bytes-=sizeof(const char*)*a->cap;h->size-=sizeof(const char*)*a->cap;free(a->data);}}
370
1034
  static void sp_StrArray_scan(void*p){sp_StrArray*a=(sp_StrArray*)p;for(mrb_int i=0;i<a->len;i++)sp_mark_string(a->data[i]);}
371
1035
  static sp_StrArray*sp_StrArray_new(void){sp_StrArray*a=(sp_StrArray*)sp_gc_alloc(sizeof(sp_StrArray),sp_StrArray_fin,sp_StrArray_scan);a->cap=SP_STRARR_INLINE;a->data=a->inline_data;a->len=0;return a;}
372
- static inline void sp_StrArray_push(sp_StrArray*a,const char*v){if(a->len>=a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));mrb_int nc=a->cap*2+1;if(a->data==a->inline_data){const char**nd=(const char**)malloc(sizeof(const char*)*nc);memcpy(nd,a->data,sizeof(const char*)*a->len);a->data=nd;}else{sp_gc_bytes-=sizeof(const char*)*a->cap;h->size-=sizeof(const char*)*a->cap;a->data=(const char**)realloc(a->data,sizeof(const char*)*nc);}a->cap=nc;h->size+=sizeof(const char*)*a->cap;sp_gc_bytes+=sizeof(const char*)*a->cap;}a->data[a->len++]=v;}
1036
+ static inline void sp_StrArray_push(sp_StrArray*a,const char*v){if(a->frozen){sp_raise_frozen_array();return;}if(a->len>=a->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)a-sizeof(sp_gc_hdr));mrb_int nc=a->cap*2+1;if(a->data==a->inline_data){const char**nd=(const char**)malloc(sizeof(const char*)*nc);if(!nd)sp_oom_die();memcpy(nd,a->data,sizeof(const char*)*a->len);a->data=nd;}else{sp_gc_bytes-=sizeof(const char*)*a->cap;h->size-=sizeof(const char*)*a->cap;void*nd=realloc(a->data,sizeof(const char*)*nc);if(!nd)sp_oom_die();a->data=(const char**)nd;}a->cap=nc;h->size+=sizeof(const char*)*a->cap;sp_gc_bytes+=sizeof(const char*)*a->cap;}a->data[a->len++]=v;}
373
1037
  static void sp_StrArray_replace(sp_StrArray*dst,sp_StrArray*src){dst->len=0;if(src->len>dst->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)dst-sizeof(sp_gc_hdr));void*nd;if(dst->data==dst->inline_data){nd=malloc(sizeof(const char*)*src->len);if(!nd){perror("malloc");exit(1);}}else{sp_gc_bytes-=sizeof(const char*)*dst->cap;h->size-=sizeof(const char*)*dst->cap;nd=realloc(dst->data,sizeof(const char*)*src->len);if(!nd){perror("realloc");exit(1);}}dst->data=(const char**)nd;dst->cap=src->len;h->size+=sizeof(const char*)*dst->cap;sp_gc_bytes+=sizeof(const char*)*dst->cap;}memcpy(dst->data,src->data,sizeof(const char*)*src->len);dst->len=src->len;}
374
- static const char*sp_StrArray_pop(sp_StrArray*a){return a->data[--a->len];}
1038
+ static const char*sp_StrArray_pop(sp_StrArray*a){if(!a||a->len<=0)return NULL;if(a->frozen){sp_raise_frozen_array();return NULL;}return a->data[--a->len];}
375
1039
  static inline mrb_int sp_StrArray_length(sp_StrArray*a){return a->len;}
376
1040
  static inline mrb_bool sp_StrArray_empty(sp_StrArray*a){return a->len==0;}
377
- static inline const char*sp_StrArray_get(sp_StrArray*a,mrb_int i){if(i<0)i+=a->len;return a->data[i];}
1041
+ static inline const char*sp_StrArray_get(sp_StrArray*a,mrb_int i){if(!a)return NULL;if(i<0)i+=a->len;if(i<0||i>=a->len)return NULL;return a->data[i];}
378
1042
  /* a[start, len] / a[start..end] for StrArray. Same negative-start and
379
1043
  * length-clamping semantics as sp_IntArray_slice. Out-of-bounds start
380
1044
  * returns an empty StrArray (we don't have a nullable form). */
381
1045
  static sp_StrArray*sp_StrArray_slice(sp_StrArray*a,mrb_int start,mrb_int len){if(start<0)start+=a->len;if(start<0)start=0;sp_StrArray*b=sp_StrArray_new();if(start>=a->len||len<=0)return b;if(start+len>a->len)len=a->len-start;for(mrb_int i=0;i<len;i++)sp_StrArray_push(b,a->data[start+i]);return b;}
382
- static inline void sp_StrArray_set(sp_StrArray*a,mrb_int i,const char*v){if(i<0)i+=a->len;while(i>=a->len)sp_StrArray_push(a,"");a->data[i]=v;}
383
- static void sp_StrArray_reverse_bang(sp_StrArray*a){for(mrb_int i=0,j=a->len-1;i<j;i++,j--){const char*t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
384
- static void sp_StrArray_rotate_bang(sp_StrArray*a,mrb_int n){if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;const char**tmp=(const char**)malloc(sizeof(const char*)*a->len);for(mrb_int i=0;i<a->len;i++)tmp[i]=a->data[(i+n)%a->len];for(mrb_int i=0;i<a->len;i++)a->data[i]=tmp[i];free(tmp);}
1046
+ /* Forward decl sp_str_succ is defined further down (it uses
1047
+ sp_utf8_decode); the StrArray_from_string_range loop below needs
1048
+ it visible early. */
1049
+ static const char *sp_str_succ(const char *s);
1050
+ /* String range to_a — single-char and multi-char ASCII ranges via
1051
+ sp_str_succ. The 4096-iteration cap stops a pathological prepend-
1052
+ style infinite loop before it eats memory. */
1053
+ static sp_StrArray *sp_StrArray_from_string_range(const char *s, const char *e, mrb_int excl) {
1054
+ sp_StrArray *a = sp_StrArray_new();
1055
+ if (!s || !e) return a;
1056
+ const char *cur = s;
1057
+ int iters = 0;
1058
+ while (iters < 4096) {
1059
+ int cmp = strcmp(cur, e);
1060
+ if (cmp > 0) break;
1061
+ if (cmp == 0 && excl) break;
1062
+ char *copy = sp_str_alloc(strlen(cur));
1063
+ strcpy(copy, cur);
1064
+ sp_StrArray_push(a, copy);
1065
+ if (cmp == 0) break;
1066
+ cur = sp_str_succ(cur);
1067
+ iters++;
1068
+ }
1069
+ return a;
1070
+ }
1071
+ /* See sp_IntArray_slice_range -- same shape, issue #496. */
1072
+ static sp_StrArray*sp_StrArray_slice_range(sp_StrArray*a,mrb_int start,mrb_int end_,mrb_int excl){if(end_<0)end_+=a->len;if(start<0)start+=a->len;mrb_int n=end_-start+(excl?0:1);if(n<0||start<0)n=0;return sp_StrArray_slice(a,start,n);}
1073
+ static inline void sp_StrArray_set(sp_StrArray*a,mrb_int i,const char*v){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}mrb_int orig=i;if(i<0)i+=a->len;if(i<0)sp_raise_cls("IndexError",sp_sprintf("index %lld too small for array; minimum: %lld",(long long)orig,(long long)-a->len));while(i>=a->len)sp_StrArray_push(a,sp_str_empty);a->data[i]=v;}
1074
+ static void sp_StrArray_reverse_bang(sp_StrArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}for(mrb_int i=0,j=a->len-1;i<j;i++,j--){const char*t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
1075
+ static void sp_StrArray_rotate_bang(sp_StrArray*a,mrb_int n){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;const char**d=a->data;mrb_int lo=0,hi=n-1;while(lo<hi){const char*t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=n;hi=a->len-1;while(lo<hi){const char*t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=0;hi=a->len-1;while(lo<hi){const char*t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}}
385
1076
  static int _sp_str_cmp(const void*a,const void*b){return strcmp(*(const char*const*)a,*(const char*const*)b);}
386
- static void sp_StrArray_sort_bang(sp_StrArray*a){qsort(a->data,a->len,sizeof(const char*),_sp_str_cmp);}
1077
+ static void sp_StrArray_sort_bang(sp_StrArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}qsort(a->data,a->len,sizeof(const char*),_sp_str_cmp);}
1078
+ /* Case-insensitive string compare. Portable across glibc / MinGW
1079
+ (avoids strcasecmp which lives in strings.h on POSIX and is named
1080
+ stricmp on Windows). Returns -1 / 0 / 1 like CRuby's String#casecmp. */
1081
+ static mrb_int sp_str_casecmp(const char*a,const char*b){if(!a||!b)return a==b?0:(a?1:-1);for(;;){int ca=tolower((unsigned char)*a),cb=tolower((unsigned char)*b);if(ca!=cb)return ca<cb?-1:1;if(!*a)return 0;a++;b++;}}
1082
+ /* String#valid_encoding? — walks the buffer and accepts pure ASCII
1083
+ or well-formed UTF-8 (RFC 3629 byte sequences with no overlong
1084
+ forms, no surrogate halves, code points <= U+10FFFF). */
1085
+ static mrb_bool sp_str_valid_encoding(const char*s){if(!s)return TRUE;const unsigned char*p=(const unsigned char*)s;while(*p){unsigned c=*p;if(c<0x80){p++;continue;}int extra;unsigned cp;unsigned min;if((c&0xE0)==0xC0){extra=1;cp=c&0x1F;min=0x80;}else if((c&0xF0)==0xE0){extra=2;cp=c&0x0F;min=0x800;}else if((c&0xF8)==0xF0){extra=3;cp=c&0x07;min=0x10000;}else return FALSE;p++;for(int i=0;i<extra;i++){if((*p&0xC0)!=0x80)return FALSE;cp=(cp<<6)|(*p&0x3F);p++;}if(cp<min)return FALSE;if(cp>=0xD800&&cp<=0xDFFF)return FALSE;if(cp>0x10FFFF)return FALSE;}return TRUE;}
387
1086
  static const char*sp_StrArray_join(sp_StrArray*a,const char*sep){size_t sl=strlen(sep),cap=256;char*buf=(char*)malloc(cap);size_t len=0;for(mrb_int i=0;i<a->len;i++){if(i>0){if(len+sl>=cap){cap*=2;buf=(char*)realloc(buf,cap);}memcpy(buf+len,sep,sl);len+=sl;}size_t el=strlen(a->data[i]);if(len+el>=cap){cap=(len+el)*2+1;buf=(char*)realloc(buf,cap);}memcpy(buf+len,a->data[i],el);len+=el;}buf[len]=0;char*r=sp_str_alloc(len);memcpy(r,buf,len);free(buf);return r;}
388
1087
  static mrb_bool sp_StrArray_include(sp_StrArray*a,const char*v){for(mrb_int i=0;i<a->len;i++)if(strcmp(a->data[i],v)==0)return TRUE;return FALSE;}
389
1088
  static sp_StrArray*sp_StrArray_intersect(sp_StrArray*a,sp_StrArray*b){sp_StrArray*r=sp_StrArray_new();for(mrb_int i=0;i<a->len;i++){const char*v=a->data[i];if(sp_StrArray_include(b,v)&&!sp_StrArray_include(r,v))sp_StrArray_push(r,v);}return r;}
390
- static sp_StrArray*sp_StrArray_union(sp_StrArray*a,sp_StrArray*b){sp_StrArray*r=sp_StrArray_new();for(mrb_int i=0;i<a->len;i++){const char*v=a->data[i];if(!sp_StrArray_include(r,v))sp_StrArray_push(r,v);}for(mrb_int i=0;i<b->len;i++){const char*v=b->data[i];if(!sp_StrArray_include(r,v))sp_StrArray_push(r,v);}return r;}
1089
+ static sp_StrArray*sp_StrArray_union(sp_StrArray*a,sp_StrArray*b){sp_StrArray*r=sp_StrArray_new();for(mrb_int i=0;i<a->len;i++){const char*v=a->data[i];if(!sp_StrArray_include(r,v))sp_StrArray_push(r,v);}if(b){for(mrb_int i=0;i<b->len;i++){const char*v=b->data[i];if(!sp_StrArray_include(r,v))sp_StrArray_push(r,v);}}return r;}
391
1090
  static sp_StrArray*sp_StrArray_difference(sp_StrArray*a,sp_StrArray*b){sp_StrArray*r=sp_StrArray_new();for(mrb_int i=0;i<a->len;i++){const char*v=a->data[i];if(!sp_StrArray_include(b,v))sp_StrArray_push(r,v);}return r;}
392
1091
  static mrb_int sp_StrArray_index(sp_StrArray*a,const char*v){for(mrb_int i=0;i<a->len;i++)if(strcmp(a->data[i],v)==0)return i;return -1;}
393
1092
  static mrb_int sp_StrArray_rindex(sp_StrArray*a,const char*v){for(mrb_int i=a->len-1;i>=0;i--)if(strcmp(a->data[i],v)==0)return i;return -1;}
394
1093
  static sp_StrArray*sp_StrArray_compact(sp_StrArray*a){sp_StrArray*r=sp_StrArray_new();for(mrb_int i=0;i<a->len;i++)if(a->data[i]!=NULL)sp_StrArray_push(r,a->data[i]);return r;}
395
- static const char*sp_StrArray_delete_at(sp_StrArray*a,mrb_int i){if(i<0)i+=a->len;if(i<0||i>=a->len)return NULL;const char*v=a->data[i];for(mrb_int j=i;j<a->len-1;j++)a->data[j]=a->data[j+1];a->len--;return v;}
396
- static const char*sp_StrArray_delete(sp_StrArray*a,const char*v){mrb_int w=0;const char*found=NULL;for(mrb_int i=0;i<a->len;i++){if(strcmp(a->data[i],v)!=0){a->data[w]=a->data[i];w++;}else{found=a->data[i];}}a->len=w;return found;}
397
- static void sp_StrArray_insert(sp_StrArray*a,mrb_int i,const char*v){if(i<0)i+=a->len+1;sp_StrArray_push(a,"");for(mrb_int j=a->len-1;j>i;j--)a->data[j]=a->data[j-1];a->data[i]=v;}
398
- static void sp_StrArray_shuffle_bang(sp_StrArray*a){for(mrb_int i=a->len-1;i>0;i--){mrb_int j=(mrb_int)(rand()%(i+1));const char*t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
1094
+ static const char*sp_StrArray_delete_at(sp_StrArray*a,mrb_int i){if(!a)return NULL;if(a->frozen){sp_raise_frozen_array();return NULL;}if(i<0)i+=a->len;if(i<0||i>=a->len)return NULL;const char*v=a->data[i];for(mrb_int j=i;j<a->len-1;j++)a->data[j]=a->data[j+1];a->len--;return v;}
1095
+ static const char*sp_StrArray_delete(sp_StrArray*a,const char*v){if(!a)return NULL;if(a->frozen){sp_raise_frozen_array();return NULL;}mrb_int w=0;const char*found=NULL;for(mrb_int i=0;i<a->len;i++){if(strcmp(a->data[i],v)!=0){a->data[w]=a->data[i];w++;}else{found=a->data[i];}}a->len=w;return found;}
1096
+ static void sp_StrArray_insert(sp_StrArray*a,mrb_int i,const char*v){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}if(i<0)i+=a->len+1;sp_StrArray_push(a,sp_str_empty);for(mrb_int j=a->len-1;j>i;j--)a->data[j]=a->data[j-1];a->data[i]=v;}
1097
+ static void sp_StrArray_shuffle_bang(sp_StrArray*a){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}for(mrb_int i=a->len-1;i>0;i--){mrb_int j=(mrb_int)(rand()%(i+1));const char*t=a->data[i];a->data[i]=a->data[j];a->data[j]=t;}}
399
1098
  static sp_StrArray*sp_StrArray_shuffle(sp_StrArray*a){sp_StrArray*r=sp_StrArray_new();sp_StrArray_replace(r,a);sp_StrArray_shuffle_bang(r);return r;}
1099
+ static const char *sp_StrArray_sample(sp_StrArray*a){if(a->len<=0)return sp_str_empty;return a->data[(mrb_int)(rand()%a->len)];}
400
1100
 
401
1101
  static inline uint64_t sp_str_hash(const char*s){uint64_t h=14695981039346656037ULL;while(*s){h^=(unsigned char)*s++;h*=1099511628211ULL;}return h;}
402
- typedef struct{const char**keys;mrb_int*vals;const char**order;mrb_int len;mrb_int cap;mrb_int mask;}sp_StrIntHash;
403
1102
  static void sp_StrIntHash_fin(void*p){sp_StrIntHash*h=(sp_StrIntHash*)p;free(h->keys);free(h->vals);free(h->order);}
404
1103
  static void sp_StrIntHash_scan(void*p){sp_StrIntHash*h=(sp_StrIntHash*)p;for(mrb_int i=0;i<h->cap;i++){if(h->keys[i])sp_mark_string(h->keys[i]);}}
405
- static sp_StrIntHash*sp_StrIntHash_new(void){sp_StrIntHash*h=(sp_StrIntHash*)sp_gc_alloc(sizeof(sp_StrIntHash),sp_StrIntHash_fin,sp_StrIntHash_scan);h->cap=16;h->mask=15;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->order=(const char**)malloc(sizeof(const char*)*h->cap);h->len=0;return h;}
1104
+ /* default_v is SP_INT_NIL for a hash with no explicit default ({} / {k=>v}),
1105
+ so a missing-key `[]` read surfaces Ruby nil (#801). Hash.new(N) sets it to
1106
+ N via _new_with_default. Proven-present internal reads use _get on present
1107
+ keys, so this only governs the miss path. */
1108
+ static sp_StrIntHash*sp_StrIntHash_new(void){sp_StrIntHash*h=(sp_StrIntHash*)sp_gc_alloc(sizeof(sp_StrIntHash),sp_StrIntHash_fin,sp_StrIntHash_scan);h->cap=16;h->mask=15;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->order=(const char**)malloc(sizeof(const char*)*h->cap);h->len=0;h->default_v=SP_INT_NIL;return h;}
1109
+ static sp_StrIntHash*sp_StrIntHash_new_with_default(mrb_int d){sp_StrIntHash*h=sp_StrIntHash_new();h->default_v=d;return h;}
406
1110
  static void sp_StrIntHash_grow(sp_StrIntHash*h){mrb_int oc=h->cap;const char**ok=h->keys;mrb_int*ov=h->vals;h->cap*=2;h->mask=h->cap-1;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->order=(const char**)realloc(h->order,sizeof(const char*)*h->cap);mrb_int ol=h->len;h->len=0;for(mrb_int i=0;i<oc;i++){if(ok[i]){mrb_int idx=(mrb_int)(sp_str_hash(ok[i])&h->mask);while(h->keys[idx])idx=(idx+1)&h->mask;h->keys[idx]=ok[i];h->vals[idx]=ov[i];h->len++;}}free(ok);free(ov);}
407
- static mrb_int sp_StrIntHash_get(sp_StrIntHash*h,const char*k){mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return h->vals[idx];idx=(idx+1)&h->mask;}return 0;}
1111
+ static mrb_int sp_StrIntHash_get(sp_StrIntHash*h,const char*k){if(!h)return 0;mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return h->vals[idx];idx=(idx+1)&h->mask;}return h->default_v;}
1112
+ /* Issue #801: maybe-missing public `[]` read. Returns default_v on a miss,
1113
+ which is SP_INT_NIL (Ruby nil at the value level) for a no-default hash and
1114
+ the explicit default for Hash.new(N). Proven-present reads keep using _get. */
1115
+ static mrb_int sp_StrIntHash_get_opt(sp_StrIntHash*h,const char*k){if(!h)return SP_INT_NIL;mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return h->vals[idx];idx=(idx+1)&h->mask;}return h->default_v;}
408
1116
  static void sp_StrIntHash_set(sp_StrIntHash*h,const char*k,mrb_int v){if(h->len*2>=h->cap)sp_StrIntHash_grow(h);mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0){h->vals[idx]=v;return;}idx=(idx+1)&h->mask;}h->keys[idx]=k;h->vals[idx]=v;h->order[h->len]=k;h->len++;}
409
1117
  static mrb_bool sp_StrIntHash_has_key(sp_StrIntHash*h,const char*k){mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return TRUE;idx=(idx+1)&h->mask;}return FALSE;}
1118
+ /* Hash#value? -- scan values in insertion order. Issue #738. */
1119
+ static mrb_bool sp_StrIntHash_has_value(sp_StrIntHash*h,mrb_int v){if(!h)return FALSE;for(mrb_int i=0;i<h->len;i++)if(sp_StrIntHash_get(h,h->order[i])==v)return TRUE;return FALSE;}
410
1120
  static mrb_int sp_StrIntHash_length(sp_StrIntHash*h){return h->len;}
411
1121
  static void sp_StrIntHash_delete(sp_StrIntHash*h,const char*k){mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0){h->keys[idx]=NULL;h->vals[idx]=0;h->len--;mrb_int j=(idx+1)&h->mask;while(h->keys[j]){mrb_int nj=(mrb_int)(sp_str_hash(h->keys[j])&h->mask);if((j>idx&&(nj<=idx||nj>j))||(j<idx&&nj<=idx&&nj>j)){h->keys[idx]=h->keys[j];h->vals[idx]=h->vals[j];h->keys[j]=NULL;h->vals[j]=0;idx=j;}j=(j+1)&h->mask;}{mrb_int oi=0;while(oi<=h->len){if(strcmp(h->order[oi],k)==0){while(oi<h->len){h->order[oi]=h->order[oi+1];oi++;}break;}oi++;}}return;}idx=(idx+1)&h->mask;}}
412
1122
  static sp_StrArray*sp_StrIntHash_keys(sp_StrIntHash*h){sp_StrArray*a=sp_StrArray_new();for(mrb_int i=0;i<h->len;i++)sp_StrArray_push(a,h->order[i]);return a;}
413
1123
  static sp_IntArray*sp_StrIntHash_values(sp_StrIntHash*h){sp_IntArray*a=sp_IntArray_new();for(mrb_int i=0;i<h->len;i++)sp_IntArray_push(a,sp_StrIntHash_get(h,h->order[i]));return a;}
414
1124
  static sp_StrIntHash*sp_StrArray_tally(sp_StrArray*a){sp_StrIntHash*h=sp_StrIntHash_new();for(mrb_int i=0;i<a->len;i++){const char*k=a->data[i];mrb_int c=sp_StrIntHash_has_key(h,k)?sp_StrIntHash_get(h,k):0;sp_StrIntHash_set(h,k,c+1);}return h;}
415
- static sp_StrIntHash*sp_StrIntHash_merge(sp_StrIntHash*a,sp_StrIntHash*b){sp_StrIntHash*r=sp_StrIntHash_new();for(mrb_int i=0;i<a->len;i++)sp_StrIntHash_set(r,a->order[i],sp_StrIntHash_get(a,a->order[i]));for(mrb_int i=0;i<b->len;i++)sp_StrIntHash_set(r,b->order[i],sp_StrIntHash_get(b,b->order[i]));return r;}
1125
+ static sp_StrIntHash*sp_StrIntHash_merge(sp_StrIntHash*a,sp_StrIntHash*b){sp_StrIntHash*r=sp_StrIntHash_new();r->default_v=a->default_v;for(mrb_int i=0;i<a->len;i++)sp_StrIntHash_set(r,a->order[i],sp_StrIntHash_get(a,a->order[i]));for(mrb_int i=0;i<b->len;i++)sp_StrIntHash_set(r,b->order[i],sp_StrIntHash_get(b,b->order[i]));return r;}
416
1126
  static void sp_StrIntHash_update(sp_StrIntHash*a,sp_StrIntHash*b){for(mrb_int i=0;i<b->len;i++)sp_StrIntHash_set(a,b->order[i],sp_StrIntHash_get(b,b->order[i]));}
1127
+ static sp_StrIntHash*sp_StrIntHash_dup(sp_StrIntHash*h){sp_StrIntHash*r=sp_StrIntHash_new();r->default_v=h->default_v;for(mrb_int i=0;i<h->len;i++)sp_StrIntHash_set(r,h->order[i],sp_StrIntHash_get(h,h->order[i]));return r;}
1128
+ static mrb_bool sp_StrIntHash_eq(sp_StrIntHash*a,sp_StrIntHash*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++){const char*k=a->order[i];if(!sp_StrIntHash_has_key(b,k))return FALSE;if(sp_StrIntHash_get(a,k)!=sp_StrIntHash_get(b,k))return FALSE;}return TRUE;}
1129
+
1130
+ /* GC.stat snapshot: String=>Integer hash over the collector globals.
1131
+ full_runs derives from sp_gc_cycle / SP_GC_FULL_INTERVAL (the major
1132
+ collection cadence). */
1133
+ static sp_StrIntHash*sp_gc_stat(void){sp_StrIntHash*h=sp_StrIntHash_new();sp_StrIntHash_set(h,"bytes",(mrb_int)sp_gc_bytes);sp_StrIntHash_set(h,"old_bytes",(mrb_int)sp_gc_old_bytes);sp_StrIntHash_set(h,"threshold",(mrb_int)sp_gc_threshold);sp_StrIntHash_set(h,"cycle",(mrb_int)sp_gc_cycle);sp_StrIntHash_set(h,"full_runs",(mrb_int)(sp_gc_cycle/SP_GC_FULL_INTERVAL));return h;}
417
1134
 
418
- typedef struct{const char**keys;const char**vals;const char**order;mrb_int len;mrb_int cap;mrb_int mask;}sp_StrStrHash;
419
1135
  static void sp_StrStrHash_fin(void*p){sp_StrStrHash*h=(sp_StrStrHash*)p;free(h->keys);free(h->vals);free(h->order);}
420
- static void sp_StrStrHash_scan(void*p){sp_StrStrHash*h=(sp_StrStrHash*)p;for(mrb_int i=0;i<h->cap;i++){if(h->keys[i]){sp_mark_string(h->keys[i]);sp_mark_string(h->vals[i]);}}}
421
- static sp_StrStrHash*sp_StrStrHash_new(void){sp_StrStrHash*h=(sp_StrStrHash*)sp_gc_alloc(sizeof(sp_StrStrHash),sp_StrStrHash_fin,sp_StrStrHash_scan);h->cap=16;h->mask=15;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(const char**)calloc(h->cap,sizeof(const char*));h->order=(const char**)malloc(sizeof(const char*)*h->cap);h->len=0;return h;}
1136
+ static void sp_StrStrHash_scan(void*p){sp_StrStrHash*h=(sp_StrStrHash*)p;for(mrb_int i=0;i<h->cap;i++){if(h->keys[i]){sp_mark_string(h->keys[i]);sp_mark_string(h->vals[i]);}}if(h->default_v)sp_mark_string(h->default_v);}
1137
+ static sp_StrStrHash*sp_StrStrHash_new(void){sp_StrStrHash*h=(sp_StrStrHash*)sp_gc_alloc(sizeof(sp_StrStrHash),sp_StrStrHash_fin,sp_StrStrHash_scan);h->cap=16;h->mask=15;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(const char**)calloc(h->cap,sizeof(const char*));h->order=(const char**)malloc(sizeof(const char*)*h->cap);h->len=0;h->default_v=NULL;return h;}
1138
+ static sp_StrStrHash*sp_StrStrHash_new_with_default(const char*d){sp_StrStrHash*h=sp_StrStrHash_new();h->default_v=d;return h;}
422
1139
  static void sp_StrStrHash_grow(sp_StrStrHash*h){mrb_int oc=h->cap;const char**ok=h->keys;const char**ov=h->vals;h->cap*=2;h->mask=h->cap-1;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(const char**)calloc(h->cap,sizeof(const char*));h->order=(const char**)realloc(h->order,sizeof(const char*)*h->cap);h->len=0;for(mrb_int i=0;i<oc;i++){if(ok[i]){mrb_int idx=(mrb_int)(sp_str_hash(ok[i])&h->mask);while(h->keys[idx])idx=(idx+1)&h->mask;h->keys[idx]=ok[i];h->vals[idx]=ov[i];h->len++;}}free(ok);free(ov);}
423
- static const char*sp_StrStrHash_get(sp_StrStrHash*h,const char*k){mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return h->vals[idx];idx=(idx+1)&h->mask;}return"";}
1140
+ static const char*sp_StrStrHash_get(sp_StrStrHash*h,const char*k){if(!h)return NULL;mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return h->vals[idx];idx=(idx+1)&h->mask;}return h->default_v;}
424
1141
  static void sp_StrStrHash_set(sp_StrStrHash*h,const char*k,const char*v){if(h->len*2>=h->cap)sp_StrStrHash_grow(h);mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0){h->vals[idx]=v;return;}idx=(idx+1)&h->mask;}h->keys[idx]=k;h->vals[idx]=v;h->order[h->len]=k;h->len++;}
425
1142
  static mrb_bool sp_StrStrHash_has_key(sp_StrStrHash*h,const char*k){mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return TRUE;idx=(idx+1)&h->mask;}return FALSE;}
1143
+ static mrb_bool sp_StrStrHash_has_value(sp_StrStrHash*h,const char*v){if(!h||!v)return FALSE;for(mrb_int i=0;i<h->len;i++){const char*x=sp_StrStrHash_get(h,h->order[i]);if(x&&strcmp(x,v)==0)return TRUE;}return FALSE;}
426
1144
  static mrb_int sp_StrStrHash_length(sp_StrStrHash*h){return h->len;}
427
1145
  static void sp_StrStrHash_delete(sp_StrStrHash*h,const char*k){mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0){h->keys[idx]=NULL;h->vals[idx]=NULL;h->len--;mrb_int j=(idx+1)&h->mask;while(h->keys[j]){mrb_int nj=(mrb_int)(sp_str_hash(h->keys[j])&h->mask);if((j>idx&&(nj<=idx||nj>j))||(j<idx&&nj<=idx&&nj>j)){h->keys[idx]=h->keys[j];h->vals[idx]=h->vals[j];h->keys[j]=NULL;h->vals[j]=NULL;idx=j;}j=(j+1)&h->mask;}{mrb_int oi=0;while(oi<=h->len){if(strcmp(h->order[oi],k)==0){while(oi<h->len){h->order[oi]=h->order[oi+1];oi++;}break;}oi++;}}return;}idx=(idx+1)&h->mask;}}
428
1146
  static sp_StrArray*sp_StrStrHash_keys(sp_StrStrHash*h){sp_StrArray*a=sp_StrArray_new();for(mrb_int i=0;i<h->len;i++)sp_StrArray_push(a,h->order[i]);return a;}
429
1147
  static sp_StrArray*sp_StrStrHash_values(sp_StrStrHash*h){sp_StrArray*a=sp_StrArray_new();for(mrb_int i=0;i<h->len;i++)sp_StrArray_push(a,sp_StrStrHash_get(h,h->order[i]));return a;}
430
1148
  static sp_StrStrHash*sp_StrStrHash_invert(sp_StrStrHash*h){sp_StrStrHash*r=sp_StrStrHash_new();for(mrb_int i=0;i<h->len;i++){const char*k=h->order[i];sp_StrStrHash_set(r,sp_StrStrHash_get(h,k),k);}return r;}
431
1149
  static void sp_StrStrHash_update(sp_StrStrHash*a,sp_StrStrHash*b){for(mrb_int i=0;i<b->len;i++)sp_StrStrHash_set(a,b->order[i],sp_StrStrHash_get(b,b->order[i]));}
1150
+ static sp_StrStrHash*sp_StrStrHash_dup(sp_StrStrHash*h){sp_StrStrHash*r=sp_StrStrHash_new();r->default_v=h->default_v;for(mrb_int i=0;i<h->len;i++)sp_StrStrHash_set(r,h->order[i],sp_StrStrHash_get(h,h->order[i]));return r;}
1151
+ static mrb_bool sp_StrStrHash_eq(sp_StrStrHash*a,sp_StrStrHash*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++){const char*k=a->order[i];if(!sp_StrStrHash_has_key(b,k))return FALSE;if(!sp_str_eq(sp_StrStrHash_get(a,k),sp_StrStrHash_get(b,k)))return FALSE;}return TRUE;}
432
1152
 
433
- typedef struct{mrb_int*keys;const char**vals;mrb_int*order;mrb_bool*used;mrb_int len;mrb_int cap;mrb_int mask;}sp_IntStrHash;
434
1153
  static void sp_IntStrHash_fin(void*p){sp_IntStrHash*h=(sp_IntStrHash*)p;free(h->keys);free(h->vals);free(h->order);free(h->used);}
435
- static void sp_IntStrHash_scan(void*p){sp_IntStrHash*h=(sp_IntStrHash*)p;for(mrb_int i=0;i<h->cap;i++)if(h->used[i])sp_mark_string(h->vals[i]);}
436
- static sp_IntStrHash*sp_IntStrHash_new(void){sp_IntStrHash*h=(sp_IntStrHash*)sp_gc_alloc(sizeof(sp_IntStrHash),sp_IntStrHash_fin,sp_IntStrHash_scan);h->cap=16;h->mask=15;h->keys=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->vals=(const char**)calloc(h->cap,sizeof(const char*));h->order=(mrb_int*)malloc(sizeof(mrb_int)*h->cap);h->used=(mrb_bool*)calloc(h->cap,sizeof(mrb_bool));h->len=0;return h;}
1154
+ static void sp_IntStrHash_scan(void*p){sp_IntStrHash*h=(sp_IntStrHash*)p;for(mrb_int i=0;i<h->cap;i++)if(h->used[i])sp_mark_string(h->vals[i]);if(h->default_v)sp_mark_string(h->default_v);}
1155
+ static sp_IntStrHash*sp_IntStrHash_new(void){sp_IntStrHash*h=(sp_IntStrHash*)sp_gc_alloc(sizeof(sp_IntStrHash),sp_IntStrHash_fin,sp_IntStrHash_scan);h->cap=16;h->mask=15;h->keys=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->vals=(const char**)calloc(h->cap,sizeof(const char*));h->order=(mrb_int*)malloc(sizeof(mrb_int)*h->cap);h->used=(mrb_bool*)calloc(h->cap,sizeof(mrb_bool));h->len=0;h->default_v=NULL;return h;}
1156
+ static sp_IntStrHash*sp_IntStrHash_new_with_default(const char*d){sp_IntStrHash*h=sp_IntStrHash_new();h->default_v=d;return h;}
437
1157
  static inline mrb_int _sp_istr_idx(mrb_int mask,mrb_int k){return(mrb_int)(((uint64_t)(unsigned long long)k*11400714819323198485ULL)&(uint64_t)mask);}
438
1158
  static void sp_IntStrHash_grow(sp_IntStrHash*h){mrb_int oc=h->cap,ol=h->len;mrb_int*ok=h->keys;const char**ov=h->vals;mrb_bool*ou=h->used;mrb_int*oo=h->order;h->cap*=2;h->mask=h->cap-1;h->keys=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->vals=(const char**)calloc(h->cap,sizeof(const char*));h->order=(mrb_int*)malloc(sizeof(mrb_int)*h->cap);h->used=(mrb_bool*)calloc(h->cap,sizeof(mrb_bool));h->len=ol;for(mrb_int i=0;i<oc;i++){if(!ou[i])continue;mrb_int k=ok[i];const char*v=ov[i];mrb_int di=_sp_istr_idx(h->mask,k);while(h->used[di])di=(di+1)&h->mask;h->used[di]=TRUE;h->keys[di]=k;h->vals[di]=v;}for(mrb_int i=0;i<ol;i++)h->order[i]=oo[i];free(ok);free(ov);free(ou);free(oo);}
439
1159
  static void sp_IntStrHash_set(sp_IntStrHash*h,mrb_int k,const char*v){if(h->len*2>=h->cap)sp_IntStrHash_grow(h);mrb_int idx=_sp_istr_idx(h->mask,k);while(h->used[idx]){if(h->keys[idx]==k){h->vals[idx]=v;return;}idx=(idx+1)&h->mask;}h->used[idx]=TRUE;h->keys[idx]=k;h->vals[idx]=v;h->order[h->len++]=k;}
440
- static const char*sp_IntStrHash_get(sp_IntStrHash*h,mrb_int k){mrb_int idx=_sp_istr_idx(h->mask,k);while(h->used[idx]){if(h->keys[idx]==k)return h->vals[idx];idx=(idx+1)&h->mask;}return"";}
1160
+ static const char*sp_IntStrHash_get(sp_IntStrHash*h,mrb_int k){if(!h)return NULL;mrb_int idx=_sp_istr_idx(h->mask,k);while(h->used[idx]){if(h->keys[idx]==k)return h->vals[idx];idx=(idx+1)&h->mask;}return h->default_v;}
441
1161
  static mrb_bool sp_IntStrHash_has_key(sp_IntStrHash*h,mrb_int k){mrb_int idx=_sp_istr_idx(h->mask,k);while(h->used[idx]){if(h->keys[idx]==k)return TRUE;idx=(idx+1)&h->mask;}return FALSE;}
1162
+ static mrb_bool sp_IntStrHash_has_value(sp_IntStrHash*h,const char*v){if(!h||!v)return FALSE;for(mrb_int i=0;i<h->len;i++){const char*x=sp_IntStrHash_get(h,h->order[i]);if(x&&strcmp(x,v)==0)return TRUE;}return FALSE;}
442
1163
  static mrb_int sp_IntStrHash_length(sp_IntStrHash*h){return h->len;}
443
1164
  static sp_IntArray*sp_IntStrHash_keys(sp_IntStrHash*h){sp_IntArray*a=sp_IntArray_new();for(mrb_int i=0;i<h->len;i++)sp_IntArray_push(a,h->order[i]);return a;}
444
1165
  static sp_StrArray*sp_IntStrHash_values(sp_IntStrHash*h){sp_StrArray*a=sp_StrArray_new();for(mrb_int i=0;i<h->len;i++)sp_StrArray_push(a,sp_IntStrHash_get(h,h->order[i]));return a;}
1166
+ static sp_IntStrHash*sp_IntStrHash_dup(sp_IntStrHash*h){sp_IntStrHash*r=sp_IntStrHash_new();r->default_v=h->default_v;for(mrb_int i=0;i<h->len;i++)sp_IntStrHash_set(r,h->order[i],sp_IntStrHash_get(h,h->order[i]));return r;}
1167
+ static mrb_bool sp_IntStrHash_eq(sp_IntStrHash*a,sp_IntStrHash*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++){mrb_int k=a->order[i];if(!sp_IntStrHash_has_key(b,k))return FALSE;if(!sp_str_eq(sp_IntStrHash_get(a,k),sp_IntStrHash_get(b,k)))return FALSE;}return TRUE;}
1168
+
1169
+ /* Int → Int typed hash. Mirrors sp_IntStrHash's open-addressing
1170
+ layout (used[] bitmap so 0/-1 keys are distinguishable from
1171
+ empty), with int-valued slots. Used by Array#tally on int
1172
+ arrays — see #865. */
1173
+ static void sp_IntIntHash_fin(void*p){sp_IntIntHash*h=(sp_IntIntHash*)p;free(h->keys);free(h->vals);free(h->order);free(h->used);}
1174
+ /* default_v is SP_INT_NIL for a hash with no explicit default, so a
1175
+ missing-key `[]` read surfaces Ruby nil (#801). Hash.new(N) sets it via
1176
+ _new_with_default. */
1177
+ static sp_IntIntHash*sp_IntIntHash_new(void){sp_IntIntHash*h=(sp_IntIntHash*)sp_gc_alloc(sizeof(sp_IntIntHash),sp_IntIntHash_fin,NULL);h->cap=16;h->mask=15;h->keys=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->vals=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->order=(mrb_int*)malloc(sizeof(mrb_int)*h->cap);h->used=(mrb_bool*)calloc(h->cap,sizeof(mrb_bool));h->len=0;h->default_v=SP_INT_NIL;return h;}
1178
+ static sp_IntIntHash*sp_IntIntHash_new_with_default(mrb_int d){sp_IntIntHash*h=sp_IntIntHash_new();h->default_v=d;return h;}
1179
+ static void sp_IntIntHash_grow(sp_IntIntHash*h){mrb_int oc=h->cap,ol=h->len;mrb_int*ok=h->keys;mrb_int*ov=h->vals;mrb_bool*ou=h->used;mrb_int*oo=h->order;h->cap*=2;h->mask=h->cap-1;h->keys=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->vals=(mrb_int*)calloc(h->cap,sizeof(mrb_int));h->order=(mrb_int*)malloc(sizeof(mrb_int)*h->cap);h->used=(mrb_bool*)calloc(h->cap,sizeof(mrb_bool));h->len=ol;for(mrb_int i=0;i<oc;i++){if(!ou[i])continue;mrb_int k=ok[i];mrb_int v=ov[i];mrb_int di=_sp_istr_idx(h->mask,k);while(h->used[di])di=(di+1)&h->mask;h->used[di]=TRUE;h->keys[di]=k;h->vals[di]=v;}for(mrb_int i=0;i<ol;i++)h->order[i]=oo[i];free(ok);free(ov);free(ou);free(oo);}
1180
+ static void sp_IntIntHash_set(sp_IntIntHash*h,mrb_int k,mrb_int v){if(h->len*2>=h->cap)sp_IntIntHash_grow(h);mrb_int idx=_sp_istr_idx(h->mask,k);while(h->used[idx]){if(h->keys[idx]==k){h->vals[idx]=v;return;}idx=(idx+1)&h->mask;}h->used[idx]=TRUE;h->keys[idx]=k;h->vals[idx]=v;h->order[h->len++]=k;}
1181
+ static mrb_int sp_IntIntHash_get(sp_IntIntHash*h,mrb_int k){if(!h)return 0;mrb_int idx=_sp_istr_idx(h->mask,k);while(h->used[idx]){if(h->keys[idx]==k)return h->vals[idx];idx=(idx+1)&h->mask;}return h->default_v;}
1182
+ /* Issue #801: maybe-missing public `[]` read. Returns default_v on a miss
1183
+ (SP_INT_NIL for a no-default hash = Ruby nil; the explicit default for
1184
+ Hash.new(N)). Proven-present reads keep using _get. */
1185
+ static mrb_int sp_IntIntHash_get_opt(sp_IntIntHash*h,mrb_int k){if(!h)return SP_INT_NIL;mrb_int idx=_sp_istr_idx(h->mask,k);while(h->used[idx]){if(h->keys[idx]==k)return h->vals[idx];idx=(idx+1)&h->mask;}return h->default_v;}
1186
+ static mrb_bool sp_IntIntHash_has_key(sp_IntIntHash*h,mrb_int k){mrb_int idx=_sp_istr_idx(h->mask,k);while(h->used[idx]){if(h->keys[idx]==k)return TRUE;idx=(idx+1)&h->mask;}return FALSE;}
1187
+ static mrb_int sp_IntIntHash_length(sp_IntIntHash*h){return h?h->len:0;}
1188
+ static sp_IntIntHash*sp_IntIntHash_dup(sp_IntIntHash*h){sp_IntIntHash*r=sp_IntIntHash_new();r->default_v=h->default_v;for(mrb_int i=0;i<h->len;i++)sp_IntIntHash_set(r,h->order[i],sp_IntIntHash_get(h,h->order[i]));return r;}
1189
+ /* Array#tally on int_array. CRuby returns an Integer-keyed Hash
1190
+ mapping each distinct element to its occurrence count. */
1191
+ static sp_IntIntHash*sp_IntArray_tally_int(sp_IntArray*a){sp_IntIntHash*h=sp_IntIntHash_new();if(!a)return h;for(mrb_int i=0;i<a->len;i++){mrb_int k=a->data[a->start+i];mrb_int c=sp_IntIntHash_has_key(h,k)?sp_IntIntHash_get(h,k):0;sp_IntIntHash_set(h,k,c+1);}return h;}
445
1192
 
446
1193
  /* Reuse an existing StrArray for split, avoiding GC alloc.
447
1194
  Clears a->len and refills. Substring strings are still malloc'd. */
1195
+ static inline void sp_str_split_push(sp_StrArray*a,const char*p,size_t n){
1196
+ char*r=sp_str_alloc_raw(n+1);
1197
+ memcpy(r,p,n);
1198
+ r[n]=0;
1199
+ sp_StrArray_push(a,r);
1200
+ }
448
1201
  static void sp_str_split_into(sp_StrArray*a,const char*s,const char*sep){
449
- a->len=0;if(*s==0)return;size_t sl=strlen(sep);
450
- if(sl==0){const char*p=s;while(*p){int cn=sp_utf8_advance(p);char*c=sp_str_alloc_raw(cn+1);memcpy(c,p,cn);c[cn]=0;sp_StrArray_push(a,c);p+=cn;}return;}
451
- const char*p=s;while(1){const char*f=strstr(p,sep);if(!f){char*r=sp_str_alloc_raw(strlen(p)+1);strcpy(r,p);sp_StrArray_push(a,r);break;}
452
- size_t n=f-p;char*r=sp_str_alloc_raw(n+1);memcpy(r,p,n);r[n]=0;sp_StrArray_push(a,r);p=f+sl;}}
1202
+ SP_GC_ROOT(a);
1203
+ SP_GC_ROOT(s);
1204
+ SP_GC_ROOT(sep);
1205
+ a->len=0;
1206
+ if(*s==0)return;
1207
+ size_t sl=strlen(sep);
1208
+ if(sl==0){
1209
+ const char*p=s;
1210
+ while(*p){
1211
+ int cn=sp_utf8_advance(p);
1212
+ sp_str_split_push(a,p,(size_t)cn);
1213
+ p+=cn;
1214
+ }
1215
+ return;
1216
+ }
1217
+ const char*p=s;
1218
+ while(1){
1219
+ const char*f=strstr(p,sep);
1220
+ if(!f){
1221
+ sp_str_split_push(a,p,strlen(p));
1222
+ break;
1223
+ }
1224
+ size_t n=f-p;
1225
+ sp_str_split_push(a,p,n);
1226
+ p=f+sl;
1227
+ }
1228
+ }
453
1229
  /* Extract the n-th field (0-based) from s split by sep, without
454
1230
  allocating a full StrArray. Returns a newly allocated string.
455
1231
  If the field doesn't exist, returns "". */
456
1232
  static const char*sp_str_field(const char*s,const char*sep,mrb_int n){
457
1233
  size_t sl=strlen(sep);mrb_int cur=0;const char*p=s;
458
- if(sl==0)return("");
459
- while(cur<n){const char*f=strstr(p,sep);if(!f)return("");p=f+sl;cur++;}
1234
+ if(sl==0)return sp_str_empty;
1235
+ while(cur<n){const char*f=strstr(p,sep);if(!f)return sp_str_empty;p=f+sl;cur++;}
460
1236
  const char*end=strstr(p,sep);size_t len=end?((size_t)(end-p)):strlen(p);
461
1237
  char*r=sp_str_alloc_raw(len+1);memcpy(r,p,len);r[len]=0;return r;}
462
1238
  /* Count fields in s split by sep (without allocating). */
463
1239
  static mrb_int sp_str_field_count(const char*s,const char*sep){
464
1240
  if(*s==0)return 0;size_t sl=strlen(sep);if(sl==0)return(mrb_int)strlen(s);
465
1241
  mrb_int c=1;const char*p=s;while((p=strstr(p,sep))!=NULL){c++;p+=sl;}return c;}
466
- static const char*sp_str_concat(const char*a,const char*b){size_t la=strlen(a),lb=strlen(b);char*r=sp_str_alloc(la+lb);memcpy(r,a,la);memcpy(r+la,b,lb);return r;}
467
- static const char*sp_str_concat3(const char*a,const char*b,const char*c){size_t la=strlen(a),lb=strlen(b),lc=strlen(c);char*r=sp_str_alloc(la+lb+lc);memcpy(r,a,la);memcpy(r+la,b,lb);memcpy(r+la+lb,c,lc);return r;}
468
- static const char*sp_str_concat4(const char*a,const char*b,const char*c,const char*d){size_t la=strlen(a),lb=strlen(b),lc=strlen(c),ld=strlen(d);char*r=sp_str_alloc(la+lb+lc+ld);memcpy(r,a,la);memcpy(r+la,b,lb);memcpy(r+la+lb,c,lc);memcpy(r+la+lb+lc,d,ld);return r;}
1242
+ static const char*sp_str_concat(const char*a,const char*b){if(!a)a=sp_str_empty;if(!b)b=sp_str_empty;size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b);char*r=sp_str_alloc(la+lb);memcpy(r,a,la);memcpy(r+la,b,lb);return r;}
1243
+ /* Issue #760: NULL src to memcpy is UB. Treat NULL as empty string. */
1244
+ static const char*sp_str_concat3(const char*a,const char*b,const char*c){if(!a)a="";if(!b)b="";if(!c)c="";size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b),lc=sp_str_byte_len(c);char*r=sp_str_alloc(la+lb+lc);memcpy(r,a,la);memcpy(r+la,b,lb);memcpy(r+la+lb,c,lc);return r;}
1245
+ static const char*sp_str_concat4(const char*a,const char*b,const char*c,const char*d){if(!a)a="";if(!b)b="";if(!c)c="";if(!d)d="";size_t la=sp_str_byte_len(a),lb=sp_str_byte_len(b),lc=sp_str_byte_len(c),ld=sp_str_byte_len(d);char*r=sp_str_alloc(la+lb+lc+ld);memcpy(r,a,la);memcpy(r+la,b,lb);memcpy(r+la+lb,c,lc);memcpy(r+la+lb+lc,d,ld);return r;}
469
1246
  /* Concatenate N strings into a single GC-managed buffer. */
470
- static const char*sp_str_concat_arr(const char *const *parts,int n){size_t total=0;for(int i=0;i<n;i++)total+=strlen(parts[i]);char*r=sp_str_alloc(total);char*p=r;for(int i=0;i<n;i++){size_t sl=strlen(parts[i]);memcpy(p,parts[i],sl);p+=sl;}return r;}
471
- static const char*sp_int_to_s(mrb_int n){char*b=sp_str_alloc_raw(32);snprintf(b,32,"%lld",(long long)n);return b;}
472
- static const char*sp_int_to_s_base(mrb_int n,mrb_int base){if(base<2||base>36)base=10;char*b=sp_str_alloc_raw(72);char tmp[72];int i=0;int neg=0;uint64_t u;if(n<0){neg=1;u=(uint64_t)(-(n+1))+1;}else{u=(uint64_t)n;}if(u==0){tmp[i++]='0';}else{while(u>0){mrb_int d=u%base;tmp[i++]=d<10?'0'+d:'a'+d-10;u/=base;}}int j=0;if(neg)b[j++]='-';while(i>0)b[j++]=tmp[--i];b[j]=0;return b;}
1247
+ /* Issue #760: NULL entries treated as empty strings. */
1248
+ static const char*sp_str_concat_arr(const char *const *parts,int n){size_t total=0;for(int i=0;i<n;i++)total+=sp_str_byte_len(parts[i]?parts[i]:"");char*r=sp_str_alloc(total);char*p=r;for(int i=0;i<n;i++){const char*s=parts[i]?parts[i]:"";size_t sl=sp_str_byte_len(s);memcpy(p,s,sl);p+=sl;}return r;}
1249
+ static const char*sp_int_to_s(mrb_int n){char*b=sp_str_alloc_raw(32);int len=snprintf(b,32,"%lld",(long long)n);if(len<0)len=0;sp_str_set_len(b,(size_t)len);return b;}
1250
+ static const char*sp_int_to_s_base(mrb_int n,mrb_int base){if(base<2||base>36)base=10;char*b=sp_str_alloc_raw(72);char tmp[72];int i=0;int neg=0;uint64_t u;if(n<0){neg=1;u=(uint64_t)(-(n+1))+1;}else{u=(uint64_t)n;}if(u==0){tmp[i++]='0';}else{while(u>0){mrb_int d=u%base;tmp[i++]=d<10?'0'+d:'a'+d-10;u/=base;}}int j=0;if(neg)b[j++]='-';while(i>0)b[j++]=tmp[--i];b[j]=0;sp_str_set_len(b,(size_t)j);return b;}
473
1251
  /* Float#to_s (Ruby semantics): produce the shortest decimal that
474
1252
  round-trips back to the same double, formatted per CRuby — fixed
475
1253
  point when the decimal exponent is in [-4, 15], scientific
@@ -500,72 +1278,502 @@ static const char*sp_float_to_s(mrb_float f){
500
1278
  if(e>=0)out[o++]='+';else{out[o++]='-';e=-e;}
501
1279
  if(e<10){out[o++]='0';out[o++]=(char)('0'+e);}else o+=snprintf(out+o,16,"%d",e);
502
1280
  }
503
- out[o]=0;return out;
1281
+ out[o]=0;sp_str_set_len(out,(size_t)o);return out;
504
1282
  }
505
1283
  #define sp_float_inspect sp_float_to_s
506
1284
  /* String#inspect: wrap in double quotes and escape \, ", \n, \t, \r,
507
1285
  plus any non-printable byte as \xNN. Output is always ASCII-safe. */
508
- static const char*sp_str_inspect(const char*s){if(!s){char*r=sp_str_alloc_raw(4);r[0]='n';r[1]='i';r[2]='l';r[3]=0;return r;}size_t sl=strlen(s);size_t cap=sl*4+3;char*r=sp_str_alloc_raw(cap);size_t o=0;r[o++]='"';for(size_t i=0;i<sl;i++){unsigned char c=(unsigned char)s[i];if(c=='\\'||c=='"'){r[o++]='\\';r[o++]=c;}else if(c=='\n'){r[o++]='\\';r[o++]='n';}else if(c=='\t'){r[o++]='\\';r[o++]='t';}else if(c=='\r'){r[o++]='\\';r[o++]='r';}else if(c<0x20||c==0x7f){snprintf(r+o,5,"\\x%02X",c);o+=4;}else{r[o++]=(char)c;}}r[o++]='"';r[o]=0;return r;}
509
- static const char*sp_str_upcase(const char*s){size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<=l;i++)r[i]=toupper((unsigned char)s[i]);return r;}
510
- static const char*sp_str_downcase(const char*s){size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<=l;i++)r[i]=tolower((unsigned char)s[i]);return r;}
511
- static const char*sp_str_swapcase(const char*s){size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<=l;i++){unsigned char c=(unsigned char)s[i];if(isupper(c))r[i]=tolower(c);else if(islower(c))r[i]=toupper(c);else r[i]=s[i];}return r;}
512
- static const char*sp_str_delete_prefix(const char*s,const char*p){size_t sl=strlen(s),pl=strlen(p);if(pl<=sl&&memcmp(s,p,pl)==0){char*r=sp_str_alloc_raw(sl-pl+1);memcpy(r,s+pl,sl-pl+1);return r;}char*r=sp_str_alloc_raw(sl+1);memcpy(r,s,sl+1);return r;}
513
- static const char*sp_str_substr(const char*s,mrb_int start,mrb_int len){if(len<=0){char*r=sp_str_alloc_raw(1);r[0]=0;return r;}char*r=sp_str_alloc_raw(len+1);memcpy(r,s+start,len);r[len]=0;return r;}
514
- static const char*sp_str_delete_suffix(const char*s,const char*p){size_t sl=strlen(s),pl=strlen(p);if(pl<=sl&&memcmp(s+sl-pl,p,pl)==0){char*r=sp_str_alloc_raw(sl-pl+1);memcpy(r,s,sl-pl);r[sl-pl]=0;return r;}char*r=sp_str_alloc_raw(sl+1);memcpy(r,s,sl+1);return r;}
515
- static const char*sp_str_succ(const char*s){size_t l=strlen(s);if(l==0){char*r=sp_str_alloc_raw(1);r[0]=0;return r;}/* Find start of last codepoint */size_t lc=l-1;while(lc>0&&((unsigned char)s[lc]&0xC0)==0x80)lc--;if((unsigned char)s[lc]>=0x80){/* Multibyte tail: increment its codepoint */uint32_t cp;sp_utf8_decode(s+lc,&cp);cp++;char enc[4];int el=sp_utf8_encode(cp,enc);char*r=sp_str_alloc_raw(lc+el+1);memcpy(r,s,lc);memcpy(r+lc,enc,el);r[lc+el]=0;return r;}/* ASCII tail: existing carry logic */char*r=sp_str_alloc_raw(l+2);memcpy(r,s,l+1);mrb_int i=(mrb_int)l-1;while(i>=0){unsigned char c=(unsigned char)r[i];if(c>='0'&&c<'9'){r[i]=c+1;return r;}if(c=='9'){r[i]='0';i--;continue;}if(c>='a'&&c<'z'){r[i]=c+1;return r;}if(c=='z'){r[i]='a';i--;continue;}if(c>='A'&&c<'Z'){r[i]=c+1;return r;}if(c=='Z'){r[i]='A';i--;continue;}r[i]=c+1;return r;}memmove(r+1,r,l+1);if(r[1]=='0')r[0]='1';else if(r[1]=='a')r[0]='a';else if(r[1]=='A')r[0]='A';else r[0]=r[1];return r;}
1286
+ static const char*sp_str_inspect(const char*s){if(!s){char*r=sp_str_alloc_raw(4);r[0]='n';r[1]='i';r[2]='l';r[3]=0;return r;}size_t sl=sp_str_byte_len(s);size_t cap=sl*4+3;char*r=sp_str_alloc_raw(cap);size_t o=0;r[o++]='"';for(size_t i=0;i<sl;i++){unsigned char c=(unsigned char)s[i];if(c=='\\'||c=='"'){r[o++]='\\';r[o++]=c;}else if(c=='\n'){r[o++]='\\';r[o++]='n';}else if(c=='\t'){r[o++]='\\';r[o++]='t';}else if(c=='\r'){r[o++]='\\';r[o++]='r';}else if(c<0x20||c==0x7f){snprintf(r+o,5,"\\x%02X",c);o+=4;}else{r[o++]=(char)c;}}r[o++]='"';r[o]=0;sp_str_set_len(r,o);return r;}
1287
+ /* Issue #791: loop to `i < l` and write the NUL terminator explicitly.
1288
+ The original `<= l` form worked because sp_str_alloc_raw(l+1) makes
1289
+ index l valid, but it's brittle if allocation changes. Issue #797
1290
+ adds the NULL guard. */
1291
+ static const char*sp_str_upcase(const char*s){if(!s)return sp_str_empty;size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<l;i++)r[i]=toupper((unsigned char)s[i]);r[l]=0;return r;}
1292
+ static const char*sp_str_downcase(const char*s){if(!s)return sp_str_empty;size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<l;i++)r[i]=tolower((unsigned char)s[i]);r[l]=0;return r;}
1293
+ static const char*sp_str_swapcase(const char*s){if(!s)return sp_str_empty;size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<l;i++){unsigned char c=(unsigned char)s[i];if(isupper(c))r[i]=tolower(c);else if(islower(c))r[i]=toupper(c);else r[i]=s[i];}r[l]=0;return r;}
1294
+ /* String#undump: reverse of String#dump. The argument must be wrapped in
1295
+ double quotes; the escapes dump can emit (\n \t \r \f \v \a \b \e \s \0
1296
+ \" \\ \# \xHH \uHHHH \u{...}) are decoded back to bytes. The decoded
1297
+ string is never longer than the dumped form, so one buffer suffices. */
1298
+ static int _sp_hexval(unsigned char d){return (d<='9')?(d-'0'):(tolower(d)-'a'+10);}
1299
+ static const char*sp_str_undump(const char*s){
1300
+ if(!s)return sp_str_empty;
1301
+ size_t n=strlen(s);
1302
+ if(n<2||s[0]!='"'||s[n-1]!='"'){sp_raise_cls("RuntimeError","invalid dumped string");return sp_str_empty;}
1303
+ const char*p=s+1;const char*pe=s+n-1;
1304
+ char*out=sp_str_alloc_raw(n+1);size_t oi=0;
1305
+ while(p<pe){
1306
+ if(*p!='\\'){out[oi++]=*p++;continue;}
1307
+ p++;if(p>=pe)break;
1308
+ char c=*p++;
1309
+ if(c=='n')out[oi++]='\n';else if(c=='t')out[oi++]='\t';else if(c=='r')out[oi++]='\r';
1310
+ else if(c=='f')out[oi++]='\f';else if(c=='v')out[oi++]='\v';else if(c=='a')out[oi++]='\a';
1311
+ else if(c=='b')out[oi++]='\b';else if(c=='e')out[oi++]='\033';else if(c=='s')out[oi++]=' ';
1312
+ else if(c=='0')out[oi++]='\0';else if(c=='\\')out[oi++]='\\';else if(c=='"')out[oi++]='"';
1313
+ else if(c=='#')out[oi++]='#';
1314
+ else if(c=='x'){int v=0,k=0;while(k<2&&p<pe&&isxdigit((unsigned char)*p)){v=v*16+_sp_hexval((unsigned char)*p);p++;k++;}out[oi++]=(char)v;}
1315
+ else if(c=='u'){
1316
+ if(p<pe&&*p=='{'){p++;while(p<pe&&*p!='}'){while(p<pe&&*p==' ')p++;uint32_t cp=0;int k=0;while(k<8&&p<pe&&isxdigit((unsigned char)*p)){cp=cp*16+(uint32_t)_sp_hexval((unsigned char)*p);p++;k++;}char enc[4];int el=sp_utf8_encode(cp,enc);for(int j=0;j<el;j++)out[oi++]=enc[j];while(p<pe&&*p==' ')p++;}if(p<pe&&*p=='}')p++;}
1317
+ else{uint32_t cp=0;int k=0;while(k<4&&p<pe&&isxdigit((unsigned char)*p)){cp=cp*16+(uint32_t)_sp_hexval((unsigned char)*p);p++;k++;}char enc[4];int el=sp_utf8_encode(cp,enc);for(int j=0;j<el;j++)out[oi++]=enc[j];}
1318
+ }
1319
+ else out[oi++]=c;
1320
+ }
1321
+ out[oi]=0;return out;
1322
+ }
1323
+ /* Issue #797: NULL guards on receiver + needle for the chunk of
1324
+ string functions that read directly into a non-checked strlen. */
1325
+ static const char*sp_str_delete_prefix(const char*s,const char*p){if(!s)return sp_str_empty;if(!p)return s;size_t sl=strlen(s),pl=strlen(p);if(pl<=sl&&memcmp(s,p,pl)==0){char*r=sp_str_alloc_raw(sl-pl+1);memcpy(r,s+pl,sl-pl+1);return r;}char*r=sp_str_alloc_raw(sl+1);memcpy(r,s,sl+1);return r;}
1326
+ /* Issue #758: NULL guard + bound the start so a negative result from
1327
+ sp_str_index doesn't underflow the source pointer. */
1328
+ static const char*sp_str_substr(const char*s,mrb_int start,mrb_int len){if(!s||len<=0){char*r=sp_str_alloc_raw(1);r[0]=0;return r;}if(start<0)start=0;char*r=sp_str_alloc_raw(len+1);memcpy(r,s+start,len);r[len]=0;return r;}
1329
+ static const char*sp_str_delete_suffix(const char*s,const char*p){if(!s)return sp_str_empty;if(!p)return s;size_t sl=strlen(s),pl=strlen(p);if(pl<=sl&&memcmp(s+sl-pl,p,pl)==0){char*r=sp_str_alloc_raw(sl-pl+1);memcpy(r,s,sl-pl);r[sl-pl]=0;return r;}char*r=sp_str_alloc_raw(sl+1);memcpy(r,s,sl+1);return r;}
1330
+ static const char*sp_str_succ(const char*s){if(!s)return sp_str_empty;size_t l=strlen(s);if(l==0){char*r=sp_str_alloc_raw(1);r[0]=0;return r;}/* Find start of last codepoint */size_t lc=l-1;while(lc>0&&((unsigned char)s[lc]&0xC0)==0x80)lc--;if((unsigned char)s[lc]>=0x80){/* Multibyte tail: increment its codepoint */uint32_t cp;sp_utf8_decode(s+lc,&cp);cp++;char enc[4];int el=sp_utf8_encode(cp,enc);char*r=sp_str_alloc_raw(lc+el+1);memcpy(r,s,lc);memcpy(r+lc,enc,el);r[lc+el]=0;return r;}/* ASCII tail: existing carry logic */char*r=sp_str_alloc_raw(l+2);memcpy(r,s,l+1);mrb_int i=(mrb_int)l-1;while(i>=0){unsigned char c=(unsigned char)r[i];if(c>='0'&&c<'9'){r[i]=c+1;return r;}if(c=='9'){r[i]='0';i--;continue;}if(c>='a'&&c<'z'){r[i]=c+1;return r;}if(c=='z'){r[i]='a';i--;continue;}if(c>='A'&&c<'Z'){r[i]=c+1;return r;}if(c=='Z'){r[i]='A';i--;continue;}r[i]=c+1;return r;}memmove(r+1,r,l+1);if(r[1]=='0')r[0]='1';else if(r[1]=='a')r[0]='a';else if(r[1]=='A')r[0]='A';else r[0]=r[1];return r;}
516
1331
  static const char*sp_gets(void){char buf[4096];if(!fgets(buf,sizeof(buf),stdin))return NULL;size_t l=strlen(buf);char*r=sp_str_alloc_raw(l+1);memcpy(r,buf,l+1);return r;}
517
1332
  static sp_StrArray*sp_readlines(void){sp_StrArray*a=sp_StrArray_new();char buf[4096];while(fgets(buf,sizeof(buf),stdin)){size_t l=strlen(buf);char*r=sp_str_alloc_raw(l+1);memcpy(r,buf,l+1);sp_StrArray_push(a,r);}return a;}
518
- static const char*sp_str_strip(const char*s){while(*s&&isspace((unsigned char)*s))s++;size_t l=strlen(s);while(l>0&&isspace((unsigned char)s[l-1]))l--;char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l);r[l]=0;return r;}
519
- static const char*sp_str_chomp(const char*s){size_t l=strlen(s);while(l>0&&(s[l-1]=='\n'||s[l-1]=='\r'))l--;char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l);r[l]=0;return r;}
520
- static const char*sp_str_chop(const char*s){size_t l=strlen(s);if(l>0){if(l>=2&&s[l-2]=='\r'&&s[l-1]=='\n')l-=2;else l--;}char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l);r[l]=0;return r;}
521
- static mrb_bool sp_str_include(const char*s,const char*sub){return strstr(s,sub)!=NULL;}
522
- static mrb_bool sp_str_start_with(const char*s,const char*p){return strncmp(s,p,strlen(p))==0;}
523
- static mrb_bool sp_str_end_with(const char*s,const char*suf){size_t ls=strlen(s),lsuf=strlen(suf);if(lsuf>ls)return FALSE;return strcmp(s+ls-lsuf,suf)==0;}
524
- static sp_StrArray*sp_str_split(const char*s,const char*sep){sp_StrArray*a=sp_StrArray_new();if(*s==0)return a;size_t sl=strlen(sep);if(sl==0){const char*p=s;while(*p){int cn=sp_utf8_advance(p);char*c=sp_str_alloc_raw(cn+1);memcpy(c,p,cn);c[cn]=0;sp_StrArray_push(a,c);p+=cn;}return a;}const char*p=s;while(1){const char*f=strstr(p,sep);if(!f){char*r=sp_str_alloc_raw(strlen(p)+1);strcpy(r,p);sp_StrArray_push(a,r);break;}size_t n=f-p;char*r=sp_str_alloc_raw(n+1);memcpy(r,p,n);r[n]=0;sp_StrArray_push(a,r);p=f+sl;}return a;}
1333
+ /* strip / lstrip / rstrip. CRuby strips the set "\0\t\n\v\f\r " from the
1334
+ ends -- i.e. isspace() plus the NUL byte. Use sp_str_byte_len (not
1335
+ strlen) so a heap string carrying an embedded NUL (e.g. from pack /
1336
+ concat) is measured and stripped correctly; the result is a
1337
+ length-tracked heap string so any interior NUL survives. (A frozen
1338
+ literal with an embedded NUL is still truncated at the C level -- that
1339
+ needs length-tracked literals, out of scope.) */
1340
+ static const char*sp_str_strip(const char*s){if(!s)return sp_str_empty;size_t len=sp_str_byte_len(s);size_t a=0;while(a<len&&(isspace((unsigned char)s[a])||s[a]=='\0'))a++;size_t b=len;while(b>a&&(isspace((unsigned char)s[b-1])||s[b-1]=='\0'))b--;size_t n=b-a;char*r=sp_str_alloc(n);memcpy(r,s+a,n);r[n]=0;return r;}
1341
+ static const char*sp_str_chomp(const char*s){if(!s)return sp_str_empty;size_t l=strlen(s);if(l>=2&&s[l-2]=='\r'&&s[l-1]=='\n')l-=2;else if(l>0&&s[l-1]=='\n')l--;else if(l>0&&s[l-1]=='\r')l--;char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l);r[l]=0;return r;}
1342
+
1343
+ /* Issue #881: `"hello!".chomp("!")` strips the explicit separator.
1344
+ Empty sep strips any trailing newlines (CRuby paragraph mode).
1345
+ NULL sep is caller's responsibility (codegen routes nil to a
1346
+ no-op before calling). */
1347
+ static const char *sp_str_chomp_sep(const char *s, const char *sep) {
1348
+ if (!s) return sp_str_empty;
1349
+ size_t l = strlen(s);
1350
+ if (!sep || !*sep) {
1351
+ /* Empty sep = paragraph mode: strip trailing \r\n pairs and
1352
+ standalone \n's, but NOT standalone \r's. A trailing \r that
1353
+ is not part of a \r\n pair stops the stripping. */
1354
+ while (l > 0) {
1355
+ if (l >= 2 && s[l-2] == '\r' && s[l-1] == '\n') { l -= 2; continue; }
1356
+ if (s[l-1] == '\n') { l--; continue; }
1357
+ break;
1358
+ }
1359
+ } else {
1360
+ size_t sl = strlen(sep);
1361
+ if (sl <= l && memcmp(s + l - sl, sep, sl) == 0) l -= sl;
1362
+ }
1363
+ char *r = sp_str_alloc_raw(l + 1);
1364
+ memcpy(r, s, l);
1365
+ r[l] = 0;
1366
+ return r;
1367
+ }
1368
+ static const char*sp_str_chop(const char*s){if(!s)return sp_str_empty;size_t l=strlen(s);if(l>0){if(l>=2&&s[l-2]=='\r'&&s[l-1]=='\n')l-=2;else l--;}char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l);r[l]=0;return r;}
1369
+ static mrb_bool sp_str_include(const char*s,const char*sub){if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");if(!s)return FALSE;return strstr(s,sub)!=NULL;}
1370
+ static mrb_bool sp_str_start_with(const char*s,const char*p){if(!p)sp_raise_cls("TypeError","no implicit conversion of nil into String");if(!s)return FALSE;return strncmp(s,p,strlen(p))==0;}
1371
+ static mrb_bool sp_str_end_with(const char*s,const char*suf){if(!suf)sp_raise_cls("TypeError","no implicit conversion of nil into String");if(!s)return FALSE;size_t ls=strlen(s),lsuf=strlen(suf);if(lsuf>ls)return FALSE;return strcmp(s+ls-lsuf,suf)==0;}
1372
+ static sp_StrArray*sp_str_split(const char*s,const char*sep){
1373
+ SP_GC_ROOT(s);
1374
+ SP_GC_ROOT(sep);
1375
+ sp_StrArray*a=sp_StrArray_new();
1376
+ sp_str_split_into(a,s,sep);
1377
+ return a;
1378
+ }
1379
+ /* Same as sp_str_split but removes trailing empty strings
1380
+ (CRuby default limit behavior: split without limit drops
1381
+ trailing empties; split(sep, -1) keeps them). */
1382
+ static sp_StrArray*sp_str_split_drop_trailing(const char*s,const char*sep){sp_StrArray*a=sp_str_split(s,sep);while(a->len>0&&a->data[a->len-1][0]==0)a->len--;return a;}
1383
+ /* `s.split(sep, n)` with explicit limit. Positive n caps the result
1384
+ at n elements: the last element holds the unsplit remainder.
1385
+ n == 0 means "no limit" and drops trailing empty strings (same as
1386
+ the no-arg default); n < 0 means "no limit" but keeps trailing
1387
+ empties. Empty separator works the same as the no-limit path --
1388
+ splits into Unicode characters; the limit caps the array.
1389
+ Issue #619 puzzle 2. */
1390
+ static sp_StrArray*sp_str_split_limit(const char*s,const char*sep,mrb_int n){
1391
+ if(n==0)return sp_str_split_drop_trailing(s,sep);
1392
+ if(n<0)return sp_str_split(s,sep);
1393
+ SP_GC_ROOT(s);
1394
+ SP_GC_ROOT(sep);
1395
+ sp_StrArray*a=sp_StrArray_new();
1396
+ SP_GC_ROOT(a);
1397
+ if(*s==0)return a;
1398
+ size_t sl=strlen(sep);
1399
+ if(sl==0){
1400
+ const char*p=s;
1401
+ mrb_int k=0;
1402
+ while(*p&&k<n-1){
1403
+ int cn=sp_utf8_advance(p);
1404
+ sp_str_split_push(a,p,(size_t)cn);
1405
+ p+=cn;
1406
+ k++;
1407
+ }
1408
+ if(*p){
1409
+ sp_str_split_push(a,p,strlen(p));
1410
+ }
1411
+ return a;
1412
+ }
1413
+ const char*p=s;
1414
+ mrb_int k=0;
1415
+ while(k<n-1){
1416
+ const char*f=strstr(p,sep);
1417
+ if(!f)break;
1418
+ size_t m=f-p;
1419
+ sp_str_split_push(a,p,m);
1420
+ p=f+sl;
1421
+ k++;
1422
+ }
1423
+ sp_str_split_push(a,p,strlen(p));
1424
+ return a;
1425
+ }
1426
+ /* `s.split` / `s.split(nil)` -- whitespace mode: split on runs of
1427
+ ASCII whitespace, skip leading whitespace. Issue #507: the no-arg
1428
+ form previously emitted `sp_str_split(s, 0)` and segfaulted at
1429
+ strlen(NULL). */
1430
+ static sp_StrArray*sp_str_split_ws(const char*s){
1431
+ SP_GC_ROOT(s);
1432
+ sp_StrArray*a=sp_StrArray_new();
1433
+ SP_GC_ROOT(a);
1434
+ const char*p=s;
1435
+ while(*p==' '||*p=='\t'||*p=='\n'||*p=='\r'||*p=='\f'||*p=='\v')p++;
1436
+ while(*p){
1437
+ const char*start=p;
1438
+ while(*p&&!(*p==' '||*p=='\t'||*p=='\n'||*p=='\r'||*p=='\f'||*p=='\v'))p++;
1439
+ size_t n=p-start;
1440
+ sp_str_split_push(a,start,n);
1441
+ while(*p==' '||*p=='\t'||*p=='\n'||*p=='\r'||*p=='\f'||*p=='\v')p++;
1442
+ }
1443
+ return a;
1444
+ }
525
1445
  /* String#lines: split on \n but PRESERVE the trailing newline on each
526
1446
  line (CRuby semantics). The last line keeps its terminator if present;
527
1447
  if absent, it just stops there. Empty string returns an empty array.
528
1448
  `end` is computed once at entry so a string with no newlines avoids
529
1449
  a redundant strlen call on the trailing piece. */
530
1450
  static sp_StrArray*sp_str_lines(const char*s){sp_StrArray*a=sp_StrArray_new();if(*s==0)return a;const char*end=s+strlen(s);const char*p=s;while(p<end){const char*nl=strchr(p,'\n');size_t n=nl?(size_t)(nl-p+1):(size_t)(end-p);char*r=sp_str_alloc_raw(n+1);memcpy(r,p,n);r[n]=0;sp_StrArray_push(a,r);if(!nl)break;p=nl+1;}return a;}
531
- static const char*sp_str_gsub(const char*s,const char*pat,const char*rep){size_t pl=strlen(pat),rl=strlen(rep),sl=strlen(s);if(pl==0)return s;size_t cap=sl*2+1;char*out=(char*)malloc(cap);size_t ol=0;const char*p=s;while(*p){const char*f=strstr(p,pat);if(!f){size_t n=strlen(p);if(ol+n>=cap){cap=(ol+n)*2+1;out=(char*)realloc(out,cap);}memcpy(out+ol,p,n);ol+=n;break;}size_t n=f-p;if(ol+n+rl>=cap){cap=(ol+n+rl)*2+1;out=(char*)realloc(out,cap);}memcpy(out+ol,p,n);ol+=n;memcpy(out+ol,rep,rl);ol+=rl;p=f+pl;}out[ol]=0;return out;}
1451
+ static sp_StrArray*sp_str_lines_chomp(const char*s){sp_StrArray*a=sp_StrArray_new();if(*s==0)return a;const char*end=s+strlen(s);const char*p=s;while(p<end){const char*nl=strchr(p,'\n');size_t n=nl?(size_t)(nl-p):(size_t)(end-p);if(nl&&nl>s&&nl[-1]=='\r')n--;char*r=sp_str_alloc_raw(n+1);memcpy(r,p,n);r[n]=0;sp_StrArray_push(a,r);if(!nl)break;p=nl+1;}return a;}
1452
+ /* Issue #827: gsub previously returned a raw malloc buffer. The GC's
1453
+ sp_mark_string writes byte[-1] = 0xfc, which on a raw malloc buffer
1454
+ clobbers malloc metadata. Build into a scratch buffer, then copy
1455
+ into a sp_str_alloc'd buffer that has the GC marker byte. */
1456
+ /* Issue #850: empty pattern inserts the replacement between every
1457
+ character (and at the start). CRuby walks codepoint-by-codepoint;
1458
+ spinel's gsub is byte-oriented so we mirror that with UTF-8 stride
1459
+ to keep multi-byte characters intact. */
1460
+ static const char*sp_str_gsub(const char*s,const char*pat,const char*rep){
1461
+ if(!s)return sp_str_empty;
1462
+ if(!pat||!rep)return s;
1463
+ size_t pl=strlen(pat),rl=strlen(rep),sl=strlen(s);
1464
+ if(pl==0){
1465
+ /* Empty pattern: insert rep between every codepoint + at start/end.
1466
+ Result size: (chars+1) * rl + sl. */
1467
+ size_t cap=sl+rl*(sl+1)+1;
1468
+ char*out=(char*)malloc(cap);
1469
+ size_t ol=0;
1470
+ memcpy(out+ol,rep,rl); ol+=rl;
1471
+ const char*p=s;
1472
+ while(*p){
1473
+ int n=sp_utf8_advance(p);
1474
+ memcpy(out+ol,p,n); ol+=n;
1475
+ memcpy(out+ol,rep,rl); ol+=rl;
1476
+ p+=n;
1477
+ }
1478
+ out[ol]=0;
1479
+ char*r=sp_str_alloc(ol); memcpy(r,out,ol+1); free(out); return r;
1480
+ }
1481
+ size_t cap=sl*2+1;
1482
+ char*out=(char*)malloc(cap);
1483
+ size_t ol=0;
1484
+ const char*p=s;
1485
+ while(*p){
1486
+ const char*f=strstr(p,pat);
1487
+ if(!f){size_t n=strlen(p);if(ol+n>=cap){cap=(ol+n)*2+1;out=(char*)realloc(out,cap);}memcpy(out+ol,p,n);ol+=n;break;}
1488
+ size_t n=f-p;
1489
+ if(ol+n+rl>=cap){cap=(ol+n+rl)*2+1;out=(char*)realloc(out,cap);}
1490
+ memcpy(out+ol,p,n);ol+=n;
1491
+ memcpy(out+ol,rep,rl);ol+=rl;
1492
+ p=f+pl;
1493
+ }
1494
+ out[ol]=0;char*r=sp_str_alloc(ol);memcpy(r,out,ol+1);free(out);return r;
1495
+ }
532
1496
  /* Returns a *character* offset (codepoint index), not a byte offset. */
533
- static mrb_int sp_str_index(const char*s,const char*sub){const char*f=strstr(s,sub);if(!f)return -1;mrb_int n=0;const char*p=s;while(p<f){p+=sp_utf8_advance(p);n++;}return n;}
534
- static mrb_int sp_str_rindex(const char*s,const char*sub){size_t sl=strlen(sub);if(sl==0)return sp_str_length(s);const char*last=NULL;const char*p=s;while((p=strstr(p,sub))){last=p;p++;}if(!last)return -1;mrb_int n=0;const char*q=s;while(q<last){q+=sp_utf8_advance(q);n++;}return n;}
1497
+ /* Issue #759: NULL sub to strstr is UB. Issue #847: nil sub raises
1498
+ TypeError per MRI rather than silently returning -1 (would conflate
1499
+ "nil arg" with "not found"). Self NULL stays -1 -- shouldn't happen
1500
+ but keep defensive. */
1501
+ static mrb_int sp_str_index(const char*s,const char*sub){if(!s)return -1;if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*f=strstr(s,sub);if(!f)return -1;mrb_int n=0;const char*p=s;while(p<f){p+=sp_utf8_advance(p);n++;}return n;}
1502
+ /* `s.index(sub, start)` -- search starts at codepoint index `start`.
1503
+ Negative start counts back from the end of the string. Returns the
1504
+ absolute codepoint offset of the match (not relative to start), or
1505
+ -1 when no match exists at or after start. Out-of-range starts
1506
+ (start > length) return -1, matching CRuby. The codepoint counter
1507
+ begins at `start` and walks from `s + boff`, so the "walk all
1508
+ matches" loop is O(N) total rather than O(N^2). */
1509
+ static mrb_int sp_str_index_from(const char*s,const char*sub,mrb_int start){mrb_int cl=sp_str_length(s);if(start<0)start+=cl;if(start<0)start=0;if(start>cl)return -1;size_t boff=sp_utf8_byte_offset(s,start);const char*f=strstr(s+boff,sub);if(!f)return -1;mrb_int n=start;const char*p=s+boff;while(p<f){p+=sp_utf8_advance(p);n++;}return n;}
1510
+ static mrb_int sp_str_rindex(const char*s,const char*sub){if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");size_t sl=strlen(sub);if(sl==0)return sp_str_length(s);const char*last=NULL;const char*p=s;while((p=strstr(p,sub))){last=p;p++;}if(!last)return -1;mrb_int n=0;const char*q=s;while(q<last){q+=sp_utf8_advance(q);n++;}return n;}
1511
+ /* `s.rindex(sub, pos)` — find the rightmost occurrence of sub at or
1512
+ before codepoint index pos. Walks all matches and keeps the last
1513
+ one whose codepoint index <= pos. */
1514
+ static mrb_int sp_str_rindex_from(const char*s,const char*sub,mrb_int pos){if(!s)return SP_INT_NIL;if(!sub)sp_raise_cls("TypeError","no implicit conversion of nil into String");mrb_int cl=sp_str_length(s);if(pos<0)pos=cl+pos;if(pos<0)return SP_INT_NIL;size_t sl=strlen(sub);if(sl==0){if(pos>=cl)return cl;return pos;}const char*p=s;mrb_int best=-1;const char*r=s;mrb_int cur_n=0;while((p=strstr(p,sub))!=NULL){while(r<p){r+=sp_utf8_advance(r);cur_n++;}if(cur_n>pos)break;best=cur_n;p++;}return best<0?SP_INT_NIL:best;}
1515
+
535
1516
  static char sp_char_cache[256][3];
536
1517
  static int sp_char_cache_init = 0;
537
1518
  /* start/len are codepoint indices/counts. */
538
- static const char*sp_str_sub_range(const char*s,mrb_int start,mrb_int len){mrb_int cl=sp_str_length(s);if(start<0)start+=cl;if(start<0)start=0;if(start>=cl||len<=0){return &("\xff" "")[1];}if(start+len>cl)len=cl-start;size_t boff=sp_utf8_byte_offset(s,start);size_t bend=sp_utf8_byte_offset(s+boff,len)+boff;size_t blen=bend-boff;if(len==1&&blen==1){unsigned char c=(unsigned char)s[boff];if(!sp_char_cache_init){for(int i=0;i<256;i++){sp_char_cache[i][0]=(char)0xff;sp_char_cache[i][1]=(char)i;sp_char_cache[i][2]=0;}sp_char_cache_init=1;}return &sp_char_cache[c][1];}char*r=sp_str_alloc_raw(blen+1);memcpy(r,s+boff,blen);r[blen]=0;return r;}
1519
+ static const char*sp_str_sub_range(const char*s,mrb_int start,mrb_int len){mrb_int cl=sp_str_length(s);if(start<0)start+=cl;if(start<0)start=0;if(start>=cl||len<=0){return &("\xff" "")[1];}if(start+len>cl)len=cl-start;size_t boff=sp_utf8_byte_offset(s,start);size_t blen_total=sp_str_byte_len(s);size_t bp=boff;mrb_int rem=len;while(rem>0&&bp<blen_total){bp+=sp_utf8_advance(s+bp);rem--;}if(bp>blen_total)bp=blen_total;size_t bend=bp;size_t blen=bend-boff;if(len==1&&blen==1){unsigned char c=(unsigned char)s[boff];if(c!=0){if(!sp_char_cache_init){for(int i=0;i<256;i++){sp_char_cache[i][0]=(char)0xff;sp_char_cache[i][1]=(char)i;sp_char_cache[i][2]=0;}sp_char_cache_init=1;}return &sp_char_cache[c][1];}}char*r=sp_str_alloc_raw(blen+1);memcpy(r,s+boff,blen);r[blen]=0;return r;}
1520
+ /* Single-character form of `s[i]`. Returns NULL on out-of-bounds to
1521
+ match CRuby's `"hello"[20] -> nil`. The two-arg `s[i, len]` /
1522
+ range forms keep returning "" on OOB via sp_str_sub_range; only
1523
+ the bare single-int index aliases here. Issue #619 puzzle 3. */
1524
+ static const char*sp_str_char_at_or_nil(const char*s,mrb_int i){mrb_int cl=sp_str_length(s);if(i<0)i+=cl;if(i<0||i>=cl)return NULL;return sp_str_sub_range(s,i,1);}
1525
+ /* String#byteslice(start,len): byte-indexed (unlike the char-indexed
1526
+ sp_str_sub_range). Negative start counts from the byte length. Out of
1527
+ range yields the empty string, matching slice's "" rather than CRuby nil. */
1528
+ static const char*sp_str_byteslice(const char*s,mrb_int start,mrb_int len){mrb_int bl=(mrb_int)sp_str_byte_len(s);if(start<0)start+=bl;if(start<0||start>bl||len<0){return &("\xff" "")[1];}if(start+len>bl)len=bl-start;if(len<=0){return &("\xff" "")[1];}char*r=sp_str_alloc_raw(len+1);memcpy(r,s+start,len);r[len]=0;return r;}
1529
+ /* String#ascii_only?: 1 iff every byte is in the 7-bit ASCII range. */
1530
+ static int sp_str_ascii_only(const char*s){mrb_int bl=(mrb_int)sp_str_byte_len(s);for(mrb_int i=0;i<bl;i++){if((unsigned char)s[i]>=0x80)return 0;}return 1;}
539
1531
  /* Char-indexed variant; the second arg used to be a hoisted byte length, now a
540
1532
  hoisted codepoint count. We don't need it for correctness, but keeping the
541
1533
  ABI lets callers pass it without a wrapper. */
542
- static const char*sp_str_sub_range_len(const char*s,mrb_int cl,mrb_int start,mrb_int len){if(start<0)start+=cl;if(start<0)start=0;if(start>=cl||len<=0){return &("\xff" "")[1];}if(start+len>cl)len=cl-start;size_t boff=sp_utf8_byte_offset(s,start);size_t bend=sp_utf8_byte_offset(s+boff,len)+boff;size_t blen=bend-boff;if(len==1&&blen==1){unsigned char c=(unsigned char)s[boff];if(!sp_char_cache_init){for(int i=0;i<256;i++){sp_char_cache[i][0]=(char)0xff;sp_char_cache[i][1]=(char)i;sp_char_cache[i][2]=0;}sp_char_cache_init=1;}return &sp_char_cache[c][1];}char*r=sp_str_alloc_raw(blen+1);memcpy(r,s+boff,blen);r[blen]=0;return r;}
543
- static const char*sp_sprintf(const char*fmt,...){char _sp_tmp[4096];va_list ap;va_start(ap,fmt);int _sp_n=vsnprintf(_sp_tmp,sizeof(_sp_tmp),fmt,ap);va_end(ap);if(_sp_n<0)_sp_n=0;if(_sp_n>=(int)sizeof(_sp_tmp))_sp_n=(int)sizeof(_sp_tmp)-1;char*b=sp_str_alloc(_sp_n);memcpy(b,_sp_tmp,_sp_n);return b;}
1534
+ static const char*sp_str_sub_range_len(const char*s,mrb_int cl,mrb_int start,mrb_int len){if(start<0)start+=cl;if(start<0)start=0;if(start>=cl||len<=0){return &("\xff" "")[1];}if(start+len>cl)len=cl-start;size_t boff=sp_utf8_byte_offset(s,start);size_t blen_total=sp_str_byte_len(s);size_t bp=boff;mrb_int rem=len;while(rem>0&&bp<blen_total){bp+=sp_utf8_advance(s+bp);rem--;}if(bp>blen_total)bp=blen_total;size_t bend=bp;size_t blen=bend-boff;if(len==1&&blen==1){unsigned char c=(unsigned char)s[boff];if(c!=0){if(!sp_char_cache_init){for(int i=0;i<256;i++){sp_char_cache[i][0]=(char)0xff;sp_char_cache[i][1]=(char)i;sp_char_cache[i][2]=0;}sp_char_cache_init=1;}return &sp_char_cache[c][1];}}char*r=sp_str_alloc_raw(blen+1);memcpy(r,s+boff,blen);r[blen]=0;return r;}
1535
+ /* String s[start..end] / s[start...end] with possibly negative
1536
+ endpoints. Mirrors sp_IntArray_slice_range; issue #496. */
1537
+ static const char*sp_str_sub_range_r(const char*s,mrb_int start,mrb_int end_,mrb_int excl){mrb_int cl=sp_str_length(s);if(end_<0)end_+=cl;if(start<0)start+=cl;mrb_int n=end_-start+(excl?0:1);if(n<0||start<0)n=0;return sp_str_sub_range_len(s,cl,start,n);}
1538
+ static const char*sp_str_sub_range_len_r(const char*s,mrb_int cl,mrb_int start,mrb_int end_,mrb_int excl){if(end_<0)end_+=cl;if(start<0)start+=cl;mrb_int n=end_-start+(excl?0:1);if(n<0||start<0)n=0;return sp_str_sub_range_len(s,cl,start,n);}
1539
+ const char*sp_sprintf(const char*fmt,...){char _sp_tmp[4096];va_list ap;va_start(ap,fmt);int _sp_n=vsnprintf(_sp_tmp,sizeof(_sp_tmp),fmt,ap);va_end(ap);if(_sp_n<0)_sp_n=0;char*b=sp_str_alloc((size_t)_sp_n);if(_sp_n<(int)sizeof(_sp_tmp)){memcpy(b,_sp_tmp,(size_t)_sp_n);}else{/* result didn't fit the stack temp; re-render at full width (sp_str_alloc gives _sp_n bytes + NUL) so long string interpolations aren't truncated. re-arm the va_list rather than va_copy so the common fast path pays nothing */va_start(ap,fmt);vsnprintf(b,(size_t)_sp_n+1,fmt,ap);va_end(ap);}return b;}
544
1540
  /* Use a temp pointer for realloc so the original buffer is not leaked
545
1541
  on allocation failure. Match the perror+exit pattern used elsewhere
546
1542
  (see sp_IntArray_replace) instead of returning a partial result. */
547
1543
  static const char*sp_str_format_strarr(const char*fmt,sp_StrArray*a){size_t cap=strlen(fmt)+64;char*buf=(char*)malloc(cap);if(!buf){perror("malloc");exit(1);}size_t out=0;mrb_int idx=0;const char*p=fmt;while(*p){if(*p=='%'){if(p[1]=='s'){const char*s=(idx<a->len)?a->data[idx]:"";size_t sl=strlen(s);if(out+sl>=cap){size_t nc=(out+sl)*2+1;char*nb=(char*)realloc(buf,nc);if(!nb){free(buf);perror("realloc");exit(1);}buf=nb;cap=nc;}memcpy(buf+out,s,sl);out+=sl;idx++;p+=2;}else if(p[1]=='%'){if(out+1>=cap){size_t nc=cap*2;char*nb=(char*)realloc(buf,nc);if(!nb){free(buf);perror("realloc");exit(1);}buf=nb;cap=nc;}buf[out++]='%';p+=2;}else{if(out+1>=cap){size_t nc=cap*2;char*nb=(char*)realloc(buf,nc);if(!nb){free(buf);perror("realloc");exit(1);}buf=nb;cap=nc;}buf[out++]=*p++;}}else{if(out+1>=cap){size_t nc=cap*2;char*nb=(char*)realloc(buf,nc);if(!nb){free(buf);perror("realloc");exit(1);}buf=nb;cap=nc;}buf[out++]=*p++;}}buf[out]=0;char*r=sp_str_alloc(out);memcpy(r,buf,out);free(buf);return r;}
548
- static const char*sp_str_reverse(const char*s){size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t end=bl;const char*p=s;while(*p){int cn=sp_utf8_advance(p);end-=cn;memcpy(r+end,p,cn);p+=cn;}r[bl]=0;return r;}
549
- static const char*sp_str_sub(const char*s,const char*pat,const char*rep){const char*f=strstr(s,pat);if(!f)return s;size_t pl=strlen(pat),rl=strlen(rep),sl=strlen(s);char*r=sp_str_alloc_raw(sl-pl+rl+1);size_t n=f-s;memcpy(r,s,n);memcpy(r+n,rep,rl);memcpy(r+n+rl,f+pl,sl-n-pl+1);return r;}
550
- static const char*sp_str_capitalize(const char*s){size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<=l;i++)r[i]=tolower((unsigned char)s[i]);if(l>0)r[0]=toupper((unsigned char)r[0]);return r;}
551
- static mrb_int sp_str_count(const char*s,const char*chars){size_t setn;uint32_t*set=sp_utf8_decode_all(chars,&setn);mrb_int c=0;const char*p=s;while(*p){uint32_t cp;p+=sp_utf8_decode(p,&cp);if(sp_utf8_set_has(set,setn,cp))c++;}free(set);return c;}
552
- static const char*sp_str_repeat(const char*s,mrb_int n){if(n<=0)return"";size_t l=strlen(s);char*r=sp_str_alloc_raw(l*n+1);for(mrb_int i=0;i<n;i++)memcpy(r+l*i,s,l);r[l*n]=0;return r;}
553
- static sp_IntArray*sp_str_bytes(const char*s){sp_IntArray*a=sp_IntArray_new();for(size_t i=0;s[i];i++)sp_IntArray_push(a,(mrb_int)(unsigned char)s[i]);return a;}
554
- static const char*sp_str_tr(const char*s,const char*from,const char*to){size_t fn,tn;uint32_t*fcps=sp_utf8_decode_all(from,&fn);uint32_t*tcps=sp_utf8_decode_all(to,&tn);size_t bl=strlen(s);size_t cap=bl*4+1;char*buf=(char*)malloc(cap);size_t n=0;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);size_t mi=fn;for(size_t j=0;j<fn;j++)if(fcps[j]==cp){mi=j;break;}if(mi<fn&&tn>0){uint32_t rep=mi<tn?tcps[mi]:tcps[tn-1];n+=sp_utf8_encode(rep,buf+n);}else{memcpy(buf+n,p,cn);n+=cn;}p+=cn;}buf[n]=0;char*r=sp_str_alloc(n);memcpy(r,buf,n+1);free(buf);free(fcps);free(tcps);return r;}
555
- static const char*sp_str_delete(const char*s,const char*chars){size_t setn;uint32_t*set=sp_utf8_decode_all(chars,&setn);size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t n=0;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);if(!sp_utf8_set_has(set,setn,cp)){memcpy(r+n,p,cn);n+=cn;}p+=cn;}r[n]=0;free(set);return r;}
556
- static const char*sp_str_squeeze(const char*s){size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t n=0;uint32_t prev=0xFFFFFFFFu;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);if(cp!=prev){memcpy(r+n,p,cn);n+=cn;prev=cp;}p+=cn;}r[n]=0;return r;}
557
- static const char*sp_str_ljust(const char*s,mrb_int w){mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);size_t pad=(size_t)(w-cl);char*r=sp_str_alloc_raw(bl+pad+1);memcpy(r,s,bl);memset(r+bl,' ',pad);r[bl+pad]=0;return r;}
558
- static const char*sp_str_rjust(const char*s,mrb_int w){mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);size_t pad=(size_t)(w-cl);char*r=sp_str_alloc_raw(bl+pad+1);memset(r,' ',pad);memcpy(r+pad,s,bl);r[bl+pad]=0;return r;}
559
- static const char*sp_str_center(const char*s,mrb_int w){mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);mrb_int pad=w-cl;mrb_int left=pad/2;mrb_int right=pad-left;char*r=sp_str_alloc_raw(bl+pad+1);memset(r,' ',left);memcpy(r+left,s,bl);memset(r+left+bl,' ',right);r[bl+pad]=0;return r;}
1544
+
1545
+ static const char*sp_str_reverse(const char*s){if(!s)return sp_str_empty;size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t end=bl;const char*p=s;while(*p){int cn=sp_utf8_advance(p);end-=cn;memcpy(r+end,p,cn);p+=cn;}r[bl]=0;return r;}
1546
+ static const char*sp_str_sub(const char*s,const char*pat,const char*rep){if(!s)return sp_str_empty;if(!pat||!rep)return s;const char*f=strstr(s,pat);if(!f)return s;size_t pl=strlen(pat),rl=strlen(rep),sl=strlen(s);char*r=sp_str_alloc_raw(sl-pl+rl+1);size_t n=f-s;memcpy(r,s,n);memcpy(r+n,rep,rl);memcpy(r+n+rl,f+pl,sl-n-pl+1);return r;}
1547
+ static const char*sp_str_capitalize(const char*s){if(!s)return sp_str_empty;size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);for(size_t i=0;i<=l;i++)r[i]=tolower((unsigned char)s[i]);if(l>0)r[0]=toupper((unsigned char)r[0]);return r;}
1548
+ static mrb_int sp_str_count(const char*s,const char*chars){if(!chars)sp_raise_cls("TypeError","no implicit conversion of nil into String");int negate=0;const char*csp=chars;if(*csp=='^'&&*(csp+1)){negate=1;csp++;}size_t setn;uint32_t*set=sp_utf8_decode_charset(csp,&setn);mrb_int c=0;const char*p=s;while(*p){uint32_t cp;p+=sp_utf8_decode(p,&cp);int in_set=sp_utf8_set_has(set,setn,cp);if(negate)in_set=!in_set;if(in_set)c++;}free(set);return c;}
1549
+ /* String#count with multiple args: intersection of charsets.
1550
+ Each additional arg further restricts which chars to count.
1551
+ sp_str_count_n(s, args[], n) computes the intersection. */
1552
+ static mrb_int sp_str_count_n(const char*s,const char**chars,mrb_int n){if(n<=0)return 0;size_t*setns=(size_t*)malloc(n*sizeof(size_t));uint32_t**sets=(uint32_t**)malloc(n*sizeof(uint32_t*));int*negs=(int*)malloc(n*sizeof(int));for(mrb_int i=0;i<n;i++){if(!chars[i])sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*cs=chars[i];negs[i]=0;if(*cs=='^'&&*(cs+1)){negs[i]=1;cs++;}sets[i]=sp_utf8_decode_charset(cs,&setns[i]);}mrb_int c=0;const char*p=s;while(*p){uint32_t cp;p+=sp_utf8_decode(p,&cp);int all=1;for(mrb_int i=0;i<n;i++){int in_set=sp_utf8_set_has(sets[i],setns[i],cp);if(negs[i])in_set=!in_set;if(!in_set){all=0;break;}}if(all)c++;}for(mrb_int i=0;i<n;i++)free(sets[i]);free(sets);free(setns);free(negs);return c;}
1553
+ /* Issue #800: clamp l*n so a malicious input can't allocate a tiny
1554
+ buffer through size_t overflow. */
1555
+ /* Issue #836: bound the multiplier so a wildly oversized request
1556
+ raises ArgumentError rather than segfaulting when malloc returns
1557
+ NULL and memcpy walks it. 1 GiB cap covers realistic use. */
1558
+ static const char*sp_str_repeat(const char*s,mrb_int n){
1559
+ if(n<0) sp_raise_cls("ArgumentError","negative argument");
1560
+ if(!s||n<=0)return sp_str_empty;
1561
+ size_t l=strlen(s);
1562
+ if(l==0) return sp_str_empty;
1563
+ if((size_t)n>SIZE_MAX/l) sp_raise_cls("ArgumentError","string size too big");
1564
+ size_t total=(size_t)n*l;
1565
+ if(total>(size_t)(1u<<30)) sp_raise_cls("ArgumentError","string size too big");
1566
+ char*r=sp_str_alloc_raw(total+1);
1567
+ for(mrb_int i=0;i<n;i++)memcpy(r+l*i,s,l);
1568
+ r[total]=0;
1569
+ return r;
1570
+ }
1571
+ static sp_IntArray*sp_str_bytes(const char*s){sp_IntArray*a=sp_IntArray_new();if(!s)return a;size_t n=sp_str_byte_len(s);for(size_t i=0;i<n;i++)sp_IntArray_push(a,(mrb_int)(unsigned char)s[i]);return a;}
1572
+ /* Issue #903: String#codepoints -- one IntArray entry per UTF-8
1573
+ codepoint (not byte). Replacement-character behaviour mirrors
1574
+ sp_utf8_decode (returns the leading byte for malformed seqs). */
1575
+ static sp_IntArray*sp_str_codepoints(const char*s){sp_IntArray*a=sp_IntArray_new();if(!s)return a;const char*p=s;while(*p){uint32_t cp;int n=sp_utf8_decode(p,&cp);sp_IntArray_push(a,(mrb_int)cp);p+=n;}return a;}
1576
+ /* Issue #798: guard NULL inputs (CRuby treats nil/no-op gracefully). */
1577
+ static const char*sp_str_tr(const char*s,const char*from,const char*to){if(!s)return sp_str_empty;if(!from||!to)return s;int negate=0;const char*fp=from;if(*fp=='^'&&*(fp+1)){negate=1;fp++;}size_t fn,tn;uint32_t*fcps=sp_utf8_decode_charset(fp,&fn);uint32_t*tcps=sp_utf8_decode_charset(to,&tn);size_t bl=strlen(s);size_t cap=bl*4+1;char*buf=(char*)malloc(cap);size_t n=0;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);size_t mi=fn;for(size_t j=0;j<fn;j++)if(fcps[j]==cp){mi=j;break;}int in_set=(mi<fn);if(negate)in_set=!in_set;if(in_set&&tn>0){uint32_t rep=negate?tcps[tn-1]:(mi<tn?tcps[mi]:tcps[tn-1]);n+=sp_utf8_encode(rep,buf+n);}else if(in_set){}else{memcpy(buf+n,p,cn);n+=cn;}p+=cn;}buf[n]=0;char*r=sp_str_alloc(n);memcpy(r,buf,n+1);free(buf);free(fcps);free(tcps);return r;}
1578
+ /* Issue #902: String#tr_s -- translate AND squeeze consecutive
1579
+ identical results into one. Walks codepoint-by-codepoint and
1580
+ collapses adjacent duplicates only among the translated bytes
1581
+ (untranslated runs keep their original characters). */
1582
+ static const char*sp_str_tr_s(const char*s,const char*from,const char*to){
1583
+ if(!s)return sp_str_empty;
1584
+ if(!from||!to)return s;
1585
+ int negate=0;const char*fp=from;
1586
+ if(*fp=='^'&&*(fp+1)){negate=1;fp++;}
1587
+ size_t fn,tn;
1588
+ uint32_t*fcps=sp_utf8_decode_charset(fp,&fn);
1589
+ uint32_t*tcps=sp_utf8_decode_charset(to,&tn);
1590
+ size_t bl=strlen(s);
1591
+ size_t cap=bl*4+1;
1592
+ char*buf=(char*)malloc(cap);
1593
+ size_t n=0;
1594
+ const char*p=s;
1595
+ uint32_t last_emit=0; int has_last=0; int last_was_translated=0;
1596
+ while(*p){
1597
+ uint32_t cp; int cn=sp_utf8_decode(p,&cp);
1598
+ size_t mi=fn;
1599
+ for(size_t j=0;j<fn;j++)if(fcps[j]==cp){mi=j;break;}
1600
+ int in_set=(mi<fn);
1601
+ if(negate)in_set=!in_set;
1602
+ uint32_t emit_cp;
1603
+ int translated=0;
1604
+ if(in_set){
1605
+ if(tn>0){
1606
+ emit_cp=negate?tcps[tn-1]:(mi<tn?tcps[mi]:tcps[tn-1]);
1607
+ translated=1;
1608
+ } else {
1609
+ p+=cn; continue;
1610
+ }
1611
+ } else {
1612
+ emit_cp=cp;
1613
+ translated=0;
1614
+ }
1615
+ /* Squeeze only when both the previous and current emit were
1616
+ translated, AND the emitted codepoints match. */
1617
+ if(has_last && last_was_translated && translated && last_emit==emit_cp){
1618
+ /* skip */
1619
+ } else {
1620
+ n+=sp_utf8_encode(emit_cp,buf+n);
1621
+ last_emit=emit_cp;
1622
+ has_last=1;
1623
+ last_was_translated=translated;
1624
+ }
1625
+ p+=cn;
1626
+ }
1627
+ buf[n]=0;
1628
+ char*r=sp_str_alloc(n);
1629
+ memcpy(r,buf,n+1);
1630
+ free(buf); free(fcps); free(tcps);
1631
+ return r;
1632
+ }
1633
+ static const char*sp_str_delete(const char*s,const char*chars){if(!s)return sp_str_empty;if(!chars)return s;int negate=0;const char*csp=chars;if(*csp=='^'&&*(csp+1)){negate=1;csp++;}size_t setn;uint32_t*set=sp_utf8_decode_charset(csp,&setn);size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t n=0;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);int in_set=sp_utf8_set_has(set,setn,cp);if(negate)in_set=!in_set;if(!in_set){memcpy(r+n,p,cn);n+=cn;}p+=cn;}r[n]=0;sp_str_set_len(r,n);free(set);return r;}
1634
+ /* Issue #921: shrink the heap-string header length to match the
1635
+ squeezed payload — the alloc gives bl+1 bytes, the squeezed
1636
+ write fills n<=bl, leaving the header's stored length stale.
1637
+ `bytes` / `length` consult the header (not strlen), so callers
1638
+ would see the alloc size and trailing NULs. */
1639
+ static const char*sp_str_squeeze(const char*s){if(!s)return sp_str_empty;size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t n=0;uint32_t prev=0xFFFFFFFFu;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);if(cp!=prev){memcpy(r+n,p,cn);n+=cn;prev=cp;}p+=cn;}r[n]=0;sp_str_set_len(r,n);return r;}
1640
+ /* String#squeeze(chars) — only squeeze chars listed in the charset
1641
+ (same charset syntax as tr: a-z, ^x, etc.). Consecutive runs of
1642
+ non-listed chars pass through untouched. */
1643
+ static const char*sp_str_squeeze_chars(const char*s,const char*cs){if(!s)return sp_str_empty;if(!cs||!*cs)return sp_str_squeeze(s);int negate=0;const char*csp=cs;if(*csp=='^'&&*(csp+1)){negate=1;csp++;}size_t fn;uint32_t*fcps=sp_utf8_decode_charset(csp,&fn);size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t n=0;uint32_t prev=0xFFFFFFFFu;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);int in_set=0;for(size_t j=0;j<fn;j++)if(fcps[j]==cp){in_set=1;break;}if(negate)in_set=!in_set;if(!in_set){memcpy(r+n,p,cn);n+=cn;prev=0xFFFFFFFFu;}else if(cp!=prev){memcpy(r+n,p,cn);n+=cn;prev=cp;}p+=cn;}r[n]=0;sp_str_set_len(r,n);free(fcps);return r;}
1644
+ /* Multi-arg delete/squeeze: delete (or squeeze runs of) characters that
1645
+ are in the INTERSECTION of all n charset args, mirroring
1646
+ sp_str_count_n. Each arg is a charset spec (^negation, a-z ranges). */
1647
+ static const char*sp_str_delete_n(const char*s,const char**chars,mrb_int n){if(!s)return sp_str_empty;if(n<=0)return s;size_t*setns=(size_t*)malloc(n*sizeof(size_t));uint32_t**sets=(uint32_t**)malloc(n*sizeof(uint32_t*));int*negs=(int*)malloc(n*sizeof(int));for(mrb_int i=0;i<n;i++){if(!chars[i])sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*cs=chars[i];negs[i]=0;if(*cs=='^'&&*(cs+1)){negs[i]=1;cs++;}sets[i]=sp_utf8_decode_charset(cs,&setns[i]);}size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t m=0;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);int all=1;for(mrb_int i=0;i<n;i++){int in_set=sp_utf8_set_has(sets[i],setns[i],cp);if(negs[i])in_set=!in_set;if(!in_set){all=0;break;}}if(!all){memcpy(r+m,p,cn);m+=cn;}p+=cn;}r[m]=0;sp_str_set_len(r,m);for(mrb_int i=0;i<n;i++)free(sets[i]);free(sets);free(setns);free(negs);return r;}
1648
+ static const char*sp_str_squeeze_n(const char*s,const char**chars,mrb_int n){if(!s)return sp_str_empty;if(n<=0)return sp_str_squeeze(s);size_t*setns=(size_t*)malloc(n*sizeof(size_t));uint32_t**sets=(uint32_t**)malloc(n*sizeof(uint32_t*));int*negs=(int*)malloc(n*sizeof(int));for(mrb_int i=0;i<n;i++){if(!chars[i])sp_raise_cls("TypeError","no implicit conversion of nil into String");const char*cs=chars[i];negs[i]=0;if(*cs=='^'&&*(cs+1)){negs[i]=1;cs++;}sets[i]=sp_utf8_decode_charset(cs,&setns[i]);}size_t bl=strlen(s);char*r=sp_str_alloc_raw(bl+1);size_t m=0;uint32_t prev=0xFFFFFFFFu;const char*p=s;while(*p){uint32_t cp;int cn=sp_utf8_decode(p,&cp);int all=1;for(mrb_int i=0;i<n;i++){int in_set=sp_utf8_set_has(sets[i],setns[i],cp);if(negs[i])in_set=!in_set;if(!in_set){all=0;break;}}if(!all){memcpy(r+m,p,cn);m+=cn;prev=0xFFFFFFFFu;}else if(cp!=prev){memcpy(r+m,p,cn);m+=cn;prev=cp;}p+=cn;}r[m]=0;sp_str_set_len(r,m);for(mrb_int i=0;i<n;i++)free(sets[i]);free(sets);free(setns);free(negs);return r;}
1649
+
1650
+ /* Forward decl from sp_crypto.h (libspinel_rt.a). Used by
1651
+ sp_str_crypt below to provide a deterministic crypt-like
1652
+ hash without dragging in libc crypt(3). */
1653
+ const char *sp_crypto_hmac_sha256_b64url(const char *key, const char *msg);
1654
+
1655
+ /* String#crypt — Spinel's crypt is NOT the libc DES crypt. It
1656
+ returns `salt[0..1] || hmac_sha256(salt, password)[0..10]` as
1657
+ a 13-char string (same length as DES crypt, deterministic,
1658
+ stronger primitive). CRuby's spec says String#crypt is impl-
1659
+ defined and "should not be used for security"; this matches
1660
+ that contract while keeping outputs reproducible across
1661
+ spinel builds. Short salts get padded with '.' so the result
1662
+ still has the canonical first-2-chars-are-salt shape. */
1663
+ static const char *sp_str_crypt(const char *s, const char *salt) {
1664
+ if (!salt) salt = "";
1665
+ char salt2[3];
1666
+ salt2[0] = salt[0] ? salt[0] : '.';
1667
+ salt2[1] = (salt[0] && salt[1]) ? salt[1] : '.';
1668
+ salt2[2] = 0;
1669
+ const char *digest = sp_crypto_hmac_sha256_b64url(salt2, s ? s : "");
1670
+ char *r = sp_str_alloc(13);
1671
+ r[0] = salt2[0];
1672
+ r[1] = salt2[1];
1673
+ for (int i = 0; i < 11; i++) {
1674
+ char c = digest[i];
1675
+ /* Map b64url's `-`/`_` to crypt-alphabet `.`/`/` so the
1676
+ output stays in `[./0-9A-Za-z]` like the historical
1677
+ crypt result. */
1678
+ if (c == '-') c = '.';
1679
+ else if (c == '_') c = '/';
1680
+ r[2 + i] = c;
1681
+ }
1682
+ r[13] = 0;
1683
+ sp_str_set_len(r, 13);
1684
+ return r;
1685
+ }
1686
+
1687
+ /* String#scrub — walk the bytes; for each valid UTF-8 lead +
1688
+ continuation sequence, copy through. For invalid bytes (lone
1689
+ continuation, truncated multi-byte, overlong, etc.), emit the
1690
+ replacement string and skip one byte. NULL replacement uses
1691
+ U+FFFD (3 UTF-8 bytes: EF BF BD), matching CRuby. */
1692
+ static const char *sp_str_scrub(const char *s, const char *repl) {
1693
+ if (!s) return sp_str_empty;
1694
+ static const char fffd[] = "\xEF\xBF\xBD";
1695
+ const char *r = repl ? repl : fffd;
1696
+ size_t rlen = strlen(r);
1697
+ size_t bl = sp_str_byte_len(s);
1698
+ size_t cap = bl + 64;
1699
+ /* malloc scratch (grown with realloc on invalid-byte runs); the final
1700
+ string is emitted at the exact length below. */
1701
+ char *out = (char *)malloc(cap);
1702
+ size_t olen = 0;
1703
+ size_t i = 0;
1704
+ while (i < bl) {
1705
+ unsigned char c = (unsigned char)s[i];
1706
+ int expected = sp_utf8_char_len(c);
1707
+ int valid = 1;
1708
+ if (expected == 1) {
1709
+ if (c >= 0x80) valid = 0;
1710
+ } else {
1711
+ if (i + (size_t)expected > bl) valid = 0;
1712
+ else {
1713
+ for (int k = 1; k < expected; k++) {
1714
+ if (((unsigned char)s[i + k] & 0xC0) != 0x80) { valid = 0; break; }
1715
+ }
1716
+ }
1717
+ }
1718
+ if (valid) {
1719
+ if (olen + (size_t)expected + 1 >= cap) { cap = (olen + expected) * 2 + 64; out = (char*)realloc(out, cap); }
1720
+ memcpy(out + olen, s + i, (size_t)expected);
1721
+ olen += (size_t)expected;
1722
+ i += (size_t)expected;
1723
+ } else {
1724
+ if (olen + rlen + 1 >= cap) { cap = (olen + rlen) * 2 + 64; out = (char*)realloc(out, cap); }
1725
+ memcpy(out + olen, r, rlen);
1726
+ olen += rlen;
1727
+ i += 1;
1728
+ }
1729
+ }
1730
+ char *res = sp_str_alloc(olen);
1731
+ memcpy(res, out, olen);
1732
+ free(out);
1733
+ return res;
1734
+ }
1735
+ static const char*sp_str_ljust(const char*s,mrb_int w){if(!s)return sp_str_empty;mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);size_t pad=(size_t)(w-cl);char*r=sp_str_alloc_raw(bl+pad+1);memcpy(r,s,bl);memset(r+bl,' ',pad);r[bl+pad]=0;return r;}
1736
+ static const char*sp_str_rjust(const char*s,mrb_int w){if(!s)return sp_str_empty;mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);size_t pad=(size_t)(w-cl);char*r=sp_str_alloc_raw(bl+pad+1);memset(r,' ',pad);memcpy(r+pad,s,bl);r[bl+pad]=0;return r;}
1737
+ static const char*sp_str_center(const char*s,mrb_int w){if(!s)return sp_str_empty;mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);mrb_int pad=w-cl;mrb_int left=pad/2;mrb_int right=pad-left;char*r=sp_str_alloc_raw(bl+pad+1);memset(r,' ',left);memcpy(r+left,s,bl);memset(r+left+bl,' ',right);r[bl+pad]=0;return r;}
560
1738
  static const char*sp_str_ljust2(const char*s,mrb_int w,const char*pad){mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);size_t pn;uint32_t*pcps=sp_utf8_decode_all(pad,&pn);if(pn==0){free(pcps);char*r=sp_str_alloc_raw(bl+1);memcpy(r,s,bl+1);return r;}mrb_int need=w-cl;size_t padb=0;for(mrb_int i=0;i<need;i++){char tmp[4];padb+=sp_utf8_encode(pcps[i%pn],tmp);}char*r=sp_str_alloc_raw(bl+padb+1);memcpy(r,s,bl);size_t n=bl;for(mrb_int i=0;i<need;i++)n+=sp_utf8_encode(pcps[i%pn],r+n);r[n]=0;free(pcps);return r;}
561
1739
  static const char*sp_str_rjust2(const char*s,mrb_int w,const char*pad){mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);size_t pn;uint32_t*pcps=sp_utf8_decode_all(pad,&pn);if(pn==0){free(pcps);char*r=sp_str_alloc_raw(bl+1);memcpy(r,s,bl+1);return r;}mrb_int need=w-cl;size_t padb=0;for(mrb_int i=0;i<need;i++){char tmp[4];padb+=sp_utf8_encode(pcps[i%pn],tmp);}char*r=sp_str_alloc_raw(bl+padb+1);size_t n=0;for(mrb_int i=0;i<need;i++)n+=sp_utf8_encode(pcps[i%pn],r+n);memcpy(r+n,s,bl);r[n+bl]=0;free(pcps);return r;}
562
1740
  static const char*sp_str_center2(const char*s,mrb_int w,const char*pad){mrb_int cl=sp_str_length(s);if(cl>=w)return s;size_t bl=strlen(s);size_t pn;uint32_t*pcps=sp_utf8_decode_all(pad,&pn);if(pn==0){free(pcps);char*r=sp_str_alloc_raw(bl+1);memcpy(r,s,bl+1);return r;}mrb_int pd=w-cl;mrb_int left=pd/2;mrb_int right=pd-left;size_t leftb=0,rightb=0;{char tmp[4];for(mrb_int i=0;i<left;i++)leftb+=sp_utf8_encode(pcps[i%pn],tmp);for(mrb_int i=0;i<right;i++)rightb+=sp_utf8_encode(pcps[i%pn],tmp);}char*r=sp_str_alloc_raw(leftb+bl+rightb+1);size_t n=0;for(mrb_int i=0;i<left;i++)n+=sp_utf8_encode(pcps[i%pn],r+n);memcpy(r+n,s,bl);n+=bl;for(mrb_int i=0;i<right;i++)n+=sp_utf8_encode(pcps[i%pn],r+n);r[n]=0;free(pcps);return r;}
563
- static const char*sp_str_lstrip(const char*s){while(*s&&isspace((unsigned char)*s))s++;char*r=sp_str_alloc_raw(strlen(s)+1);strcpy(r,s);return r;}
564
- static const char*sp_str_rstrip(const char*s){size_t l=strlen(s);while(l>0&&isspace((unsigned char)s[l-1]))l--;char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l);r[l]=0;return r;}
565
- static const char*sp_str_dup(const char*s){char*r=sp_str_alloc_raw(strlen(s)+1);strcpy(r,s);return r;}
1741
+ static const char*sp_str_lstrip(const char*s){if(!s)return sp_str_empty;size_t len=sp_str_byte_len(s);size_t a=0;while(a<len&&(isspace((unsigned char)s[a])||s[a]=='\0'))a++;size_t n=len-a;char*r=sp_str_alloc(n);memcpy(r,s+a,n);r[n]=0;return r;}
1742
+ static const char*sp_str_rstrip(const char*s){if(!s)return sp_str_empty;size_t len=sp_str_byte_len(s);size_t b=len;while(b>0&&(isspace((unsigned char)s[b-1])||s[b-1]=='\0'))b--;char*r=sp_str_alloc(b);memcpy(r,s,b);r[b]=0;return r;}
1743
+ static const char*sp_str_dup(const char*s){if(!s)return NULL;size_t l=strlen(s);char*r=sp_str_alloc_raw(l+1);memcpy(r,s,l+1);return r;}
1744
+
1745
+ /* String#setbyte: mutate s[i] = v in place. Spinel adopts
1746
+ `# frozen_string_literal: true` semantics globally — all
1747
+ string literals are frozen, mutation requires a heap-allocated
1748
+ buffer (e.g. via .dup or string concatenation). The runtime
1749
+ marker byte at s[-1] tells us the provenance:
1750
+ 0xfe / 0xfc -> sp_str_alloc heap (writable, GC unmarked / marked)
1751
+ 0xfd -> sp_String wrapper buffer (writable)
1752
+ 0xff -> rodata literal (frozen -> FrozenError)
1753
+ other -> FFI / unknown provenance, treated as frozen
1754
+ Returns the byte value (CRuby setbyte return). */
1755
+ static inline mrb_int sp_str_setbyte(const char *s, mrb_int i, mrb_int v) {
1756
+ if (!s) {
1757
+ sp_raise_cls("FrozenError", "can't modify frozen String");
1758
+ return v;
1759
+ }
1760
+ unsigned char m = ((const unsigned char *)s)[-1];
1761
+ if (m == 0xfe || m == 0xfc || m == 0xfd) {
1762
+ ((char *)s)[i] = (char)v;
1763
+ return v;
1764
+ }
1765
+ sp_raise_cls("FrozenError", "can't modify frozen String");
1766
+ return v;
1767
+ }
566
1768
 
567
1769
  typedef struct{char*data;int64_t len;int64_t cap;}sp_String;
568
- static void sp_String_fin(void*p){free(((sp_String*)p)->data);}
1770
+ /* Per-mutable-string freeze flag rides in the GC header alongside
1771
+ `marked`. sp_String_freeze sets it; the in-place mutators below
1772
+ raise FrozenError when the bit is set. Literal `const char *`
1773
+ strings stay frozen via the 0xff marker byte (see sp_str_setbyte). */
1774
+ static inline mrb_bool sp_String_is_frozen(sp_String*s){if(!s)return TRUE;sp_gc_hdr*h=(sp_gc_hdr*)((char*)s-sizeof(sp_gc_hdr));return h->frozen;}
1775
+ static inline sp_String*sp_String_freeze(sp_String*s){if(s){sp_gc_hdr*h=(sp_gc_hdr*)((char*)s-sizeof(sp_gc_hdr));h->frozen=1;}return s;}
1776
+ static void sp_String_fin(void*p){free(((sp_String*)p)->data-1);}
569
1777
  static sp_String*sp_String_new(const char*s){
570
1778
  /* Copy s's payload into a raw-malloc'd buffer BEFORE sp_gc_alloc.
571
1779
  If s is a heap string (sp_str_alloc) whose only liveness anchor
@@ -578,37 +1786,191 @@ static sp_String*sp_String_new(const char*s){
578
1786
  any GC inside sp_gc_alloc. */
579
1787
  int64_t len=(int64_t)strlen(s);
580
1788
  int64_t cap=len*2+16;
581
- char*data=(char*)malloc(cap+1);
1789
+ char*raw=(char*)malloc(cap+2);
1790
+ raw[0]=(char)0xfd;
1791
+ char*data=raw+1;
582
1792
  memcpy(data,s,len+1);
583
1793
  sp_String*r=(sp_String*)sp_gc_alloc(sizeof(sp_String),sp_String_fin,NULL);
584
1794
  r->len=len;r->cap=cap;r->data=data;
585
- {sp_gc_hdr*h=(sp_gc_hdr*)((char*)r-sizeof(sp_gc_hdr));h->size+=r->cap+1;sp_gc_bytes+=r->cap+1;}
1795
+ {sp_gc_hdr*h=(sp_gc_hdr*)((char*)r-sizeof(sp_gc_hdr));h->size+=r->cap+2;sp_gc_bytes+=r->cap+2;}
586
1796
  return r;
587
1797
  }
588
- static inline void sp_String_append(sp_String*s,const char*t){int64_t tl=(int64_t)strlen(t);if(s->len+tl>=s->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)s-sizeof(sp_gc_hdr));sp_gc_bytes-=s->cap+1;h->size-=s->cap+1;s->cap=(s->len+tl)*2+16;s->data=(char*)realloc(s->data,s->cap+1);h->size+=s->cap+1;sp_gc_bytes+=s->cap+1;}memcpy(s->data+s->len,t,tl+1);s->len+=tl;}
589
- static inline void sp_String_prepend(sp_String*s,const char*t){int64_t tl=(int64_t)strlen(t);if(s->len+tl>=s->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)s-sizeof(sp_gc_hdr));sp_gc_bytes-=s->cap+1;h->size-=s->cap+1;s->cap=(s->len+tl)*2+16;s->data=(char*)realloc(s->data,s->cap+1);h->size+=s->cap+1;sp_gc_bytes+=s->cap+1;}memmove(s->data+tl,s->data,s->len+1);memcpy(s->data,t,tl);s->len+=tl;}
1798
+ /* Issue #757: realloc on growth used to overwrite s->data unconditionally,
1799
+ leaking the old buffer + null-dereferencing if realloc fails. Now we
1800
+ check the result and bail without mutating on failure. */
1801
+ static inline void sp_String_append(sp_String*s,const char*t){if(!s||!t)return;if(sp_String_is_frozen(s)){sp_raise_cls("FrozenError","can't modify frozen String");return;}int64_t tl=(int64_t)strlen(t);if(s->len+tl>=s->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)s-sizeof(sp_gc_hdr));int64_t new_cap=(s->len+tl)*2+16;char*raw=(char*)realloc(s->data-1,new_cap+2);if(!raw)return;sp_gc_bytes-=s->cap+2;h->size-=s->cap+2;s->cap=new_cap;raw[0]=(char)0xfd;s->data=raw+1;h->size+=s->cap+2;sp_gc_bytes+=s->cap+2;}memcpy(s->data+s->len,t,tl+1);s->len+=tl;}
1802
+ static inline void sp_String_prepend(sp_String*s,const char*t){if(!s||!t)return;if(sp_String_is_frozen(s)){sp_raise_cls("FrozenError","can't modify frozen String");return;}int64_t tl=(int64_t)strlen(t);if(s->len+tl>=s->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)s-sizeof(sp_gc_hdr));int64_t new_cap=(s->len+tl)*2+16;char*raw=(char*)realloc(s->data-1,new_cap+2);if(!raw)return;sp_gc_bytes-=s->cap+2;h->size-=s->cap+2;s->cap=new_cap;raw[0]=(char)0xfd;s->data=raw+1;h->size+=s->cap+2;sp_gc_bytes+=s->cap+2;}memmove(s->data+tl,s->data,s->len+1);memcpy(s->data,t,tl);s->len+=tl;}
1803
+ /* Issue #741: String#insert(idx, str) -- insert str at idx. Negative
1804
+ idx is relative to len+1 (insert before tail). */
1805
+ static inline void sp_String_insert(sp_String*s,int64_t idx,const char*t){if(!s||!t)return;if(sp_String_is_frozen(s)){sp_raise_cls("FrozenError","can't modify frozen String");return;}int64_t tl=(int64_t)strlen(t);if(tl==0)return;if(idx<0)idx+=s->len+1;if(idx<0)idx=0;if(idx>s->len)idx=s->len;if(s->len+tl>=s->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)s-sizeof(sp_gc_hdr));int64_t new_cap=(s->len+tl)*2+16;char*raw=(char*)realloc(s->data-1,new_cap+2);if(!raw)return;sp_gc_bytes-=s->cap+2;h->size-=s->cap+2;s->cap=new_cap;raw[0]=(char)0xfd;s->data=raw+1;h->size+=s->cap+2;sp_gc_bytes+=s->cap+2;}memmove(s->data+idx+tl,s->data+idx,s->len-idx+1);memcpy(s->data+idx,t,tl);s->len+=tl;}
1806
+ /* Issue #740/#741 sibling: String#replace(s) -- replace entire content. */
1807
+ static inline void sp_String_replace(sp_String*s,const char*t){if(!s||!t)return;if(sp_String_is_frozen(s)){sp_raise_cls("FrozenError","can't modify frozen String");return;}int64_t tl=(int64_t)strlen(t);if(tl>=s->cap){sp_gc_hdr*h=(sp_gc_hdr*)((char*)s-sizeof(sp_gc_hdr));int64_t new_cap=tl*2+16;char*raw=(char*)realloc(s->data-1,new_cap+2);if(!raw)return;sp_gc_bytes-=s->cap+2;h->size-=s->cap+2;s->cap=new_cap;raw[0]=(char)0xfd;s->data=raw+1;h->size+=s->cap+2;sp_gc_bytes+=s->cap+2;}memcpy(s->data,t,tl);s->data[tl]='\0';s->len=tl;}
590
1808
  static inline const char*sp_String_cstr(sp_String*s){return s->data;}
591
1809
  static inline int64_t sp_String_length(sp_String*s){return s->len;}
592
1810
  static sp_String*sp_String_dup(sp_String*s){return sp_String_new(s->data);}
1811
+
1812
+ /* `File.open(path, mode)` without a block returns an sp_File * — a
1813
+ GC-managed wrapper around `FILE *fp`. The finalizer fclose()s any
1814
+ still-open fp so a dropped file handle doesn't leak. `path` and
1815
+ `mode` are kept live for `f.path` / `f.mode` introspection; they
1816
+ come from the caller's already-live string slot so no extra mark
1817
+ is needed. */
1818
+ /* File / IO handle ops live in libspinel_rt.a (lib/sp_io.c); the sp_File
1819
+ struct and the allocation-free op prototypes come from sp_io.h. The
1820
+ string-returning readers below (gets / read / read_n / path) stay
1821
+ inline here because they allocate via the hot static sp_str_alloc,
1822
+ whose per-TU sp_str_heap can't be shared across translation units. */
1823
+ #include "sp_io.h"
1824
+ static inline const char *sp_File_gets(sp_File *f) {
1825
+ if (!f || !f->fp) return NULL;
1826
+ char buf[65536];
1827
+ if (!fgets(buf, (int)sizeof(buf), f->fp)) return NULL;
1828
+ size_t n = strlen(buf);
1829
+ char *r = sp_str_alloc(n);
1830
+ memcpy(r, buf, n);
1831
+ return r;
1832
+ }
1833
+ static inline const char *sp_File_read(sp_File *f) {
1834
+ if (!f || !f->fp) return sp_str_empty;
1835
+ long pos = ftell(f->fp);
1836
+ if (pos >= 0 && fseek(f->fp, 0, SEEK_END) == 0) {
1837
+ long end = ftell(f->fp);
1838
+ fseek(f->fp, pos, SEEK_SET);
1839
+ long n = (end > pos) ? (end - pos) : 0;
1840
+ if (n <= 0) return sp_str_empty;
1841
+ char *r = sp_str_alloc((size_t)n);
1842
+ size_t got = fread(r, 1, (size_t)n, f->fp);
1843
+ r[got] = 0;
1844
+ return r;
1845
+ }
1846
+ /* Non-seekable stream (IO.pipe end, socket): read to EOF in chunks. */
1847
+ size_t cap = 256, len = 0;
1848
+ char *buf = (char *)malloc(cap);
1849
+ if (!buf) return sp_str_empty;
1850
+ for (;;) {
1851
+ if (len + 4096 + 1 > cap) {
1852
+ cap = (len + 4096 + 1) * 2;
1853
+ char *nb = (char *)realloc(buf, cap);
1854
+ if (!nb) { free(buf); return sp_str_empty; }
1855
+ buf = nb;
1856
+ }
1857
+ size_t got = fread(buf + len, 1, 4096, f->fp);
1858
+ len += got;
1859
+ if (got < 4096) break;
1860
+ }
1861
+ char *r = sp_str_alloc(len);
1862
+ memcpy(r, buf, len);
1863
+ r[len] = 0;
1864
+ free(buf);
1865
+ return r;
1866
+ }
1867
+ /* IO#read(n): read up to n bytes from the current position. Returns NULL
1868
+ (nil) at EOF for a positive n, "" for n == 0, and the whole rest for a
1869
+ negative n (treated as the no-count read). A short read produces a
1870
+ string of the bytes actually read. */
1871
+ static inline const char *sp_File_read_n(sp_File *f, mrb_int n) {
1872
+ if (!f || !f->fp) return NULL;
1873
+ if (n < 0) return sp_File_read(f);
1874
+ if (n == 0) return sp_str_empty;
1875
+ char *r = sp_str_alloc((size_t)n);
1876
+ size_t got = fread(r, 1, (size_t)n, f->fp);
1877
+ if (got == 0) return NULL;
1878
+ if ((mrb_int)got == n) { r[got] = 0; return r; }
1879
+ char *s = sp_str_alloc(got);
1880
+ memcpy(s, r, got);
1881
+ s[got] = 0;
1882
+ return s;
1883
+ }
1884
+ static inline const char *sp_File_path(sp_File *f) { return f && f->path ? f->path : sp_str_empty; }
1885
+
593
1886
  /* Array#inspect for each typed array: `[elem1, elem2, ...]` with each
594
1887
  element rendered via its own primitive inspect. Matches CRuby's
595
1888
  Array#inspect output byte-for-byte. Returns a GC-managed C string. */
596
- static const char*sp_IntArray_inspect(sp_IntArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_int_to_s(a->data[a->start+i]));}sp_String_append(s,"]");return s->data;}
597
- static const char*sp_FloatArray_inspect(sp_FloatArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_float_inspect(a->data[i]));}sp_String_append(s,"]");return s->data;}
598
- static const char*sp_StrArray_inspect(sp_StrArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_str_inspect(a->data[i]));}sp_String_append(s,"]");return s->data;}
1889
+ /* The inspect helpers NULL-guard their receiver: a NULL array can
1890
+ reach here when an unresolved call (e.g. an unsupported Array method)
1891
+ emits a 0 placeholder that flows into `.inspect`, and dereferencing
1892
+ a->len would segfault. Rendering "[]" stops the crash and degrades to
1893
+ the same shape as the empty-array case. */
1894
+ static const char*sp_IntArray_inspect(sp_IntArray*a){if(!a)return "[]";sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_int_to_s(a->data[a->start+i]));}sp_String_append(s,"]");return s->data;}
1895
+ static const char*sp_FloatArray_inspect(sp_FloatArray*a){if(!a)return "[]";sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_float_inspect(a->data[i]));}sp_String_append(s,"]");return s->data;}
1896
+ /* Array#join for float arrays -- each element via the Ruby-faithful
1897
+ sp_float_to_s ("1.0", not "1"). Mirrors sp_IntArray_join exactly: build in a
1898
+ malloc buffer, return an sp_str_alloc'd copy. (Not sp_String#data, whose owner
1899
+ isn't GC-rooted across the return.) sp_float_to_s's result is copied
1900
+ immediately, before the next call can reuse its buffer. */
1901
+ static const char*sp_FloatArray_join(sp_FloatArray*a,const char*sep){size_t sl=strlen(sep),cap=256;char*buf=(char*)malloc(cap);size_t len=0;if(a){for(mrb_int i=0;i<a->len;i++){if(i>0){if(len+sl>=cap){cap*=2;buf=(char*)realloc(buf,cap);}memcpy(buf+len,sep,sl);len+=sl;}const char*es=sp_float_to_s(a->data[i]);size_t el=strlen(es);if(len+el>=cap){while(len+el>=cap)cap*=2;buf=(char*)realloc(buf,cap);}memcpy(buf+len,es,el);len+=el;}}buf[len]=0;char*r=sp_str_alloc(len);memcpy(r,buf,len);free(buf);return r;}
1902
+ static mrb_bool sp_FloatArray_eq(sp_FloatArray*a,sp_FloatArray*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++)if(a->data[i]!=b->data[i])return FALSE;return TRUE;}
1903
+ static const char*sp_StrArray_inspect(sp_StrArray*a){if(!a)return "[]";sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_str_inspect(a->data[i]));}sp_String_append(s,"]");return s->data;}
1904
+ static mrb_bool sp_StrArray_eq(sp_StrArray*a,sp_StrArray*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++)if(!sp_str_eq(a->data[i],b->data[i]))return FALSE;return TRUE;}
599
1905
  /* Symbol arrays share the IntArray representation (sp_sym = mrb_int),
600
1906
  but each element is rendered as ":name" via sp_sym_to_s. */
601
- static const char*sp_SymArray_inspect(sp_IntArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,":");sp_String_append(s,sp_sym_to_s((sp_sym)a->data[a->start+i]));}sp_String_append(s,"]");return s->data;}
1907
+ static inline const char*sp_SymArray_inspect(sp_IntArray*a){if(!a)return "[]";sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,":");sp_String_append(s,sp_sym_to_s((sp_sym)a->data[a->start+i]));}sp_String_append(s,"]");return s->data;}
602
1908
  /* PtrArray elements are object pointers without a per-element class
603
1909
  tag, so we render them as `#<Object>` rather than recursing. */
604
- static const char*sp_PtrArray_inspect(sp_PtrArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,"#<Object>");}sp_String_append(s,"]");return s->data;}
1910
+ static const char*sp_PtrArray_inspect(sp_PtrArray*a){if(!a)return "[]";sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,"#<Object>");}sp_String_append(s,"]");return s->data;}
1911
+ /* Issue #851: Hash#inspect for typed-hash variants beyond
1912
+ sym_int_hash. Renders Ruby's `{"k"=>v, ...}` (string keys),
1913
+ `{42=>"v", ...}` (int keys), or `{:k=>v, ...}` (sym keys but
1914
+ non-int value, since the bare `k: v` shorthand only applies
1915
+ when values are inspectable as one-liners — match CRuby). */
1916
+ static const char*sp_StrIntHash_inspect(sp_StrIntHash*h){sp_String*s=sp_String_new("{");if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_str_inspect(h->order[i]));sp_String_append(s,"=>");sp_String_append(s,sp_int_to_s(sp_StrIntHash_get(h,h->order[i])));}}sp_String_append(s,"}");return s->data;}
1917
+ /* Hash#to_proc lookup fn — cap is the hash, args[0] the string key. */
1918
+ static mrb_int sp_StrIntHash_proc_fn(void *cap, mrb_int *args) { return sp_StrIntHash_get((sp_StrIntHash *)cap, (const char *)(uintptr_t)args[0]); }
1919
+ static const char*sp_StrStrHash_inspect(sp_StrStrHash*h){sp_String*s=sp_String_new("{");if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_str_inspect(h->order[i]));sp_String_append(s,"=>");sp_String_append(s,sp_str_inspect(sp_StrStrHash_get(h,h->order[i])));}}sp_String_append(s,"}");return s->data;}
1920
+ static const char*sp_IntStrHash_inspect(sp_IntStrHash*h){sp_String*s=sp_String_new("{");if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_int_to_s(h->order[i]));sp_String_append(s,"=>");sp_String_append(s,sp_str_inspect(sp_IntStrHash_get(h,h->order[i])));}}sp_String_append(s,"}");return s->data;}
1921
+ static const char*sp_IntIntHash_inspect(sp_IntIntHash*h){sp_String*s=sp_String_new("{");if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_int_to_s(h->order[i]));sp_String_append(s,"=>");sp_String_append(s,sp_int_to_s(sp_IntIntHash_get(h,h->order[i])));}}sp_String_append(s,"}");return s->data;}
605
1922
  /* Nested-array inspect: when codegen knows the ptr_array's element
606
1923
  type is one of the four built-in T_array shapes, recurse into the
607
- matching primitive inspect (issue #169). */
1924
+ matching primitive inspect . */
608
1925
  static const char*sp_IntArrayPtrArray_inspect(sp_PtrArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_IntArray_inspect((sp_IntArray*)a->data[i]));}sp_String_append(s,"]");return s->data;}
1926
+ /* Issue #742: Array#combination(k) on int_array -- emit all
1927
+ k-element ordered combinations as a PtrArray of IntArrays. */
1928
+ static void sp_int_combination_recur(sp_IntArray*src,mrb_int start,mrb_int k,sp_IntArray*acc,sp_PtrArray*out){if(k==0){sp_IntArray*cp=sp_IntArray_new();for(mrb_int i=0;i<acc->len;i++)sp_IntArray_push(cp,acc->data[acc->start+i]);sp_PtrArray_push(out,cp);return;}for(mrb_int i=start;i<=src->len-k;i++){sp_IntArray_push(acc,src->data[src->start+i]);sp_int_combination_recur(src,i+1,k-1,acc,out);acc->len--;}}
1929
+ static sp_PtrArray*sp_IntArray_combination(sp_IntArray*a,mrb_int k){sp_PtrArray*out=sp_PtrArray_new();if(!a||k<0||k>a->len)return out;sp_IntArray*acc=sp_IntArray_new();sp_int_combination_recur(a,0,k,acc,out);return out;}
1930
+ /* repeated_combination: like combination but an index may repeat, so the
1931
+ recursion restarts at `i` rather than `i+1`. */
1932
+ static void sp_int_repeated_combination_recur(sp_IntArray*src,mrb_int start,mrb_int k,sp_IntArray*acc,sp_PtrArray*out){if(k==0){sp_IntArray*cp=sp_IntArray_new();for(mrb_int i=0;i<acc->len;i++)sp_IntArray_push(cp,acc->data[acc->start+i]);sp_PtrArray_push(out,cp);return;}for(mrb_int i=start;i<src->len;i++){sp_IntArray_push(acc,src->data[src->start+i]);sp_int_repeated_combination_recur(src,i,k-1,acc,out);acc->len--;}}
1933
+ static sp_PtrArray*sp_IntArray_repeated_combination(sp_IntArray*a,mrb_int k){sp_PtrArray*out=sp_PtrArray_new();if(!a||k<0)return out;sp_IntArray*acc=sp_IntArray_new();sp_int_repeated_combination_recur(a,0,k,acc,out);return out;}
1934
+
1935
+ /* Cartesian product of two int arrays. Returns a PtrArray of
1936
+ 2-element IntArrays. */
1937
+ static sp_PtrArray *sp_IntArray_product(sp_IntArray *a, sp_IntArray *b) {
1938
+ sp_PtrArray *out = sp_PtrArray_new();
1939
+ if (!a || !b) return out;
1940
+ for (mrb_int i = 0; i < a->len; i++) {
1941
+ for (mrb_int j = 0; j < b->len; j++) {
1942
+ sp_IntArray *pair = sp_IntArray_new();
1943
+ sp_IntArray_push(pair, a->data[a->start + i]);
1944
+ sp_IntArray_push(pair, b->data[b->start + j]);
1945
+ sp_PtrArray_push(out, pair);
1946
+ }
1947
+ }
1948
+ return out;
1949
+ }
1950
+ /* Array#permutation(k) -- ordered k-permutations. */
1951
+ static void sp_int_permutation_recur(sp_IntArray*src,mrb_int k,sp_IntArray*used,sp_IntArray*acc,sp_PtrArray*out){if(k==0){sp_IntArray*cp=sp_IntArray_new();for(mrb_int i=0;i<acc->len;i++)sp_IntArray_push(cp,acc->data[acc->start+i]);sp_PtrArray_push(out,cp);return;}for(mrb_int i=0;i<src->len;i++){if(used->data[used->start+i])continue;used->data[used->start+i]=1;sp_IntArray_push(acc,src->data[src->start+i]);sp_int_permutation_recur(src,k-1,used,acc,out);acc->len--;used->data[used->start+i]=0;}}
1952
+ static sp_PtrArray*sp_IntArray_permutation(sp_IntArray*a,mrb_int k){sp_PtrArray*out=sp_PtrArray_new();if(!a||k<0||k>a->len)return out;sp_IntArray*used=sp_IntArray_new();for(mrb_int i=0;i<a->len;i++)sp_IntArray_push(used,0);sp_IntArray*acc=sp_IntArray_new();sp_int_permutation_recur(a,k,used,acc,out);return out;}
609
1953
  static const char*sp_FloatArrayPtrArray_inspect(sp_PtrArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_FloatArray_inspect((sp_FloatArray*)a->data[i]));}sp_String_append(s,"]");return s->data;}
610
1954
  static const char*sp_StrArrayPtrArray_inspect(sp_PtrArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_StrArray_inspect((sp_StrArray*)a->data[i]));}sp_String_append(s,"]");return s->data;}
1955
+ /* sp_PolyArrayPtrArray_inspect lives below sp_PolyArray_inspect's
1956
+ forward declaration (sp_PolyArray isn't defined until ~2542). */
611
1957
  static const char*sp_SymArrayPtrArray_inspect(sp_PtrArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_SymArray_inspect((sp_IntArray*)a->data[i]));}sp_String_append(s,"]");return s->data;}
1958
+ /* issue #526: join for a sp_PtrArray of sp_String* (mutable_str_ptr_array).
1959
+ Sibling to sp_StrArray_join, but takes advantage of sp_String's known
1960
+ length: two-pass — sum the exact total, sp_str_alloc once, then memcpy
1961
+ each element by its s->len (preserves embedded NULs). Avoids the
1962
+ realloc-grow loop's leak-on-failure and long-separator overflow
1963
+ risks, and skips an intermediate malloc'd buffer. NULL entries
1964
+ contribute zero length. */
1965
+ static const char*sp_PtrArray_str_join(sp_PtrArray*a,const char*sep){mrb_int al=a->len;if(al==0)return sp_str_empty;size_t sl=strlen(sep),total=0;for(mrb_int i=0;i<al;i++){if(i>0)total+=sl;sp_String*s=(sp_String*)a->data[i];if(s)total+=(size_t)s->len;}char*r=sp_str_alloc(total);size_t cur=0;for(mrb_int i=0;i<al;i++){if(i>0){memcpy(r+cur,sep,sl);cur+=sl;}sp_String*s=(sp_String*)a->data[i];if(s&&s->len){memcpy(r+cur,s->data,(size_t)s->len);cur+=(size_t)s->len;}}return r;}
1966
+
1967
+ #ifdef __FreeBSD__
1968
+
1969
+ #define re_exec spinel_re_exec
1970
+
1971
+ #define MAP_NORESERVE 0
1972
+
1973
+ #endif
612
1974
 
613
1975
  /* Regexp engine (link with libspre.a from lib/regexp/) */
614
1976
  typedef struct mrb_regexp_pattern mrb_regexp_pattern;
@@ -637,18 +1999,71 @@ static const char *sp_re_match_str = NULL;
637
1999
  static const char *sp_re_match_pre = NULL;
638
2000
  static const char *sp_re_match_post = NULL;
639
2001
 
2002
+ /* ARGV runtime: argv[i] strings are dup'd via sp_str_dup_external on
2003
+ main() entry, which allocates from the str-heap with mark byte
2004
+ 0xfe. Without explicit marking they get reaped on the first
2005
+ sp_str_sweep, leaving sp_argv.data[i] as a dangling pointer that
2006
+ later `ARGV[i]` reads dereference. The exact length boundary
2007
+ triggering the segfault depends on malloc's reuse pattern (so the
2008
+ bug surfaces non-deterministically by string length), but the
2009
+ underlying issue is unconditional. */
2010
+ typedef struct{const char**data;mrb_int len;}sp_Argv;
2011
+ static sp_Argv sp_argv;
2012
+
2013
+ /* Mark active in-flight exception messages. Most raises pass string
2014
+ literals (rodata, marker byte ≠ 0xfe → no-op for sp_mark_string),
2015
+ but raises that build messages dynamically via sp_sprintf (e.g.
2016
+ sp_str_to_i_strict's `"invalid value for Integer(): \"%s\""`)
2017
+ hand a heap-allocated string to sp_raise_cls. Without marking, a
2018
+ GC cycle between the raise and the rescue handler reading the
2019
+ message would sweep the message and leave sp_exc_msg[i] dangling.
2020
+ The helper itself is defined further down (after sp_exc_msg /
2021
+ sp_exc_top are declared); only the prototype lives here. */
2022
+ static void sp_mark_in_flight_exceptions(void);
2023
+
640
2024
  /* Mark the regex globals as live during GC. Each holds a pointer to a
641
2025
  string allocated via sp_str_alloc_raw on the str-heap; without this
642
2026
  sp_str_sweep would reap them on the next collect, leaving dangling
643
2027
  pointers in $1..$9, $&, $`, $'. sp_mark_string is null-safe and
644
2028
  no-ops on non-heap strings (the empty-string default of
645
2029
  sp_re_last_str), so it's safe to call unconditionally. */
2030
+ /* sp_fiber_root.storage needs marking here because sp_fiber_root is
2031
+ a static (not sp_gc_alloc'd), so its normal sp_Fiber_scan never
2032
+ runs via the heap walker. Without this, top-level `Fiber[:k] = v`
2033
+ writes get prematurely collected. The forward declaration is
2034
+ needed because sp_fiber_root is defined further down in the
2035
+ Fiber runtime block. */
2036
+ /* External linkage: lib/sp_gc.c's sp_gc_mark_all reaches this by name. */
646
2037
  static void sp_re_mark_globals(void) {
647
2038
  sp_mark_string(sp_re_last_str);
648
2039
  for (int i = 0; i < 10; i++) sp_mark_string(sp_re_captures[i]);
649
2040
  sp_mark_string(sp_re_match_str);
650
2041
  sp_mark_string(sp_re_match_pre);
651
2042
  sp_mark_string(sp_re_match_post);
2043
+ for (mrb_int i = 0; i < sp_argv.len; i++) sp_mark_string(sp_argv.data[i]);
2044
+ sp_mark_in_flight_exceptions();
2045
+ sp_mark_fiber_root_storage();
2046
+ }
2047
+
2048
+ /* Hand the collector (lib/sp_gc.c) this TU's root-marking and string-heap
2049
+ sweep. Runs before main, so the hooks are set before the first
2050
+ allocation can trigger a collection. */
2051
+ __attribute__((constructor)) static void sp_gc_install_tu_hooks(void) {
2052
+ sp_gc_mark_globals_hook = sp_re_mark_globals;
2053
+ sp_gc_str_sweep_hook = sp_str_sweep;
2054
+ }
2055
+
2056
+ /* `$+` / `$LAST_PAREN_MATCH` — contents of the highest-indexed group
2057
+ that participated in the match. Walks sp_re_captures[] from 9 down
2058
+ and returns the first non-NULL entry. NULL when no group matched
2059
+ (codegen ternary falls back to ""). Matches CRuby's behaviour:
2060
+ for /(a)(b)?/ matching "a", $+ is "a"; for /(a)(b)/ matching "ab",
2061
+ $+ is "b". */
2062
+ static const char *sp_re_last_paren_match(void) {
2063
+ for (int i = 9; i >= 1; i--) {
2064
+ if (sp_re_captures[i]) return sp_re_captures[i];
2065
+ }
2066
+ return NULL;
652
2067
  }
653
2068
 
654
2069
  static void sp_re_set_captures(const char *str, int *caps, int ncaps) {
@@ -684,12 +2099,50 @@ static void sp_re_set_captures(const char *str, int *caps, int ncaps) {
684
2099
  }
685
2100
  }
686
2101
 
2102
+ /* `=~` returns the match position (0-indexed) or -1 on miss.
2103
+ Codegen's regex truthy check (regex_match_call_node? arm in
2104
+ compile_cond_expr) compares against -1 so match-at-position-0
2105
+ is correctly truthy. Direct value use lines up with CRuby's
2106
+ `String#=~` int semantics: "abc" =~ /b/ -> 1, not 2. */
687
2107
  static mrb_int sp_re_match(mrb_regexp_pattern *pat, const char *str) {
688
2108
  int64_t slen = (int64_t)strlen(str);
689
2109
  int ncaps = 32;
690
2110
  int n = re_exec(pat, str, slen, 0, sp_re_caps, ncaps);
691
- if (n > 0) { sp_re_set_captures(str, sp_re_caps, n/2); return sp_re_caps[0] + 1; }
692
- return 0;
2111
+ if (n > 0) { sp_re_set_captures(str, sp_re_caps, n/2); return sp_re_caps[0]; }
2112
+ /* Issue #848: clear backrefs on no-match so a subsequent `$1`
2113
+ reads as nil rather than the previous match's group. */
2114
+ for (int i = 0; i < 10; i++) sp_re_captures[i] = NULL;
2115
+ sp_re_last_str = NULL;
2116
+ sp_re_match_str = NULL;
2117
+ sp_re_match_pre = NULL;
2118
+ sp_re_match_post = NULL;
2119
+ return -1;
2120
+ }
2121
+
2122
+ /* `s.rindex(regex)` — last match start, in BYTE offset (matches
2123
+ the way sp_str_rindex reports indices for plain-string search;
2124
+ codepoint translation would require a UTF-8 walk and the
2125
+ handful of call sites that consume rindex don't need it).
2126
+ Walks forward through successive matches and remembers the
2127
+ latest start. Issue #504: previously the codegen routed
2128
+ `s.rindex(/re/)` to sp_str_rindex(s, 0) and SEGV'd at
2129
+ strlen(NULL). Returns -1 on no match. */
2130
+ static mrb_int sp_re_rindex(mrb_regexp_pattern *pat, const char *str) {
2131
+ int64_t slen = (int64_t)strlen(str);
2132
+ int caps[2];
2133
+ int64_t pos = 0;
2134
+ mrb_int last = -1;
2135
+ while (pos <= slen) {
2136
+ int n = re_exec(pat, str, slen, pos, caps, 2);
2137
+ if (n <= 0) break;
2138
+ last = caps[0];
2139
+ /* Advance past the match; for zero-width matches step by 1
2140
+ to avoid an infinite loop. */
2141
+ int64_t next = caps[1];
2142
+ if (next <= pos) next = pos + 1;
2143
+ pos = next;
2144
+ }
2145
+ return last;
693
2146
  }
694
2147
 
695
2148
  static mrb_bool sp_re_match_p(mrb_regexp_pattern *pat, const char *str) {
@@ -698,9 +2151,76 @@ static mrb_bool sp_re_match_p(mrb_regexp_pattern *pat, const char *str) {
698
2151
  return re_exec(pat, str, slen, 0, caps, 2) > 0;
699
2152
  }
700
2153
 
2154
+ /* Issue #869: Regexp#match?(str, pos) starts matching at byte
2155
+ offset `pos`. Negative pos counts from the end (CRuby compat).
2156
+ Out-of-range pos returns false. */
2157
+ static mrb_bool sp_re_match_p_at(mrb_regexp_pattern *pat, const char *str, mrb_int pos) {
2158
+ int64_t slen = (int64_t)strlen(str);
2159
+ if (pos < 0) pos += slen;
2160
+ if (pos < 0 || pos > slen) return FALSE;
2161
+ int caps[2];
2162
+ return re_exec(pat, str, slen, (mrb_int)pos, caps, 2) > 0;
2163
+ }
2164
+ /* String#match?(/re/, pos) — pos is a codepoint index (CRuby semantics),
2165
+ unlike Regexp#match?(str, pos) which uses byte offset. Convert the
2166
+ codepoint index to a byte offset before dispatching to re_exec. */
2167
+ static mrb_bool sp_str_re_match_p_at(mrb_regexp_pattern *pat, const char *str, mrb_int cpos) {
2168
+ mrb_int cl = sp_str_length(str);
2169
+ if (cpos < 0) cpos += cl;
2170
+ if (cpos < 0 || cpos > cl) return FALSE;
2171
+ size_t boff = sp_utf8_byte_offset(str, cpos);
2172
+ int64_t slen = (int64_t)strlen(str);
2173
+ int caps[2];
2174
+ return re_exec(pat, str, slen, (mrb_int)boff, caps, 2) > 0;
2175
+ }
2176
+
2177
+ /* Issue #855: expand `\1`..`\9` / `\&` / `\0` backreferences in
2178
+ the replacement string against the current caps[] array. `\\`
2179
+ is a literal backslash. Writes to *out_io at *olen_io, growing
2180
+ *out_io / *cap_io as needed. */
2181
+ static void sp_re_expand_rep(char **out_io, size_t *olen_io, size_t *cap_io,
2182
+ const char *rep, size_t rlen,
2183
+ const char *src, int *caps, int ncaps) {
2184
+ size_t olen = *olen_io;
2185
+ char *out = *out_io;
2186
+ size_t cap = *cap_io;
2187
+ size_t i = 0;
2188
+ while (i < rlen) {
2189
+ char c = rep[i];
2190
+ if (c == '\\' && i + 1 < rlen) {
2191
+ char d = rep[i+1];
2192
+ if ((d >= '0' && d <= '9') || d == '&') {
2193
+ int gi = (d == '&') ? 0 : (d - '0');
2194
+ if (gi*2 + 1 < ncaps && caps[gi*2] >= 0 && caps[gi*2+1] >= 0) {
2195
+ int g_len = caps[gi*2+1] - caps[gi*2];
2196
+ if (olen + g_len + 1 >= cap) { cap = (olen + g_len) * 2 + 64; out = (char*)realloc(out, cap); }
2197
+ memcpy(out+olen, src + caps[gi*2], g_len);
2198
+ olen += g_len;
2199
+ }
2200
+ i += 2;
2201
+ continue;
2202
+ } else if (d == '\\') {
2203
+ if (olen + 1 >= cap) { cap = cap * 2 + 64; out = (char*)realloc(out, cap); }
2204
+ out[olen++] = '\\';
2205
+ i += 2;
2206
+ continue;
2207
+ }
2208
+ }
2209
+ if (olen + 1 >= cap) { cap = cap * 2 + 64; out = (char*)realloc(out, cap); }
2210
+ out[olen++] = c;
2211
+ i++;
2212
+ }
2213
+ *out_io = out; *olen_io = olen; *cap_io = cap;
2214
+ }
2215
+
701
2216
  static const char *sp_re_gsub(mrb_regexp_pattern *pat, const char *str, const char *rep) {
702
2217
  int64_t slen = (int64_t)strlen(str); size_t rlen = strlen(rep);
703
- size_t cap = slen * 2 + 64; char *out = sp_str_alloc_raw(cap); size_t olen = 0;
2218
+ size_t cap = slen * 2 + rlen * 4 + 64;
2219
+ /* Build into a plain malloc scratch: the buffer is grown with realloc
2220
+ here and inside sp_re_expand_rep, which is only valid on a real
2221
+ malloc base (a sp_str body pointer is offset past its header). The
2222
+ final string is allocated at the exact length below. */
2223
+ char *out = (char *)malloc(cap); size_t olen = 0;
704
2224
  int64_t pos = 0; int caps[64];
705
2225
  while (pos <= slen) {
706
2226
  int n = re_exec(pat, str, slen, pos, caps, 64);
@@ -708,13 +2228,127 @@ static const char *sp_re_gsub(mrb_regexp_pattern *pat, const char *str, const ch
708
2228
  size_t before = caps[0] - pos;
709
2229
  if (olen+before+rlen >= cap) { cap = (olen+before+rlen)*2+64; out = (char*)realloc(out, cap); }
710
2230
  memcpy(out+olen, str+pos, before); olen += before;
711
- memcpy(out+olen, rep, rlen); olen += rlen;
712
- pos = caps[1]; if (caps[0] == caps[1]) pos++;
2231
+ sp_re_expand_rep(&out, &olen, &cap, rep, rlen, str, caps, n);
2232
+ if (caps[0] == caps[1]) {
2233
+ /* Zero-width match (/^/, /$/, /\b/, an empty pattern): Ruby inserts
2234
+ the replacement before the char at this position, keeps that char,
2235
+ and advances past it. Copy the char and step by one so the scan
2236
+ makes progress without dropping it or spinning on the same spot. */
2237
+ if (caps[1] < slen) {
2238
+ if (olen+1 >= cap) { cap = olen*2+64; out = (char*)realloc(out, cap); }
2239
+ out[olen++] = str[caps[1]];
2240
+ }
2241
+ pos = caps[1] + 1;
2242
+ } else {
2243
+ pos = caps[1];
2244
+ }
2245
+ }
2246
+ /* pos can land at slen+1 after a zero-width match at the end; guard the
2247
+ tail copy so `slen - pos` doesn't underflow size_t. */
2248
+ if (pos < slen) {
2249
+ size_t rest = slen - pos;
2250
+ if (olen+rest+1 >= cap) { cap = olen+rest+64; out = (char*)realloc(out, cap); }
2251
+ memcpy(out+olen, str+pos, rest); olen += rest;
2252
+ }
2253
+ /* Emit a string sized to exactly the bytes written (sp_str_alloc sets
2254
+ the length and null-terminates); release the scratch. */
2255
+ char *res = sp_str_alloc(olen);
2256
+ memcpy(res, out, olen);
2257
+ free(out);
2258
+ return res;
2259
+ }
2260
+
2261
+ /* String#gsub(regex, hash) — per-match hash lookup form. CRuby's
2262
+ * semantics: each matched substring is looked up as a key in the
2263
+ * hash; the value (if present) is the replacement, otherwise the
2264
+ * matched substring is dropped (CRuby returns "", not the match).
2265
+ * Used by html_escape / json_escape idioms (gsub(/[&<>]/, ESCAPES)). */
2266
+ static const char *sp_re_gsub_str_str_hash(mrb_regexp_pattern *pat, const char *str, sp_StrStrHash *h) {
2267
+ int64_t slen = (int64_t)strlen(str);
2268
+ /* malloc scratch (realloc-safe); exact-sized string emitted below. */
2269
+ size_t cap = slen * 2 + 64; char *out = (char *)malloc(cap); size_t olen = 0;
2270
+ int64_t pos = 0; int caps[64];
2271
+ while (pos <= slen) {
2272
+ int n = re_exec(pat, str, slen, pos, caps, 64);
2273
+ if (n <= 0 || caps[0] < 0) break;
2274
+ size_t before = caps[0] - pos;
2275
+ int mlen = caps[1] - caps[0];
2276
+ char keybuf[64];
2277
+ char *key = mlen < (int)sizeof(keybuf) ? keybuf : (char *)malloc(mlen + 1);
2278
+ memcpy(key, str + caps[0], mlen);
2279
+ key[mlen] = 0;
2280
+ const char *rep = sp_StrStrHash_has_key(h, key) ? sp_StrStrHash_get(h, key) : "";
2281
+ size_t rlen = strlen(rep);
2282
+ if (olen + before + rlen >= cap) { cap = (olen + before + rlen) * 2 + 64; out = (char *)realloc(out, cap); }
2283
+ memcpy(out + olen, str + pos, before); olen += before;
2284
+ memcpy(out + olen, rep, rlen); olen += rlen;
2285
+ if (key != keybuf) free(key);
2286
+ if (caps[0] == caps[1]) {
2287
+ /* Zero-width match: keep the source char at this position and step
2288
+ past it (see sp_re_gsub for the rationale). */
2289
+ if (caps[1] < slen) {
2290
+ if (olen + 1 >= cap) { cap = olen * 2 + 64; out = (char *)realloc(out, cap); }
2291
+ out[olen++] = str[caps[1]];
2292
+ }
2293
+ pos = caps[1] + 1;
2294
+ } else {
2295
+ pos = caps[1];
2296
+ }
2297
+ }
2298
+ if (pos < slen) {
2299
+ size_t rest = slen - pos;
2300
+ if (olen + rest >= cap) { cap = olen + rest + 1; out = (char *)realloc(out, cap); }
2301
+ memcpy(out + olen, str + pos, rest); olen += rest;
713
2302
  }
714
- size_t rest = slen - pos;
715
- if (olen+rest >= cap) { cap = olen+rest+1; out = (char*)realloc(out, cap); }
716
- memcpy(out+olen, str+pos, rest); olen += rest;
717
- out[olen] = 0; return out;
2303
+ char *res = sp_str_alloc(olen);
2304
+ memcpy(res, out, olen);
2305
+ free(out);
2306
+ return res;
2307
+ }
2308
+
2309
+ /* Issue #910: sub(regex, hash) — same lookup semantics as
2310
+ sp_re_gsub_str_str_hash but only the first match. */
2311
+ static const char *sp_re_sub_str_str_hash(mrb_regexp_pattern *pat, const char *str, sp_StrStrHash *h) {
2312
+ int64_t slen = (int64_t)strlen(str);
2313
+ int caps[64];
2314
+ int n = re_exec(pat, str, slen, 0, caps, 64);
2315
+ if (n <= 0 || caps[0] < 0) return str;
2316
+ int mlen = caps[1] - caps[0];
2317
+ char keybuf[64];
2318
+ char *key = mlen < (int)sizeof(keybuf) ? keybuf : (char *)malloc(mlen + 1);
2319
+ memcpy(key, str + caps[0], mlen);
2320
+ key[mlen] = 0;
2321
+ const char *rep = (h && sp_StrStrHash_has_key(h, key)) ? sp_StrStrHash_get(h, key) : "";
2322
+ size_t rlen = strlen(rep);
2323
+ size_t rest = slen - caps[1];
2324
+ size_t total = caps[0] + rlen + rest;
2325
+ char *out = sp_str_alloc_raw(total + 1);
2326
+ memcpy(out, str, caps[0]);
2327
+ memcpy(out + caps[0], rep, rlen);
2328
+ memcpy(out + caps[0] + rlen, str + caps[1], rest);
2329
+ out[total] = 0;
2330
+ if (key != keybuf) free(key);
2331
+ return out;
2332
+ }
2333
+
2334
+ /* Issue #910: sub(string, hash) — literal-substring pattern
2335
+ with a hash replacement. Replaces only the first match. */
2336
+ static const char *sp_str_sub_str_str_hash(const char *str, const char *pat, sp_StrStrHash *h) {
2337
+ if (!str || !pat) return str;
2338
+ const char *found = strstr(str, pat);
2339
+ if (!found) return str;
2340
+ size_t before = (size_t)(found - str);
2341
+ size_t plen = strlen(pat);
2342
+ const char *rep = (h && sp_StrStrHash_has_key(h, pat)) ? sp_StrStrHash_get(h, pat) : "";
2343
+ size_t rlen = strlen(rep);
2344
+ size_t rest = strlen(str) - before - plen;
2345
+ size_t total = before + rlen + rest;
2346
+ char *out = sp_str_alloc_raw(total + 1);
2347
+ memcpy(out, str, before);
2348
+ memcpy(out + before, rep, rlen);
2349
+ memcpy(out + before + rlen, found + plen, rest);
2350
+ out[total] = 0;
2351
+ return out;
718
2352
  }
719
2353
 
720
2354
  static const char *sp_re_sub(mrb_regexp_pattern *pat, const char *str, const char *rep) {
@@ -722,12 +2356,21 @@ static const char *sp_re_sub(mrb_regexp_pattern *pat, const char *str, const cha
722
2356
  int caps[64];
723
2357
  int n = re_exec(pat, str, slen, 0, caps, 64);
724
2358
  if (n <= 0 || caps[0] < 0) return str;
725
- size_t out_len = caps[0] + rlen + (slen - caps[1]);
726
- char *out = sp_str_alloc_raw(out_len + 1);
2359
+ /* Issue #855: expand `\1`..`\9` / `\&` from rep against caps. */
2360
+ size_t cap = caps[0] + rlen * 4 + (slen - caps[1]) + 64;
2361
+ /* malloc scratch: sp_re_expand_rep and the tail grow it with realloc,
2362
+ which needs a real malloc base. Exact-sized string emitted below. */
2363
+ char *out = (char *)malloc(cap);
727
2364
  memcpy(out, str, caps[0]);
728
- memcpy(out+caps[0], rep, rlen);
729
- memcpy(out+caps[0]+rlen, str+caps[1], slen-caps[1]+1);
730
- return out;
2365
+ size_t olen = caps[0];
2366
+ sp_re_expand_rep(&out, &olen, &cap, rep, rlen, str, caps, n);
2367
+ size_t rest = slen - caps[1];
2368
+ if (olen + rest + 1 >= cap) { cap = olen + rest + 64; out = (char*)realloc(out, cap); }
2369
+ memcpy(out+olen, str+caps[1], rest); olen += rest;
2370
+ char *res = sp_str_alloc(olen);
2371
+ memcpy(res, out, olen);
2372
+ free(out);
2373
+ return res;
731
2374
  }
732
2375
 
733
2376
  static sp_StrArray *sp_re_scan(mrb_regexp_pattern *pat, const char *str) {
@@ -755,6 +2398,20 @@ static sp_StrArray *sp_re_split(mrb_regexp_pattern *pat, const char *str) {
755
2398
  }
756
2399
  int len = caps[0] - pos; char *m = sp_str_alloc_raw(len+1);
757
2400
  memcpy(m, str+pos, len); m[len] = 0; sp_StrArray_push(arr, m);
2401
+ /* Issue #856: when the splitter regex has capture groups,
2402
+ Ruby splices each captured substring into the result
2403
+ between the surrounding segments (caps[2..n-1] hold groups
2404
+ 1..(n/2-1); group 0 is the whole match). */
2405
+ for (int gi = 1; gi * 2 + 1 < n; gi++) {
2406
+ if (caps[gi*2] >= 0 && caps[gi*2+1] >= 0) {
2407
+ int glen = caps[gi*2+1] - caps[gi*2];
2408
+ char *gm = sp_str_alloc_raw(glen+1);
2409
+ memcpy(gm, str + caps[gi*2], glen); gm[glen] = 0;
2410
+ sp_StrArray_push(arr, gm);
2411
+ } else {
2412
+ sp_StrArray_push(arr, sp_str_empty);
2413
+ }
2414
+ }
758
2415
  pos = caps[1]; if (caps[0] == caps[1]) pos++;
759
2416
  }
760
2417
  return arr;
@@ -762,39 +2419,47 @@ static sp_StrArray *sp_re_split(mrb_regexp_pattern *pat, const char *str) {
762
2419
 
763
2420
  /* NaN-boxed polymorphic value */
764
2421
  typedef uint64_t sp_RbValue;
765
- #define SP_TAG_INT 0
766
- #define SP_TAG_STR 1
767
- #define SP_TAG_FLT 2
2422
+ #define SP_TAG_INT 0
2423
+ #define SP_TAG_STR 1
2424
+ #define SP_TAG_FLT 2
768
2425
  #define SP_TAG_BOOL 3
769
- #define SP_TAG_NIL 4
770
- #define SP_TAG_OBJ 5
771
- #define SP_TAG_SYM 6
2426
+ #define SP_TAG_NIL 4
2427
+ #define SP_TAG_OBJ 5
2428
+ #define SP_TAG_SYM 6
2429
+ /* Class values boxed into a poly slot (e.g.
2430
+ as ancestors-array elements). cls_id of the sp_RbVal carries
2431
+ the boxed sp_Class's cls_id directly so unboxing is just a
2432
+ field read. */
2433
+ #define SP_TAG_CLASS 7
2434
+ /* Encoding values boxed into a poly slot. v.s carries the canonical
2435
+ encoding name (`"UTF-8"` in Spinel's current runtime model). */
2436
+ #define SP_TAG_ENCODING 8
772
2437
  /* Negative cls_id values let SP_TAG_OBJ also carry built-in pointer
773
2438
  types (IntArray, FloatArray, ...) — avoids minting a new SP_TAG_*
774
2439
  per type. Non-negative cls_id stays an index into the user-class
775
2440
  table as before. The element-type tag and the array cls_id are
776
2441
  paired by `array_cls_id = -element_tag - 1`. */
777
2442
  #define SP_BUILTIN_ARRAY_OF(tag) (-(tag) - 1)
778
- #define SP_BUILTIN_INT_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_INT) /* -1 */
779
- #define SP_BUILTIN_STR_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_STR) /* -2 */
780
- #define SP_BUILTIN_FLT_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_FLT) /* -3 */
781
- #define SP_BUILTIN_PTR_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_OBJ) /* -6 */
782
- #define SP_BUILTIN_SYM_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_SYM) /* -7 */
783
- #define SP_BUILTIN_PROC (-9) /* sp_Proc *, distinct from any tag-based id */
784
- #define SP_BUILTIN_RANGE (-10) /* sp_Range *, heap copy of stack-typed sp_Range when crossing into poly */
785
- #define SP_BUILTIN_TIME (-11) /* sp_Time *, heap copy of stack-typed sp_Time when crossing into poly */
786
- #define SP_BUILTIN_POLY_ARRAY (-12) /* sp_PolyArray *, array of sp_RbVal */
2443
+ #define SP_BUILTIN_INT_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_INT) /* -1 */
2444
+ #define SP_BUILTIN_STR_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_STR) /* -2 */
2445
+ #define SP_BUILTIN_FLT_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_FLT) /* -3 */
2446
+ #define SP_BUILTIN_PTR_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_OBJ) /* -6 */
2447
+ #define SP_BUILTIN_SYM_ARRAY SP_BUILTIN_ARRAY_OF(SP_TAG_SYM) /* -7 */
2448
+ #define SP_BUILTIN_PROC (-9) /* sp_Proc *, distinct from any tag-based id */
2449
+ #define SP_BUILTIN_RANGE (-10) /* sp_Range *, heap copy of stack-typed sp_Range when crossing into poly */
2450
+ #define SP_BUILTIN_TIME (-11) /* sp_Time *, heap copy of stack-typed sp_Time when crossing into poly */
2451
+ #define SP_BUILTIN_POLY_ARRAY (-12) /* sp_PolyArray *, array of sp_RbVal */
787
2452
  /* Hash variant cls_ids — boxed into the cls_id of a poly slot so
788
2453
  Hash#dig can recover the concrete hash type at runtime. */
789
- #define SP_BUILTIN_STR_INT_HASH (-13)
790
- #define SP_BUILTIN_STR_STR_HASH (-14)
791
- #define SP_BUILTIN_INT_STR_HASH (-15)
792
- #define SP_BUILTIN_SYM_INT_HASH (-16)
793
- #define SP_BUILTIN_SYM_STR_HASH (-17)
2454
+ #define SP_BUILTIN_STR_INT_HASH (-13)
2455
+ #define SP_BUILTIN_STR_STR_HASH (-14)
2456
+ #define SP_BUILTIN_INT_STR_HASH (-15)
2457
+ #define SP_BUILTIN_SYM_INT_HASH (-16)
2458
+ #define SP_BUILTIN_SYM_STR_HASH (-17)
794
2459
  #define SP_BUILTIN_STR_POLY_HASH (-18)
795
2460
  #define SP_BUILTIN_SYM_POLY_HASH (-19)
796
2461
  #define SP_BUILTIN_POLY_POLY_HASH (-20)
797
- typedef struct { int tag; int cls_id; union { mrb_int i; const char *s; mrb_float f; mrb_bool b; void *p; } v; } sp_RbVal;
2462
+ /* sp_RbVal is defined in sp_gc.h (the mark helpers dispatch on its tag). */
798
2463
  static sp_RbVal sp_box_int(mrb_int v) { sp_RbVal r; r.tag = SP_TAG_INT; r.cls_id = 0; r.v.i = v; return r; }
799
2464
  static sp_RbVal sp_box_str(const char *v) { sp_RbVal r; r.tag = SP_TAG_STR; r.cls_id = 0; r.v.s = v; return r; }
800
2465
  static sp_RbVal sp_box_float(mrb_float v) { sp_RbVal r; r.tag = SP_TAG_FLT; r.cls_id = 0; r.v.f = v; return r; }
@@ -802,7 +2467,98 @@ static sp_RbVal sp_box_bool(mrb_bool v) { sp_RbVal r; r.tag = SP_TAG_BOOL; r.cls
802
2467
  static sp_RbVal sp_box_nil(void) { sp_RbVal r; r.tag = SP_TAG_NIL; r.cls_id = 0; r.v.i = 0; return r; }
803
2468
  static sp_RbVal sp_box_obj(void *p, int cls_id) { sp_RbVal r; r.tag = SP_TAG_OBJ; r.cls_id = cls_id; r.v.p = p; return r; }
804
2469
  static sp_RbVal sp_box_sym(sp_sym v) { sp_RbVal r; r.tag = SP_TAG_SYM; r.cls_id = 0; r.v.i = (mrb_int)v; return r; }
2470
+ /* box a sp_Class into a poly slot. */
2471
+ static sp_RbVal sp_box_class(sp_Class c) { sp_RbVal r; r.tag = SP_TAG_CLASS; r.cls_id = (int)c.cls_id; r.v.i = c.cls_id; return r; }
2472
+ static sp_RbVal sp_box_encoding(sp_Encoding e) { sp_RbVal r; r.tag = SP_TAG_ENCODING; r.cls_id = 0; r.v.s = sp_encoding_name(e); return r; }
2473
+
2474
+ /* every non-value-type sp_<C> starts
2475
+ with `mrb_int cls_id`. Read it back from a void* when
2476
+ the static type at the call site has lost the cls_id (e.g.
2477
+ sp_PtrArray_get returning void*). NULL-safe via the guard
2478
+ on p; expects p to actually point at a user-class struct
2479
+ when non-NULL. */
2480
+ static inline mrb_int sp_obj_cls_id_of(void *p) { return p ? *(mrb_int *)p : 0; }
805
2481
  static sp_RbVal sp_box_nullable_str(const char *v) { return v ? sp_box_str(v) : sp_box_nil(); }
2482
+ /* String#index / #rindex return a boxed nil for not-found, boxed
2483
+ int for found. Issue #532: typed-int slot can't represent CRuby's
2484
+ nil-vs-real-index distinction in-band; widening the result type
2485
+ to sp_RbVal at the call site lets `pos.nil?` and `puts pos.inspect`
2486
+ work via the standard poly-tag dispatch. The -1 sentinel comes
2487
+ from the underlying sp_str_*_index helpers; we widen here at the
2488
+ boxing layer so existing call sites that want the raw int still
2489
+ work via `sp_str_index` directly. */
2490
+ static sp_RbVal sp_str_index_poly(const char *s, const char *sub) { mrb_int n = sp_str_index(s, sub); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2491
+ static sp_RbVal sp_str_index_from_poly(const char *s, const char *sub, mrb_int start) { mrb_int n = sp_str_index_from(s, sub, start); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2492
+ static sp_RbVal sp_str_rindex_poly(const char *s, const char *sub) { mrb_int n = sp_str_rindex(s, sub); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2493
+ /* int? siblings of the String#index family. Same not-found
2494
+ semantics as the _poly variants, but the result is the int?
2495
+ sentinel (SP_INT_NIL) rather than a boxed sp_RbVal — keeps the
2496
+ `i = s.index(sub); return ... if i.nil?; i + 1` idiom on the
2497
+ direct integer arithmetic path. */
2498
+ static mrb_int sp_str_index_opt(const char *s, const char *sub) { mrb_int n = sp_str_index(s, sub); return n < 0 ? SP_INT_NIL : n; }
2499
+ static mrb_int sp_str_index_from_opt(const char *s, const char *sub, mrb_int start) { mrb_int n = sp_str_index_from(s, sub, start); return n < 0 ? SP_INT_NIL : n; }
2500
+ static mrb_int sp_str_rindex_opt(const char *s, const char *sub) { mrb_int n = sp_str_rindex(s, sub); return n < 0 ? SP_INT_NIL : n; }
2501
+ static sp_RbVal sp_re_rindex_poly(mrb_regexp_pattern *pat, const char *str) { mrb_int n = sp_re_rindex(pat, str); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2502
+ /* `s.index(regex)` -- first match start (byte offset, as sp_re_match reports;
2503
+ matches the rindex regex variant, which also reports bytes). sp_re_match
2504
+ sets $~ as a side effect, which CRuby's String#index(regex) also does.
2505
+ Boxed Integer | nil. The plain-string path would lower the regex pattern to
2506
+ 0 and feed a bogus arg to sp_str_index_opt. */
2507
+ static sp_RbVal sp_re_index_poly(mrb_regexp_pattern *pat, const char *str) { mrb_int n = sp_re_match(pat, str); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2508
+ /* CRuby-compatible =~ wrapper: SP_TAG_INT(pos) on match, SP_TAG_NIL
2509
+ on miss. Codegen routes the `=~` operator here so
2510
+ `("abc" =~ /xyz/).nil?` answers true and `puts("abc" =~ /xyz/)`
2511
+ prints an empty line, matching CRuby. The raw `sp_re_match`
2512
+ (returning -1) stays available for internal callers needing the
2513
+ sentinel form. */
2514
+ static sp_RbVal sp_re_match_poly(mrb_regexp_pattern *pat, const char *str) { mrb_int n = sp_re_match(pat, str); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2515
+
2516
+ /* Regexp.escape(s) / Regexp.quote(s) -- prefix every regex metachar
2517
+ and whitespace byte with a single backslash, returning a heap
2518
+ string that callers can feed into `Regexp.new(...)` to match the
2519
+ original bytes literally. Matches CRuby's rb_reg_quote for the
2520
+ ASCII range (the only range Spinel's regex engine indexes today,
2521
+ so multibyte passes through unchanged).
2522
+
2523
+ The metachars covered: \\ . ? * + ^ $ | ( ) [ ] { } # -
2524
+ The whitespace covered: ' ' \t \n \r \f \v
2525
+ Everything else copies byte-for-byte. */
2526
+ static const char *sp_re_escape(const char *src) {
2527
+ size_t i, in_len = strlen(src);
2528
+ size_t out_len = 0;
2529
+ for (i = 0; i < in_len; i++) {
2530
+ unsigned char c = (unsigned char)src[i];
2531
+ if (c == '\\' || c == '.' || c == '?' || c == '*' || c == '+' ||
2532
+ c == '^' || c == '$' || c == '|' || c == '(' || c == ')' ||
2533
+ c == '[' || c == ']' || c == '{' || c == '}' || c == '#' ||
2534
+ c == '-' || c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
2535
+ c == '\f' || c == '\v') {
2536
+ out_len += 2;
2537
+ } else {
2538
+ out_len += 1;
2539
+ }
2540
+ }
2541
+ if (out_len == in_len) {
2542
+ return src;
2543
+ }
2544
+ char *buf = sp_str_alloc(out_len);
2545
+ size_t j = 0;
2546
+ for (i = 0; i < in_len; i++) {
2547
+ unsigned char c = (unsigned char)src[i];
2548
+ if (c == '\\' || c == '.' || c == '?' || c == '*' || c == '+' ||
2549
+ c == '^' || c == '$' || c == '|' || c == '(' || c == ')' ||
2550
+ c == '[' || c == ']' || c == '{' || c == '}' || c == '#' ||
2551
+ c == '-' || c == ' ' || c == '\t' || c == '\n' || c == '\r' ||
2552
+ c == '\f' || c == '\v') {
2553
+ buf[j++] = '\\';
2554
+ buf[j++] = (char)c;
2555
+ } else {
2556
+ buf[j++] = (char)c;
2557
+ }
2558
+ }
2559
+ buf[j] = 0;
2560
+ return buf;
2561
+ }
806
2562
  static sp_RbVal sp_box_nullable_obj(void *p, int cls_id) { return p ? sp_box_obj(p, cls_id) : sp_box_nil(); }
807
2563
  /* Built-in pointer boxes — share SP_TAG_OBJ with a reserved negative
808
2564
  cls_id so the dispatch path is uniform. */
@@ -812,6 +2568,41 @@ static sp_RbVal sp_box_str_array(void *p) { return sp_box_obj(p, SP_BUILTIN_ST
812
2568
  static sp_RbVal sp_box_sym_array(void *p) { return sp_box_obj(p, SP_BUILTIN_SYM_ARRAY); }
813
2569
  static sp_RbVal sp_box_ptr_array(void *p) { return sp_box_obj(p, SP_BUILTIN_PTR_ARRAY); }
814
2570
  static sp_RbVal sp_box_proc(void *p) { return sp_box_obj(p, SP_BUILTIN_PROC); }
2571
+
2572
+ /* CRuby-compatible Array#index / #rindex / #find_index: returns
2573
+ sp_RbVal (nil tag for not-found, int tag with the position when
2574
+ found). spinel's raw `_index` helpers return the -1 sentinel,
2575
+ which diverges from CRuby's nil. Codegen routes
2576
+ `arr.index(x)` / `arr.find_index(x)` / `arr.rindex(x)` through
2577
+ these `_poly` wrappers so `.nil?` / `== nil` / `inspect` etc.
2578
+ on the result behave the CRuby way. Sibling to
2579
+ `sp_str_index_poly` above; same widening rationale.
2580
+ Issue raised during #585 follow-up: spinel positions itself
2581
+ as a Ruby SUBSET, so documented Ruby APIs must match CRuby
2582
+ behavior. */
2583
+ static sp_RbVal sp_IntArray_index_poly(sp_IntArray *a, mrb_int v) { mrb_int n = sp_IntArray_index(a, v); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2584
+ static sp_RbVal sp_IntArray_rindex_poly(sp_IntArray *a, mrb_int v) { mrb_int n = sp_IntArray_rindex(a, v); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2585
+ static sp_RbVal sp_StrArray_index_poly(sp_StrArray *a, const char *v) { mrb_int n = sp_StrArray_index(a, v); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2586
+ static sp_RbVal sp_StrArray_rindex_poly(sp_StrArray *a, const char *v) { mrb_int n = sp_StrArray_rindex(a, v); return n < 0 ? sp_box_nil() : sp_box_int(n); }
2587
+ /* int? siblings of the *_index_poly wrappers above. Same not-found
2588
+ semantics, but return the int? sentinel (SP_INT_NIL) instead of
2589
+ boxing into sp_RbVal. Used when the call site's static type
2590
+ tracking carries the result as int? rather than poly — eliminates
2591
+ the box/unbox round-trip for the common `i = arr.index(x);
2592
+ i.nil? ? ... : <use i as int>` idiom. */
2593
+ static mrb_int sp_IntArray_index_opt(sp_IntArray *a, mrb_int v) { mrb_int n = sp_IntArray_index(a, v); return n < 0 ? SP_INT_NIL : n; }
2594
+ static mrb_int sp_IntArray_rindex_opt(sp_IntArray *a, mrb_int v) { mrb_int n = sp_IntArray_rindex(a, v); return n < 0 ? SP_INT_NIL : n; }
2595
+ /* Inspect / to_s for an int? value. CRuby distinguishes the two on
2596
+ nil: `nil.to_s` is "" while `nil.inspect` is "nil". For a real
2597
+ integer they agree (Integer#to_s and #inspect are both the decimal
2598
+ form). Two wrappers keep call-site emit local. */
2599
+ static const char *sp_int_opt_inspect(mrb_int v) { return sp_int_is_nil(v) ? "nil" : sp_int_to_s(v); }
2600
+ static const char *sp_int_opt_to_s(mrb_int v) { return sp_int_is_nil(v) ? "" : sp_int_to_s(v); }
2601
+ /* float? (nullable float) counterparts: a non-nil value formats exactly
2602
+ like a plain Float (delegates to sp_float_to_s), nil renders "nil"
2603
+ (inspect) / "" (to_s). */
2604
+ static const char *sp_float_opt_inspect(mrb_float v) { return sp_float_is_nil(v) ? "nil" : sp_float_to_s(v); }
2605
+ static const char *sp_float_opt_to_s(mrb_float v) { return sp_float_is_nil(v) ? "" : sp_float_to_s(v); }
815
2606
  /* sp_Range is a 16-byte value type that doesn't fit in sp_RbVal's union
816
2607
  (max 8 bytes). When a Range crosses into a poly slot (heterogeneous
817
2608
  hash / array / param / ivar), copy it onto the GC heap and box the
@@ -823,9 +2614,10 @@ static sp_RbVal sp_box_range(sp_Range v) {
823
2614
  return sp_box_obj(p, SP_BUILTIN_RANGE);
824
2615
  }
825
2616
  static const char *sp_Range_inspect(sp_Range *r) {
826
- /* "first..last" form. Buffer sized for two int64s plus the dots. */
2617
+ /* "first..last" / "first...last" form. Buffer sized for two int64s
2618
+ plus the dots. */
827
2619
  char *buf = sp_str_alloc_raw(48);
828
- snprintf(buf, 48, "%lld..%lld", (long long)r->first, (long long)r->last);
2620
+ snprintf(buf, 48, r->excl ? "%lld...%lld" : "%lld..%lld", (long long)r->first, (long long)r->last);
829
2621
  return buf;
830
2622
  }
831
2623
  /* Same heap-box rationale as sp_Range: sp_Time is 12+ bytes (tv_sec +
@@ -852,7 +2644,8 @@ static const char *sp_Time_inspect(sp_Time *t) {
852
2644
  return buf;
853
2645
  }
854
2646
  static sp_RbVal sp_box_poly_array(void *p) { return sp_box_obj(p, SP_BUILTIN_POLY_ARRAY); }
855
- static void sp_poly_puts(sp_RbVal v) {
2647
+ static const char *sp_class_to_s(sp_Class c); /* fwd decl: sp_poly_puts' SP_TAG_CLASS arm */
2648
+ static inline void sp_poly_puts(sp_RbVal v) {
856
2649
  switch (v.tag) {
857
2650
  case SP_TAG_INT: printf("%lld\n", (long long)v.v.i); break;
858
2651
  case SP_TAG_STR: if (v.v.s) { fputs(v.v.s, stdout); if (!*v.v.s || v.v.s[strlen(v.v.s)-1] != '\n') putchar('\n'); } else putchar('\n'); break;
@@ -860,17 +2653,42 @@ static void sp_poly_puts(sp_RbVal v) {
860
2653
  case SP_TAG_BOOL: puts(v.v.b ? "true" : "false"); break;
861
2654
  case SP_TAG_NIL: putchar('\n'); break;
862
2655
  case SP_TAG_SYM: { const char *_ss = sp_sym_to_s((sp_sym)v.v.i); fputs(_ss, stdout); putchar('\n'); break; }
2656
+ case SP_TAG_ENCODING: { const char *_es = v.v.s ? v.v.s : sp_str_empty; fputs(_es, stdout); putchar('\n'); break; }
2657
+ case SP_TAG_CLASS: { sp_Class _c = {v.v.i}; fputs(sp_class_to_s(_c), stdout); putchar('\n'); break; }
863
2658
  case SP_TAG_OBJ: {
864
- /* Built-in pointer types route through their inspect helpers
865
- (one line per array, matching `puts arr`'s short form). User
866
- classes (cls_id >= 0) fall through to the raw-pointer path
867
- since we don't have per-class inspect dispatch here. */
2659
+ /* MRI's `puts arr` iterates an Array, printing one element per
2660
+ line (using to_s on each); a non-Array OBJ falls back to
2661
+ inspect / class-name. */
868
2662
  switch (v.cls_id) {
869
- case SP_BUILTIN_INT_ARRAY: puts(sp_IntArray_inspect((sp_IntArray *)v.v.p)); break;
870
- case SP_BUILTIN_FLT_ARRAY: puts(sp_FloatArray_inspect((sp_FloatArray *)v.v.p)); break;
871
- case SP_BUILTIN_STR_ARRAY: puts(sp_StrArray_inspect((sp_StrArray *)v.v.p)); break;
872
- case SP_BUILTIN_SYM_ARRAY: puts(sp_SymArray_inspect((sp_IntArray *)v.v.p)); break;
873
- case SP_BUILTIN_PTR_ARRAY: puts(sp_PtrArray_inspect((sp_PtrArray *)v.v.p)); break;
2663
+ case SP_BUILTIN_INT_ARRAY: {
2664
+ sp_IntArray *_a = (sp_IntArray *)v.v.p;
2665
+ for (mrb_int _i = 0; _i < _a->len; _i++)
2666
+ printf("%lld\n", (long long)_a->data[_a->start + _i]);
2667
+ break;
2668
+ }
2669
+ case SP_BUILTIN_FLT_ARRAY: {
2670
+ sp_FloatArray *_a = (sp_FloatArray *)v.v.p;
2671
+ for (mrb_int _i = 0; _i < _a->len; _i++) {
2672
+ fputs(sp_float_to_s(_a->data[_i]), stdout); putchar('\n');
2673
+ }
2674
+ break;
2675
+ }
2676
+ case SP_BUILTIN_STR_ARRAY: {
2677
+ sp_StrArray *_a = (sp_StrArray *)v.v.p;
2678
+ for (mrb_int _i = 0; _i < _a->len; _i++) {
2679
+ const char *_s = _a->data[_i];
2680
+ if (_s) { fputs(_s, stdout); if (!*_s || _s[strlen(_s)-1] != '\n') putchar('\n'); } else putchar('\n');
2681
+ }
2682
+ break;
2683
+ }
2684
+ case SP_BUILTIN_SYM_ARRAY: {
2685
+ sp_IntArray *_a = (sp_IntArray *)v.v.p;
2686
+ for (mrb_int _i = 0; _i < _a->len; _i++) {
2687
+ const char *_s = sp_sym_to_s((sp_sym)_a->data[_a->start + _i]);
2688
+ fputs(_s, stdout); putchar('\n');
2689
+ }
2690
+ break;
2691
+ }
874
2692
  case SP_BUILTIN_RANGE: puts(sp_Range_inspect((sp_Range *)v.v.p)); break;
875
2693
  case SP_BUILTIN_TIME: puts(sp_Time_inspect((sp_Time *)v.v.p)); break;
876
2694
  default: printf("#<Object:0x%p>\n", v.v.p); break;
@@ -882,7 +2700,14 @@ static void sp_poly_puts(sp_RbVal v) {
882
2700
  }
883
2701
  static mrb_bool sp_poly_nil_p(sp_RbVal v) { return v.tag == SP_TAG_NIL; }
884
2702
  static mrb_bool sp_poly_truthy(sp_RbVal v) { return !(v.tag == SP_TAG_NIL || (v.tag == SP_TAG_BOOL && !v.v.b)); }
885
- static const char *sp_poly_to_s(sp_RbVal v) {
2703
+ /* forward-declare the program-emitted class
2704
+ name lookup so sp_poly_to_s's SP_TAG_CLASS arm resolves.
2705
+ The codegen emits a 1-line stub when no class const is used,
2706
+ or the real body when @needs_class_table fires. The forward
2707
+ decl always needs a definition somewhere because -Werror
2708
+ trips on "used but never defined" otherwise. */
2709
+ static const char *sp_class_to_s(sp_Class c);
2710
+ static inline const char *sp_poly_to_s(sp_RbVal v) {
886
2711
  switch (v.tag) {
887
2712
  case SP_TAG_INT: return sp_int_to_s(v.v.i);
888
2713
  case SP_TAG_STR: return v.v.s ? v.v.s : sp_str_empty;
@@ -890,6 +2715,8 @@ static const char *sp_poly_to_s(sp_RbVal v) {
890
2715
  case SP_TAG_BOOL: return v.v.b ? SPL("true") : SPL("false");
891
2716
  case SP_TAG_NIL: return sp_str_empty;
892
2717
  case SP_TAG_SYM: return sp_sym_to_s((sp_sym)v.v.i);
2718
+ case SP_TAG_CLASS: { sp_Class c = {v.v.i}; return sp_class_to_s(c); }
2719
+ case SP_TAG_ENCODING: return v.v.s ? v.v.s : sp_str_empty;
893
2720
  case SP_TAG_OBJ:
894
2721
  switch (v.cls_id) {
895
2722
  case SP_BUILTIN_INT_ARRAY: return sp_IntArray_inspect((sp_IntArray *)v.v.p);
@@ -910,7 +2737,7 @@ static sp_RbVal sp_poly_mul(sp_RbVal a, sp_RbVal b) { if (a.tag == SP_TAG_INT &&
910
2737
  static mrb_int sp_poly_to_i(sp_RbVal v) { if (v.tag == SP_TAG_INT || v.tag == SP_TAG_SYM) return v.v.i; if (v.tag == SP_TAG_STR) return (mrb_int)strtoll(v.v.s ? v.v.s : sp_str_empty, NULL, 10); if (v.tag == SP_TAG_FLT) return (mrb_int)v.v.f; if (v.tag == SP_TAG_BOOL) return v.v.b ? 1 : 0; return 0; }
911
2738
  static mrb_float sp_poly_to_f(sp_RbVal v) { if (v.tag == SP_TAG_FLT) return v.v.f; if (v.tag == SP_TAG_INT || v.tag == SP_TAG_SYM) return (mrb_float)v.v.i; if (v.tag == SP_TAG_BOOL) return v.v.b ? 1.0 : 0.0; return 0.0; }
912
2739
  static mrb_bool sp_poly_numeric_p(sp_RbVal v) { return v.tag == SP_TAG_INT || v.tag == SP_TAG_FLT; }
913
- static mrb_bool sp_poly_eq(sp_RbVal a, sp_RbVal b) { if (sp_poly_numeric_p(a) && sp_poly_numeric_p(b)) return sp_poly_to_f(a) == sp_poly_to_f(b); if (a.tag != b.tag) return FALSE; switch (a.tag) { case SP_TAG_INT: return a.v.i == b.v.i; case SP_TAG_STR: return (a.v.s == NULL || b.v.s == NULL) ? (a.v.s == b.v.s) : (strcmp(a.v.s, b.v.s) == 0); case SP_TAG_FLT: return a.v.f == b.v.f; case SP_TAG_BOOL: return a.v.b == b.v.b; case SP_TAG_NIL: return TRUE; case SP_TAG_SYM: return a.v.i == b.v.i; case SP_TAG_OBJ: return a.cls_id == b.cls_id && a.v.p == b.v.p; default: return FALSE; } }
2740
+ static mrb_bool sp_poly_eq(sp_RbVal a, sp_RbVal b) { if (sp_poly_numeric_p(a) && sp_poly_numeric_p(b)) return sp_poly_to_f(a) == sp_poly_to_f(b); if (a.tag != b.tag) return FALSE; switch (a.tag) { case SP_TAG_INT: return a.v.i == b.v.i; case SP_TAG_STR: return (a.v.s == NULL || b.v.s == NULL) ? (a.v.s == b.v.s) : (strcmp(a.v.s, b.v.s) == 0); case SP_TAG_FLT: return a.v.f == b.v.f; case SP_TAG_BOOL: return a.v.b == b.v.b; case SP_TAG_NIL: return TRUE; case SP_TAG_SYM: return a.v.i == b.v.i; case SP_TAG_ENCODING: return (a.v.s == NULL || b.v.s == NULL) ? (a.v.s == b.v.s) : (strcmp(a.v.s, b.v.s) == 0); case SP_TAG_OBJ: return a.cls_id == b.cls_id && a.v.p == b.v.p; case SP_TAG_CLASS: return a.v.i == b.v.i; default: return FALSE; } }
914
2741
  static mrb_int sp_poly_cmp(sp_RbVal a, sp_RbVal b, mrb_bool *comparable) { if (sp_poly_numeric_p(a) && sp_poly_numeric_p(b)) { mrb_float af = sp_poly_to_f(a), bf = sp_poly_to_f(b); *comparable = TRUE; return (af > bf) - (af < bf); } if (a.tag == SP_TAG_STR && b.tag == SP_TAG_STR) { if (a.v.s == NULL || b.v.s == NULL) { *comparable = (a.v.s == b.v.s); return 0; } *comparable = TRUE; return strcmp(a.v.s, b.v.s); } if (a.tag == SP_TAG_SYM && b.tag == SP_TAG_SYM) { *comparable = TRUE; return (a.v.i > b.v.i) - (a.v.i < b.v.i); } *comparable = FALSE; return 0; }
915
2742
  static mrb_bool sp_poly_lt(sp_RbVal a, sp_RbVal b) { mrb_bool comparable; mrb_int cmp = sp_poly_cmp(a, b, &comparable); return comparable ? (cmp < 0) : FALSE; }
916
2743
  static mrb_bool sp_poly_le(sp_RbVal a, sp_RbVal b) { mrb_bool comparable; mrb_int cmp = sp_poly_cmp(a, b, &comparable); return comparable ? (cmp <= 0) : FALSE; }
@@ -918,7 +2745,13 @@ static mrb_bool sp_poly_gt(sp_RbVal a, sp_RbVal b) { mrb_bool comparable; mrb_in
918
2745
  static mrb_bool sp_poly_ge(sp_RbVal a, sp_RbVal b) { mrb_bool comparable; mrb_int cmp = sp_poly_cmp(a, b, &comparable); return comparable ? (cmp >= 0) : FALSE; }
919
2746
  static sp_RbVal sp_poly_div(sp_RbVal a, sp_RbVal b) { if (a.tag == SP_TAG_FLT || b.tag == SP_TAG_FLT) return sp_box_float(sp_poly_to_f(a) / sp_poly_to_f(b)); return sp_box_int(sp_idiv(sp_poly_to_i(a), sp_poly_to_i(b))); }
920
2747
  static sp_RbVal sp_poly_mod(sp_RbVal a, sp_RbVal b) { if (a.tag == SP_TAG_FLT || b.tag == SP_TAG_FLT) return sp_box_float(fmod(sp_poly_to_f(a), sp_poly_to_f(b))); return sp_box_int(sp_imod(sp_poly_to_i(a), sp_poly_to_i(b))); }
921
- static sp_RbVal sp_poly_pow(sp_RbVal a, sp_RbVal b) { double r = pow((double)sp_poly_to_f(a), (double)sp_poly_to_f(b)); if (a.tag == SP_TAG_INT && b.tag == SP_TAG_INT && b.v.i >= 0) return sp_box_int((mrb_int)r); return sp_box_float((mrb_float)r); }
2748
+ /* Integer #** : Spinel has no Rational, so a negative integer exponent --
2749
+ which CRuby evaluates to a Rational like (1/2) -- raises RangeError rather
2750
+ than silently truncating toward 0. Float ** negative stays a float
2751
+ (CRuby-compatible: 2.0 ** -1 == 0.5). See docs/INCOMPATIBILITIES.md. */
2752
+ static mrb_int sp_int_pow(mrb_int base, mrb_int exp) __attribute__((unused));
2753
+ static mrb_int sp_int_pow(mrb_int base, mrb_int exp) { if (exp < 0) sp_raise_cls("RangeError", "negative exponent"); return (mrb_int)pow((double)base, (double)exp); }
2754
+ static sp_RbVal sp_poly_pow(sp_RbVal a, sp_RbVal b) { if (a.tag == SP_TAG_INT && b.tag == SP_TAG_INT && b.v.i < 0) sp_raise_cls("RangeError", "negative exponent"); double r = pow((double)sp_poly_to_f(a), (double)sp_poly_to_f(b)); if (a.tag == SP_TAG_INT && b.tag == SP_TAG_INT && b.v.i >= 0) return sp_box_int((mrb_int)r); return sp_box_float((mrb_float)r); }
922
2755
  /* sp_poly_shl is defined after sp_PolyArray_push (below) so the
923
2756
  push-dispatch path can call it directly. The Integer-bit-shift
924
2757
  semantics fall through when the recv isn't an array. */
@@ -930,12 +2763,207 @@ static sp_RbVal sp_poly_bxor(sp_RbVal a, sp_RbVal b) { return sp_box_int(sp_poly
930
2763
  static sp_RbVal sp_poly_neg(sp_RbVal a) { if (a.tag == SP_TAG_FLT) return sp_box_float(-a.v.f); return sp_box_int(-sp_poly_to_i(a)); }
931
2764
 
932
2765
  /* PolyArray: array of sp_RbVal */
933
- typedef struct { sp_RbVal *data; mrb_int len; mrb_int cap; } sp_PolyArray;
934
- static inline void sp_mark_rbval(sp_RbVal v);
2766
+ typedef struct { sp_RbVal *data; mrb_int len; mrb_int cap; mrb_int frozen; } sp_PolyArray;
2767
+ /* sp_mark_rbval: inline helper in sp_gc.h. */
2768
+ /* Definition of the root-entry marker forward-declared near
2769
+ sp_gc_mark_all: a low-bit-tagged slot is an sp_RbVal* root. */
2770
+ /* sp_gc_mark_root_entry is an inline helper in sp_gc.h. */
935
2771
  static void sp_PolyArray_scan(void *p) { sp_PolyArray *a = (sp_PolyArray *)p; for (mrb_int i = 0; i < a->len; i++) sp_mark_rbval(a->data[i]); }
936
2772
  static void sp_PolyArray_fin(void *p) { sp_PolyArray *a = (sp_PolyArray *)p; sp_gc_hdr *h = (sp_gc_hdr *)((char *)a - sizeof(sp_gc_hdr)); sp_gc_bytes -= sizeof(sp_RbVal) * a->cap; h->size -= sizeof(sp_RbVal) * a->cap; free(a->data); }
937
- static sp_PolyArray *sp_PolyArray_new(void) { sp_PolyArray *a = (sp_PolyArray *)sp_gc_alloc(sizeof(sp_PolyArray), sp_PolyArray_fin, sp_PolyArray_scan); a->cap = 16; a->data = (sp_RbVal *)malloc(sizeof(sp_RbVal) * a->cap); a->len = 0; { sp_gc_hdr *h = (sp_gc_hdr *)((char *)a - sizeof(sp_gc_hdr)); h->size += sizeof(sp_RbVal) * a->cap; sp_gc_bytes += sizeof(sp_RbVal) * a->cap; } return a; }
938
- static void sp_PolyArray_push(sp_PolyArray *a, sp_RbVal v) { if (a->len >= a->cap) { sp_gc_hdr *h = (sp_gc_hdr *)((char *)a - sizeof(sp_gc_hdr)); sp_gc_bytes -= sizeof(sp_RbVal) * a->cap; h->size -= sizeof(sp_RbVal) * a->cap; a->cap = a->cap * 2 + 1; a->data = (sp_RbVal *)realloc(a->data, sizeof(sp_RbVal) * a->cap); h->size += sizeof(sp_RbVal) * a->cap; sp_gc_bytes += sizeof(sp_RbVal) * a->cap; } a->data[a->len++] = v; }
2773
+ static sp_PolyArray *sp_PolyArray_new(void) { sp_PolyArray *a = (sp_PolyArray *)sp_gc_alloc(sizeof(sp_PolyArray), sp_PolyArray_fin, sp_PolyArray_scan); a->cap = 16; a->data = (sp_RbVal *)malloc(sizeof(sp_RbVal) * a->cap); if (!a->data) sp_oom_die(); a->len = 0; { sp_gc_hdr *h = (sp_gc_hdr *)((char *)a - sizeof(sp_gc_hdr)); h->size += sizeof(sp_RbVal) * a->cap; sp_gc_bytes += sizeof(sp_RbVal) * a->cap; } return a; }
2774
+ static void sp_PolyArray_push(sp_PolyArray *a, sp_RbVal v) { if (!a) return; if (a->frozen) { sp_raise_frozen_array(); return; } if (a->len >= a->cap) { sp_gc_hdr *h = (sp_gc_hdr *)((char *)a - sizeof(sp_gc_hdr)); sp_gc_bytes -= sizeof(sp_RbVal) * a->cap; h->size -= sizeof(sp_RbVal) * a->cap; a->cap = a->cap * 2 + 1; void *nd = realloc(a->data, sizeof(sp_RbVal) * a->cap); if (!nd) sp_oom_die(); a->data = (sp_RbVal *)nd; h->size += sizeof(sp_RbVal) * a->cap; sp_gc_bytes += sizeof(sp_RbVal) * a->cap; } a->data[a->len++] = v; }
2775
+ static sp_PolyArray *sp_re_scan_poly(mrb_regexp_pattern *pat, const char *str) {
2776
+ sp_PolyArray *arr = sp_PolyArray_new();
2777
+ SP_GC_ROOT(arr);
2778
+ int64_t slen = (int64_t)strlen(str);
2779
+ int64_t pos = 0;
2780
+ int ncaps = 64;
2781
+ int caps[64];
2782
+ while (pos <= slen) {
2783
+ int n = re_exec(pat, str, slen, pos, caps, ncaps);
2784
+ if (n <= 0 || caps[0] < 0) break;
2785
+ int pairs = (n > ncaps ? ncaps : n) / 2;
2786
+ if (pairs <= 1) {
2787
+ int len = caps[1] - caps[0];
2788
+ char *m = sp_str_alloc_raw(len + 1);
2789
+ memcpy(m, str + caps[0], len);
2790
+ m[len] = 0;
2791
+ sp_PolyArray_push(arr, sp_box_str(m));
2792
+ } else {
2793
+ sp_PolyArray *row = sp_PolyArray_new();
2794
+ for (int gi = 1; gi < pairs; gi++) {
2795
+ if (caps[gi * 2] >= 0 && caps[gi * 2 + 1] >= 0) {
2796
+ int glen = caps[gi * 2 + 1] - caps[gi * 2];
2797
+ char *gm = sp_str_alloc_raw(glen + 1);
2798
+ memcpy(gm, str + caps[gi * 2], glen);
2799
+ gm[glen] = 0;
2800
+ sp_PolyArray_push(row, sp_box_str(gm));
2801
+ } else {
2802
+ sp_PolyArray_push(row, sp_box_nil());
2803
+ }
2804
+ }
2805
+ sp_PolyArray_push(arr, sp_box_poly_array(row));
2806
+ }
2807
+ pos = caps[1];
2808
+ if (caps[0] == caps[1]) pos++;
2809
+ }
2810
+ return arr;
2811
+ }
2812
+ static sp_PolyArray *sp_re_match_data(mrb_regexp_pattern *pat, const char *str) {
2813
+ int64_t slen = (int64_t)strlen(str);
2814
+ int ncaps = 64;
2815
+ int n = re_exec(pat, str, slen, 0, sp_re_caps, ncaps);
2816
+ if (n <= 0 || sp_re_caps[0] < 0) {
2817
+ for (int i = 0; i < 10; i++) sp_re_captures[i] = NULL;
2818
+ sp_re_last_str = NULL;
2819
+ sp_re_match_str = NULL;
2820
+ sp_re_match_pre = NULL;
2821
+ sp_re_match_post = NULL;
2822
+ return NULL;
2823
+ }
2824
+ int pairs = (n > ncaps ? ncaps : n) / 2;
2825
+ sp_re_set_captures(str, sp_re_caps, pairs);
2826
+ sp_PolyArray *arr = sp_PolyArray_new();
2827
+ for (int i = 0; i < pairs; i++) {
2828
+ int start = sp_re_caps[i * 2];
2829
+ int end = sp_re_caps[i * 2 + 1];
2830
+ if (start >= 0 && end >= start) {
2831
+ int len = end - start;
2832
+ char *buf = sp_str_alloc_raw(len + 1);
2833
+ memcpy(buf, str + start, len);
2834
+ buf[len] = 0;
2835
+ sp_PolyArray_push(arr, sp_box_str(buf));
2836
+ } else {
2837
+ sp_PolyArray_push(arr, sp_box_nil());
2838
+ }
2839
+ }
2840
+ return arr;
2841
+ }
2842
+ /* MatchData — holds the source string and the per-group byte offsets
2843
+ the engine produced. `[]`/captures extract substrings on demand;
2844
+ offset/begin/end report CHARACTER offsets (CRuby semantics), so
2845
+ byte offsets are converted via sp_str_count_chars. Group i occupies
2846
+ caps[2i] (begin) and caps[2i+1] (end); -1 marks a non-participating
2847
+ group. Issue #974. */
2848
+ typedef struct { const char *source; int caps[64]; int ncap; } sp_MatchData;
2849
+ static void sp_MatchData_scan(void *p) { sp_MatchData *m = (sp_MatchData *)p; if (m->source) sp_mark_string(m->source); }
2850
+ static sp_MatchData *sp_re_matchdata(mrb_regexp_pattern *pat, const char *str) {
2851
+ int64_t slen = (int64_t)strlen(str);
2852
+ int caps[64];
2853
+ int n = re_exec(pat, str, slen, 0, caps, 64);
2854
+ if (n <= 0 || caps[0] < 0) {
2855
+ for (int i = 0; i < 10; i++) sp_re_captures[i] = NULL;
2856
+ sp_re_last_str = NULL; sp_re_match_str = NULL;
2857
+ sp_re_match_pre = NULL; sp_re_match_post = NULL;
2858
+ return NULL;
2859
+ }
2860
+ int pairs = (n > 64 ? 64 : n) / 2;
2861
+ sp_re_set_captures(str, caps, pairs);
2862
+ sp_MatchData *m = (sp_MatchData *)sp_gc_alloc(sizeof(sp_MatchData), NULL, sp_MatchData_scan);
2863
+ m->source = str;
2864
+ m->ncap = pairs;
2865
+ for (int i = 0; i < pairs * 2; i++) m->caps[i] = caps[i];
2866
+ return m;
2867
+ }
2868
+ /* String#match(/re/, pos) — pos is a codepoint index (CRuby semantics). */
2869
+ static sp_MatchData *sp_re_matchdata_at(mrb_regexp_pattern *pat, const char *str, mrb_int cpos) {
2870
+ mrb_int cl = sp_str_length(str);
2871
+ if (cpos < 0) cpos += cl;
2872
+ if (cpos < 0 || cpos > cl) return NULL;
2873
+ size_t boff = sp_utf8_byte_offset(str, cpos);
2874
+ int64_t slen = (int64_t)strlen(str);
2875
+ int caps[64];
2876
+ int n = re_exec(pat, str, slen, (mrb_int)boff, caps, 64);
2877
+ if (n <= 0 || caps[0] < 0) {
2878
+ for (int i = 0; i < 10; i++) sp_re_captures[i] = NULL;
2879
+ sp_re_last_str = NULL; sp_re_match_str = NULL;
2880
+ sp_re_match_pre = NULL; sp_re_match_post = NULL;
2881
+ return NULL;
2882
+ }
2883
+ int pairs = (n > 64 ? 64 : n) / 2;
2884
+ sp_re_set_captures(str, caps, pairs);
2885
+ sp_MatchData *m = (sp_MatchData *)sp_gc_alloc(sizeof(sp_MatchData), NULL, sp_MatchData_scan);
2886
+ m->source = str;
2887
+ m->ncap = pairs;
2888
+ for (int i = 0; i < pairs * 2; i++) m->caps[i] = caps[i];
2889
+ return m;
2890
+ }
2891
+ /* group i substring, or NULL for a non-participating / out-of-range group */
2892
+ static const char *sp_MatchData_aref(sp_MatchData *m, mrb_int i) {
2893
+ if (!m || i < 0 || i >= m->ncap) return NULL;
2894
+ int s = m->caps[i * 2], e = m->caps[i * 2 + 1];
2895
+ if (s < 0 || e < s) return NULL;
2896
+ int len = e - s;
2897
+ char *b = sp_str_alloc((size_t)len);
2898
+ memcpy(b, m->source + s, len);
2899
+ b[len] = 0;
2900
+ sp_str_set_len(b, (size_t)len);
2901
+ return b;
2902
+ }
2903
+ static mrb_int sp_MatchData_length(sp_MatchData *m) { return m ? m->ncap : 0; }
2904
+ /* char offset of a byte position within source */
2905
+ static mrb_int sp_md_char_off(sp_MatchData *m, int byteoff) {
2906
+ if (byteoff < 0) return SP_INT_NIL;
2907
+ return sp_str_count_chars(m->source, (size_t)byteoff);
2908
+ }
2909
+ static mrb_int sp_MatchData_begin(sp_MatchData *m, mrb_int i) {
2910
+ if (!m || i < 0 || i >= m->ncap) return SP_INT_NIL;
2911
+ return sp_md_char_off(m, m->caps[i * 2]);
2912
+ }
2913
+ static mrb_int sp_MatchData_end(sp_MatchData *m, mrb_int i) {
2914
+ if (!m || i < 0 || i >= m->ncap) return SP_INT_NIL;
2915
+ return sp_md_char_off(m, m->caps[i * 2 + 1]);
2916
+ }
2917
+ static sp_IntArray *sp_MatchData_offset(sp_MatchData *m, mrb_int i) {
2918
+ sp_IntArray *a = sp_IntArray_new();
2919
+ if (!m || i < 0 || i >= m->ncap) { sp_IntArray_push(a, SP_INT_NIL); sp_IntArray_push(a, SP_INT_NIL); return a; }
2920
+ sp_IntArray_push(a, sp_md_char_off(m, m->caps[i * 2]));
2921
+ sp_IntArray_push(a, sp_md_char_off(m, m->caps[i * 2 + 1]));
2922
+ return a;
2923
+ }
2924
+ /* whole-match string (group 0) — also MatchData#to_s */
2925
+ static const char *sp_MatchData_to_s(sp_MatchData *m) { const char *r = sp_MatchData_aref(m, 0); return r ? r : sp_str_empty; }
2926
+ /* captures: groups 1..n-1 as a poly array (nil for non-participating) */
2927
+ static sp_PolyArray *sp_MatchData_captures(sp_MatchData *m) {
2928
+ sp_PolyArray *r = sp_PolyArray_new();
2929
+ if (!m) return r;
2930
+ SP_GC_ROOT(r);
2931
+ for (mrb_int i = 1; i < m->ncap; i++) {
2932
+ const char *g = sp_MatchData_aref(m, i);
2933
+ sp_PolyArray_push(r, g ? sp_box_str(g) : sp_box_nil());
2934
+ }
2935
+ return r;
2936
+ }
2937
+ /* to_a: group 0 + captures */
2938
+ static sp_PolyArray *sp_MatchData_to_a(sp_MatchData *m) {
2939
+ sp_PolyArray *r = sp_PolyArray_new();
2940
+ if (!m) return r;
2941
+ SP_GC_ROOT(r);
2942
+ for (mrb_int i = 0; i < m->ncap; i++) {
2943
+ const char *g = sp_MatchData_aref(m, i);
2944
+ sp_PolyArray_push(r, g ? sp_box_str(g) : sp_box_nil());
2945
+ }
2946
+ return r;
2947
+ }
2948
+ static const char *sp_MatchData_pre_match(sp_MatchData *m) {
2949
+ if (!m) return sp_str_empty;
2950
+ int e = m->caps[0];
2951
+ if (e <= 0) return sp_str_empty;
2952
+ char *b = sp_str_alloc((size_t)e);
2953
+ memcpy(b, m->source, e); b[e] = 0; sp_str_set_len(b, (size_t)e);
2954
+ return b;
2955
+ }
2956
+ static const char *sp_MatchData_post_match(sp_MatchData *m) {
2957
+ if (!m) return sp_str_empty;
2958
+ int s = m->caps[1];
2959
+ size_t sl = strlen(m->source);
2960
+ if (s < 0 || (size_t)s >= sl) return sp_str_empty;
2961
+ size_t len = sl - (size_t)s;
2962
+ char *b = sp_str_alloc(len);
2963
+ memcpy(b, m->source + s, len); b[len] = 0; sp_str_set_len(b, len);
2964
+ return b;
2965
+ }
2966
+
939
2967
  static sp_RbVal sp_poly_shl(sp_RbVal a, sp_RbVal b) {
940
2968
  /* Dispatch by recv cls_id: an IntArray / PolyArray / etc. boxed
941
2969
  into a poly slot still wants Array#<< (push), not Integer#<<
@@ -959,17 +2987,20 @@ static sp_RbVal sp_poly_shl(sp_RbVal a, sp_RbVal b) {
959
2987
  return a;
960
2988
  }
961
2989
  if (a.cls_id == SP_BUILTIN_STR_ARRAY) {
962
- sp_StrArray_push((sp_StrArray *)a.v.p, b.tag == SP_TAG_STR ? (const char *)b.v.p : "");
2990
+ sp_StrArray_push((sp_StrArray *)a.v.p, b.tag == SP_TAG_STR ? (const char *)b.v.p : sp_str_empty);
963
2991
  return a;
964
2992
  }
965
2993
  }
966
2994
  return sp_box_int(sp_poly_to_i(a) << sp_poly_to_i(b));
967
2995
  }
968
- static mrb_int sp_PolyArray_length(sp_PolyArray *a) { return a->len; }
969
- static sp_RbVal sp_PolyArray_get(sp_PolyArray *a, mrb_int i) { if (i < 0) i += a->len; return a->data[i]; }
970
- static void sp_PolyArray_set(sp_PolyArray *a, mrb_int i, sp_RbVal v) { if (i < 0) i += a->len; a->data[i] = v; }
2996
+ static mrb_int sp_PolyArray_length(sp_PolyArray *a) { if (!a) return 0; return a->len; }
2997
+ static sp_RbVal sp_PolyArray_get(sp_PolyArray *a, mrb_int i) { if (!a) return sp_box_nil(); if (i < 0) i += a->len; if (i < 0 || i >= a->len) return sp_box_nil(); return a->data[i]; }
2998
+ /* Issues #770, #789: NULL + bounds guard. Out-of-range set no-ops. */
2999
+ static void sp_PolyArray_set(sp_PolyArray *a, mrb_int i, sp_RbVal v) { if (!a) return; if (a->frozen) { sp_raise_frozen_array(); return; } mrb_int orig=i; if (i < 0) i += a->len; if (i < 0) sp_raise_cls("IndexError", sp_sprintf("index %lld too small for array; minimum: %lld",(long long)orig,(long long)-a->len)); if (i >= a->len) return; a->data[i] = v; }
971
3000
  static sp_PolyArray *sp_PolyArray_slice(sp_PolyArray *a, mrb_int start, mrb_int len) { if (start < 0) start += a->len; if (start < 0) start = 0; sp_PolyArray *b = sp_PolyArray_new(); if (start >= a->len || len <= 0) return b; if (start + len > a->len) len = a->len - start; for (mrb_int i = 0; i < len; i++) sp_PolyArray_push(b, a->data[start + i]); return b; }
972
3001
  static sp_PolyArray *sp_PolyArray_slice_bang(sp_PolyArray *a, mrb_int from, mrb_int n) {
3002
+ if (!a) return sp_PolyArray_new();
3003
+ if (a->frozen) { sp_raise_frozen_array(); return sp_PolyArray_new(); }
973
3004
  if (from < 0) from += a->len;
974
3005
  if (from < 0) from = 0;
975
3006
  if (from > a->len) from = a->len;
@@ -982,30 +3013,278 @@ static sp_PolyArray *sp_PolyArray_slice_bang(sp_PolyArray *a, mrb_int from, mrb_
982
3013
  return r;
983
3014
  }
984
3015
  static sp_PolyArray *sp_PolyArray_dup(sp_PolyArray *a) { sp_PolyArray *b = sp_PolyArray_new(); for (mrb_int i = 0; i < a->len; i++) sp_PolyArray_push(b, a->data[i]); return b; }
985
- static void sp_PolyArray_shuffle_bang(sp_PolyArray *a) { for (mrb_int i = a->len - 1; i > 0; i--) { mrb_int j = (mrb_int)(rand() % (i + 1)); sp_RbVal t = a->data[i]; a->data[i] = a->data[j]; a->data[j] = t; } }
986
- static void sp_PolyArray_rotate_bang(sp_PolyArray*a,mrb_int n){if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;sp_RbVal*tmp=(sp_RbVal*)malloc(sizeof(sp_RbVal)*a->len);for(mrb_int i=0;i<a->len;i++)tmp[i]=a->data[(i+n)%a->len];for(mrb_int i=0;i<a->len;i++)a->data[i]=tmp[i];free(tmp);}
3016
+ /* Array#compact for poly_array: keep elements whose tag is not SP_TAG_NIL. */
3017
+ static sp_PolyArray *sp_PolyArray_compact(sp_PolyArray *a) { sp_PolyArray *b = sp_PolyArray_new(); if (!a) return b; for (mrb_int i = 0; i < a->len; i++) { if (a->data[i].tag != SP_TAG_NIL) sp_PolyArray_push(b, a->data[i]); } return b; }
3018
+ /* Issue #738: Hash#to_a as poly_array of [key, value] poly_array pairs. */
3019
+ static sp_PolyArray*sp_StrIntHash_to_a(sp_StrIntHash*h){sp_PolyArray*r=sp_PolyArray_new();if(!h)return r;for(mrb_int i=0;i<h->len;i++){sp_PolyArray*p=sp_PolyArray_new();sp_PolyArray_push(p,sp_box_str(h->order[i]));sp_PolyArray_push(p,sp_box_int(sp_StrIntHash_get(h,h->order[i])));sp_PolyArray_push(r,sp_box_poly_array(p));}return r;}
3020
+ static sp_PolyArray*sp_StrStrHash_to_a(sp_StrStrHash*h){sp_PolyArray*r=sp_PolyArray_new();if(!h)return r;for(mrb_int i=0;i<h->len;i++){sp_PolyArray*p=sp_PolyArray_new();sp_PolyArray_push(p,sp_box_str(h->order[i]));sp_PolyArray_push(p,sp_box_str(sp_StrStrHash_get(h,h->order[i])));sp_PolyArray_push(r,sp_box_poly_array(p));}return r;}
3021
+ static sp_PolyArray*sp_IntStrHash_to_a(sp_IntStrHash*h){sp_PolyArray*r=sp_PolyArray_new();if(!h)return r;for(mrb_int i=0;i<h->len;i++){sp_PolyArray*p=sp_PolyArray_new();sp_PolyArray_push(p,sp_box_int(h->order[i]));sp_PolyArray_push(p,sp_box_str(sp_IntStrHash_get(h,h->order[i])));sp_PolyArray_push(r,sp_box_poly_array(p));}return r;}
3022
+ /* Array#flatten -- walk into nested array values recursively. Each
3023
+ array-tagged element (IntArray / StrArray / SymArray / FloatArray /
3024
+ PolyArray) is expanded inline; scalars are appended as-is. Issue
3025
+ #739. */
3026
+ static void sp_PolyArray_flatten_into(sp_PolyArray *dst, sp_RbVal v) {
3027
+ if (v.tag != SP_TAG_OBJ) { sp_PolyArray_push(dst, v); return; }
3028
+ if (v.cls_id == SP_BUILTIN_INT_ARRAY) { sp_IntArray *ia = (sp_IntArray *)v.v.p; for (mrb_int i = 0; i < ia->len; i++) sp_PolyArray_push(dst, sp_box_int(ia->data[ia->start + i])); return; }
3029
+ if (v.cls_id == SP_BUILTIN_STR_ARRAY) { sp_StrArray *sa = (sp_StrArray *)v.v.p; for (mrb_int i = 0; i < sa->len; i++) sp_PolyArray_push(dst, sp_box_str(sa->data[i])); return; }
3030
+ if (v.cls_id == SP_BUILTIN_POLY_ARRAY) { sp_PolyArray *pa = (sp_PolyArray *)v.v.p; for (mrb_int i = 0; i < pa->len; i++) sp_PolyArray_flatten_into(dst, pa->data[i]); return; }
3031
+ /* Other array variants fall through as opaque elements; rare for
3032
+ deep-flatten use cases. */
3033
+ sp_PolyArray_push(dst, v);
3034
+ }
3035
+ static sp_PolyArray *sp_PolyArray_flatten(sp_PolyArray *a) { sp_PolyArray *b = sp_PolyArray_new(); if (!a) return b; for (mrb_int i = 0; i < a->len; i++) sp_PolyArray_flatten_into(b, a->data[i]); return b; }
3036
+
3037
+ /* Box-into-poly converters used by the printf-with-array codegen
3038
+ (`"%fmt" % typed_array`). The format helper expects sp_RbVal
3039
+ slots so it can dispatch per-element. */
3040
+ static sp_PolyArray *sp_IntArray_to_poly(sp_IntArray *a) {
3041
+ sp_PolyArray *r = sp_PolyArray_new();
3042
+ if (!a) return r;
3043
+ for (mrb_int i = 0; i < a->len; i++) sp_PolyArray_push(r, sp_box_int(a->data[a->start + i]));
3044
+ return r;
3045
+ }
3046
+ static sp_PolyArray *sp_StrArray_to_poly_fmt(sp_StrArray *a) {
3047
+ sp_PolyArray *r = sp_PolyArray_new();
3048
+ if (!a) return r;
3049
+ for (mrb_int i = 0; i < a->len; i++) sp_PolyArray_push(r, sp_box_str(a->data[i]));
3050
+ return r;
3051
+ }
3052
+
3053
+ /* String#% with a poly_array argument. Walks the format and for
3054
+ each spec ("%s", "%d", "%f", "%x", "%o", etc.) pulls the next
3055
+ array element. Width / flag chars between `%` and the conv
3056
+ letter (`-+0 #`, digits, `.`) are copied verbatim so printf
3057
+ does the substitution work. */
3058
+ static const char *sp_str_format_polyarr(const char *fmt, sp_PolyArray *a) {
3059
+ size_t cap = strlen(fmt) + 64;
3060
+ char *buf = (char *)malloc(cap);
3061
+ if (!buf) { perror("malloc"); exit(1); }
3062
+ size_t out = 0; mrb_int idx = 0; const char *p = fmt;
3063
+ while (*p) {
3064
+ if (*p != '%') {
3065
+ if (out + 1 >= cap) { cap = cap * 2; buf = (char *)realloc(buf, cap); }
3066
+ buf[out++] = *p++; continue;
3067
+ }
3068
+ if (p[1] == '%') {
3069
+ if (out + 1 >= cap) { cap = cap * 2; buf = (char *)realloc(buf, cap); }
3070
+ buf[out++] = '%'; p += 2; continue;
3071
+ }
3072
+ char spec[64]; size_t sl = 0; spec[sl++] = *p++;
3073
+ while (*p && sl < sizeof(spec) - 4) {
3074
+ char c = *p;
3075
+ if (c == '-' || c == '+' || c == ' ' || c == '#' || c == '0' || c == '.' || (c >= '0' && c <= '9')) { spec[sl++] = c; p++; }
3076
+ else break;
3077
+ }
3078
+ if (!*p) break;
3079
+ char conv = *p++; spec[sl++] = conv;
3080
+ char fmt_use[80];
3081
+ if (conv == 'd' || conv == 'i' || conv == 'x' || conv == 'X' || conv == 'o') {
3082
+ memcpy(fmt_use, spec, sl - 1);
3083
+ fmt_use[sl - 1] = 'l'; fmt_use[sl] = 'l'; fmt_use[sl + 1] = conv; fmt_use[sl + 2] = 0;
3084
+ } else {
3085
+ memcpy(fmt_use, spec, sl); fmt_use[sl] = 0;
3086
+ }
3087
+ char tmp[256]; int wn = 0;
3088
+ sp_RbVal v = (idx < a->len) ? a->data[idx] : sp_box_nil();
3089
+ idx++;
3090
+ if (conv == 'd' || conv == 'i' || conv == 'x' || conv == 'X' || conv == 'o') {
3091
+ long long lv = 0;
3092
+ if (v.tag == SP_TAG_INT) lv = (long long)v.v.i;
3093
+ else if (v.tag == SP_TAG_FLT) lv = (long long)v.v.f;
3094
+ else if (v.tag == SP_TAG_STR && v.v.s) lv = strtoll(v.v.s, NULL, 10);
3095
+ wn = snprintf(tmp, sizeof(tmp), fmt_use, lv);
3096
+ } else if (conv == 'f' || conv == 'e' || conv == 'E' || conv == 'g' || conv == 'G') {
3097
+ double dv = 0;
3098
+ if (v.tag == SP_TAG_FLT) dv = v.v.f;
3099
+ else if (v.tag == SP_TAG_INT) dv = (double)v.v.i;
3100
+ wn = snprintf(tmp, sizeof(tmp), fmt_use, dv);
3101
+ } else if (conv == 's') {
3102
+ const char *sv = "";
3103
+ char num_buf[32];
3104
+ if (v.tag == SP_TAG_STR) sv = v.v.s ? v.v.s : "";
3105
+ else if (v.tag == SP_TAG_INT) { snprintf(num_buf, sizeof(num_buf), "%lld", (long long)v.v.i); sv = num_buf; }
3106
+ else if (v.tag == SP_TAG_FLT) { snprintf(num_buf, sizeof(num_buf), "%g", v.v.f); sv = num_buf; }
3107
+ wn = snprintf(tmp, sizeof(tmp), fmt_use, sv);
3108
+ } else if (conv == 'c') {
3109
+ int cv = 0;
3110
+ if (v.tag == SP_TAG_INT) cv = (int)v.v.i;
3111
+ else if (v.tag == SP_TAG_STR && v.v.s && v.v.s[0]) cv = (unsigned char)v.v.s[0];
3112
+ wn = snprintf(tmp, sizeof(tmp), fmt_use, cv);
3113
+ } else {
3114
+ memcpy(tmp, spec, sl); tmp[sl] = 0; wn = (int)sl; idx--;
3115
+ }
3116
+ if (wn < 0) continue;
3117
+ if (out + (size_t)wn + 1 >= cap) { cap = (out + wn) * 2 + 64; buf = (char *)realloc(buf, cap); }
3118
+ memcpy(buf + out, tmp, wn); out += wn;
3119
+ }
3120
+ buf[out] = 0;
3121
+ char *r = sp_str_alloc(out); memcpy(r, buf, out); free(buf); return r;
3122
+ }
3123
+
3124
+ /* Array#assoc — return the first sub-array (poly_array element)
3125
+ whose first element equals `key`. Returns NULL when no match
3126
+ so the caller's `.inspect` round-trips to "nil". */
3127
+ static sp_PolyArray *sp_PolyArray_assoc(sp_PolyArray *a, sp_RbVal key) {
3128
+ if (!a) return NULL;
3129
+ for (mrb_int i = 0; i < a->len; i++) {
3130
+ sp_RbVal el = a->data[i];
3131
+ if (el.tag != SP_TAG_OBJ) continue;
3132
+ if (el.cls_id != SP_BUILTIN_POLY_ARRAY) continue;
3133
+ sp_PolyArray *sub = (sp_PolyArray *)el.v.p;
3134
+ if (sub && sub->len >= 1 && sp_poly_eq(sub->data[0], key)) return sub;
3135
+ }
3136
+ return NULL;
3137
+ }
3138
+
3139
+ /* Array#rassoc — same as assoc but matches against the second
3140
+ element of each sub-array. */
3141
+ static sp_PolyArray *sp_PolyArray_rassoc(sp_PolyArray *a, sp_RbVal val) {
3142
+ if (!a) return NULL;
3143
+ for (mrb_int i = 0; i < a->len; i++) {
3144
+ sp_RbVal el = a->data[i];
3145
+ if (el.tag != SP_TAG_OBJ) continue;
3146
+ if (el.cls_id != SP_BUILTIN_POLY_ARRAY) continue;
3147
+ sp_PolyArray *sub = (sp_PolyArray *)el.v.p;
3148
+ if (sub && sub->len >= 2 && sp_poly_eq(sub->data[1], val)) return sub;
3149
+ }
3150
+ return NULL;
3151
+ }
3152
+ /* Depth-bounded variant. depth==0 returns a shallow copy; each
3153
+ recursive step decrements depth, and a negative depth means
3154
+ "unlimited" (same as flatten without arg). Used by
3155
+ `Array#flatten(n)`. */
3156
+ static void sp_PolyArray_flatten_into_n(sp_PolyArray *dst, sp_RbVal v, mrb_int depth) {
3157
+ if (depth == 0 || v.tag != SP_TAG_OBJ) { sp_PolyArray_push(dst, v); return; }
3158
+ if (v.cls_id == SP_BUILTIN_INT_ARRAY) { sp_IntArray *ia = (sp_IntArray *)v.v.p; for (mrb_int i = 0; i < ia->len; i++) sp_PolyArray_push(dst, sp_box_int(ia->data[ia->start + i])); return; }
3159
+ if (v.cls_id == SP_BUILTIN_STR_ARRAY) { sp_StrArray *sa = (sp_StrArray *)v.v.p; for (mrb_int i = 0; i < sa->len; i++) sp_PolyArray_push(dst, sp_box_str(sa->data[i])); return; }
3160
+ if (v.cls_id == SP_BUILTIN_POLY_ARRAY) { sp_PolyArray *pa = (sp_PolyArray *)v.v.p; for (mrb_int i = 0; i < pa->len; i++) sp_PolyArray_flatten_into_n(dst, pa->data[i], depth - 1); return; }
3161
+ sp_PolyArray_push(dst, v);
3162
+ }
3163
+ static sp_PolyArray *sp_PolyArray_flatten_n(sp_PolyArray *a, mrb_int depth) {
3164
+ sp_PolyArray *b = sp_PolyArray_new();
3165
+ if (!a) return b;
3166
+ if (depth < 0) depth = INT64_MAX;
3167
+ for (mrb_int i = 0; i < a->len; i++) sp_PolyArray_flatten_into_n(b, a->data[i], depth);
3168
+ return b;
3169
+ }
3170
+ /* Transpose a poly-array of int-arrays (or int-arrays boxed in poly).
3171
+ Used by `Array#transpose` on a matrix whose rows are IntArrays. */
3172
+ /* sp_poly_array_transpose is defined below, after the sp_poly_length /
3173
+ sp_poly_int_get element accessors it relies on. */
3174
+ /* Runtime dispatch: .length/.size on a boxed array value (sp_RbVal). */
3175
+ static mrb_int sp_poly_length(sp_RbVal v) {
3176
+ if (v.tag != SP_TAG_OBJ) return 0;
3177
+ if (v.cls_id == SP_BUILTIN_INT_ARRAY) return sp_IntArray_length((sp_IntArray *)v.v.p);
3178
+ if (v.cls_id == SP_BUILTIN_STR_ARRAY) return ((sp_StrArray *)v.v.p)->len;
3179
+ if (v.cls_id == SP_BUILTIN_POLY_ARRAY) return ((sp_PolyArray *)v.v.p)->len;
3180
+ if (v.cls_id == SP_BUILTIN_FLT_ARRAY) return ((sp_FloatArray *)v.v.p)->len;
3181
+ return 0;
3182
+ }
3183
+ /* Runtime dispatch: [] on a boxed array value with integer index -> boxed element. */
3184
+ static sp_RbVal sp_poly_int_get(sp_RbVal v, mrb_int i) {
3185
+ if (v.tag != SP_TAG_OBJ) return sp_box_nil();
3186
+ if (v.cls_id == SP_BUILTIN_INT_ARRAY) {
3187
+ mrb_int val = sp_IntArray_get((sp_IntArray *)v.v.p, i);
3188
+ return val == SP_INT_NIL ? sp_box_nil() : sp_box_int(val);
3189
+ }
3190
+ if (v.cls_id == SP_BUILTIN_POLY_ARRAY) {
3191
+ sp_PolyArray *a = (sp_PolyArray *)v.v.p;
3192
+ if (i < 0 || i >= a->len) return sp_box_nil();
3193
+ return a->data[i];
3194
+ }
3195
+ if (v.cls_id == SP_BUILTIN_STR_ARRAY) {
3196
+ sp_StrArray *a = (sp_StrArray *)v.v.p;
3197
+ if (i < 0 || i >= a->len) return sp_box_nil();
3198
+ return sp_box_str(sp_StrArray_get(a, i));
3199
+ }
3200
+ if (v.cls_id == SP_BUILTIN_FLT_ARRAY) {
3201
+ sp_FloatArray *a = (sp_FloatArray *)v.v.p;
3202
+ if (i < 0 || i >= a->len) return sp_box_nil();
3203
+ return sp_box_float(sp_FloatArray_get(a, i));
3204
+ }
3205
+ return sp_box_nil();
3206
+ }
3207
+ /* Array#transpose on a poly_array (untyped nested array). Swaps rows and
3208
+ columns, preserving each element's runtime type via sp_poly_int_get
3209
+ (int / float / str / poly rows). Returns a poly_array of poly_array
3210
+ columns; the statically-typed nested-array paths are handled inline by
3211
+ codegen. Row length is taken from the first row (CRuby raises on a
3212
+ ragged matrix; here a short row reads nil past its end). */
3213
+ static sp_PolyArray *sp_poly_array_transpose(sp_PolyArray *rows) {
3214
+ SP_GC_SAVE();
3215
+ if (!rows || rows->len == 0) return sp_PolyArray_new();
3216
+ mrb_int ncols = sp_poly_length(rows->data[0]);
3217
+ sp_PolyArray *result = sp_PolyArray_new();
3218
+ SP_GC_ROOT(result);
3219
+ for (mrb_int j = 0; j < ncols; j++) {
3220
+ sp_PolyArray *col = sp_PolyArray_new();
3221
+ SP_GC_ROOT(col);
3222
+ for (mrb_int i = 0; i < rows->len; i++) {
3223
+ sp_PolyArray_push(col, sp_poly_int_get(rows->data[i], j));
3224
+ }
3225
+ sp_PolyArray_push(result, sp_box_poly_array(col));
3226
+ }
3227
+ return result;
3228
+ }
3229
+ /* Sum the integer-tagged elements of a poly_array. Used by
3230
+ `Array#sum` on a poly_array whose runtime tags are uniform int
3231
+ (e.g. the result of `arr.map { _1[:int_key] }`). Non-int tags
3232
+ contribute zero. */
3233
+ static mrb_int sp_PolyArray_sum_int(sp_PolyArray *a) { if (!a) return 0; mrb_int s = 0; for (mrb_int i = 0; i < a->len; i++) { if (a->data[i].tag == SP_TAG_INT) s += a->data[i].v.i; } return s; }
3234
+ static void sp_PolyArray_shuffle_bang(sp_PolyArray *a) { if (!a || a->frozen) { if (a && a->frozen) sp_raise_frozen_array(); return; } for (mrb_int i = a->len - 1; i > 0; i--) { mrb_int j = (mrb_int)(rand() % (i + 1)); sp_RbVal t = a->data[i]; a->data[i] = a->data[j]; a->data[j] = t; } }
3235
+ static void sp_PolyArray_rotate_bang(sp_PolyArray*a,mrb_int n){if(!a)return;if(a->frozen){sp_raise_frozen_array();return;}if(a->len<=0)return;n=((n%a->len)+a->len)%a->len;if(n==0)return;sp_RbVal*d=a->data;mrb_int lo=0,hi=n-1;while(lo<hi){sp_RbVal t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=n;hi=a->len-1;while(lo<hi){sp_RbVal t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}lo=0;hi=a->len-1;while(lo<hi){sp_RbVal t=d[lo];d[lo]=d[hi];d[hi]=t;lo++;hi--;}}
987
3236
  static sp_PolyArray *sp_PolyArray_shuffle(sp_PolyArray *a) { sp_PolyArray *b = sp_PolyArray_dup(a); sp_PolyArray_shuffle_bang(b); return b; }
3237
+ static sp_RbVal sp_PolyArray_sample(sp_PolyArray *a) { if (a->len <= 0) return sp_box_nil(); return a->data[(mrb_int)(rand()%a->len)]; }
3238
+
3239
+ /* Forward decl: sp_poly_inspect dispatches into sp_PolyArray_inspect
3240
+ for nested poly arrays (under promote, an `each_cons` chain's outer
3241
+ accumulator boxes each inner poly_array element), but the
3242
+ sp_PolyArray_inspect body lives a few lines below. */
3243
+ static const char *sp_PolyArray_inspect(sp_PolyArray *a);
3244
+ static const char*sp_PolyArrayPtrArray_inspect(sp_PtrArray*a){sp_String*s=sp_String_new("[");for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_PolyArray_inspect((sp_PolyArray*)a->data[i]));}sp_String_append(s,"]");return s->data;}
988
3245
 
989
3246
  /* Object#inspect for a tagged sp_RbVal. Dispatches on the runtime tag;
990
3247
  each branch reuses the matching primitive inspect helper. Falls back
991
3248
  to "#<Object>" for SP_TAG_OBJ because the runtime has no class-name
992
3249
  table yet (follow-up PR). Returns a GC-managed C string. */
993
- static const char *sp_poly_inspect(sp_RbVal v) {
3250
+ static inline const char *sp_poly_inspect(sp_RbVal v) {
994
3251
  switch (v.tag) {
995
3252
  case SP_TAG_INT: return sp_int_to_s(v.v.i);
996
3253
  case SP_TAG_STR: return sp_str_inspect(v.v.s);
997
3254
  case SP_TAG_FLT: return sp_float_to_s(v.v.f);
998
3255
  case SP_TAG_BOOL: return v.v.b ? SPL("true") : SPL("false");
999
3256
  case SP_TAG_NIL: return SPL("nil");
1000
- case SP_TAG_SYM: return sp_str_concat(":", sp_sym_to_s((sp_sym)v.v.i));
1001
- case SP_TAG_OBJ: return SPL("#<Object>");
3257
+ case SP_TAG_SYM: return sp_str_concat(SPL(":"), sp_sym_to_s((sp_sym)v.v.i));
3258
+ case SP_TAG_ENCODING: return sp_sprintf("#<Encoding:%s>", v.v.s ? v.v.s : "");
3259
+ case SP_TAG_OBJ:
3260
+ /* Built-in container / value-type tags get their typed inspect
3261
+ helper. Matches the dispatch shape in sp_poly_to_s above and the
3262
+ `puts` poly arm earlier in this file; without it, a Range / Time
3263
+ / typed Array stored as an sp_RbVal value (e.g. a sym_poly_hash
3264
+ that mixes `200..299` and `404`) reported "#<Object>" from
3265
+ `.inspect`, which was both wrong for CRuby parity and useless
3266
+ for debugging. */
3267
+ switch (v.cls_id) {
3268
+ case SP_BUILTIN_INT_ARRAY: return sp_IntArray_inspect((sp_IntArray *)v.v.p);
3269
+ case SP_BUILTIN_FLT_ARRAY: return sp_FloatArray_inspect((sp_FloatArray *)v.v.p);
3270
+ case SP_BUILTIN_STR_ARRAY: return sp_StrArray_inspect((sp_StrArray *)v.v.p);
3271
+ case SP_BUILTIN_SYM_ARRAY: return sp_SymArray_inspect((sp_IntArray *)v.v.p);
3272
+ case SP_BUILTIN_PTR_ARRAY: return sp_PtrArray_inspect((sp_PtrArray *)v.v.p);
3273
+ case SP_BUILTIN_POLY_ARRAY: return sp_PolyArray_inspect((sp_PolyArray *)v.v.p);
3274
+ case SP_BUILTIN_RANGE: return sp_Range_inspect((sp_Range *)v.v.p);
3275
+ case SP_BUILTIN_TIME: return sp_Time_inspect((sp_Time *)v.v.p);
3276
+ default: return SPL("#<Object>");
3277
+ }
1002
3278
  default: return sp_str_empty;
1003
3279
  }
1004
3280
  }
1005
3281
  /* Array#inspect for heterogeneous poly arrays. Each element dispatches
1006
3282
  through sp_poly_inspect, so a mixed `[1, "x", :y]` renders
1007
- `[1, "x", :y]` byte-for-byte identical to CRuby. */
3283
+ `[1, "x", :y]` byte-for-byte identical to CRuby. NULL renders
3284
+ "nil" so callers that store a nil-returning slot (assoc/rassoc
3285
+ miss, etc.) round-trip cleanly through `.inspect`. */
1008
3286
  static const char *sp_PolyArray_inspect(sp_PolyArray *a) {
3287
+ if (!a) { char *r = sp_str_alloc(3); r[0] = 'n'; r[1] = 'i'; r[2] = 'l'; r[3] = 0; sp_str_set_len(r, 3); return r; }
1009
3288
  sp_String *s = sp_String_new("[");
1010
3289
  for (mrb_int i = 0; i < a->len; i++) {
1011
3290
  if (i > 0) sp_String_append(s, ", ");
@@ -1014,40 +3293,98 @@ static const char *sp_PolyArray_inspect(sp_PolyArray *a) {
1014
3293
  sp_String_append(s, "]");
1015
3294
  return s->data;
1016
3295
  }
3296
+ /* Array#join for a mixed-element (poly) array: to_s each element via
3297
+ sp_poly_to_s and concatenate with sep. Mirrors sp_StrArray_join for
3298
+ the boxed-element case. */
3299
+ static const char *sp_PolyArray_join(sp_PolyArray *a, const char *sep) {
3300
+ if (!a) return sp_str_empty;
3301
+ sp_String *s = sp_String_new("");
3302
+ for (mrb_int i = 0; i < a->len; i++) {
3303
+ if (i > 0 && sep) sp_String_append(s, sep);
3304
+ sp_String_append(s, sp_poly_to_s(a->data[i]));
3305
+ }
3306
+ return s->data;
3307
+ }
3308
+ static mrb_bool sp_PolyArray_eq(sp_PolyArray *a, sp_PolyArray *b) {
3309
+ if (!a || !b) return a == b;
3310
+ if (a->len != b->len) return FALSE;
3311
+ for (mrb_int i = 0; i < a->len; i++) {
3312
+ if (!sp_poly_eq(a->data[i], b->data[i])) return FALSE;
3313
+ }
3314
+ return TRUE;
3315
+ }
3316
+ static mrb_bool sp_PolyArray_include(sp_PolyArray *a, sp_RbVal v) {
3317
+ if (!a) return FALSE;
3318
+ for (mrb_int i = 0; i < a->len; i++) {
3319
+ if (sp_poly_eq(a->data[i], v)) return TRUE;
3320
+ }
3321
+ return FALSE;
3322
+ }
1017
3323
 
1018
3324
  /* Mark the embedded GC reference inside an sp_RbVal (string or obj).
1019
3325
  Used as the scan hook for containers that store polymorphic values. */
1020
- static inline void sp_mark_rbval(sp_RbVal v) {
1021
- if (v.tag == SP_TAG_STR) sp_mark_string(v.v.s);
1022
- else if (v.tag == SP_TAG_OBJ) sp_gc_mark(v.v.p);
1023
- }
3326
+ /* sp_mark_rbval is an inline helper in sp_gc.h. */
1024
3327
 
1025
3328
  /* StrPolyHash: string keys, sp_RbVal values — for hashes with mixed value types. */
1026
- typedef struct{const char**keys;sp_RbVal*vals;const char**order;mrb_int len;mrb_int cap;mrb_int mask;}sp_StrPolyHash;
3329
+ /* `dproc` holds a Hash.new{} default block, lowered to a dedicated C
3330
+ fn `sp_RbVal (*)(sp_StrPolyHash *self, const char *key)` with typed
3331
+ params (codegen emits it). Called by _get on a miss. Issue #912. */
3332
+ typedef struct sp_StrPolyHash sp_StrPolyHash;
3333
+ typedef sp_RbVal (*sp_strpoly_dproc_t)(sp_StrPolyHash *, const char *);
3334
+ struct sp_StrPolyHash{const char**keys;sp_RbVal*vals;const char**order;mrb_int len;mrb_int cap;mrb_int mask;sp_RbVal default_v;sp_strpoly_dproc_t dproc;};
1027
3335
  static void sp_StrPolyHash_fin(void*p){sp_StrPolyHash*h=(sp_StrPolyHash*)p;free(h->keys);free(h->vals);free(h->order);}
1028
- static void sp_StrPolyHash_scan(void*p){sp_StrPolyHash*h=(sp_StrPolyHash*)p;for(mrb_int i=0;i<h->cap;i++){if(h->keys[i]){sp_mark_string(h->keys[i]);sp_mark_rbval(h->vals[i]);}}}
1029
- static sp_StrPolyHash*sp_StrPolyHash_new(void){sp_StrPolyHash*h=(sp_StrPolyHash*)sp_gc_alloc(sizeof(sp_StrPolyHash),sp_StrPolyHash_fin,sp_StrPolyHash_scan);h->cap=16;h->mask=15;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->order=(const char**)malloc(sizeof(const char*)*h->cap);h->len=0;return h;}
3336
+ static void sp_StrPolyHash_scan(void*p){sp_StrPolyHash*h=(sp_StrPolyHash*)p;for(mrb_int i=0;i<h->cap;i++){if(h->keys[i]){sp_mark_string(h->keys[i]);sp_mark_rbval(h->vals[i]);}}sp_mark_rbval(h->default_v);}
3337
+ static sp_StrPolyHash*sp_StrPolyHash_new(void){sp_StrPolyHash*h=(sp_StrPolyHash*)sp_gc_alloc(sizeof(sp_StrPolyHash),sp_StrPolyHash_fin,sp_StrPolyHash_scan);h->cap=16;h->mask=15;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->order=(const char**)malloc(sizeof(const char*)*h->cap);h->len=0;h->default_v=sp_box_nil();return h;}
3338
+ static sp_StrPolyHash*sp_StrPolyHash_new_with_default(sp_RbVal d){sp_StrPolyHash*h=sp_StrPolyHash_new();h->default_v=d;return h;}
3339
+ static sp_StrPolyHash*sp_StrPolyHash_new_dproc(sp_strpoly_dproc_t fn){sp_StrPolyHash*h=sp_StrPolyHash_new();h->dproc=fn;return h;}
1030
3340
  static void sp_StrPolyHash_grow(sp_StrPolyHash*h){mrb_int oc=h->cap;const char**ok=h->keys;sp_RbVal*ov=h->vals;h->cap*=2;h->mask=h->cap-1;h->keys=(const char**)calloc(h->cap,sizeof(const char*));h->vals=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->order=(const char**)realloc(h->order,sizeof(const char*)*h->cap);h->len=0;for(mrb_int i=0;i<oc;i++){if(ok[i]){mrb_int idx=(mrb_int)(sp_str_hash(ok[i])&h->mask);while(h->keys[idx])idx=(idx+1)&h->mask;h->keys[idx]=ok[i];h->vals[idx]=ov[i];h->len++;}}free(ok);free(ov);}
1031
- static sp_RbVal sp_StrPolyHash_get(sp_StrPolyHash*h,const char*k){mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return h->vals[idx];idx=(idx+1)&h->mask;}return sp_box_nil();}
3341
+ static sp_RbVal sp_StrPolyHash_get(sp_StrPolyHash*h,const char*k){if(!h)return sp_box_nil();mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return h->vals[idx];idx=(idx+1)&h->mask;}if(h->dproc)return h->dproc(h,k);return h->default_v;}
1032
3342
  static void sp_StrPolyHash_set(sp_StrPolyHash*h,const char*k,sp_RbVal v){if(h->len*2>=h->cap)sp_StrPolyHash_grow(h);mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0){h->vals[idx]=v;return;}idx=(idx+1)&h->mask;}h->keys[idx]=k;h->vals[idx]=v;h->order[h->len]=k;h->len++;}
1033
- static mrb_bool sp_StrPolyHash_has_key(sp_StrPolyHash*h,const char*k){mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return TRUE;idx=(idx+1)&h->mask;}return FALSE;}
3343
+ static mrb_bool sp_StrPolyHash_has_key(sp_StrPolyHash*h,const char*k){if(!h)return FALSE;mrb_int idx=(mrb_int)(sp_str_hash(k)&h->mask);while(h->keys[idx]){if(strcmp(h->keys[idx],k)==0)return TRUE;idx=(idx+1)&h->mask;}return FALSE;}
1034
3344
  static mrb_int sp_StrPolyHash_length(sp_StrPolyHash*h){return h->len;}
1035
- static sp_StrArray*sp_StrPolyHash_keys(sp_StrPolyHash*h){sp_StrArray*a=sp_StrArray_new();for(mrb_int i=0;i<h->len;i++)sp_StrArray_push(a,h->order[i]);return a;}
3345
+ static sp_StrArray*sp_StrPolyHash_keys(sp_StrPolyHash*h){sp_StrArray*a=sp_StrArray_new();if(!h)return a;for(mrb_int i=0;i<h->len;i++)sp_StrArray_push(a,h->order[i]);return a;}
1036
3346
  static sp_PolyArray*sp_StrPolyHash_values(sp_StrPolyHash*h){sp_PolyArray*a=sp_PolyArray_new();for(mrb_int i=0;i<h->len;i++)sp_PolyArray_push(a,sp_StrPolyHash_get(h,h->order[i]));return a;}
3347
+ /* Hash#merge for str_poly_hash. Same shape as the
3348
+ StrIntHash / SymPolyHash siblings -- copy recv's entries into a
3349
+ fresh hash, then overlay other's. */
3350
+ static sp_StrPolyHash*sp_StrPolyHash_merge(sp_StrPolyHash*a,sp_StrPolyHash*b){sp_StrPolyHash*r=sp_StrPolyHash_new();r->default_v=a->default_v;for(mrb_int i=0;i<a->len;i++)sp_StrPolyHash_set(r,a->order[i],sp_StrPolyHash_get(a,a->order[i]));for(mrb_int i=0;i<b->len;i++)sp_StrPolyHash_set(r,b->order[i],sp_StrPolyHash_get(b,b->order[i]));return r;}
3351
+ static sp_StrPolyHash*sp_StrPolyHash_dup(sp_StrPolyHash*h){sp_StrPolyHash*r=sp_StrPolyHash_new();r->default_v=h->default_v;for(mrb_int i=0;i<h->len;i++)sp_StrPolyHash_set(r,h->order[i],sp_StrPolyHash_get(h,h->order[i]));return r;}
3352
+ static mrb_bool sp_StrPolyHash_eq(sp_StrPolyHash*a,sp_StrPolyHash*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++){const char*k=a->order[i];if(!sp_StrPolyHash_has_key(b,k))return FALSE;if(!sp_poly_eq(sp_StrPolyHash_get(a,k),sp_StrPolyHash_get(b,k)))return FALSE;}return TRUE;}
3353
+ /* Issue #851: inspect for str_poly_hash. */
3354
+ static const char*sp_StrPolyHash_inspect(sp_StrPolyHash*h){sp_String*s=sp_String_new("{");if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_str_inspect(h->order[i]));sp_String_append(s,"=>");sp_String_append(s,sp_poly_inspect(sp_StrPolyHash_get(h,h->order[i])));}}sp_String_append(s,"}");return s->data;}
3355
+ /* Convert a narrower StrStrHash to a StrPolyHash. Needed when the
3356
+ analyzer widens an LV slot to sp_StrPolyHash* (e.g. later poly-value
3357
+ writes) but the initial RHS is a sibling narrower hash variant —
3358
+ raw pointer assignment would mix incompatible struct layouts
3359
+ (vals[] of const char** vs sp_RbVal*). See issue #614. */
3360
+ static sp_StrPolyHash*sp_StrPolyHash_from_str_str_hash(sp_StrStrHash*h){sp_StrPolyHash*r=sp_StrPolyHash_new();if(!h)return r;if(h->default_v)r->default_v=sp_box_str(h->default_v);for(mrb_int i=0;i<h->len;i++){const char*k=h->order[i];sp_StrPolyHash_set(r,k,sp_box_str(sp_StrStrHash_get(h,k)));}return r;}
3361
+ static sp_StrPolyHash*sp_StrPolyHash_from_str_int_hash(sp_StrIntHash*h){sp_StrPolyHash*r=sp_StrPolyHash_new();if(!h)return r;r->default_v=sp_box_int(h->default_v);for(mrb_int i=0;i<h->len;i++){const char*k=h->order[i];sp_StrPolyHash_set(r,k,sp_box_int(sp_StrIntHash_get(h,k)));}return r;}
1037
3362
 
1038
3363
  /* SymPolyHash: symbol keys, sp_RbVal values — same shape as SymStrHash but with poly values. */
1039
- typedef struct{sp_sym*keys;sp_RbVal*vals;sp_sym*order;mrb_int len;mrb_int cap;mrb_int mask;}sp_SymPolyHash;
3364
+ /* Named struct so lib/sp_fiber.c can forward-declare it for sp_Fiber's
3365
+ `storage` member without pulling in the full poly-hash machinery. */
3366
+ typedef struct sp_SymPolyHash{sp_sym*keys;sp_RbVal*vals;sp_sym*order;mrb_int len;mrb_int cap;mrb_int mask;sp_RbVal default_v;}sp_SymPolyHash;
1040
3367
  static void sp_SymPolyHash_fin(void*p){sp_SymPolyHash*h=(sp_SymPolyHash*)p;free(h->keys);free(h->vals);free(h->order);}
1041
- static void sp_SymPolyHash_scan(void*p){sp_SymPolyHash*h=(sp_SymPolyHash*)p;for(mrb_int i=0;i<h->cap;i++){if(h->keys[i]>=0)sp_mark_rbval(h->vals[i]);}}
1042
- static sp_SymPolyHash*sp_SymPolyHash_new(void){sp_SymPolyHash*h=(sp_SymPolyHash*)sp_gc_alloc(sizeof(sp_SymPolyHash),sp_SymPolyHash_fin,sp_SymPolyHash_scan);h->cap=16;h->mask=15;h->keys=(sp_sym*)malloc(sizeof(sp_sym)*h->cap);for(mrb_int i=0;i<h->cap;i++)h->keys[i]=-1;h->vals=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->order=(sp_sym*)malloc(sizeof(sp_sym)*h->cap);h->len=0;return h;}
3368
+ static void sp_SymPolyHash_scan(void*p){sp_SymPolyHash*h=(sp_SymPolyHash*)p;for(mrb_int i=0;i<h->cap;i++){if(h->keys[i]>=0)sp_mark_rbval(h->vals[i]);}sp_mark_rbval(h->default_v);}
3369
+ static sp_SymPolyHash*sp_SymPolyHash_new(void){sp_SymPolyHash*h=(sp_SymPolyHash*)sp_gc_alloc(sizeof(sp_SymPolyHash),sp_SymPolyHash_fin,sp_SymPolyHash_scan);h->cap=16;h->mask=15;h->keys=(sp_sym*)malloc(sizeof(sp_sym)*h->cap);for(mrb_int i=0;i<h->cap;i++)h->keys[i]=-1;h->vals=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->order=(sp_sym*)malloc(sizeof(sp_sym)*h->cap);h->len=0;h->default_v=sp_box_nil();return h;}
3370
+ static sp_SymPolyHash*sp_SymPolyHash_new_with_default(sp_RbVal d){sp_SymPolyHash*h=sp_SymPolyHash_new();h->default_v=d;return h;}
1043
3371
  static void sp_SymPolyHash_grow(sp_SymPolyHash*h){mrb_int oc=h->cap;sp_sym*ok=h->keys;sp_RbVal*ov=h->vals;h->cap*=2;h->mask=h->cap-1;h->keys=(sp_sym*)malloc(sizeof(sp_sym)*h->cap);for(mrb_int i=0;i<h->cap;i++)h->keys[i]=-1;h->vals=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->order=(sp_sym*)realloc(h->order,sizeof(sp_sym)*h->cap);h->len=0;for(mrb_int i=0;i<oc;i++){if(ok[i]>=0){mrb_int idx=(mrb_int)(((mrb_int)ok[i])&h->mask);while(h->keys[idx]>=0)idx=(idx+1)&h->mask;h->keys[idx]=ok[i];h->vals[idx]=ov[i];h->len++;}}free(ok);free(ov);}
1044
- static sp_RbVal sp_SymPolyHash_get(sp_SymPolyHash*h,sp_sym k){mrb_int idx=(mrb_int)(((mrb_int)k)&h->mask);while(h->keys[idx]>=0){if(h->keys[idx]==k)return h->vals[idx];idx=(idx+1)&h->mask;}return sp_box_nil();}
3372
+ static sp_RbVal sp_SymPolyHash_get(sp_SymPolyHash*h,sp_sym k){if(!h)return sp_box_nil();mrb_int idx=(mrb_int)(((mrb_int)k)&h->mask);while(h->keys[idx]>=0){if(h->keys[idx]==k)return h->vals[idx];idx=(idx+1)&h->mask;}return h->default_v;}
1045
3373
  static void sp_SymPolyHash_set(sp_SymPolyHash*h,sp_sym k,sp_RbVal v){if(h->len*2>=h->cap)sp_SymPolyHash_grow(h);mrb_int idx=(mrb_int)(((mrb_int)k)&h->mask);while(h->keys[idx]>=0){if(h->keys[idx]==k){h->vals[idx]=v;return;}idx=(idx+1)&h->mask;}h->keys[idx]=k;h->vals[idx]=v;h->order[h->len]=k;h->len++;}
1046
3374
  static mrb_bool sp_SymPolyHash_has_key(sp_SymPolyHash*h,sp_sym k){mrb_int idx=(mrb_int)(((mrb_int)k)&h->mask);while(h->keys[idx]>=0){if(h->keys[idx]==k)return TRUE;idx=(idx+1)&h->mask;}return FALSE;}
1047
3375
  static mrb_int sp_SymPolyHash_length(sp_SymPolyHash*h){return h->len;}
1048
3376
  static sp_IntArray*sp_SymPolyHash_keys(sp_SymPolyHash*h){sp_IntArray*a=sp_IntArray_new();for(mrb_int i=0;i<h->len;i++)sp_IntArray_push(a,(mrb_int)h->order[i]);return a;}
1049
3377
  static sp_PolyArray*sp_SymPolyHash_values(sp_SymPolyHash*h){sp_PolyArray*a=sp_PolyArray_new();for(mrb_int i=0;i<h->len;i++)sp_PolyArray_push(a,sp_SymPolyHash_get(h,h->order[i]));return a;}
1050
- static sp_SymPolyHash*sp_SymPolyHash_merge(sp_SymPolyHash*a,sp_SymPolyHash*b){sp_SymPolyHash*r=sp_SymPolyHash_new();for(mrb_int i=0;i<a->len;i++)sp_SymPolyHash_set(r,a->order[i],sp_SymPolyHash_get(a,a->order[i]));for(mrb_int i=0;i<b->len;i++)sp_SymPolyHash_set(r,b->order[i],sp_SymPolyHash_get(b,b->order[i]));return r;}
3378
+ static sp_SymPolyHash*sp_SymPolyHash_merge(sp_SymPolyHash*a,sp_SymPolyHash*b){sp_SymPolyHash*r=sp_SymPolyHash_new();r->default_v=a->default_v;for(mrb_int i=0;i<a->len;i++)sp_SymPolyHash_set(r,a->order[i],sp_SymPolyHash_get(a,a->order[i]));for(mrb_int i=0;i<b->len;i++)sp_SymPolyHash_set(r,b->order[i],sp_SymPolyHash_get(b,b->order[i]));return r;}
3379
+ /* Hash#delete for sym_poly_hash. Removes key and re-tombstones the
3380
+ slot, shifting probe-chain successors backward and dropping the
3381
+ key from the insertion-order list. Issue #510. */
3382
+ static void sp_SymPolyHash_delete(sp_SymPolyHash*h,sp_sym k){mrb_int idx=(mrb_int)(((mrb_int)k)&h->mask);while(h->keys[idx]>=0){if(h->keys[idx]==k){h->keys[idx]=-1;h->vals[idx]=sp_box_nil();h->len--;mrb_int j=(idx+1)&h->mask;while(h->keys[j]>=0){mrb_int nj=(mrb_int)(((mrb_int)h->keys[j])&h->mask);if((j>idx&&(nj<=idx||nj>j))||(j<idx&&nj<=idx&&nj>j)){h->keys[idx]=h->keys[j];h->vals[idx]=h->vals[j];h->keys[j]=-1;h->vals[j]=sp_box_nil();idx=j;}j=(j+1)&h->mask;}{mrb_int oi=0;while(oi<=h->len){if(h->order[oi]==k){while(oi<h->len){h->order[oi]=h->order[oi+1];oi++;}break;}oi++;}}return;}idx=(idx+1)&h->mask;}}
3383
+ static sp_SymPolyHash*sp_SymPolyHash_dup(sp_SymPolyHash*h){sp_SymPolyHash*r=sp_SymPolyHash_new();r->default_v=h->default_v;for(mrb_int i=0;i<h->len;i++)sp_SymPolyHash_set(r,h->order[i],sp_SymPolyHash_get(h,h->order[i]));return r;}
3384
+ static mrb_bool sp_SymPolyHash_eq(sp_SymPolyHash*a,sp_SymPolyHash*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++){sp_sym k=a->order[i];if(!sp_SymPolyHash_has_key(b,k))return FALSE;if(!sp_poly_eq(sp_SymPolyHash_get(a,k),sp_SymPolyHash_get(b,k)))return FALSE;}return TRUE;}
3385
+ /* Hash#inspect for sym_poly_hash. CRuby 4.0 renders symbol keys
3386
+ in shorthand: `{a: 1, b: "x"}` rather than `{:a=>1, :b=>"x"}`. */
3387
+ static const char*sp_SymPolyHash_inspect(sp_SymPolyHash*h){if(!h){char*r=sp_str_alloc_raw(3);r[0]='{';r[1]='}';r[2]=0;sp_str_set_len(r,2);return r;}sp_String*s=sp_String_new("{");for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(s,", ");sp_String_append(s,sp_sym_to_s(h->order[i]));sp_String_append(s,": ");sp_String_append(s,sp_poly_inspect(sp_SymPolyHash_get(h,h->order[i])));}sp_String_append(s,"}");return s->data;}
1051
3388
 
1052
3389
  /* PolyPolyHash: heterogeneous keys + values (both sp_RbVal). For
1053
3390
  primitives the hash/eql is tag-based (value equality); for OBJ tag
@@ -1066,6 +3403,8 @@ static mrb_int sp_rbval_hash_key(sp_RbVal v) {
1066
3403
  return (mrb_int)v.v.i;
1067
3404
  case SP_TAG_STR:
1068
3405
  return v.v.s ? (mrb_int)sp_str_hash(v.v.s) : 0;
3406
+ case SP_TAG_ENCODING:
3407
+ return v.v.s ? (mrb_int)sp_str_hash(v.v.s) : 0;
1069
3408
  case SP_TAG_FLT: { uint64_t b; memcpy(&b, &v.v.f, sizeof(b)); return (mrb_int)b; }
1070
3409
  case SP_TAG_OBJ:
1071
3410
  if (sp_obj_hash_hook) return sp_obj_hash_hook(v.cls_id, v.v.p);
@@ -1082,6 +3421,10 @@ static mrb_bool sp_rbval_eql_key(sp_RbVal a, sp_RbVal b) {
1082
3421
  if (a.v.s == b.v.s) return TRUE;
1083
3422
  if (!a.v.s || !b.v.s) return FALSE;
1084
3423
  return strcmp(a.v.s, b.v.s) == 0;
3424
+ case SP_TAG_ENCODING:
3425
+ if (a.v.s == b.v.s) return TRUE;
3426
+ if (!a.v.s || !b.v.s) return FALSE;
3427
+ return strcmp(a.v.s, b.v.s) == 0;
1085
3428
  case SP_TAG_FLT:
1086
3429
  return a.v.f == b.v.f;
1087
3430
  case SP_TAG_OBJ:
@@ -1097,12 +3440,96 @@ static void sp_PolyPolyHash_fin(void*p){sp_PolyPolyHash*h=(sp_PolyPolyHash*)p;fr
1097
3440
  static void sp_PolyPolyHash_scan(void*p){sp_PolyPolyHash*h=(sp_PolyPolyHash*)p;for(mrb_int i=0;i<h->cap;i++){if(h->occ[i]){sp_mark_rbval(h->keys[i]);sp_mark_rbval(h->vals[i]);}}}
1098
3441
  static sp_PolyPolyHash*sp_PolyPolyHash_new(void){sp_PolyPolyHash*h=(sp_PolyPolyHash*)sp_gc_alloc(sizeof(sp_PolyPolyHash),sp_PolyPolyHash_fin,sp_PolyPolyHash_scan);h->cap=16;h->mask=15;h->keys=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->vals=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->order=(mrb_int*)malloc(sizeof(mrb_int)*h->cap);h->occ=(mrb_bool*)calloc(h->cap,sizeof(mrb_bool));h->len=0;return h;}
1099
3442
  static void sp_PolyPolyHash_grow(sp_PolyPolyHash*h){mrb_int oc=h->cap;sp_RbVal*ok=h->keys;sp_RbVal*ov=h->vals;mrb_bool*oo=h->occ;mrb_int*oord=h->order;mrb_int olen=h->len;h->cap*=2;h->mask=h->cap-1;h->keys=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->vals=(sp_RbVal*)calloc(h->cap,sizeof(sp_RbVal));h->order=(mrb_int*)malloc(sizeof(mrb_int)*h->cap);h->occ=(mrb_bool*)calloc(h->cap,sizeof(mrb_bool));for(mrb_int i=0;i<olen;i++){mrb_int oi=oord[i];sp_RbVal k=ok[oi];mrb_int idx=(mrb_int)(sp_rbval_hash_key(k)&h->mask);while(h->occ[idx])idx=(idx+1)&h->mask;h->keys[idx]=k;h->vals[idx]=ov[oi];h->occ[idx]=TRUE;h->order[i]=idx;}free(ok);free(ov);free(oo);free(oord);}
1100
- static sp_RbVal sp_PolyPolyHash_get(sp_PolyPolyHash*h,sp_RbVal k){mrb_int idx=(mrb_int)(sp_rbval_hash_key(k)&h->mask);while(h->occ[idx]){if(sp_rbval_eql_key(h->keys[idx],k))return h->vals[idx];idx=(idx+1)&h->mask;}return sp_box_nil();}
3443
+ static sp_RbVal sp_PolyPolyHash_get(sp_PolyPolyHash*h,sp_RbVal k){if(!h)return sp_box_nil();mrb_int idx=(mrb_int)(sp_rbval_hash_key(k)&h->mask);while(h->occ[idx]){if(sp_rbval_eql_key(h->keys[idx],k))return h->vals[idx];idx=(idx+1)&h->mask;}return sp_box_nil();}
1101
3444
  static void sp_PolyPolyHash_set(sp_PolyPolyHash*h,sp_RbVal k,sp_RbVal v){if(h->len*2>=h->cap)sp_PolyPolyHash_grow(h);mrb_int idx=(mrb_int)(sp_rbval_hash_key(k)&h->mask);while(h->occ[idx]){if(sp_rbval_eql_key(h->keys[idx],k)){h->vals[idx]=v;return;}idx=(idx+1)&h->mask;}h->keys[idx]=k;h->vals[idx]=v;h->occ[idx]=TRUE;h->order[h->len]=idx;h->len++;}
1102
3445
  static mrb_bool sp_PolyPolyHash_has_key(sp_PolyPolyHash*h,sp_RbVal k){mrb_int idx=(mrb_int)(sp_rbval_hash_key(k)&h->mask);while(h->occ[idx]){if(sp_rbval_eql_key(h->keys[idx],k))return TRUE;idx=(idx+1)&h->mask;}return FALSE;}
1103
3446
  static mrb_int sp_PolyPolyHash_length(sp_PolyPolyHash*h){return h->len;}
1104
3447
  static sp_PolyArray*sp_PolyPolyHash_keys(sp_PolyPolyHash*h){sp_PolyArray*a=sp_PolyArray_new();for(mrb_int i=0;i<h->len;i++)sp_PolyArray_push(a,h->keys[h->order[i]]);return a;}
1105
3448
  static sp_PolyArray*sp_PolyPolyHash_values(sp_PolyPolyHash*h){sp_PolyArray*a=sp_PolyArray_new();for(mrb_int i=0;i<h->len;i++)sp_PolyArray_push(a,h->vals[h->order[i]]);return a;}
3449
+ static sp_PolyPolyHash*sp_PolyPolyHash_dup(sp_PolyPolyHash*h){sp_PolyPolyHash*r=sp_PolyPolyHash_new();for(mrb_int i=0;i<h->len;i++)sp_PolyPolyHash_set(r,h->keys[h->order[i]],h->vals[h->order[i]]);return r;}
3450
+ /* Issue #738: poly_poly_hash inspect using sp_poly_inspect on each
3451
+ k,v. Output mirrors Ruby's `{k=>v, ...}` for non-symbol keys and
3452
+ `{k: v, ...}` shorthand for symbol keys. */
3453
+ static const char *sp_poly_inspect(sp_RbVal v);
3454
+ static const char*sp_PolyPolyHash_inspect(sp_PolyPolyHash*h){sp_String*s=sp_String_new("{");if(!h){sp_String_append(s,"}");return s->data;}for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(s,", ");sp_RbVal k=h->keys[h->order[i]];if(k.tag==SP_TAG_SYM){sp_String_append(s,sp_sym_to_s((sp_sym)k.v.i));sp_String_append(s,": ");}else{sp_String_append(s,sp_poly_inspect(k));sp_String_append(s,"=>");}sp_String_append(s,sp_poly_inspect(h->vals[h->order[i]]));}sp_String_append(s,"}");return s->data;}
3455
+ /* Issue #738: Hash#invert -- swap keys and values. Returns a
3456
+ poly_poly_hash so any (key, value) pair shape is uniformly
3457
+ representable. str_str_hash_invert lives above (line ~1132)
3458
+ and stays as a same-type round-trip. */
3459
+ static sp_PolyPolyHash*sp_StrIntHash_invert_poly(sp_StrIntHash*h){sp_PolyPolyHash*r=sp_PolyPolyHash_new();if(!h)return r;for(mrb_int i=0;i<h->len;i++)sp_PolyPolyHash_set(r,sp_box_int(sp_StrIntHash_get(h,h->order[i])),sp_box_str(h->order[i]));return r;}
3460
+ static sp_PolyPolyHash*sp_IntStrHash_invert(sp_IntStrHash*h){sp_PolyPolyHash*r=sp_PolyPolyHash_new();if(!h)return r;for(mrb_int i=0;i<h->len;i++)sp_PolyPolyHash_set(r,sp_box_str(sp_IntStrHash_get(h,h->order[i])),sp_box_int(h->order[i]));return r;}
3461
+ static mrb_bool sp_PolyPolyHash_eq(sp_PolyPolyHash*a,sp_PolyPolyHash*b){if(!a||!b)return a==b;if(a->len!=b->len)return FALSE;for(mrb_int i=0;i<a->len;i++){sp_RbVal k=a->keys[a->order[i]];if(!sp_PolyPolyHash_has_key(b,k))return FALSE;if(!sp_poly_eq(sp_PolyPolyHash_get(a,k),sp_PolyPolyHash_get(b,k)))return FALSE;}return TRUE;}
3462
+
3463
+ /* JSON.generate support. sp_json_str quotes + escapes a string per
3464
+ JSON rules (control chars -> \uNNNN, plus the \" \\ \n \t \r \b \f
3465
+ shorthands; UTF-8 bytes >= 0x80 pass through unescaped, matching
3466
+ CRuby's default). sp_json_val recursively serializes any boxed
3467
+ value -- scalars, arrays, and the typed hash variants -- in CRuby's
3468
+ compact form (no spaces, `:` separator). Object keys are coerced to
3469
+ JSON strings (symbol -> name, integer -> decimal). Raw container
3470
+ element / value types are boxed and routed back through sp_json_val
3471
+ so nesting works. The sp_String accumulator is GC-rooted so a
3472
+ collection triggered by a nested allocation can't free it. */
3473
+ static const char *sp_json_str(const char *s) __attribute__((unused));
3474
+ static const char *sp_json_str(const char *s) {
3475
+ sp_String *o = sp_String_new("\"");
3476
+ SP_GC_ROOT(o);
3477
+ if (s) { const char *p = s; while (*p) { unsigned char c = (unsigned char)*p;
3478
+ if (c == '"') sp_String_append(o, "\\\"");
3479
+ else if (c == '\\') sp_String_append(o, "\\\\");
3480
+ else if (c == '\n') sp_String_append(o, "\\n");
3481
+ else if (c == '\t') sp_String_append(o, "\\t");
3482
+ else if (c == '\r') sp_String_append(o, "\\r");
3483
+ else if (c == '\b') sp_String_append(o, "\\b");
3484
+ else if (c == '\f') sp_String_append(o, "\\f");
3485
+ else if (c < 0x20) { char b[8]; snprintf(b, sizeof b, "\\u%04x", (unsigned)c); sp_String_append(o, b); }
3486
+ else { char ch[2]; ch[0] = (char)c; ch[1] = 0; sp_String_append(o, ch); }
3487
+ p++; } }
3488
+ sp_String_append(o, "\"");
3489
+ return o->data;
3490
+ }
3491
+ static const char *sp_json_val(sp_RbVal v) __attribute__((unused));
3492
+ static const char *sp_json_key(sp_RbVal k) {
3493
+ if (k.tag == SP_TAG_STR) return sp_json_str(k.v.s);
3494
+ if (k.tag == SP_TAG_SYM) return sp_json_str(sp_sym_to_s((sp_sym)k.v.i));
3495
+ if (k.tag == SP_TAG_INT) return sp_json_str(sp_int_to_s(k.v.i));
3496
+ if (k.tag == SP_TAG_BOOL) return sp_json_str(k.v.b ? "true" : "false");
3497
+ if (k.tag == SP_TAG_NIL) return sp_json_str("");
3498
+ return sp_json_str("");
3499
+ }
3500
+ static const char *sp_json_val(sp_RbVal v) {
3501
+ switch (v.tag) {
3502
+ case SP_TAG_INT: return sp_int_to_s(v.v.i);
3503
+ case SP_TAG_FLT: return sp_float_to_s(v.v.f);
3504
+ case SP_TAG_BOOL: return v.v.b ? SPL("true") : SPL("false");
3505
+ case SP_TAG_NIL: return SPL("null");
3506
+ case SP_TAG_STR: return sp_json_str(v.v.s);
3507
+ case SP_TAG_SYM: return sp_json_str(sp_sym_to_s((sp_sym)v.v.i));
3508
+ case SP_TAG_OBJ: {
3509
+ sp_String *o;
3510
+ switch (v.cls_id) {
3511
+ case SP_BUILTIN_INT_ARRAY: { sp_IntArray *a=(sp_IntArray*)v.v.p; o=sp_String_new("["); SP_GC_ROOT(o); if(a){for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_int_to_s(a->data[a->start+i]));}} sp_String_append(o,"]"); return o->data; }
3512
+ case SP_BUILTIN_FLT_ARRAY: { sp_FloatArray *a=(sp_FloatArray*)v.v.p; o=sp_String_new("["); SP_GC_ROOT(o); if(a){for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_float_to_s(a->data[i]));}} sp_String_append(o,"]"); return o->data; }
3513
+ case SP_BUILTIN_STR_ARRAY: { sp_StrArray *a=(sp_StrArray*)v.v.p; o=sp_String_new("["); SP_GC_ROOT(o); if(a){for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_str(a->data[i]));}} sp_String_append(o,"]"); return o->data; }
3514
+ case SP_BUILTIN_SYM_ARRAY: { sp_IntArray *a=(sp_IntArray*)v.v.p; o=sp_String_new("["); SP_GC_ROOT(o); if(a){for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_str(sp_sym_to_s((sp_sym)a->data[a->start+i])));}} sp_String_append(o,"]"); return o->data; }
3515
+ case SP_BUILTIN_POLY_ARRAY: { sp_PolyArray *a=(sp_PolyArray*)v.v.p; o=sp_String_new("["); SP_GC_ROOT(o); if(a){for(mrb_int i=0;i<a->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_val(a->data[i]));}} sp_String_append(o,"]"); return o->data; }
3516
+ case SP_BUILTIN_STR_INT_HASH: { sp_StrIntHash *h=(sp_StrIntHash*)v.v.p; o=sp_String_new("{"); SP_GC_ROOT(o); if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_str(h->order[i]));sp_String_append(o,":");sp_String_append(o,sp_int_to_s(sp_StrIntHash_get(h,h->order[i])));}} sp_String_append(o,"}"); return o->data; }
3517
+ case SP_BUILTIN_STR_STR_HASH: { sp_StrStrHash *h=(sp_StrStrHash*)v.v.p; o=sp_String_new("{"); SP_GC_ROOT(o); if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_str(h->order[i]));sp_String_append(o,":");sp_String_append(o,sp_json_str(sp_StrStrHash_get(h,h->order[i])));}} sp_String_append(o,"}"); return o->data; }
3518
+ case SP_BUILTIN_INT_STR_HASH: { sp_IntStrHash *h=(sp_IntStrHash*)v.v.p; o=sp_String_new("{"); SP_GC_ROOT(o); if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_str(sp_int_to_s(h->order[i])));sp_String_append(o,":");sp_String_append(o,sp_json_str(sp_IntStrHash_get(h,h->order[i])));}} sp_String_append(o,"}"); return o->data; }
3519
+ /* SYM_INT_HASH / SYM_STR_HASH structs are codegen-emitted (not
3520
+ in this static header), so their serializers are emitted by
3521
+ codegen as sp_SymIntHash_json / sp_SymStrHash_json and called
3522
+ directly from the JSON dispatch. A nested sym-int/str hash
3523
+ reaching here as a boxed poly value falls through to null. */
3524
+ case SP_BUILTIN_STR_POLY_HASH: { sp_StrPolyHash *h=(sp_StrPolyHash*)v.v.p; o=sp_String_new("{"); SP_GC_ROOT(o); if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_str(h->order[i]));sp_String_append(o,":");sp_String_append(o,sp_json_val(sp_StrPolyHash_get(h,h->order[i])));}} sp_String_append(o,"}"); return o->data; }
3525
+ case SP_BUILTIN_SYM_POLY_HASH: { sp_SymPolyHash *h=(sp_SymPolyHash*)v.v.p; o=sp_String_new("{"); SP_GC_ROOT(o); if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_str(sp_sym_to_s((sp_sym)h->order[i])));sp_String_append(o,":");sp_String_append(o,sp_json_val(sp_SymPolyHash_get(h,h->order[i])));}} sp_String_append(o,"}"); return o->data; }
3526
+ case SP_BUILTIN_POLY_POLY_HASH: { sp_PolyPolyHash *h=(sp_PolyPolyHash*)v.v.p; o=sp_String_new("{"); SP_GC_ROOT(o); if(h){for(mrb_int i=0;i<h->len;i++){if(i>0)sp_String_append(o,",");sp_String_append(o,sp_json_key(h->keys[h->order[i]]));sp_String_append(o,":");sp_String_append(o,sp_json_val(h->vals[h->order[i]]));}} sp_String_append(o,"}"); return o->data; }
3527
+ default: return SPL("null");
3528
+ }
3529
+ }
3530
+ default: return SPL("null");
3531
+ }
3532
+ }
1106
3533
 
1107
3534
  #include <setjmp.h>
1108
3535
  #define SP_EXC_STACK_MAX 64
@@ -1110,10 +3537,218 @@ static jmp_buf sp_exc_stack[SP_EXC_STACK_MAX];
1110
3537
  static const char *sp_exc_msg[SP_EXC_STACK_MAX];
1111
3538
  static volatile int sp_exc_top = 0;
1112
3539
  static const char *sp_exc_cls[SP_EXC_STACK_MAX];
1113
- static volatile const char *sp_last_exc_cls = "";
1114
- static void sp_raise_cls(const char *cls, const char *msg) { if (sp_exc_top > 0) { sp_exc_msg[sp_exc_top-1] = msg; sp_exc_cls[sp_exc_top-1] = cls; sp_last_exc_cls = cls; longjmp(sp_exc_stack[sp_exc_top-1], 1); } fprintf(stderr, "unhandled exception: %s\n", msg); exit(1); }
3540
+ static volatile const char *sp_last_exc_cls = sp_str_empty;
3541
+ /* ---- Native backtrace formatting (spinel --debug) ---------------------- */
3542
+ /* True for sp_<name> symbols that are runtime helpers, not user Ruby methods.
3543
+ A denylist of the lowercase runtime prefixes; user methods are sp_<rubyname>
3544
+ (top-level) or sp_<Class>_<method>, which don't match. Heuristic — a leaf
3545
+ runtime frame may occasionally slip through; refine with an emitted
3546
+ user-method allowlist later. */
3547
+ #if SP_BT_AVAILABLE
3548
+ static int sp_bt_is_runtime(const char *n) {
3549
+ static const char *pfx[] = {
3550
+ "int_", "str_", "float_", "sym_", "gc_", "bigint", "sprintf", "raise",
3551
+ "exc_", "range", "utf8", "oom", "bt_", "backtrace", "caller", "StrArray",
3552
+ "IntArray", "FloatArray", "PtrArray", "PolyArray", "Str", "Int", "Float",
3553
+ "Hash", "Range", "Complex", "Rational", "Sym", "alloc", "free", "to_s",
3554
+ "dup", "new", "pack", "unpack", "regex", "re_",
3555
+ /* arithmetic/runtime helpers that can raise and sit between the raise
3556
+ and the user frame (ZeroDivisionError via sp_idiv/sp_imod, etc.) */
3557
+ "idiv", "imod", "gcd", "fdiv", "ipow", "iclamp", "div_", "mod_", 0
3558
+ };
3559
+ for (int i = 0; pfx[i]; i++) {
3560
+ size_t l = strlen(pfx[i]);
3561
+ if (strncmp(n, pfx[i], l) == 0) return 1;
3562
+ }
3563
+ return 0;
3564
+ }
3565
+
3566
+ /* Extract the symbol token from a backtrace_symbols line. Two formats:
3567
+ - glibc/Linux: "<module>(<symbol>+0x<off>) [0x<addr>]". The symbol is
3568
+ empty for unresolved frames (static or stripped fns) -> skip.
3569
+ - macOS: "<idx> <image> <addr> <symbol> + <off>".
3570
+ Returns NULL if it isn't a keepable user frame. Detect Linux by the '('
3571
+ that delimits the symbol (the macOS format has none). */
3572
+ static const char *sp_bt_symbol(const char *line) {
3573
+ char sym[256];
3574
+ const char *lp = strchr(line, '(');
3575
+ if (lp) { /* glibc/Linux paren form */
3576
+ const char *p = lp + 1;
3577
+ const char *end = p;
3578
+ while (*end && *end != '+' && *end != ')') end++;
3579
+ size_t len = (size_t)(end - p);
3580
+ if (len == 0 || len > 250) return 0; /* unresolved (static/stripped) */
3581
+ memcpy(sym, p, len); sym[len] = 0;
3582
+ } else { /* macOS: "<idx> <image> <addr> <symbol> + <off>" */
3583
+ /* The symbol is the token just before the " + <off>" delimiter. Parse
3584
+ backward from the last " + " rather than forward from "0x" — an image
3585
+ path containing "0x" (e.g. /path/0x_proj/bin) would otherwise misparse. */
3586
+ const char *plus = 0, *q = line;
3587
+ while ((q = strstr(q, " + ")) != 0) { plus = q; q += 3; }
3588
+ if (!plus) return 0;
3589
+ const char *end = plus;
3590
+ const char *p = end;
3591
+ while (p > line && p[-1] != ' ') p--; /* back up to the symbol's start */
3592
+ size_t len = (size_t)(end - p);
3593
+ if (len == 0 || len > 250) return 0;
3594
+ memcpy(sym, p, len); sym[len] = 0;
3595
+ }
3596
+ if (strcmp(sym, "main") == 0) return strdup("<main>");
3597
+ if (strncmp(sym, "sp_", 3) != 0) return 0; /* skip non-Spinel frames */
3598
+ const char *name = sym + 3;
3599
+ if (sp_bt_is_runtime(name)) return 0;
3600
+ /* De-mangle sp_<Class>_<method> back to Ruby. A Spinel symbol is a path of
3601
+ CamelCase class segments (each from a `::`, joined by `_`) followed by the
3602
+ method; the method is the first segment that starts lowercase. A literal
3603
+ `cls` segment marks a singleton method:
3604
+ sp_Helper_cls_boom -> Helper.boom
3605
+ sp_Tep_Url_parse_query -> Tep::Url#parse_query
3606
+ sp_Tep_AuthOAuth2_cls_find -> Tep::AuthOAuth2.find
3607
+ sp_toplevel -> toplevel (top-level method, no class)
3608
+ (Method names stay sanitized — e.g. enabled? is enabled_p; reversing that
3609
+ needs the emitted name table, a separate refinement.) */
3610
+ const char *mstart = 0; /* first lowercase-starting segment = the method */
3611
+ for (const char *p = name; *p; p++) {
3612
+ int seg_start = (p == name) || (p[-1] == '_');
3613
+ if (seg_start && *p >= 'a' && *p <= 'z') { mstart = p; break; }
3614
+ }
3615
+ if (!mstart) return strdup(name); /* all-uppercase: leave as-is */
3616
+ if (mstart == name) { /* no class path: top-level */
3617
+ if (strncmp(name, "cls_", 4) == 0) return strdup(name + 4); /* top-level singleton */
3618
+ return strdup(name);
3619
+ }
3620
+ char out[256]; size_t o = 0;
3621
+ const char *meth; char sep;
3622
+ if (strncmp(mstart, "cls_", 4) == 0) { meth = mstart + 4; sep = '.'; } /* singleton */
3623
+ else { meth = mstart; sep = '#'; } /* instance */
3624
+ for (const char *p = name; p < mstart - 1 && o + 2 < sizeof(out); p++) {
3625
+ if (*p == '_') { out[o++] = ':'; out[o++] = ':'; } /* class-path `_` was a `::` */
3626
+ else out[o++] = *p;
3627
+ }
3628
+ if (o + 1 < sizeof(out)) out[o++] = sep;
3629
+ size_t ml = strlen(meth);
3630
+ if (o + ml < sizeof(out)) { memcpy(out + o, meth, ml); o += ml; }
3631
+ out[o] = 0;
3632
+ return strdup(out);
3633
+ }
3634
+
3635
+ static sp_StrArray *sp_bt_format(void **buf, int n) {
3636
+ sp_StrArray *a = sp_StrArray_new();
3637
+ if (!sp_bt_enabled || n <= 0) return a;
3638
+ char **syms = backtrace_symbols(buf, n);
3639
+ if (!syms) return a;
3640
+ const char *src = (sp_bt_srcfile && sp_bt_srcfile[0]) ? sp_bt_srcfile : "(spinel)";
3641
+ for (int i = 0; i < n; i++) {
3642
+ char *name = (char *)sp_bt_symbol(syms[i]); /* always strdup'd; free after use */
3643
+ if (!name) continue;
3644
+ sp_StrArray_push(a, sp_sprintf("%s:in `%s'", src, name));
3645
+ free(name);
3646
+ }
3647
+ free(syms);
3648
+ return a;
3649
+ }
3650
+ #endif
3651
+
3652
+ /* Backtrace captured at the most recent raise (for Exception#backtrace). */
3653
+ static sp_StrArray *sp_backtrace_captured(void) {
3654
+ #if SP_BT_AVAILABLE
3655
+ return sp_bt_format(sp_bt_buf, sp_bt_n);
3656
+ #else
3657
+ return sp_StrArray_new();
3658
+ #endif
3659
+ }
3660
+
3661
+ /* The current stack, captured now (for Kernel#caller). */
3662
+ static sp_StrArray *sp_caller_now(void) {
3663
+ #if SP_BT_AVAILABLE
3664
+ if (!sp_bt_enabled) return sp_StrArray_new();
3665
+ void *buf[256];
3666
+ int n = backtrace(buf, 256);
3667
+ return sp_bt_format(buf, n);
3668
+ #else
3669
+ return sp_StrArray_new();
3670
+ #endif
3671
+ }
3672
+
3673
+ SP_NORETURN SP_COLD void sp_raise_cls(const char *cls, const char *msg) {
3674
+ #if SP_BT_AVAILABLE
3675
+ if (sp_bt_enabled) sp_bt_n = backtrace(sp_bt_buf, 256);
3676
+ #endif
3677
+ if (sp_exc_top > 0) { sp_exc_msg[sp_exc_top-1] = msg; sp_exc_cls[sp_exc_top-1] = cls; sp_last_exc_cls = cls; longjmp(sp_exc_stack[sp_exc_top-1], 1); } fprintf(stderr, "unhandled exception: %s\n", msg); exit(1); }
1115
3678
  static void sp_raise(const char *msg) { sp_raise_cls("RuntimeError", msg); }
1116
3679
 
3680
+ /* Issue #781: bridge between the regex compile-error path (which lives
3681
+ in the .a library and can't see the user program's static-inline
3682
+ sp_raise_cls) and the user's Ruby-level exception machinery. The
3683
+ library calls sp_re_set_error_handler at startup -- codegen emits
3684
+ the install call after the exception infrastructure is set up. */
3685
+ static void sp_re_default_error_handler(const char *msg) {
3686
+ sp_raise_cls("RegexpError", msg);
3687
+ }
3688
+ /* Issue #846: during sp_re_init (before main()'s setjmp scope is
3689
+ active), a bad literal `Regexp.new("[invalid")` pattern would
3690
+ route through sp_raise_cls -> "unhandled exception" + exit
3691
+ because sp_exc_top is 0. Install a startup handler that longjmps
3692
+ back to sp_re_init's wrapping setjmp; the codegen-emitted loop
3693
+ then stashes the error per slot for a deferred raise from the
3694
+ first use site (where the user's begin/rescue is active). The
3695
+ re_compile contract requires the error callback to not return
3696
+ (otherwise the library's fall-through fprintf+exit fires) so
3697
+ we longjmp out. */
3698
+ static const char *sp_re_startup_err = NULL;
3699
+ static jmp_buf sp_re_startup_jmp;
3700
+ static void sp_re_startup_error_handler(const char *msg) {
3701
+ if (msg) {
3702
+ size_t n = strlen(msg);
3703
+ char *buf = sp_str_alloc_raw(n + 1);
3704
+ memcpy(buf, msg, n);
3705
+ buf[n] = 0;
3706
+ sp_re_startup_err = buf;
3707
+ }
3708
+ longjmp(sp_re_startup_jmp, 1);
3709
+ }
3710
+ extern void sp_re_set_error_handler(void (*fn)(const char *msg));
3711
+ static void sp_mark_in_flight_exceptions(void) { for (int i = 0; i < sp_exc_top; i++) sp_mark_string(sp_exc_msg[i]); }
3712
+
3713
+ /* sp_Exception: first-class exception object. cls_name is a pointer
3714
+ to the per-class const string literal emitted by codegen
3715
+ (sp_class_names[] entry; not GC-managed). msg is GC-managed
3716
+ (sp_str_alloc'd). */
3717
+ typedef struct sp_Exception_s {
3718
+ const char *cls_name;
3719
+ const char *msg;
3720
+ } sp_Exception;
3721
+ static void sp_exc_gc_scan(void *p) {
3722
+ sp_Exception *e = (sp_Exception *)p;
3723
+ if (e->msg) sp_mark_string(e->msg);
3724
+ /* cls_name points into the .rodata sp_class_names[] table -- not
3725
+ a GC-managed string, no mark needed. */
3726
+ }
3727
+ static sp_Exception *sp_exc_new(const char *cls_name, const char *msg) {
3728
+ sp_Exception *e = (sp_Exception *)sp_gc_alloc(sizeof(sp_Exception), NULL, sp_exc_gc_scan);
3729
+ e->cls_name = cls_name ? cls_name : "RuntimeError";
3730
+ e->msg = msg ? msg : sp_str_empty;
3731
+ return e;
3732
+ }
3733
+ /* Accept `volatile` pointers: LV slots holding sp_Exception * are
3734
+ declared volatile when they live across setjmp, so callers may
3735
+ pass volatile-qualified pointers in. The pointee itself isn't
3736
+ volatile (cls_name/msg are stable post-construction), so we
3737
+ strip volatile internally for one access. */
3738
+ static const char *sp_exc_class_name(volatile sp_Exception *ve) {
3739
+ sp_Exception *e = (sp_Exception *)ve;
3740
+ return e ? e->cls_name : "RuntimeError";
3741
+ }
3742
+ static const char *sp_exc_message(volatile sp_Exception *ve) {
3743
+ sp_Exception *e = (sp_Exception *)ve;
3744
+ return e ? e->msg : sp_str_empty;
3745
+ }
3746
+ static void sp_raise_exc(volatile sp_Exception *ve) {
3747
+ sp_Exception *e = (sp_Exception *)ve;
3748
+ if (!e) sp_raise("(nil exception)");
3749
+ sp_raise_cls(e->cls_name, e->msg);
3750
+ }
3751
+
1117
3752
  /* Cross-TU bridge for sp_bigint.c (compiled as a separate translation
1118
3753
  unit; can't see static helpers in this header). Defined non-static
1119
3754
  so sp_bigint.c's mrb_raise macro can dispatch into spinel's
@@ -1128,6 +3763,64 @@ static mrb_int sp_catch_val[SP_CATCH_STACK_MAX];
1128
3763
  static volatile int sp_catch_top = 0;
1129
3764
  static void sp_throw(const char *tag, mrb_int val) { int i = sp_catch_top - 1; while (i >= 0) { if (strcmp(sp_catch_tag[i], tag) == 0) { sp_catch_val[i] = val; sp_catch_top = i + 1; longjmp(sp_catch_stack[i], 1); } i--; } fprintf(stderr, "uncaught throw: %s\n", tag); exit(1); }
1130
3765
 
3766
+ /* Kernel#sleep with sub-second precision. Argument is seconds as a
3767
+ double so `sleep(0.5)` actually waits 500ms; the legacy `sleep((unsigned)0.5)`
3768
+ cast truncated to 0 and returned immediately. POSIX uses
3769
+ nanosleep(); Windows uses Sleep() (milliseconds). Negative or NaN
3770
+ inputs no-op. */
3771
+ static void sp_sleep(mrb_float s) {
3772
+ if (!(s > 0.0)) return;
3773
+ #ifdef _WIN32
3774
+ DWORD ms = (DWORD)(s * 1000.0);
3775
+ if (ms == 0) ms = 1;
3776
+ Sleep(ms);
3777
+ #else
3778
+ struct timespec req;
3779
+ req.tv_sec = (time_t)s;
3780
+ req.tv_nsec = (long)((s - (double)req.tv_sec) * 1e9);
3781
+ if (req.tv_nsec < 0) req.tv_nsec = 0;
3782
+ if (req.tv_nsec >= 1000000000L) req.tv_nsec = 999999999L;
3783
+ while (nanosleep(&req, &req) == -1 && errno == EINTR) {}
3784
+ #endif
3785
+ }
3786
+
3787
+ #ifdef _WIN32
3788
+ static DWORD sp_file_attributes(const char *path) {
3789
+ if (!path) return INVALID_FILE_ATTRIBUTES;
3790
+ int len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, NULL, 0);
3791
+ if (len <= 0) return INVALID_FILE_ATTRIBUTES;
3792
+ wchar_t *wpath = (wchar_t *)malloc(sizeof(wchar_t) * (size_t)len);
3793
+ if (!wpath) return INVALID_FILE_ATTRIBUTES;
3794
+ if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wpath, len) <= 0) {
3795
+ free(wpath);
3796
+ return INVALID_FILE_ATTRIBUTES;
3797
+ }
3798
+ DWORD attrs = GetFileAttributesW(wpath);
3799
+ free(wpath);
3800
+ return attrs;
3801
+ }
3802
+
3803
+ static mrb_bool sp_file_directory(const char *path) {
3804
+ DWORD attrs = sp_file_attributes(path);
3805
+ return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY);
3806
+ }
3807
+
3808
+ static mrb_bool sp_file_file(const char *path) {
3809
+ DWORD attrs = sp_file_attributes(path);
3810
+ return attrs != INVALID_FILE_ATTRIBUTES && !(attrs & FILE_ATTRIBUTE_DIRECTORY);
3811
+ }
3812
+ #else
3813
+ static mrb_bool sp_file_directory(const char *path) {
3814
+ struct stat st;
3815
+ return path && stat(path, &st) == 0 && S_ISDIR(st.st_mode);
3816
+ }
3817
+
3818
+ static mrb_bool sp_file_file(const char *path) {
3819
+ struct stat st;
3820
+ return path && stat(path, &st) == 0 && S_ISREG(st.st_mode);
3821
+ }
3822
+ #endif
3823
+
1131
3824
  /* Text mode ("r") matches CRuby's File.read: on Windows, CRLF is
1132
3825
  normalized to LF on read, which cancels out fopen("w")'s
1133
3826
  LF→CRLF on write. Without this, content from File.read passed to
@@ -1135,10 +3828,79 @@ static void sp_throw(const char *tag, mrb_int val) { int i = sp_catch_top - 1; w
1135
3828
  and `\r\n` becomes `\r\r\n`. fread's actual byte count drives
1136
3829
  null-termination because text mode shrinks the byte count below
1137
3830
  ftell's raw-file size. */
1138
- static const char *sp_file_read(const char *path) { FILE *f = fopen(path, "r"); if (!f) return &("\xff" "")[1]; fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET); char *buf = sp_str_alloc(sz); size_t n = 0; if (sz > 0) { n = fread(buf, 1, sz, f); } buf[n] = 0; fclose(f); return buf; }
1139
- static void sp_file_write(const char *path, const char *data) { FILE *f = fopen(path, "w"); if (f) { fputs(data, f); fclose(f); } }
3831
+ static const char *sp_file_read(const char *path) {
3832
+ if (sp_file_directory(path)) {
3833
+ sp_raise_cls("Errno::EISDIR", sp_sprintf("Is a directory @ io_fread - %s", path));
3834
+ }
3835
+ FILE *f = fopen(path, "r");
3836
+ if (!f) return &("\xff" "")[1];
3837
+ fseek(f, 0, SEEK_END);
3838
+ long sz = ftell(f);
3839
+ fseek(f, 0, SEEK_SET);
3840
+ char *buf = sp_str_alloc(sz);
3841
+ size_t n = 0;
3842
+ if (sz > 0) {
3843
+ n = fread(buf, 1, sz, f);
3844
+ }
3845
+ buf[n] = 0;
3846
+ fclose(f);
3847
+ return buf;
3848
+ }
3849
+
3850
+ static void sp_file_write(const char *path, const char *data) {
3851
+ if (sp_file_directory(path)) {
3852
+ sp_raise_cls("Errno::EISDIR", sp_sprintf("Is a directory @ rb_sysopen - %s", path));
3853
+ }
3854
+ FILE *f = fopen(path, "wb");
3855
+ if (f) {
3856
+ fwrite(data, 1, sp_str_byte_len(data), f);
3857
+ fclose(f);
3858
+ }
3859
+ }
1140
3860
  static mrb_bool sp_file_exist(const char *path) { FILE *f = fopen(path, "r"); if (f) { fclose(f); return TRUE; } return FALSE; }
1141
3861
  static void sp_file_delete(const char *path) { remove(path); }
3862
+ static sp_Time sp_file_mtime(const char *path) {
3863
+ if (!path) {
3864
+ sp_raise_cls("TypeError", "no implicit conversion of nil into String");
3865
+ return (sp_Time){0, 0, 0};
3866
+ }
3867
+ #if defined(_WIN32)
3868
+ int len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, NULL, 0);
3869
+ if (len > 0) {
3870
+ wchar_t *wpath = (wchar_t *)malloc(sizeof(wchar_t) * (size_t)len);
3871
+ if (wpath) {
3872
+ if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wpath, len) > 0) {
3873
+ WIN32_FILE_ATTRIBUTE_DATA attr_data;
3874
+ if (GetFileAttributesExW(wpath, GetFileExInfoStandard, &attr_data)) {
3875
+ free(wpath);
3876
+ ULARGE_INTEGER ull;
3877
+ ull.LowPart = attr_data.ftLastWriteTime.dwLowDateTime;
3878
+ ull.HighPart = attr_data.ftLastWriteTime.dwHighDateTime;
3879
+ int64_t total_ns = (ull.QuadPart - 116444736000000000ULL) * 100;
3880
+ return (sp_Time){total_ns / 1000000000LL, (int32_t)(total_ns % 1000000000LL), 0};
3881
+ }
3882
+ }
3883
+ free(wpath);
3884
+ }
3885
+ }
3886
+ sp_raise_cls("Errno::ENOENT", sp_sprintf("No such file or directory @ File.mtime - %s", path));
3887
+ return (sp_Time){0, 0, 0};
3888
+ #else
3889
+ struct stat st;
3890
+ if (stat(path, &st) == -1) {
3891
+ sp_raise_cls(errno == ENOENT ? "Errno::ENOENT" : "RuntimeError", sp_sprintf("%s @ File.mtime - %s", strerror(errno), path));
3892
+ return (sp_Time){0, 0, 0};
3893
+ }
3894
+ #if defined(__APPLE__)
3895
+ return (sp_Time){(int64_t)st.st_mtimespec.tv_sec, (int32_t)st.st_mtimespec.tv_nsec, 0};
3896
+ #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
3897
+ return (sp_Time){(int64_t)st.st_mtimespec.tv_sec, (int32_t)st.st_mtimespec.tv_nsec, 0};
3898
+ #else
3899
+ /* Linux / others with st_mtim */
3900
+ return (sp_Time){(int64_t)st.st_mtim.tv_sec, (int32_t)st.st_mtim.tv_nsec, 0};
3901
+ #endif
3902
+ #endif
3903
+ }
1142
3904
  static const char *sp_backtick(const char *cmd) { FILE *p = popen(cmd, "r"); if (!p) return sp_str_empty; char *buf = sp_str_alloc_raw(4096); size_t n = fread(buf, 1, 4095, p); buf[n] = 0; pclose(p); return buf; }
1143
3905
  static const char *sp_file_basename(const char *path) {
1144
3906
  const char *s = strrchr(path, '/');
@@ -1153,12 +3915,213 @@ static const char *sp_file_basename(const char *path) {
1153
3915
  memcpy(buf, base, n + 1);
1154
3916
  return buf;
1155
3917
  }
3918
+ /* Issue #892: File.dirname / File.extname / Dir.pwd. */
3919
+ static const char *sp_file_dirname(const char *path) {
3920
+ const char *s = strrchr(path, '/');
3921
+ if (!s) { char *r = sp_str_alloc(1); r[0] = '.'; r[1] = 0; return r; }
3922
+ if (s == path) { char *r = sp_str_alloc(1); r[0] = '/'; r[1] = 0; return r; }
3923
+ size_t n = (size_t)(s - path);
3924
+ char *buf = sp_str_alloc(n);
3925
+ memcpy(buf, path, n); buf[n] = 0;
3926
+ return buf;
3927
+ }
3928
+ static const char *sp_file_extname(const char *path) {
3929
+ const char *base = strrchr(path, '/');
3930
+ base = base ? base + 1 : path;
3931
+ const char *dot = strrchr(base, '.');
3932
+ /* CRuby: leading-dot files (".bashrc") return "". Trailing-dot
3933
+ paths ("foo.") keep the dot since Ruby 2.7. */
3934
+ if (!dot || dot == base) return sp_str_empty;
3935
+ size_t n = strlen(dot);
3936
+ char *buf = sp_str_alloc(n);
3937
+ memcpy(buf, dot, n + 1);
3938
+ return buf;
3939
+ }
3940
+ static const char *sp_dir_pwd(void) {
3941
+ char tmp[4096];
3942
+ if (!getcwd(tmp, sizeof(tmp))) { return sp_str_empty; }
3943
+ size_t n = strlen(tmp);
3944
+ char *buf = sp_str_alloc(n);
3945
+ memcpy(buf, tmp, n + 1);
3946
+ return buf;
3947
+ }
3948
+ /* Dir singleton methods. mkdir/rmdir/chdir use the platform call (the
3949
+ Windows _-prefixed variants take a single path argument); each returns
3950
+ 0 on success, matching CRuby's `Dir.mkdir` etc. */
3951
+ static mrb_int sp_dir_mkdir(const char *path) {
3952
+ #ifdef _WIN32
3953
+ return (mrb_int)_mkdir(path);
3954
+ #else
3955
+ return (mrb_int)mkdir(path, 0777);
3956
+ #endif
3957
+ }
3958
+ static mrb_int sp_dir_rmdir(const char *path) {
3959
+ #ifdef _WIN32
3960
+ return (mrb_int)_rmdir(path);
3961
+ #else
3962
+ return (mrb_int)rmdir(path);
3963
+ #endif
3964
+ }
3965
+ static mrb_int sp_dir_chdir(const char *path) {
3966
+ #ifdef _WIN32
3967
+ return (mrb_int)_chdir(path);
3968
+ #else
3969
+ return (mrb_int)chdir(path);
3970
+ #endif
3971
+ }
3972
+ static const char *sp_dir_home(void) {
3973
+ const char *h = getenv("HOME");
3974
+ #ifdef _WIN32
3975
+ if (!h || !*h) h = getenv("USERPROFILE");
3976
+ #endif
3977
+ if (!h) return sp_str_empty;
3978
+ return sp_str_dup_external(h);
3979
+ }
3980
+ /* Wildcard match for a single path component: `*` (any run, no `/`),
3981
+ `?` (one char). Recursive over `*`; adequate for the common
3982
+ single-directory glob patterns. */
3983
+ static int sp_fnmatch1(const char *pat, const char *str) {
3984
+ while (*pat) {
3985
+ if (*pat == '*') {
3986
+ pat++;
3987
+ if (!*pat) return 1;
3988
+ while (*str) { if (sp_fnmatch1(pat, str)) return 1; str++; }
3989
+ return sp_fnmatch1(pat, str);
3990
+ } else if (*pat == '?') {
3991
+ if (!*str) return 0;
3992
+ pat++; str++;
3993
+ } else {
3994
+ if (*pat != *str) return 0;
3995
+ pat++; str++;
3996
+ }
3997
+ }
3998
+ return *str == 0;
3999
+ }
4000
+ /* Dir.glob(pattern): list directory entries matching the last component
4001
+ of `pattern` (an optional leading `dir/` selects the directory). Hidden
4002
+ entries match only when the pattern itself begins with `.`. Results are
4003
+ sorted, matching Ruby 3.0+ default glob ordering. */
4004
+ static sp_StrArray *sp_dir_glob(const char *pattern) {
4005
+ sp_StrArray *a = sp_StrArray_new();
4006
+ if (!pattern) return a;
4007
+ const char *slash = strrchr(pattern, '/');
4008
+ char dirbuf[1024];
4009
+ const char *dirpath;
4010
+ const char *base_pat;
4011
+ if (slash) {
4012
+ size_t dl = (size_t)(slash - pattern);
4013
+ if (dl >= sizeof(dirbuf)) return a;
4014
+ memcpy(dirbuf, pattern, dl);
4015
+ dirbuf[dl] = 0;
4016
+ dirpath = (dl == 0) ? "/" : dirbuf;
4017
+ base_pat = slash + 1;
4018
+ } else {
4019
+ dirpath = ".";
4020
+ base_pat = pattern;
4021
+ }
4022
+ DIR *d = opendir(dirpath);
4023
+ if (!d) return a;
4024
+ struct dirent *e;
4025
+ while ((e = readdir(d)) != NULL) {
4026
+ const char *name = e->d_name;
4027
+ if (name[0] == '.' && base_pat[0] != '.') continue;
4028
+ if (sp_fnmatch1(base_pat, name)) {
4029
+ char full[2048];
4030
+ if (slash) snprintf(full, sizeof(full), "%s/%s", dirbuf, name);
4031
+ else snprintf(full, sizeof(full), "%s", name);
4032
+ char *copy = sp_str_alloc(strlen(full));
4033
+ strcpy(copy, full);
4034
+ sp_StrArray_push(a, copy);
4035
+ }
4036
+ }
4037
+ closedir(d);
4038
+ sp_StrArray_sort_bang(a);
4039
+ return a;
4040
+ }
4041
+
4042
+ /* File.expand_path(path[, base]) -- CRuby-compatible pure-string
4043
+ expansion (does NOT require the path to exist). A leading `~` / `~/`
4044
+ becomes $HOME; a relative path is resolved against `base` (itself
4045
+ expanded; NULL means the current working directory); and `.` / `..` /
4046
+ duplicate-slash segments are collapsed. `~user` is unsupported and is
4047
+ left as-is. */
4048
+ static const char *sp_file_expand_path(const char *path, const char *base) {
4049
+ char raw[8192];
4050
+ char cwd[4096];
4051
+ const char *home = getenv("HOME");
4052
+ if (!home) home = "";
4053
+ if (!path) path = "";
4054
+
4055
+ /* The `%.4000s` precision caps each component so the compiler can
4056
+ prove the combined output (<= 8001) fits in the 8192 buffer -- this
4057
+ silences -Wformat-truncation (an error under the test harness's
4058
+ -Werror). 4000 chars/component is well past PATH_MAX, so real paths
4059
+ never truncate. */
4060
+ if (path[0] == '~' && (path[1] == '\0' || path[1] == '/')) {
4061
+ snprintf(raw, sizeof(raw), "%.4000s%.4000s", home, path + 1);
4062
+ } else if (path[0] == '/') {
4063
+ snprintf(raw, sizeof(raw), "%.4000s", path);
4064
+ } else {
4065
+ char basebuf[8192];
4066
+ const char *b;
4067
+ if (base && base[0]) {
4068
+ if (base[0] == '~' && (base[1] == '\0' || base[1] == '/')) {
4069
+ snprintf(basebuf, sizeof(basebuf), "%.4000s%.4000s", home, base + 1);
4070
+ b = basebuf;
4071
+ } else if (base[0] == '/') {
4072
+ b = base;
4073
+ } else {
4074
+ if (!getcwd(cwd, sizeof(cwd))) cwd[0] = 0;
4075
+ snprintf(basebuf, sizeof(basebuf), "%.4000s/%.4000s", cwd, base);
4076
+ b = basebuf;
4077
+ }
4078
+ } else {
4079
+ if (!getcwd(cwd, sizeof(cwd))) cwd[0] = 0;
4080
+ b = cwd;
4081
+ }
4082
+ snprintf(raw, sizeof(raw), "%.4000s/%.4000s", b, path);
4083
+ }
4084
+
4085
+ /* Normalize: walk segments, collapsing `.`/`..`/`//`. seg_start[k]
4086
+ records the output length to roll back to when a `..` pops the
4087
+ k-th kept segment. */
4088
+ size_t rawlen = strlen(raw);
4089
+ char *out = sp_str_alloc(rawlen + 1);
4090
+ size_t seg_start[1024];
4091
+ int nseg = 0;
4092
+ size_t olen = 0;
4093
+ out[olen++] = '/';
4094
+ const char *p = raw;
4095
+ while (*p) {
4096
+ if (*p == '/') { p++; continue; }
4097
+ const char *q = p;
4098
+ while (*q && *q != '/') q++;
4099
+ size_t slen = (size_t)(q - p);
4100
+ if (slen == 1 && p[0] == '.') {
4101
+ /* current dir -- skip */
4102
+ } else if (slen == 2 && p[0] == '.' && p[1] == '.') {
4103
+ if (nseg > 0) { nseg--; olen = seg_start[nseg]; }
4104
+ } else {
4105
+ size_t mark = olen;
4106
+ if (olen > 1) out[olen++] = '/';
4107
+ memcpy(out + olen, p, slen);
4108
+ olen += slen;
4109
+ if (nseg < 1024) seg_start[nseg++] = mark;
4110
+ }
4111
+ p = q;
4112
+ }
4113
+ out[olen] = 0;
4114
+ return out;
4115
+ }
1156
4116
 
1157
4117
  /* Read a file's bytes into a fresh IntArray. Distinct from
1158
4118
  `sp_str_bytes(sp_file_read(path))` because plain sp_str_bytes uses
1159
4119
  null-termination and stops at the first 0x00 byte — wrong for
1160
4120
  binary data (e.g. .nes ROM files). */
1161
4121
  static sp_IntArray *sp_file_binread_bytes(const char *path) {
4122
+ if (sp_file_directory(path)) {
4123
+ sp_raise_cls("Errno::EISDIR", sp_sprintf("Is a directory @ io_fread - %s", path));
4124
+ }
1162
4125
  FILE *f = fopen(path, "rb");
1163
4126
  sp_IntArray *a = sp_IntArray_new();
1164
4127
  if (!f) return a;
@@ -1182,6 +4145,8 @@ static sp_IntArray *sp_file_binread_bytes(const char *path) {
1182
4145
  `start` field for an O(1) head peel (from == 0); the others
1183
4146
  shift the tail down to fill the hole. */
1184
4147
  static sp_IntArray *sp_IntArray_slice_bang(sp_IntArray *a, mrb_int from, mrb_int n) {
4148
+ if (!a) return sp_IntArray_new();
4149
+ if (a->frozen) { sp_raise_frozen_array(); return sp_IntArray_new(); }
1185
4150
  if (from < 0) from += a->len;
1186
4151
  if (from < 0) from = 0;
1187
4152
  if (from > a->len) from = a->len;
@@ -1199,6 +4164,8 @@ static sp_IntArray *sp_IntArray_slice_bang(sp_IntArray *a, mrb_int from, mrb_int
1199
4164
  return r;
1200
4165
  }
1201
4166
  static sp_FloatArray *sp_FloatArray_slice_bang(sp_FloatArray *a, mrb_int from, mrb_int n) {
4167
+ if (!a) return sp_FloatArray_new();
4168
+ if (a->frozen) { sp_raise_frozen_array(); return sp_FloatArray_new(); }
1202
4169
  if (from < 0) from += a->len;
1203
4170
  if (from < 0) from = 0;
1204
4171
  if (from > a->len) from = a->len;
@@ -1211,6 +4178,8 @@ static sp_FloatArray *sp_FloatArray_slice_bang(sp_FloatArray *a, mrb_int from, m
1211
4178
  return r;
1212
4179
  }
1213
4180
  static sp_StrArray *sp_StrArray_slice_bang(sp_StrArray *a, mrb_int from, mrb_int n) {
4181
+ if (!a) return sp_StrArray_new();
4182
+ if (a->frozen) { sp_raise_frozen_array(); return sp_StrArray_new(); }
1214
4183
  if (from < 0) from += a->len;
1215
4184
  if (from < 0) from = 0;
1216
4185
  if (from > a->len) from = a->len;
@@ -1222,7 +4191,16 @@ static sp_StrArray *sp_StrArray_slice_bang(sp_StrArray *a, mrb_int from, mrb_int
1222
4191
  a->len -= n;
1223
4192
  return r;
1224
4193
  }
4194
+ /* at_exit hooks: a static LIFO of registered procs. Initialized
4195
+ zero-len in BSS; main()'s tail walks it in reverse-registration
4196
+ order before returning. */
4197
+ #define SP_AT_EXIT_MAX 256
4198
+ struct sp_Proc;
4199
+ static struct sp_Proc *sp_at_exit_hooks[SP_AT_EXIT_MAX];
4200
+ static mrb_int sp_at_exit_count = 0;
1225
4201
  static sp_PtrArray *sp_PtrArray_slice_bang(sp_PtrArray *a, mrb_int from, mrb_int n) {
4202
+ if (!a) return sp_PtrArray_new_scan(NULL);
4203
+ if (a->frozen) { sp_raise_frozen_array(); return sp_PtrArray_new_scan(a->scan_elem); }
1226
4204
  if (from < 0) from += a->len;
1227
4205
  if (from < 0) from = 0;
1228
4206
  if (from > a->len) from = a->len;
@@ -1235,18 +4213,105 @@ static sp_PtrArray *sp_PtrArray_slice_bang(sp_PtrArray *a, mrb_int from, mrb_int
1235
4213
  return r;
1236
4214
  }
1237
4215
 
1238
- typedef struct sp_Proc { void *fn; void *cap; void (*cap_scan)(void *); } sp_Proc;
4216
+ typedef struct sp_Proc { void *fn; void *cap; void (*cap_scan)(void *); mrb_int arity; mrb_bool lambda_p; mrb_int param_count; const sp_sym *param_kinds; const sp_sym *param_names; } sp_Proc;
1239
4217
  static void sp_Proc_scan(void *p) { sp_Proc *pr = (sp_Proc *)p; if (pr->cap && pr->cap_scan) pr->cap_scan(pr->cap); }
1240
- static sp_Proc *sp_proc_new(void *fn, void *cap, void (*cap_scan)(void *)) { sp_Proc *p = (sp_Proc *)sp_gc_alloc(sizeof(sp_Proc), NULL, sp_Proc_scan); p->fn = fn; p->cap = cap; p->cap_scan = cap_scan; return p; }
1241
- static mrb_int sp_proc_call(sp_Proc *p, mrb_int *args) { return (p && p->fn) ? ((mrb_int (*)(void *, mrb_int *))p->fn)(p->cap, args) : 0; }
4218
+ static sp_Proc *sp_proc_new_meta(void *fn, void *cap, void (*cap_scan)(void *), mrb_int arity, mrb_bool lambda_p, mrb_int param_count, const sp_sym *param_kinds, const sp_sym *param_names) { sp_Proc *p = (sp_Proc *)sp_gc_alloc(sizeof(sp_Proc), NULL, sp_Proc_scan); p->fn = fn; p->cap = cap; p->cap_scan = cap_scan; p->arity = arity; p->lambda_p = lambda_p; p->param_count = param_count; p->param_kinds = param_kinds; p->param_names = param_names; return p; }
4219
+ static sp_Proc *sp_proc_new(void *fn, void *cap, void (*cap_scan)(void *)) { return sp_proc_new_meta(fn, cap, cap_scan, 0, FALSE, 0, NULL, NULL); }
4220
+ static mrb_int sp_proc_arity(sp_Proc *p) { return p ? p->arity : 0; }
4221
+ static mrb_bool sp_proc_lambda_p(sp_Proc *p) { return p ? p->lambda_p : FALSE; }
4222
+ static mrb_int sp_proc_call(sp_Proc *p, mrb_int *args) { if (!p || !p->fn) return 0; if (!args) { mrb_int noargs[16] = {0}; return ((mrb_int (*)(void *, mrb_int *))p->fn)(p->cap, noargs); } return ((mrb_int (*)(void *, mrb_int *))p->fn)(p->cap, args); }
4223
+ static sp_PolyArray *sp_proc_parameters(sp_Proc *p) { sp_PolyArray *r = sp_PolyArray_new(); if (!p || p->param_count <= 0 || !p->param_kinds) return r; SP_GC_ROOT(r); for (mrb_int i = 0; i < p->param_count; i++) { sp_PolyArray *pair = sp_PolyArray_new(); sp_PolyArray_push(pair, sp_box_sym(p->param_kinds[i])); if (p->param_names && p->param_names[i] >= 0) sp_PolyArray_push(pair, sp_box_sym(p->param_names[i])); sp_PolyArray_push(r, sp_box_poly_array(pair)); } return r; }
4224
+
4225
+ /* Proc#<< / Proc#>> composition. The composed proc captures the two
4226
+ operands and, on call, threads its single argument through inner
4227
+ then outer: `(f << g).call(x)` == f(g(x)). For `>>` the codegen
4228
+ swaps the operands so `(f >> g).call(x)` == g(f(x)). */
4229
+ typedef struct { sp_Proc *outer; sp_Proc *inner; } sp_ProcCompose;
4230
+ static void sp_proc_compose_scan(void *p) { sp_ProcCompose *c = (sp_ProcCompose *)p; if (c->outer) sp_gc_mark(c->outer); if (c->inner) sp_gc_mark(c->inner); }
4231
+ static mrb_int sp_proc_compose_fn(void *cap, mrb_int *args) {
4232
+ sp_ProcCompose *c = (sp_ProcCompose *)cap;
4233
+ mrb_int inner_args[16] = {0};
4234
+ if (args) inner_args[0] = args[0];
4235
+ mrb_int mid = sp_proc_call(c->inner, inner_args);
4236
+ mrb_int outer_args[16] = {0};
4237
+ outer_args[0] = mid;
4238
+ return sp_proc_call(c->outer, outer_args);
4239
+ }
4240
+ static sp_Proc *sp_proc_compose(sp_Proc *outer, sp_Proc *inner) {
4241
+ sp_ProcCompose *c = (sp_ProcCompose *)sp_gc_alloc(sizeof(sp_ProcCompose), NULL, sp_proc_compose_scan);
4242
+ c->outer = outer;
4243
+ c->inner = inner;
4244
+ return sp_proc_new_meta((void *)sp_proc_compose_fn, c, sp_proc_compose_scan, 1, TRUE, 1, NULL, NULL);
4245
+ }
4246
+ /* Hash#to_proc cap-scan: the proc's `cap` field IS the source hash
4247
+ (a single GC pointer), so marking it keeps the hash alive for the
4248
+ proc's lifetime. The per-variant lookup fn is emitted by codegen
4249
+ alongside the hash type it closes over. */
4250
+ static void sp_hashproc_cap_scan(void *p) { sp_gc_mark(p); }
4251
+
4252
+ /* Random — per-instance PRNG. CRuby uses MT19937; spinel uses a
4253
+ portable xorshift64 (rand_r is POSIX-only, absent on MinGW), so
4254
+ the *sequence* differs from MRI but each Random object keeps its
4255
+ own reproducible stream from its seed. Issue #898. */
4256
+ typedef struct { uint64_t state; } sp_Random;
4257
+ static uint64_t sp_random_next(sp_Random *r) {
4258
+ uint64_t x = r->state ? r->state : 0x9E3779B97F4A7C15ULL;
4259
+ x ^= x << 13; x ^= x >> 7; x ^= x << 17;
4260
+ r->state = x;
4261
+ return x;
4262
+ }
4263
+ static sp_Random *sp_Random_new(mrb_int seed) {
4264
+ sp_Random *r = (sp_Random *)sp_gc_alloc(sizeof(sp_Random), NULL, NULL);
4265
+ r->state = (uint64_t)seed ^ 0x9E3779B97F4A7C15ULL;
4266
+ return r;
4267
+ }
4268
+ static mrb_int sp_Random_rand_int(sp_Random *r, mrb_int n) {
4269
+ if (!r || n <= 0) return 0;
4270
+ return (mrb_int)(sp_random_next(r) % (uint64_t)n);
4271
+ }
4272
+ static mrb_float sp_Random_rand_float(sp_Random *r) {
4273
+ if (!r) return 0.0;
4274
+ return (mrb_float)(sp_random_next(r) >> 11) / (mrb_float)(1ULL << 53);
4275
+ }
4276
+ /* Class-method forms (`Random.rand` / `Random.bytes`) share one
4277
+ lazily-seeded default instance, mirroring CRuby's Random::DEFAULT. */
4278
+ static sp_Random sp_random_default = { 0 };
4279
+ static sp_Random *sp_random_default_get(void) {
4280
+ if (sp_random_default.state == 0) sp_random_default.state = (uint64_t)time(NULL) ^ 0x9E3779B97F4A7C15ULL;
4281
+ return &sp_random_default;
4282
+ }
4283
+ /* Random#bytes(n) — n random bytes as a String. Uses sp_str_set_len
4284
+ so embedded NULs are preserved and #length reports n. */
4285
+ static const char *sp_Random_bytes(sp_Random *r, mrb_int n) {
4286
+ if (n < 0) n = 0;
4287
+ char *b = sp_str_alloc((size_t)n);
4288
+ for (mrb_int i = 0; i < n; i++) b[i] = (char)(sp_random_next(r) & 0xff);
4289
+ b[n] = 0;
4290
+ sp_str_set_len(b, (size_t)n);
4291
+ return b;
4292
+ }
1242
4293
 
1243
4294
  /* ---- StringIO runtime ---- */
1244
4295
  typedef struct { char *buf; int64_t len; int64_t cap; int64_t pos; int64_t lineno; int closed; } sp_StringIO;
1245
4296
  static void sio_grow(sp_StringIO *sio, int64_t need) { int64_t req = sio->pos + need; if (req <= sio->cap) return; int64_t nc = sio->cap ? sio->cap : 64; while (nc < req) nc *= 2; sio->buf = (char *)realloc(sio->buf, nc + 1); sio->cap = nc; }
1246
4297
  static int64_t sio_write(sp_StringIO *sio, const char *d, int64_t dl) { sio_grow(sio, dl); if (sio->pos > sio->len) memset(sio->buf + sio->len, 0, sio->pos - sio->len); memcpy(sio->buf + sio->pos, d, dl); sio->pos += dl; if (sio->pos > sio->len) sio->len = sio->pos; sio->buf[sio->len] = '\0'; return dl; }
1247
4298
  static sp_StringIO *sp_StringIO_new(void) { sp_StringIO *s = (sp_StringIO *)calloc(1, sizeof(sp_StringIO)); s->buf = (char *)calloc(1, 64); s->cap = 63; return s; }
1248
- static sp_StringIO *sp_StringIO_new_s(const char *init) { sp_StringIO *s = (sp_StringIO *)calloc(1, sizeof(sp_StringIO)); int64_t l = (int64_t)strlen(init); int64_t c = l < 63 ? 63 : l; s->buf = (char*)malloc(c+1); memcpy(s->buf, init, l); s->buf[l]='\0'; s->len = l; s->cap = c; return s; }
1249
- static const char *sp_StringIO_string(sp_StringIO *s) { return s->buf ? s->buf : ""; }
4299
+ static sp_StringIO *sp_StringIO_new_s(const char *init) { if (!init) sp_raise_cls("TypeError", "no implicit conversion of nil into String"); sp_StringIO *s = (sp_StringIO *)calloc(1, sizeof(sp_StringIO)); int64_t l = (int64_t)strlen(init); int64_t c = l < 63 ? 63 : l; s->buf = (char*)malloc(c+1); memcpy(s->buf, init, l); s->buf[l]='\0'; s->len = l; s->cap = c; return s; }
4300
+ /* StringIO.new(str, mode): the mode's first char selects the initial
4301
+ content/position. "w"/"w+" truncate to empty; "a"/"a+" keep the
4302
+ content and seek to the end (subsequent writes append); "r"/"r+" and
4303
+ anything else keep the content at position 0. Read-only enforcement
4304
+ ("r" rejecting writes) is not modelled. */
4305
+ static sp_StringIO *sp_StringIO_new_sm(const char *init, const char *mode) {
4306
+ if (!init) sp_raise_cls("TypeError", "no implicit conversion of nil into String");
4307
+ if (!mode || !mode[0]) return sp_StringIO_new_s(init);
4308
+ char m0 = mode[0];
4309
+ if (m0 == 'w') return sp_StringIO_new();
4310
+ sp_StringIO *s = sp_StringIO_new_s(init);
4311
+ if (m0 == 'a') s->pos = s->len;
4312
+ return s;
4313
+ }
4314
+ static const char *sp_StringIO_string(sp_StringIO *s) { return s->buf ? s->buf : sp_str_empty; }
1250
4315
  static int64_t sp_StringIO_pos(sp_StringIO *s) { return s->pos; }
1251
4316
  static int64_t sp_StringIO_size(sp_StringIO *s) { return s->len; }
1252
4317
  static int64_t sp_StringIO_write(sp_StringIO *s, const char *str) { return sio_write(s, str, (int64_t)strlen(str)); }
@@ -1286,39 +4351,47 @@ static sp_Val *sp_lam_int(mrb_int n) { sp_Val *v = (sp_Val *)sp_lam_alloc(sizeof
1286
4351
  static sp_Val *sp_lam_bool(mrb_bool b) { sp_Val *v = (sp_Val *)sp_lam_alloc(sizeof(sp_Val)); v->tag = SP_BOOL2; v->u.bval = b; return v; }
1287
4352
  static sp_Val sp_lam_nil_val = { .tag = SP_NIL2 };
1288
4353
  static sp_Val *sp_lam_call(sp_Val *f, sp_Val *arg) { return f->u.proc.fn(f, arg); }
4354
+ /* Multi-arg lambda dispatch . The fn pointer is stored
4355
+ typed as `sp_fn_t` (1-arg) so re-cast at the call site to the
4356
+ right arity. The generated lambda body is declared with the
4357
+ matching arity so the actual ABI matches. */
4358
+ typedef sp_Val *(*sp_fn2_t)(sp_Val *self, sp_Val *a, sp_Val *b);
4359
+ typedef sp_Val *(*sp_fn3_t)(sp_Val *self, sp_Val *a, sp_Val *b, sp_Val *c);
4360
+ typedef sp_Val *(*sp_fn4_t)(sp_Val *self, sp_Val *a, sp_Val *b, sp_Val *c, sp_Val *d);
4361
+ static sp_Val *sp_lam_call2(sp_Val *f, sp_Val *a, sp_Val *b) { return ((sp_fn2_t)(uintptr_t)f->u.proc.fn)(f, a, b); }
4362
+ static sp_Val *sp_lam_call3(sp_Val *f, sp_Val *a, sp_Val *b, sp_Val *c) { return ((sp_fn3_t)(uintptr_t)f->u.proc.fn)(f, a, b, c); }
4363
+ static sp_Val *sp_lam_call4(sp_Val *f, sp_Val *a, sp_Val *b, sp_Val *c, sp_Val *d) { return ((sp_fn4_t)(uintptr_t)f->u.proc.fn)(f, a, b, c, d); }
4364
+ /* lambda#<< / #>> composition over the sp_Val * representation.
4365
+ captures[0] = outer, captures[1] = inner; `(f << g).(x)` == f(g(x)).
4366
+ The codegen swaps operands for `>>`. */
4367
+ static sp_Val *sp_lam_compose_fn(sp_Val *self, sp_Val *arg) { return sp_lam_call(self->captures[0], sp_lam_call(self->captures[1], arg)); }
4368
+ static sp_Val *sp_lam_compose(sp_Val *outer, sp_Val *inner) { sp_Val *v = sp_lam_proc(sp_lam_compose_fn, 2); v->captures[0] = outer; v->captures[1] = inner; return v; }
4369
+ /* Proc#curry over the sp_Val * representation. The curried value is a
4370
+ proc whose captures are [target, arity, arg0, arg1, ...]; applying
4371
+ one more arg either returns a fresh curried proc (still short of
4372
+ arity) or invokes the target with the full argument list. Each `[]`
4373
+ application supplies a single argument. */
4374
+ static sp_Val *sp_lam_curry_fn(sp_Val *self, sp_Val *arg) {
4375
+ sp_Val *target = self->captures[0];
4376
+ mrb_int arity = self->captures[1]->u.ival;
4377
+ int have = self->u.proc.ncaptures - 2;
4378
+ if (have + 1 < arity) {
4379
+ sp_Val *v = sp_lam_proc(sp_lam_curry_fn, self->u.proc.ncaptures + 1);
4380
+ v->captures[0] = target;
4381
+ v->captures[1] = self->captures[1];
4382
+ for (int i = 0; i < have; i++) v->captures[2 + i] = self->captures[2 + i];
4383
+ v->captures[2 + have] = arg;
4384
+ return v;
4385
+ }
4386
+ if (arity == 1) return sp_lam_call(target, arg);
4387
+ if (arity == 2) return sp_lam_call2(target, self->captures[2], arg);
4388
+ if (arity == 3) return sp_lam_call3(target, self->captures[2], self->captures[3], arg);
4389
+ if (arity == 4) return sp_lam_call4(target, self->captures[2], self->captures[3], self->captures[4], arg);
4390
+ return &sp_lam_nil_val;
4391
+ }
4392
+ static sp_Val *sp_lam_curry(sp_Val *f, mrb_int arity) { sp_Val *v = sp_lam_proc(sp_lam_curry_fn, 2); v->captures[0] = f; v->captures[1] = sp_lam_int(arity); return v; }
1289
4393
  static mrb_int sp_lam_to_int(sp_Val *v) { return v->u.ival; }
1290
4394
 
1291
- /* ---- Fiber runtime (ucontext) ---- */
1292
- #ifdef _WIN32
1293
- #define SP_FIBER_STACK_SIZE (64*1024)
1294
- typedef struct sp_Fiber{LPVOID ctx;LPVOID caller_ctx;char*stack;int state;int transferred;sp_RbVal yielded_value;sp_RbVal resumed_value;void(*body)(struct sp_Fiber*);void*user_data;int saved_exc_top;int saved_catch_top;struct sp_Fiber*caller;}sp_Fiber;
1295
- static sp_Fiber sp_fiber_root;
1296
- static sp_Fiber*sp_fiber_current=&sp_fiber_root;
1297
- static void sp_Fiber_fin(void*p){sp_Fiber*f=(sp_Fiber*)p;if(f->ctx)DeleteFiber(f->ctx);}
1298
- static void sp_Fiber_scan(void*p){sp_Fiber*f=(sp_Fiber*)p;if(f->user_data)sp_gc_mark(f->user_data);}
1299
- static sp_Fiber*sp_Fiber_new(void(*body)(sp_Fiber*)){sp_Fiber*f=(sp_Fiber*)sp_gc_alloc(sizeof(sp_Fiber),sp_Fiber_fin,sp_Fiber_scan);f->ctx=NULL;f->caller_ctx=NULL;f->stack=NULL;f->state=0;f->transferred=0;f->body=body;f->yielded_value=sp_box_nil();f->resumed_value=sp_box_nil();f->user_data=NULL;f->saved_exc_top=0;f->saved_catch_top=0;f->caller=NULL;return f;}
1300
- static VOID CALLBACK sp_fiber_trampoline(LPVOID param){sp_Fiber*f=(sp_Fiber*)param;f->body(f);f->state=3;if(f->transferred){sp_fiber_current=&sp_fiber_root;SwitchToFiber(sp_fiber_root.ctx);}else{SwitchToFiber(f->caller->ctx);}}
1301
- static void sp_fiber_ensure_root(void){if(!sp_fiber_root.ctx){sp_fiber_root.ctx=ConvertThreadToFiber(NULL);if(!sp_fiber_root.ctx)sp_fiber_root.ctx=GetCurrentFiber();}}
1302
- static sp_RbVal sp_Fiber_resume(sp_Fiber*f,sp_RbVal val){if(f->state==3){sp_raise_cls("FiberError","attempt to resume a terminated fiber");}sp_fiber_ensure_root();f->resumed_value=val;sp_Fiber*prev=sp_fiber_current;f->caller=prev;sp_fiber_current=f;if(f->state==0){f->state=1;f->ctx=CreateFiber(SP_FIBER_STACK_SIZE,sp_fiber_trampoline,f);if(!f->ctx)sp_raise("failed to create fiber");}else{f->state=1;}SwitchToFiber(f->ctx);sp_fiber_current=prev;return f->yielded_value;}
1303
- static sp_RbVal sp_Fiber_yield(sp_RbVal val){sp_Fiber*f=sp_fiber_current;f->yielded_value=val;f->state=2;SwitchToFiber(f->caller->ctx);return f->resumed_value;}
1304
- static mrb_bool sp_Fiber_alive(sp_Fiber*f){return f->state!=3;}
1305
- static sp_RbVal sp_Fiber_transfer(sp_Fiber*f,sp_RbVal val){sp_fiber_ensure_root();f->resumed_value=val;sp_Fiber*prev=sp_fiber_current;sp_fiber_current=f;if(f->state==0){f->state=1;f->transferred=1;f->caller=prev;f->ctx=CreateFiber(SP_FIBER_STACK_SIZE,sp_fiber_trampoline,f);if(!f->ctx)sp_raise("failed to create fiber");}else{f->state=1;}SwitchToFiber(f->ctx);sp_fiber_current=prev;return prev->resumed_value;}
1306
- #else
1307
- #include <ucontext.h>
1308
- #include <sys/mman.h>
1309
- #define SP_FIBER_STACK_SIZE (64*1024)
1310
- typedef struct sp_Fiber{ucontext_t ctx;ucontext_t caller_ctx;char*stack;int state;int transferred;sp_RbVal yielded_value;sp_RbVal resumed_value;void(*body)(struct sp_Fiber*);void*user_data;int saved_exc_top;int saved_catch_top;}sp_Fiber;
1311
- static sp_Fiber sp_fiber_root;
1312
- static sp_Fiber*sp_fiber_current=&sp_fiber_root;
1313
- static void sp_Fiber_fin(void*p){sp_Fiber*f=(sp_Fiber*)p;if(f->stack)munmap(f->stack,SP_FIBER_STACK_SIZE);}
1314
- static void sp_Fiber_scan(void*p){sp_Fiber*f=(sp_Fiber*)p;if(f->user_data)sp_gc_mark(f->user_data);}
1315
- static sp_Fiber*sp_Fiber_new(void(*body)(sp_Fiber*)){sp_Fiber*f=(sp_Fiber*)sp_gc_alloc(sizeof(sp_Fiber),sp_Fiber_fin,sp_Fiber_scan);f->stack=(char*)mmap(NULL,SP_FIBER_STACK_SIZE,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);f->state=0;f->transferred=0;f->body=body;f->yielded_value=sp_box_nil();f->resumed_value=sp_box_nil();f->user_data=NULL;f->saved_exc_top=0;f->saved_catch_top=0;return f;}
1316
- static void sp_fiber_trampoline(void){sp_Fiber*f=sp_fiber_current;f->body(f);f->state=3;if(f->transferred){sp_fiber_current=&sp_fiber_root;setcontext(&sp_fiber_root.ctx);}else{swapcontext(&f->ctx,&f->caller_ctx);}}
1317
- static sp_RbVal sp_Fiber_resume(sp_Fiber*f,sp_RbVal val){if(f->state==3){sp_raise_cls("FiberError","attempt to resume a terminated fiber");}f->resumed_value=val;sp_Fiber*prev=sp_fiber_current;sp_fiber_current=f;if(f->state==0){f->state=1;getcontext(&f->ctx);f->ctx.uc_stack.ss_sp=f->stack;f->ctx.uc_stack.ss_size=SP_FIBER_STACK_SIZE;f->ctx.uc_link=&f->caller_ctx;makecontext(&f->ctx,(void(*)(void))sp_fiber_trampoline,0);swapcontext(&f->caller_ctx,&f->ctx);}else{f->state=1;swapcontext(&f->caller_ctx,&f->ctx);}sp_fiber_current=prev;return f->yielded_value;}
1318
- static sp_RbVal sp_Fiber_yield(sp_RbVal val){sp_Fiber*f=sp_fiber_current;f->yielded_value=val;f->state=2;swapcontext(&f->ctx,&f->caller_ctx);return f->resumed_value;}
1319
- static mrb_bool sp_Fiber_alive(sp_Fiber*f){return f->state!=3;}
1320
- static sp_RbVal sp_Fiber_transfer(sp_Fiber*f,sp_RbVal val){f->resumed_value=val;sp_Fiber*prev=sp_fiber_current;sp_fiber_current=f;if(f->state==0){f->state=1;f->transferred=1;getcontext(&f->ctx);f->ctx.uc_stack.ss_sp=f->stack;f->ctx.uc_stack.ss_size=SP_FIBER_STACK_SIZE;f->ctx.uc_link=&prev->ctx;makecontext(&f->ctx,(void(*)(void))sp_fiber_trampoline,0);swapcontext(&prev->ctx,&f->ctx);}else{f->state=1;swapcontext(&prev->ctx,&f->ctx);}sp_fiber_current=prev;return prev->resumed_value;}
1321
- #endif
1322
4395
 
1323
4396
  /* Bigint (linked from sp_bigint.o) */
1324
4397
  typedef struct sp_Bigint sp_Bigint;
@@ -1335,9 +4408,23 @@ int64_t sp_bigint_to_int(sp_Bigint *b);
1335
4408
  const char *sp_bigint_to_s(sp_Bigint *b);
1336
4409
  void sp_bigint_free(sp_Bigint *b);
1337
4410
 
4411
+ /* Bitwise ops on bigint operands. Used by --int-overflow=promote
4412
+ mode where all int slots widen to sp_Bigint *. Implemented via
4413
+ int64 round-trip: bigint values produced by promotion are
4414
+ almost always derived from small ints (counters, masks, bit-
4415
+ width-sized values <= 64 bits), so the int64 path preserves
4416
+ the full Ruby-side semantics for any value that fits. Values
4417
+ exceeding int64 lose precision through these helpers -- those
4418
+ are extremely rare in practice (Ruby bitops on integers >
4419
+ 2^63), and proper mpz_and / mpz_or / mpz_xor support can be
4420
+ added later in lib/sp_bigint.c if a real workload needs it. */
4421
+ sp_Bigint *sp_bigint_and(sp_Bigint *a, sp_Bigint *b);
4422
+ sp_Bigint *sp_bigint_or(sp_Bigint *a, sp_Bigint *b);
4423
+ sp_Bigint *sp_bigint_xor(sp_Bigint *a, sp_Bigint *b);
4424
+ sp_Bigint *sp_bigint_shl(sp_Bigint *a, int64_t n);
4425
+ sp_Bigint *sp_bigint_shr(sp_Bigint *a, int64_t n);
4426
+ sp_Bigint *sp_bigint_not(sp_Bigint *a);
1338
4427
 
1339
- /* System/backtick support */
1340
- static int sp_last_status = 0;
1341
4428
 
1342
4429
  /* Bigint (linked from libspinel_rt.a) */
1343
4430
  typedef struct sp_Bigint sp_Bigint;
@@ -1353,6 +4440,63 @@ int sp_bigint_cmp(sp_Bigint *a, sp_Bigint *b);
1353
4440
  int64_t sp_bigint_to_int(sp_Bigint *b);
1354
4441
  const char *sp_bigint_to_s(sp_Bigint *b);
1355
4442
  void sp_bigint_free(sp_Bigint *b);
4443
+ sp_Bigint *sp_bigint_and(sp_Bigint *a, sp_Bigint *b);
4444
+ sp_Bigint *sp_bigint_or(sp_Bigint *a, sp_Bigint *b);
4445
+ sp_Bigint *sp_bigint_xor(sp_Bigint *a, sp_Bigint *b);
4446
+ sp_Bigint *sp_bigint_shl(sp_Bigint *a, int64_t n);
4447
+ sp_Bigint *sp_bigint_shr(sp_Bigint *a, int64_t n);
4448
+ sp_Bigint *sp_bigint_not(sp_Bigint *a);
4449
+
4450
+ /* ---- Pack / Unpack (linked from sp_pack.o) ----
4451
+ Implementation lives in libspinel_rt.a; the entry points
4452
+ below call into the static GC helpers in this header via the
4453
+ sp_ext_* shims defined further down. */
4454
+ const char *sp_IntArray_pack(sp_IntArray *arr, const char *fmt);
4455
+ const char *sp_PolyArray_pack(sp_PolyArray *arr, const char *fmt);
4456
+ sp_PolyArray *sp_str_unpack(const char *str, const char *fmt);
4457
+
4458
+ /* ---- StringScanner (linked from sp_strscan.o) ----
4459
+ GC-allocated; the source / matched fields are marked by
4460
+ sp_StringScanner_scan_gc through the shim layer. */
4461
+ typedef struct sp_StringScanner sp_StringScanner;
4462
+ sp_StringScanner *sp_StringScanner_new(const char *str);
4463
+ const char *sp_StringScanner_scan(sp_StringScanner *sc, mrb_regexp_pattern *pat);
4464
+ const char *sp_StringScanner_check(sp_StringScanner *sc, mrb_regexp_pattern *pat);
4465
+ const char *sp_StringScanner_scan_until(sp_StringScanner *sc, mrb_regexp_pattern *pat);
4466
+ const char *sp_StringScanner_aref(sp_StringScanner *sc, mrb_int n);
4467
+ const char *sp_StringScanner_matched(sp_StringScanner *sc);
4468
+ mrb_bool sp_StringScanner_matched_p(sp_StringScanner *sc);
4469
+ mrb_int sp_StringScanner_pos(sp_StringScanner *sc);
4470
+ mrb_int sp_StringScanner_pos_set(sp_StringScanner *sc, mrb_int p);
4471
+ mrb_bool sp_StringScanner_eos_p(sp_StringScanner *sc);
4472
+ const char *sp_StringScanner_getch(sp_StringScanner *sc);
4473
+ const char *sp_StringScanner_peek(sp_StringScanner *sc, mrb_int n);
4474
+ sp_StringScanner *sp_StringScanner_unscan(sp_StringScanner *sc);
4475
+ const char *sp_StringScanner_rest(sp_StringScanner *sc);
4476
+ mrb_int sp_StringScanner_rest_size(sp_StringScanner *sc);
4477
+ mrb_bool sp_StringScanner_rest_p(sp_StringScanner *sc);
4478
+ sp_StringScanner *sp_StringScanner_terminate(sp_StringScanner *sc);
4479
+ const char *sp_StringScanner_string(sp_StringScanner *sc);
4480
+ const char *sp_StringScanner_pre_match(sp_StringScanner *sc);
4481
+ const char *sp_StringScanner_post_match(sp_StringScanner *sc);
4482
+ sp_StringScanner *sp_StringScanner_reset(sp_StringScanner *sc);
4483
+
4484
+ /* External-TU shim wrappers. The runtime's GC-allocating helpers
4485
+ are `static inline`; separately-compiled .c files in
4486
+ libspinel_rt.a can't reach the per-TU state. Define plain
4487
+ external-linkage wrappers here so the main binary's single TU
4488
+ owns the GC state and lib code calls in via stable symbols.
4489
+ sp_runtime.h is included exactly once per binary (in the
4490
+ generated main file), so there's no multi-definition risk. */
4491
+ sp_PolyArray *sp_ext_poly_array_new(void) { return sp_PolyArray_new(); }
4492
+ void sp_ext_poly_array_push_int(sp_PolyArray *a, int64_t v) { sp_PolyArray_push(a, sp_box_int((mrb_int)v)); }
4493
+ void sp_ext_poly_array_push_str(sp_PolyArray *a, const char *s) { sp_PolyArray_push(a, sp_box_str(s)); }
4494
+ char *sp_ext_str_alloc(size_t n) { return sp_str_alloc(n); }
4495
+ void sp_ext_str_set_len(char *s, size_t n) { sp_str_set_len(s, n); }
4496
+ const char *sp_ext_str_empty(void) { return sp_str_empty; }
4497
+ size_t sp_ext_str_byte_len(const char *s) { return sp_str_byte_len(s); }
4498
+ void *sp_ext_gc_alloc(size_t sz, void (*fin)(void *), void (*scan)(void *)) { return sp_gc_alloc(sz, fin, scan); }
4499
+ void sp_ext_mark_string(const char *s) { sp_mark_string(s); }
1356
4500
 
1357
4501
  #ifdef __APPLE__
1358
4502
  #pragma clang diagnostic pop