herb 0.11.1 → 0.11.2

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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +4 -2
  3. data/config.yml +6 -0
  4. data/ext/herb/extconf.rb +8 -31
  5. data/ext/herb/nodes.c +75 -75
  6. data/ext/herb/parser_options_helpers.c +26 -0
  7. data/lib/herb/analysis/partial_index.rb +1 -1
  8. data/lib/herb/analysis/partial_resolution.rb +27 -0
  9. data/lib/herb/ast/helpers.rb +1 -1
  10. data/lib/herb/configuration.rb +11 -1
  11. data/lib/herb/dev/classifier.rb +1 -1
  12. data/lib/herb/engine/compiler.rb +5 -8
  13. data/lib/herb/engine/slots/dynamics_compiler.rb +53 -14
  14. data/lib/herb/engine/slots/subtree_compiler.rb +1 -1
  15. data/lib/herb/engine/slots/visitor.rb +32 -4
  16. data/lib/herb/engine/visitors/instrumentation_visitor.rb +18 -6
  17. data/lib/herb/parser_options.rb +6 -3
  18. data/lib/herb/project.rb +19 -8
  19. data/lib/herb/version.rb +1 -1
  20. data/lib/herb.rb +1 -1
  21. data/sig/herb/analysis/partial_resolution.rbs +8 -0
  22. data/sig/herb/configuration.rbs +3 -0
  23. data/sig/herb/engine/slots/dynamics_compiler.rbs +24 -7
  24. data/sig/herb/engine/slots/visitor.rbs +14 -0
  25. data/sig/herb/engine/visitors/instrumentation_visitor.rbs +5 -1
  26. data/sig/herb/parser_options.rbs +6 -4
  27. data/sig/herb.rbs +2 -2
  28. data/src/analyze/action_view/attribute_extraction_helpers.c +40 -21
  29. data/src/analyze/action_view/generated_handlers.c +36 -36
  30. data/src/analyze/action_view/image_tag.c +7 -3
  31. data/src/analyze/action_view/javascript_include_tag.c +6 -3
  32. data/src/analyze/action_view/link_to.c +23 -23
  33. data/src/analyze/action_view/registry.c +7 -3
  34. data/src/analyze/action_view/tag_helper_node_builders.c +6 -13
  35. data/src/analyze/action_view/tag_helpers.c +139 -90
  36. data/src/analyze/action_view/turbo_frame_tag.c +5 -4
  37. data/src/analyze/analyze.c +35 -19
  38. data/src/analyze/analyze_helpers.c +161 -188
  39. data/src/analyze/analyzed_ruby.c +8 -7
  40. data/src/analyze/control_type.c +29 -22
  41. data/src/analyze/herb_directives.c +16 -20
  42. data/src/analyze/iteration_nodes.c +69 -24
  43. data/src/analyze/parse_errors.c +32 -22
  44. data/src/analyze/postfix_conditionals.c +23 -13
  45. data/src/analyze/prism_annotate.c +29 -28
  46. data/src/analyze/render_nodes.c +51 -41
  47. data/src/analyze/strict_locals.c +14 -15
  48. data/src/analyze/ternary_conditionals.c +14 -10
  49. data/src/herb.c +10 -8
  50. data/src/include/analyze/action_view/tag_helper_handler.h +1 -1
  51. data/src/include/analyze/action_view/tag_helper_node_builders.h +1 -0
  52. data/src/include/analyze/action_view/tag_helpers.h +1 -1
  53. data/src/include/analyze/analyze.h +3 -2
  54. data/src/include/analyze/analyzed_ruby.h +4 -2
  55. data/src/include/analyze/helpers.h +6 -6
  56. data/src/include/herb.h +3 -2
  57. data/src/include/parser/parser_options.h +5 -0
  58. data/src/include/prism/prism_context.h +12 -12
  59. data/src/include/prism/prism_helpers.h +30 -0
  60. data/src/include/prism_version.h +11 -0
  61. data/src/include/version.h +1 -1
  62. data/src/parser/parser_helpers.c +1 -2
  63. data/src/parser.c +6 -7
  64. data/src/parser_options_defaults.c +1 -0
  65. data/src/prism/prism_helpers.c +84 -50
  66. data/src/prism/ruby_parser.c +20 -20
  67. data/templates/ext/herb/nodes.c.erb +5 -5
  68. data/templates/ext/herb/parser_options_helpers.c.erb +33 -0
  69. data/templates/java/nodes.c.erb +6 -6
  70. data/templates/java/parser_options_helpers.c.erb +41 -2
  71. data/templates/java/parser_options_helpers.h.erb +2 -1
  72. data/templates/javascript/packages/node/extension/nodes.cpp.erb +5 -5
  73. data/templates/javascript/packages/node/extension/parser_options_helpers.cpp.erb +34 -2
  74. data/templates/javascript/packages/node/extension/parser_options_helpers.h.erb +1 -1
  75. data/templates/lib/herb/parser_options.rb.erb +1 -1
  76. data/templates/rust/src/prism/generated_deserialize.rs.erb +90 -44
  77. data/templates/rust/src/prism/generated_prettyprint.rs.erb +18 -27
  78. data/templates/src/analyze/action_view/generated_handlers.c.erb +23 -22
  79. data/templates/src/include/parser/parser_options.h.erb +10 -0
  80. data/templates/src/include/prism_version.h.erb +17 -0
  81. data/templates/src/parser_options_defaults.c.erb +1 -1
  82. data/templates/template.rb +17 -2
  83. data/templates/wasm/nodes.cpp.erb +5 -5
  84. data/templates/wasm/parser_options_helpers.cpp.erb +24 -0
  85. data/vendor/prism/Rakefile +76 -7
  86. data/vendor/prism/config.yml +174 -161
  87. data/vendor/prism/include/prism/arena.h +45 -0
  88. data/vendor/prism/include/prism/arena.hpp +42 -0
  89. data/vendor/prism/include/prism/ast.h +2738 -611
  90. data/vendor/prism/include/prism/ast.hpp +641 -0
  91. data/vendor/prism/include/prism/buffer.h +60 -0
  92. data/vendor/prism/include/prism/buffer.hpp +55 -0
  93. data/vendor/prism/include/prism/comments.h +72 -0
  94. data/vendor/prism/include/prism/compiler/accel.h +26 -0
  95. data/vendor/prism/include/prism/compiler/align.h +36 -0
  96. data/vendor/prism/include/prism/compiler/assume.h +25 -0
  97. data/vendor/prism/include/prism/compiler/branch.h +26 -0
  98. data/vendor/prism/include/prism/compiler/bswap.h +46 -0
  99. data/vendor/prism/include/prism/compiler/cplusplus.hpp +16 -0
  100. data/vendor/prism/include/prism/compiler/endianness.h +34 -0
  101. data/vendor/prism/include/prism/compiler/exceptions.hpp +17 -0
  102. data/vendor/prism/include/prism/compiler/exported.h +24 -0
  103. data/vendor/prism/include/prism/compiler/fallthrough.h +22 -0
  104. data/vendor/prism/include/prism/compiler/filesystem.h +32 -0
  105. data/vendor/prism/include/prism/compiler/flex_array.h +19 -0
  106. data/vendor/prism/include/prism/compiler/force_inline.h +21 -0
  107. data/vendor/prism/include/prism/compiler/format.h +27 -0
  108. data/vendor/prism/include/prism/compiler/inline.h +17 -0
  109. data/vendor/prism/include/prism/compiler/lifetimebound.hpp +25 -0
  110. data/vendor/prism/include/prism/compiler/nodiscard.h +22 -0
  111. data/vendor/prism/include/prism/compiler/nonnull.h +18 -0
  112. data/vendor/prism/include/prism/compiler/unused.h +18 -0
  113. data/vendor/prism/include/prism/constant_pool.h +106 -0
  114. data/vendor/prism/include/prism/diagnostic.h +55 -402
  115. data/vendor/prism/include/prism/errors_format.h +61 -0
  116. data/vendor/prism/include/prism/excludes.h +29 -0
  117. data/vendor/prism/include/prism/integer.h +41 -0
  118. data/vendor/prism/include/prism/internal/allocator.h +68 -0
  119. data/vendor/prism/include/prism/internal/allocator_debug.h +88 -0
  120. data/vendor/prism/include/prism/internal/arena.h +108 -0
  121. data/vendor/prism/include/prism/internal/bit.h +42 -0
  122. data/vendor/prism/include/prism/internal/buffer.h +94 -0
  123. data/vendor/prism/include/prism/internal/char.h +145 -0
  124. data/vendor/prism/include/prism/internal/comments.h +54 -0
  125. data/vendor/prism/include/prism/internal/constant_pool.h +152 -0
  126. data/vendor/prism/include/prism/internal/diagnostic.h +389 -0
  127. data/vendor/prism/include/prism/{encoding.h → internal/encoding.h} +27 -68
  128. data/vendor/prism/include/prism/internal/integer.h +68 -0
  129. data/vendor/prism/include/prism/internal/isinf.h +16 -0
  130. data/vendor/prism/include/prism/internal/line_offset_list.h +34 -0
  131. data/vendor/prism/include/prism/{util/pm_list.h → internal/list.h} +11 -52
  132. data/vendor/prism/include/prism/internal/magic_comments.h +23 -0
  133. data/vendor/prism/include/prism/internal/memchr.h +15 -0
  134. data/vendor/prism/include/prism/internal/node.h +42 -0
  135. data/vendor/prism/include/prism/internal/options.h +232 -0
  136. data/vendor/prism/include/prism/internal/parser.h +1015 -0
  137. data/vendor/prism/include/prism/internal/regexp.h +26 -0
  138. data/vendor/prism/include/prism/internal/serialize.h +39 -0
  139. data/vendor/prism/include/prism/internal/source.h +72 -0
  140. data/vendor/prism/include/prism/internal/static_literals.h +60 -0
  141. data/vendor/prism/include/prism/internal/stringy.h +30 -0
  142. data/vendor/prism/include/prism/{util/pm_strncasecmp.h → internal/strncasecmp.h} +3 -17
  143. data/vendor/prism/include/prism/{util/pm_strpbrk.h → internal/strpbrk.h} +7 -20
  144. data/vendor/prism/include/prism/internal/tokens.h +11 -0
  145. data/vendor/prism/include/prism/internal/xxh3.h +16 -0
  146. data/vendor/prism/include/prism/json.h +40 -0
  147. data/vendor/prism/include/prism/line_offset_list.h +69 -0
  148. data/vendor/prism/include/prism/magic_comments.h +53 -0
  149. data/vendor/prism/include/prism/node.h +59 -49
  150. data/vendor/prism/include/prism/node.hpp +272 -0
  151. data/vendor/prism/include/prism/node_type.hpp +38 -0
  152. data/vendor/prism/include/prism/options.h +154 -279
  153. data/vendor/prism/include/prism/options.hpp +43 -0
  154. data/vendor/prism/include/prism/parser.h +367 -870
  155. data/vendor/prism/include/prism/parser.hpp +274 -0
  156. data/vendor/prism/include/prism/prettyprint.h +14 -9
  157. data/vendor/prism/include/prism/serialize.h +123 -0
  158. data/vendor/prism/include/prism/source.h +163 -0
  159. data/vendor/prism/include/prism/stream.h +36 -0
  160. data/vendor/prism/include/prism/string_query.h +82 -0
  161. data/vendor/prism/include/prism/stringy.h +61 -0
  162. data/vendor/prism/include/prism/stringy.hpp +31 -0
  163. data/vendor/prism/include/prism/version.h +20 -3
  164. data/vendor/prism/include/prism.h +35 -320
  165. data/vendor/prism/include/prism.hpp +24 -0
  166. data/vendor/prism/src/arena.c +122 -0
  167. data/vendor/prism/src/{util/pm_buffer.c → buffer.c} +53 -21
  168. data/vendor/prism/src/{util/pm_char.c → char.c} +28 -57
  169. data/vendor/prism/src/comments.c +455 -0
  170. data/vendor/prism/src/constant_pool.c +406 -0
  171. data/vendor/prism/src/diagnostic.c +120 -68
  172. data/vendor/prism/src/encoding.c +8 -3
  173. data/vendor/prism/src/errors_format.c +519 -0
  174. data/vendor/prism/src/{util/pm_integer.c → integer.c} +38 -22
  175. data/vendor/prism/src/json.c +5733 -0
  176. data/vendor/prism/src/line_offset_list.c +100 -0
  177. data/vendor/prism/src/list.c +24 -0
  178. data/vendor/prism/src/{util/pm_memchr.c → memchr.c} +8 -6
  179. data/vendor/prism/src/node.c +3465 -7787
  180. data/vendor/prism/src/options.c +161 -54
  181. data/vendor/prism/src/parser.c +371 -0
  182. data/vendor/prism/src/prettyprint.c +421 -354
  183. data/vendor/prism/src/prism.c +8522 -7280
  184. data/vendor/prism/src/regexp.c +960 -45
  185. data/vendor/prism/src/serialize.c +2434 -2048
  186. data/vendor/prism/src/source.c +524 -0
  187. data/vendor/prism/src/static_literals.c +303 -360
  188. data/vendor/prism/src/string_query.c +180 -0
  189. data/vendor/prism/src/stringy.c +61 -0
  190. data/vendor/prism/src/{util/pm_strncasecmp.c → strncasecmp.c} +5 -4
  191. data/vendor/prism/src/strpbrk.c +447 -0
  192. data/vendor/prism/src/{token_type.c → tokens.c} +39 -18
  193. data/vendor/prism/src/xxh3.c +292 -0
  194. data/vendor/prism/templates/ext/prism/api_node.c.erb +58 -44
  195. data/vendor/prism/templates/include/prism/ast.h.erb +67 -17
  196. data/vendor/prism/templates/include/prism/ast.hpp.erb +31 -0
  197. data/vendor/prism/templates/include/prism/internal/diagnostic.h.erb +60 -0
  198. data/vendor/prism/templates/java/{org → api/src/main/java-templates/org/ruby_lang}/prism/AbstractNodeVisitor.java.erb +1 -1
  199. data/vendor/prism/templates/java/{org → api/src/main/java-templates/org/ruby_lang}/prism/Loader.java.erb +232 -133
  200. data/vendor/prism/templates/java/{org → api/src/main/java-templates/org/ruby_lang}/prism/Nodes.java.erb +62 -23
  201. data/vendor/prism/templates/javascript/src/deserialize.js.erb +153 -146
  202. data/vendor/prism/templates/javascript/src/nodes.js.erb +4 -9
  203. data/vendor/prism/templates/lib/prism/compiler.rb.erb +14 -5
  204. data/vendor/prism/templates/lib/prism/dispatcher.rb.erb +20 -12
  205. data/vendor/prism/templates/lib/prism/dot_visitor.rb.erb +37 -11
  206. data/vendor/prism/templates/lib/prism/dsl.rb.erb +43 -4
  207. data/vendor/prism/templates/lib/prism/inspect_visitor.rb.erb +26 -10
  208. data/vendor/prism/templates/lib/prism/mutation_compiler.rb.erb +5 -2
  209. data/vendor/prism/templates/lib/prism/node.rb.erb +283 -105
  210. data/vendor/prism/templates/lib/prism/reflection.rb.erb +11 -2
  211. data/vendor/prism/templates/lib/prism/serialize.rb.erb +291 -134
  212. data/vendor/prism/templates/lib/prism/visitor.rb.erb +18 -0
  213. data/vendor/prism/templates/src/diagnostic.c.erb +114 -63
  214. data/vendor/prism/templates/src/json.c.erb +130 -0
  215. data/vendor/prism/templates/src/node.c.erb +79 -236
  216. data/vendor/prism/templates/src/prettyprint.c.erb +24 -13
  217. data/vendor/prism/templates/src/serialize.c.erb +369 -183
  218. data/vendor/prism/templates/src/{token_type.c.erb → tokens.c.erb} +22 -11
  219. data/vendor/prism/templates/template.rb +193 -91
  220. metadata +100 -42
  221. data/vendor/prism/include/prism/defines.h +0 -260
  222. data/vendor/prism/include/prism/pack.h +0 -163
  223. data/vendor/prism/include/prism/regexp.h +0 -43
  224. data/vendor/prism/include/prism/static_literals.h +0 -121
  225. data/vendor/prism/include/prism/util/pm_buffer.h +0 -236
  226. data/vendor/prism/include/prism/util/pm_char.h +0 -204
  227. data/vendor/prism/include/prism/util/pm_constant_pool.h +0 -218
  228. data/vendor/prism/include/prism/util/pm_integer.h +0 -130
  229. data/vendor/prism/include/prism/util/pm_memchr.h +0 -29
  230. data/vendor/prism/include/prism/util/pm_newline_list.h +0 -113
  231. data/vendor/prism/include/prism/util/pm_string.h +0 -200
  232. data/vendor/prism/src/pack.c +0 -509
  233. data/vendor/prism/src/util/pm_constant_pool.c +0 -342
  234. data/vendor/prism/src/util/pm_list.c +0 -49
  235. data/vendor/prism/src/util/pm_newline_list.c +0 -125
  236. data/vendor/prism/src/util/pm_string.c +0 -381
  237. data/vendor/prism/src/util/pm_strpbrk.c +0 -206
  238. data/vendor/prism/templates/include/prism/diagnostic.h.erb +0 -130
  239. data/vendor/prism/templates/rbi/prism/dsl.rbi.erb +0 -68
  240. data/vendor/prism/templates/rbi/prism/node.rbi.erb +0 -167
  241. data/vendor/prism/templates/rbi/prism/visitor.rbi.erb +0 -18
  242. data/vendor/prism/templates/sig/prism/_private/dot_visitor.rbs.erb +0 -45
  243. data/vendor/prism/templates/sig/prism/dsl.rbs.erb +0 -31
  244. data/vendor/prism/templates/sig/prism/mutation_compiler.rbs.erb +0 -7
  245. data/vendor/prism/templates/sig/prism/node.rbs.erb +0 -136
  246. data/vendor/prism/templates/sig/prism/visitor.rbs.erb +0 -17
  247. data/vendor/prism/templates/sig/prism.rbs.erb +0 -92
@@ -1,5 +1,20 @@
1
- #include "prism/regexp.h"
2
-
1
+ #include "prism/internal/regexp.h"
2
+
3
+ #include "prism/compiler/inline.h"
4
+ #include "prism/compiler/fallthrough.h"
5
+ #include "prism/internal/buffer.h"
6
+ #include "prism/internal/char.h"
7
+ #include "prism/internal/diagnostic.h"
8
+ #include "prism/internal/encoding.h"
9
+ #include "prism/internal/memchr.h"
10
+ #include "prism/internal/parser.h"
11
+ #include "prism/internal/stringy.h"
12
+ #include "prism/internal/strncasecmp.h"
13
+
14
+ #include <assert.h>
15
+ #include <string.h>
16
+
17
+ /** The maximum depth of nested groups allowed in a regular expression. */
3
18
  #define PM_REGEXP_PARSE_DEPTH_MAX 4096
4
19
 
5
20
  /**
@@ -18,6 +33,54 @@ typedef struct {
18
33
  /** A pointer to the end of the source that we are parsing. */
19
34
  const uint8_t *end;
20
35
 
36
+ /** The encoding of the source. */
37
+ const pm_encoding_t *encoding;
38
+
39
+ /** The callback to call when a named capture group is found. */
40
+ pm_regexp_name_callback_t name_callback;
41
+
42
+ /** The data to pass to the name callback. */
43
+ void *name_data;
44
+
45
+ /** The start of the regexp node (for error locations). */
46
+ const uint8_t *node_start;
47
+
48
+ /** The end of the regexp node (for error locations). */
49
+ const uint8_t *node_end;
50
+
51
+ /**
52
+ * The explicit encoding determined by escape sequences. NULL if no
53
+ * encoding-setting escape has been seen, UTF-8 for `\u` escapes, or the
54
+ * source encoding for `\x` escapes.
55
+ */
56
+ const pm_encoding_t *explicit_encoding;
57
+
58
+ /**
59
+ * Pointer to the first non-POSIX property name (for /n error messages).
60
+ * POSIX properties (Alnum, Alpha, etc.) work in all encodings.
61
+ * Script properties (Hiragana, Katakana, etc.) work in /e, /s, /u.
62
+ * Unicode-only properties (L, Ll, etc.) work only in /u.
63
+ */
64
+ const uint8_t *property_name;
65
+
66
+ /** Length of the first non-POSIX property name found. */
67
+ size_t property_name_length;
68
+
69
+ /**
70
+ * Pointer to the first Unicode-only property name (for /e, /s error
71
+ * messages). NULL if only POSIX or script properties have been seen.
72
+ */
73
+ const uint8_t *unicode_property_name;
74
+
75
+ /** Length of the first Unicode-only property name found. */
76
+ size_t unicode_property_name_length;
77
+
78
+ /** Buffer of hex escape byte values >= 0x80, separated by 0x00 sentinels. */
79
+ pm_buffer_t hex_escape_buffer;
80
+
81
+ /** Count of non-ASCII literal bytes (not from escapes). */
82
+ uint32_t non_ascii_literal_count;
83
+
21
84
  /**
22
85
  * Whether or not the regular expression currently being parsed is in
23
86
  * extended mode, wherein whitespace is ignored and comments are allowed.
@@ -27,30 +90,68 @@ typedef struct {
27
90
  /** Whether the encoding has changed from the default. */
28
91
  bool encoding_changed;
29
92
 
30
- /** The encoding of the source. */
31
- const pm_encoding_t *encoding;
93
+ /**
94
+ * Whether syntax errors are reported. The named capture pass walks content
95
+ * that may be an owned copy of the source (so pointers into it have no
96
+ * offset in the source), and whose errors the validation pass has already
97
+ * reported, so it reports nothing.
98
+ */
99
+ bool report_errors;
32
100
 
33
- /** The callback to call when a named capture group is found. */
34
- pm_regexp_name_callback_t name_callback;
101
+ /** Whether a `\u{...}` escape with value >= 0x80 was seen. */
102
+ bool has_unicode_escape;
35
103
 
36
- /** The data to pass to the name callback. */
37
- void *name_data;
104
+ /** Whether a `\xNN` escape (or `\M-x`, etc.) with value >= 0x80 was seen. */
105
+ bool has_hex_escape;
106
+
107
+ /**
108
+ * Tracks whether the last encoding-setting escape was `\u` (true) or `\x`
109
+ * (false). This matters for error messages when both types are mixed.
110
+ */
111
+ bool last_escape_was_unicode;
112
+
113
+ /** Whether any `\p{...}` or `\P{...}` property escape was found. */
114
+ bool has_property_escape;
38
115
 
39
- /** The callback to call when a parse error is found. */
40
- pm_regexp_error_callback_t error_callback;
116
+ /** Whether a Unicode-only property escape was found (not POSIX or script). */
117
+ bool has_unicode_property_escape;
41
118
 
42
- /** The data to pass to the error callback. */
43
- void *error_data;
119
+ /** Whether a `\u` escape with invalid range (surrogate or > 0x10FFFF) was seen. */
120
+ bool invalid_unicode_range;
121
+
122
+ /** Whether we are accumulating consecutive hex escape bytes. */
123
+ bool hex_group_active;
124
+
125
+ /** Whether an invalid multibyte character was found during parsing. */
126
+ bool has_invalid_multibyte;
44
127
  } pm_regexp_parser_t;
45
128
 
46
129
  /**
47
- * Append an error to the parser.
130
+ * Append a syntax error to the parser's error list. Errors are only reported
131
+ * when report_errors is set, which is the case for the validation pass that
132
+ * walks the raw source, so start and end always point into the original source.
48
133
  */
49
- static inline void
50
- pm_regexp_parse_error(pm_regexp_parser_t *parser, const uint8_t *start, const uint8_t *end, const char *message) {
51
- parser->error_callback(start, end, message, parser->error_data);
134
+ static PRISM_INLINE void
135
+ pm_regexp_parse_error(pm_regexp_parser_t *regexp_parser, const uint8_t *start, const uint8_t *end, const char *message) {
136
+ if (regexp_parser->report_errors) {
137
+ pm_parser_t *parser = regexp_parser->parser;
138
+ pm_diagnostic_list_append_format(&parser->metadata_arena, &parser->error_list, (uint32_t) (start - parser->start), (uint32_t) (end - start), PM_ERR_REGEXP_PARSE_ERROR, message);
139
+ }
52
140
  }
53
141
 
142
+ /**
143
+ * Append a formatted syntax error to the parser's error list, following the
144
+ * same rules as pm_regexp_parse_error. This is a macro because we need
145
+ * variadic args for the format string.
146
+ */
147
+ #define pm_regexp_parse_error_format(regexp_parser_, err_start_, err_end_, diag_id, ...) \
148
+ do { \
149
+ if ((regexp_parser_)->report_errors) { \
150
+ pm_parser_t *parser_ = (regexp_parser_)->parser; \
151
+ pm_diagnostic_list_append_format(&parser_->metadata_arena, &parser_->error_list, (uint32_t) ((err_start_) - parser_->start), (uint32_t) ((err_end_) - (err_start_)), diag_id, __VA_ARGS__); \
152
+ } \
153
+ } while (0)
154
+
54
155
  /**
55
156
  * This appends a new string to the list of named captures. This function
56
157
  * assumes the caller has already checked the validity of the name callback.
@@ -59,14 +160,13 @@ static void
59
160
  pm_regexp_parser_named_capture(pm_regexp_parser_t *parser, const uint8_t *start, const uint8_t *end) {
60
161
  pm_string_t string;
61
162
  pm_string_shared_init(&string, start, end);
62
- parser->name_callback(&string, parser->name_data);
63
- pm_string_free(&string);
163
+ parser->name_callback(parser->parser, &string, parser->name_data);
64
164
  }
65
165
 
66
166
  /**
67
167
  * Returns true if the next character is the end of the source.
68
168
  */
69
- static inline bool
169
+ static PRISM_INLINE bool
70
170
  pm_regexp_char_is_eof(pm_regexp_parser_t *parser) {
71
171
  return parser->cursor >= parser->end;
72
172
  }
@@ -74,7 +174,7 @@ pm_regexp_char_is_eof(pm_regexp_parser_t *parser) {
74
174
  /**
75
175
  * Optionally accept a char and consume it if it exists.
76
176
  */
77
- static inline bool
177
+ static PRISM_INLINE bool
78
178
  pm_regexp_char_accept(pm_regexp_parser_t *parser, uint8_t value) {
79
179
  if (!pm_regexp_char_is_eof(parser) && *parser->cursor == value) {
80
180
  parser->cursor++;
@@ -86,7 +186,7 @@ pm_regexp_char_accept(pm_regexp_parser_t *parser, uint8_t value) {
86
186
  /**
87
187
  * Expect a character to be present and consume it.
88
188
  */
89
- static inline bool
189
+ static PRISM_INLINE bool
90
190
  pm_regexp_char_expect(pm_regexp_parser_t *parser, uint8_t value) {
91
191
  if (!pm_regexp_char_is_eof(parser) && *parser->cursor == value) {
92
192
  parser->cursor++;
@@ -113,6 +213,47 @@ pm_regexp_char_find(pm_regexp_parser_t *parser, uint8_t value) {
113
213
  return true;
114
214
  }
115
215
 
216
+ /**
217
+ * Mark a group boundary in the hex escape byte buffer. When consecutive hex
218
+ * escape bytes >= 0x80 are followed by a non-hex-escape, this appends a 0x00
219
+ * sentinel to separate the groups for later multibyte validation.
220
+ */
221
+ static PRISM_INLINE void
222
+ pm_regexp_hex_group_boundary(pm_regexp_parser_t *parser) {
223
+ if (parser->hex_group_active) {
224
+ pm_buffer_append_byte(&parser->hex_escape_buffer, 0x00);
225
+ parser->hex_group_active = false;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Track a hex escape byte value >= 0x80 for multibyte validation.
231
+ */
232
+ static PRISM_INLINE void
233
+ pm_regexp_track_hex_escape(pm_regexp_parser_t *parser, uint8_t byte) {
234
+ if (byte >= 0x80) {
235
+ pm_buffer_append_byte(&parser->hex_escape_buffer, byte);
236
+ parser->hex_group_active = true;
237
+ parser->has_hex_escape = true;
238
+
239
+ parser->explicit_encoding = parser->encoding;
240
+ parser->last_escape_was_unicode = false;
241
+ } else {
242
+ pm_regexp_hex_group_boundary(parser);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Parse a hex digit character and return its value, or -1 if not a hex digit.
248
+ */
249
+ static PRISM_INLINE int
250
+ pm_regexp_hex_digit_value(uint8_t byte) {
251
+ if (byte >= '0' && byte <= '9') return byte - '0';
252
+ if (byte >= 'a' && byte <= 'f') return byte - 'a' + 10;
253
+ if (byte >= 'A' && byte <= 'F') return byte - 'A' + 10;
254
+ return -1;
255
+ }
256
+
116
257
  /**
117
258
  * Range quantifiers are a special class of quantifiers that look like
118
259
  *
@@ -121,13 +262,12 @@ pm_regexp_char_find(pm_regexp_parser_t *parser, uint8_t value) {
121
262
  * * {digit,digit}
122
263
  * * {,digit}
123
264
  *
124
- * Unfortunately, if there are any spaces in between, then this just becomes a
125
- * regular character match expression and we have to backtrack. So when this
126
- * function first starts running, we'll create a "save" point and then attempt
127
- * to parse the quantifier. If it fails, we'll restore the save point and
128
- * return.
265
+ * If there are any spaces in between, then this just becomes a regular
266
+ * character match expression and we have to backtrack. So when this function
267
+ * first starts running, we'll create a "save" point and then attempt to parse
268
+ * the quantifier. If it fails, we'll restore the save point and return.
129
269
  *
130
- * The properly track everything, we're going to build a little state machine.
270
+ * To properly track everything, we're going to build a little state machine.
131
271
  * It looks something like the following:
132
272
  *
133
273
  * +-------+ +---------+ ------------+
@@ -275,10 +415,392 @@ pm_regexp_parse_posix_class(pm_regexp_parser_t *parser) {
275
415
  );
276
416
  }
277
417
 
418
+ /**
419
+ * Property escape classification. Onigmo supports three tiers of property
420
+ * names depending on the encoding:
421
+ *
422
+ * - POSIX properties (Alnum, Alpha, ASCII, Blank, Cntrl, Digit, Graph, Lower,
423
+ * Print, Punct, Space, Upper, XDigit, Word): valid in all encodings.
424
+ * - Script properties (Hiragana, Katakana, Han, Latin, Greek, Cyrillic): valid
425
+ * in EUC-JP (/e), Windows-31J (/s), and UTF-8 (/u), but not ASCII-8BIT (/n).
426
+ * - Unicode-only properties (general categories like L, Ll, Lu, etc., plus
427
+ * Any, Assigned): valid only in UTF-8 (/u).
428
+ */
429
+ typedef enum {
430
+ PM_REGEXP_PROPERTY_POSIX,
431
+ PM_REGEXP_PROPERTY_SCRIPT,
432
+ PM_REGEXP_PROPERTY_UNICODE
433
+ } pm_regexp_property_type_t;
434
+
435
+ /**
436
+ * Classify a property name. The name may start with '^' for negation, which
437
+ * is skipped before matching.
438
+ */
439
+ static pm_regexp_property_type_t
440
+ pm_regexp_classify_property(const uint8_t *name, size_t length) {
441
+ // Skip leading '^' for negated properties like \p{^Hiragana}.
442
+ if (length > 0 && name[0] == '^') {
443
+ name++;
444
+ length--;
445
+ }
446
+
447
+ #define PM_REGEXP_CASECMP(str_) (pm_strncasecmp(name, (const uint8_t *) (str_), length) == 0)
448
+
449
+ switch (length) {
450
+ case 3:
451
+ if (PM_REGEXP_CASECMP("Han")) return PM_REGEXP_PROPERTY_SCRIPT;
452
+ break;
453
+ case 4:
454
+ if (PM_REGEXP_CASECMP("Word")) return PM_REGEXP_PROPERTY_POSIX;
455
+ break;
456
+ case 5:
457
+ /* Most properties are length 5, so dispatch on first character. */
458
+ switch (name[0] | 0x20) {
459
+ case 'a':
460
+ if (PM_REGEXP_CASECMP("Alnum")) return PM_REGEXP_PROPERTY_POSIX;
461
+ if (PM_REGEXP_CASECMP("Alpha")) return PM_REGEXP_PROPERTY_POSIX;
462
+ if (PM_REGEXP_CASECMP("ASCII")) return PM_REGEXP_PROPERTY_POSIX;
463
+ break;
464
+ case 'b':
465
+ if (PM_REGEXP_CASECMP("Blank")) return PM_REGEXP_PROPERTY_POSIX;
466
+ break;
467
+ case 'c':
468
+ if (PM_REGEXP_CASECMP("Cntrl")) return PM_REGEXP_PROPERTY_POSIX;
469
+ break;
470
+ case 'd':
471
+ if (PM_REGEXP_CASECMP("Digit")) return PM_REGEXP_PROPERTY_POSIX;
472
+ break;
473
+ case 'g':
474
+ if (PM_REGEXP_CASECMP("Graph")) return PM_REGEXP_PROPERTY_POSIX;
475
+ if (PM_REGEXP_CASECMP("Greek")) return PM_REGEXP_PROPERTY_SCRIPT;
476
+ break;
477
+ case 'l':
478
+ if (PM_REGEXP_CASECMP("Lower")) return PM_REGEXP_PROPERTY_POSIX;
479
+ if (PM_REGEXP_CASECMP("Latin")) return PM_REGEXP_PROPERTY_SCRIPT;
480
+ break;
481
+ case 'p':
482
+ if (PM_REGEXP_CASECMP("Print")) return PM_REGEXP_PROPERTY_POSIX;
483
+ if (PM_REGEXP_CASECMP("Punct")) return PM_REGEXP_PROPERTY_POSIX;
484
+ break;
485
+ case 's':
486
+ if (PM_REGEXP_CASECMP("Space")) return PM_REGEXP_PROPERTY_POSIX;
487
+ break;
488
+ case 'u':
489
+ if (PM_REGEXP_CASECMP("Upper")) return PM_REGEXP_PROPERTY_POSIX;
490
+ break;
491
+ }
492
+ break;
493
+ case 6:
494
+ if (PM_REGEXP_CASECMP("XDigit")) return PM_REGEXP_PROPERTY_POSIX;
495
+ break;
496
+ case 8:
497
+ if (PM_REGEXP_CASECMP("Hiragana")) return PM_REGEXP_PROPERTY_SCRIPT;
498
+ if (PM_REGEXP_CASECMP("Katakana")) return PM_REGEXP_PROPERTY_SCRIPT;
499
+ if (PM_REGEXP_CASECMP("Cyrillic")) return PM_REGEXP_PROPERTY_SCRIPT;
500
+ break;
501
+ }
502
+
503
+ #undef PM_REGEXP_CASECMP
504
+
505
+ // Everything else is Unicode-only (general categories, other scripts, etc.).
506
+ return PM_REGEXP_PROPERTY_UNICODE;
507
+ }
508
+
509
+ /**
510
+ * Check for and skip a `\p{...}` or `\P{...}` Unicode property escape. The
511
+ * cursor should be pointing at 'p' or 'P' when this is called. If a property
512
+ * escape is found, record it on the regexp parser and advance past the closing
513
+ * '}'.
514
+ *
515
+ * Properties are classified into three tiers (POSIX, script, Unicode-only) to
516
+ * determine which encoding modifiers they are valid with.
517
+ */
518
+ static bool
519
+ pm_regexp_parse_property_escape(pm_regexp_parser_t *parser) {
520
+ assert(*parser->cursor == 'p' || *parser->cursor == 'P');
521
+
522
+ if (parser->cursor + 1 < parser->end && parser->cursor[1] == '{') {
523
+ const uint8_t *name_start = parser->cursor + 2;
524
+ const uint8_t *search = name_start;
525
+
526
+ while (search < parser->end && *search != '}') search++;
527
+
528
+ if (search < parser->end) {
529
+ size_t name_length = (size_t) (search - name_start);
530
+ parser->has_property_escape = true;
531
+
532
+ pm_regexp_property_type_t type = pm_regexp_classify_property(name_start, name_length);
533
+
534
+ // Track the first non-POSIX property name (for /n error messages).
535
+ if (type >= PM_REGEXP_PROPERTY_SCRIPT && parser->property_name == NULL) {
536
+ parser->property_name = name_start;
537
+ parser->property_name_length = name_length;
538
+ }
539
+
540
+ // Track the first Unicode-only property name (for /e, /s error messages).
541
+ if (type == PM_REGEXP_PROPERTY_UNICODE) {
542
+ parser->has_unicode_property_escape = true;
543
+ if (parser->unicode_property_name == NULL) {
544
+ parser->unicode_property_name = name_start;
545
+ parser->unicode_property_name_length = name_length;
546
+ }
547
+ }
548
+
549
+ parser->cursor = search + 1; // skip past '}'
550
+ return true;
551
+ }
552
+ }
553
+
554
+ // Not a property escape, just skip the single character after '\'.
555
+ parser->cursor++;
556
+ return false;
557
+ }
558
+
559
+ /**
560
+ * Validate and skip a \u escape sequence in a regular expression. The cursor
561
+ * should be pointing at the character after 'u' when this is called. This
562
+ * handles both the \u{NNNN MMMM} and \uNNNN forms. Also tracks encoding
563
+ * state for validation.
564
+ */
565
+ static void
566
+ pm_regexp_parse_unicode_escape(pm_regexp_parser_t *parser) {
567
+ const uint8_t *escape_start = parser->cursor - 2; // points to '\'
568
+
569
+ if (pm_regexp_char_is_eof(parser)) {
570
+ pm_regexp_parse_error(parser, escape_start, parser->cursor, "invalid Unicode escape");
571
+ return;
572
+ }
573
+
574
+ if (*parser->cursor == '{') {
575
+ parser->cursor++; // skip '{'
576
+
577
+ // Skip leading whitespace.
578
+ while (!pm_regexp_char_is_eof(parser) && pm_char_is_whitespace(*parser->cursor)) {
579
+ parser->cursor++;
580
+ }
581
+
582
+ bool has_codepoint = false;
583
+
584
+ while (!pm_regexp_char_is_eof(parser) && *parser->cursor != '}') {
585
+ // Parse the hex digits to compute the codepoint value.
586
+ uint32_t value = 0;
587
+ size_t hex_count = 0;
588
+
589
+ int digit;
590
+ while (!pm_regexp_char_is_eof(parser) && (digit = pm_regexp_hex_digit_value(*parser->cursor)) >= 0) {
591
+ value = (value << 4) | (uint32_t) digit;
592
+ hex_count++;
593
+ parser->cursor++;
594
+ }
595
+
596
+ if (hex_count == 0) {
597
+ // Skip to '}' or end of regexp to find the full extent.
598
+ while (!pm_regexp_char_is_eof(parser) && *parser->cursor != '}') {
599
+ parser->cursor++;
600
+ }
601
+
602
+ const uint8_t *escape_end = parser->cursor;
603
+ if (!pm_regexp_char_is_eof(parser)) {
604
+ escape_end++;
605
+ parser->cursor++; // skip '}'
606
+ }
607
+
608
+ pm_regexp_parse_error_format(parser, escape_start, escape_end, PM_ERR_ESCAPE_INVALID_UNICODE_LIST, (int) (escape_end - escape_start), (const char *) escape_start);
609
+ return;
610
+ }
611
+
612
+ if (hex_count > 6) {
613
+ pm_regexp_parse_error(parser, escape_start, parser->cursor, "invalid Unicode range");
614
+ }
615
+
616
+ // Track encoding state for this codepoint.
617
+ if (value >= 0x80) {
618
+ parser->has_unicode_escape = true;
619
+ parser->explicit_encoding = PM_ENCODING_UTF_8_ENTRY;
620
+ parser->last_escape_was_unicode = true;
621
+ pm_regexp_hex_group_boundary(parser);
622
+ }
623
+
624
+ // Check for invalid Unicode range (surrogates or > 0x10FFFF).
625
+ if (value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF)) {
626
+ parser->invalid_unicode_range = true;
627
+ }
628
+
629
+ has_codepoint = true;
630
+
631
+ // Skip whitespace between codepoints.
632
+ while (!pm_regexp_char_is_eof(parser) && pm_char_is_whitespace(*parser->cursor)) {
633
+ parser->cursor++;
634
+ }
635
+ }
636
+
637
+ if (pm_regexp_char_is_eof(parser)) {
638
+ pm_regexp_parse_error(parser, escape_start, parser->cursor, "unterminated Unicode escape");
639
+ } else {
640
+ if (!has_codepoint) {
641
+ pm_regexp_parse_error_format(parser, escape_start, parser->cursor + 1, PM_ERR_ESCAPE_INVALID_UNICODE_LIST, (int) (parser->cursor + 1 - escape_start), (const char *) escape_start);
642
+ }
643
+ parser->cursor++; // skip '}'
644
+ }
645
+ } else {
646
+ // \uNNNN form — need exactly 4 hex digits.
647
+ uint32_t value = 0;
648
+ size_t hex_count = 0;
649
+
650
+ int digit;
651
+ while (hex_count < 4 && !pm_regexp_char_is_eof(parser) && (digit = pm_regexp_hex_digit_value(*parser->cursor)) >= 0) {
652
+ value = (value << 4) | (uint32_t) digit;
653
+ hex_count++;
654
+ parser->cursor++;
655
+ }
656
+
657
+ if (hex_count < 4) {
658
+ pm_regexp_parse_error(parser, escape_start, parser->cursor, "invalid Unicode escape");
659
+ } else if (value >= 0x80) {
660
+ parser->has_unicode_escape = true;
661
+ parser->explicit_encoding = PM_ENCODING_UTF_8_ENTRY;
662
+ parser->last_escape_was_unicode = true;
663
+ pm_regexp_hex_group_boundary(parser);
664
+ }
665
+
666
+ // Check for invalid Unicode range.
667
+ if (hex_count == 4 && (value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF))) {
668
+ parser->invalid_unicode_range = true;
669
+ }
670
+ }
671
+ }
672
+
278
673
  // Forward declaration because character sets can be nested.
279
674
  static bool
280
675
  pm_regexp_parse_lbracket(pm_regexp_parser_t *parser, uint16_t depth);
281
676
 
677
+ /**
678
+ * Parse a \x escape and return the byte value. The cursor should be pointing
679
+ * at the character after 'x'. Returns -1 if no hex digits follow.
680
+ */
681
+ static int
682
+ pm_regexp_parse_hex_escape(pm_regexp_parser_t *parser) {
683
+ int value = -1;
684
+
685
+ if (!pm_regexp_char_is_eof(parser)) {
686
+ int digit = pm_regexp_hex_digit_value(*parser->cursor);
687
+ if (digit >= 0) {
688
+ value = digit;
689
+ parser->cursor++;
690
+
691
+ if (!pm_regexp_char_is_eof(parser)) {
692
+ digit = pm_regexp_hex_digit_value(*parser->cursor);
693
+ if (digit >= 0) {
694
+ value = (value << 4) | digit;
695
+ parser->cursor++;
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ if (value >= 0) {
702
+ pm_regexp_track_hex_escape(parser, (uint8_t) value);
703
+ }
704
+
705
+ return value;
706
+ }
707
+
708
+ /**
709
+ * Parse a backslash escape sequence in a regexp, handling \u (unicode),
710
+ * \p/\P (property), \x (hex), and other single-character escapes. Also
711
+ * tracks encoding state for \M-x and \C-\M-x escapes.
712
+ */
713
+ static void
714
+ pm_regexp_parse_backslash_escape(pm_regexp_parser_t *parser) {
715
+ if (pm_regexp_char_is_eof(parser)) return;
716
+
717
+ switch (*parser->cursor) {
718
+ case 'u':
719
+ parser->cursor++; // skip 'u'
720
+ pm_regexp_parse_unicode_escape(parser);
721
+ break;
722
+ case 'p':
723
+ case 'P':
724
+ pm_regexp_parse_property_escape(parser);
725
+ break;
726
+ case 'x':
727
+ parser->cursor++; // skip 'x'
728
+ pm_regexp_parse_hex_escape(parser);
729
+ break;
730
+ case 'M':
731
+ // \M-x produces (x | 0x80), always >= 0x80
732
+ if (parser->cursor + 2 < parser->end && parser->cursor[1] == '-') {
733
+ parser->cursor += 2; // skip 'M-'
734
+ if (!pm_regexp_char_is_eof(parser)) {
735
+ if (*parser->cursor == '\\') {
736
+ parser->cursor++;
737
+ // \M-\C-x or \M-\cx — the resulting byte is always >= 0x80
738
+ // We just need to track it as a hex escape >= 0x80.
739
+ pm_regexp_parse_backslash_escape(parser);
740
+ } else {
741
+ parser->cursor++;
742
+ }
743
+ // \M-x always produces a byte >= 0x80
744
+ pm_regexp_track_hex_escape(parser, 0x80);
745
+ }
746
+ } else {
747
+ parser->cursor++;
748
+ }
749
+ break;
750
+ case 'C':
751
+ // \C-x produces (x & 0x1F)
752
+ if (parser->cursor + 2 < parser->end && parser->cursor[1] == '-') {
753
+ parser->cursor += 2; // skip 'C-'
754
+ if (!pm_regexp_char_is_eof(parser)) {
755
+ if (*parser->cursor == '\\') {
756
+ parser->cursor++;
757
+ pm_regexp_parse_backslash_escape(parser);
758
+ } else {
759
+ parser->cursor++;
760
+ }
761
+ }
762
+ } else {
763
+ parser->cursor++;
764
+ }
765
+ break;
766
+ case 'c':
767
+ // \cx produces (x & 0x1F)
768
+ parser->cursor++; // skip 'c'
769
+ if (!pm_regexp_char_is_eof(parser)) {
770
+ if (*parser->cursor == '\\') {
771
+ parser->cursor++;
772
+ pm_regexp_parse_backslash_escape(parser);
773
+ } else {
774
+ parser->cursor++;
775
+ }
776
+ }
777
+ break;
778
+ default:
779
+ pm_regexp_hex_group_boundary(parser);
780
+ parser->cursor++;
781
+ break;
782
+ }
783
+ }
784
+
785
+ /**
786
+ * Check if a byte at the current position is a non-ASCII byte in a multibyte
787
+ * encoding that produces an invalid character. If so, emit an error at the
788
+ * byte location immediately.
789
+ */
790
+ static void
791
+ pm_regexp_parse_invalid_multibyte(pm_regexp_parser_t *parser, const uint8_t *cursor) {
792
+ uint8_t byte = *cursor;
793
+ if (byte >= 0x80 && parser->encoding_changed && parser->encoding->multibyte) {
794
+ size_t width = parser->encoding->char_width(cursor, (ptrdiff_t) (parser->end - cursor));
795
+ if (width > 1) {
796
+ parser->cursor += width - 1;
797
+ } else if (width == 0) {
798
+ parser->has_invalid_multibyte = true;
799
+ pm_regexp_parse_error_format(parser, cursor, cursor + 1, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
800
+ }
801
+ }
802
+ }
803
+
282
804
  /**
283
805
  * match-char-set : '[' '^'? (match-range | match-char)* ']'
284
806
  * ;
@@ -293,12 +815,16 @@ pm_regexp_parse_character_set(pm_regexp_parser_t *parser, uint16_t depth) {
293
815
  pm_regexp_parse_lbracket(parser, (uint16_t) (depth + 1));
294
816
  break;
295
817
  case '\\':
296
- if (!pm_regexp_char_is_eof(parser)) {
297
- parser->cursor++;
298
- }
818
+ pm_regexp_parse_backslash_escape(parser);
299
819
  break;
300
820
  default:
301
- // do nothing, we've already advanced the cursor
821
+ // We've already advanced the cursor by one byte. If the byte
822
+ // was >= 0x80 in a multibyte encoding, we may need to consume
823
+ // additional continuation bytes and validate the character.
824
+ if (*(parser->cursor - 1) >= 0x80) {
825
+ parser->non_ascii_literal_count++;
826
+ }
827
+ pm_regexp_parse_invalid_multibyte(parser, parser->cursor - 1);
302
828
  break;
303
829
  }
304
830
  }
@@ -354,8 +880,13 @@ typedef enum {
354
880
  // These are the options that are configurable on the regular expression (or
355
881
  // from within a group).
356
882
 
883
+ /** The minimum character value for a regexp option slot. */
357
884
  #define PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM 'a'
885
+
886
+ /** The maximum character value for a regexp option slot. */
358
887
  #define PRISM_REGEXP_OPTION_STATE_SLOT_MAXIMUM 'x'
888
+
889
+ /** The number of regexp option slots. */
359
890
  #define PRISM_REGEXP_OPTION_STATE_SLOTS (PRISM_REGEXP_OPTION_STATE_SLOT_MAXIMUM - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM + 1)
360
891
 
361
892
  /**
@@ -498,7 +1029,15 @@ pm_regexp_parse_group(pm_regexp_parser_t *parser, uint16_t depth) {
498
1029
  }
499
1030
 
500
1031
  size_t width = parser->encoding->char_width(parser->cursor, (ptrdiff_t) (parser->end - parser->cursor));
501
- if (width == 0) return false;
1032
+ if (width == 0) {
1033
+ if (*parser->cursor >= 0x80) {
1034
+ parser->has_invalid_multibyte = true;
1035
+ pm_regexp_parse_error_format(parser, parser->cursor, parser->cursor + 1, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
1036
+ parser->cursor++;
1037
+ continue;
1038
+ }
1039
+ return false;
1040
+ }
502
1041
 
503
1042
  escaped = (width == 1) && (*parser->cursor == '\\');
504
1043
  parser->cursor += width;
@@ -686,9 +1225,7 @@ pm_regexp_parse_item(pm_regexp_parser_t *parser, uint16_t depth) {
686
1225
  return pm_regexp_parse_quantifier(parser);
687
1226
  case '\\':
688
1227
  parser->cursor++;
689
- if (!pm_regexp_char_is_eof(parser)) {
690
- parser->cursor++;
691
- }
1228
+ pm_regexp_parse_backslash_escape(parser);
692
1229
  return pm_regexp_parse_quantifier(parser);
693
1230
  case '(':
694
1231
  parser->cursor++;
@@ -720,9 +1257,30 @@ pm_regexp_parse_item(pm_regexp_parser_t *parser, uint16_t depth) {
720
1257
  width = parser->encoding->char_width(parser->cursor, (ptrdiff_t) (parser->end - parser->cursor));
721
1258
  }
722
1259
 
723
- if (width == 0) return false; // TODO: add appropriate error
724
- parser->cursor += width;
1260
+ if (width == 0) {
1261
+ if (*parser->cursor >= 0x80 && parser->encoding_changed) {
1262
+ if (parser->encoding->multibyte) {
1263
+ // Invalid multibyte character in a multibyte encoding.
1264
+ // Emit the error at the byte location immediately.
1265
+ parser->has_invalid_multibyte = true;
1266
+ pm_regexp_parse_error_format(parser, parser->cursor, parser->cursor + 1, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
1267
+ } else {
1268
+ // Non-ASCII byte in a single-byte encoding (e.g.,
1269
+ // US-ASCII). Count it for later error reporting.
1270
+ parser->non_ascii_literal_count++;
1271
+ }
1272
+ parser->cursor++;
1273
+ return pm_regexp_parse_quantifier(parser);
1274
+ }
1275
+ return false;
1276
+ }
725
1277
 
1278
+ // Count non-ASCII literal bytes.
1279
+ for (size_t i = 0; i < width; i++) {
1280
+ if (parser->cursor[i] >= 0x80) parser->non_ascii_literal_count++;
1281
+ }
1282
+
1283
+ parser->cursor += width;
726
1284
  return pm_regexp_parse_quantifier(parser);
727
1285
  }
728
1286
  }
@@ -768,13 +1326,353 @@ pm_regexp_parse_pattern(pm_regexp_parser_t *parser) {
768
1326
  return pm_regexp_char_is_eof(parser);
769
1327
  }
770
1328
 
1329
+ // ---------------------------------------------------------------------------
1330
+ // Encoding validation
1331
+ // ---------------------------------------------------------------------------
1332
+
771
1333
  /**
772
- * Parse a regular expression and extract the names of all of the named capture
773
- * groups.
1334
+ * Validate that groups of hex escape bytes in the buffer form valid multibyte
1335
+ * characters in the given encoding. Groups are separated by 0x00 sentinels.
1336
+ */
1337
+ static bool
1338
+ pm_regexp_validate_hex_escapes(const pm_encoding_t *encoding, const pm_buffer_t *buffer) {
1339
+ const uint8_t *data = (const uint8_t *) pm_buffer_value(buffer);
1340
+ size_t len = pm_buffer_length(buffer);
1341
+ size_t i = 0;
1342
+
1343
+ while (i < len) {
1344
+ size_t group_start = i;
1345
+ while (i < len && data[i] != 0x00) i++;
1346
+
1347
+ for (size_t j = group_start; j < i; ) {
1348
+ size_t width = encoding->char_width(data + j, (ptrdiff_t) (i - j));
1349
+ if (width == 0) return false;
1350
+ j += width;
1351
+ }
1352
+
1353
+ if (i < len) i++; // skip sentinel
1354
+ }
1355
+
1356
+ return true;
1357
+ }
1358
+
1359
+ /**
1360
+ * Format regexp source content for use in error messages, hex-escaping
1361
+ * non-ASCII bytes.
1362
+ */
1363
+ static void
1364
+ pm_regexp_format_for_error(pm_buffer_t *buffer, const pm_encoding_t *encoding, const uint8_t *source, size_t length) {
1365
+ size_t index = 0;
1366
+
1367
+ if (encoding == PM_ENCODING_UTF_8_ENTRY) {
1368
+ pm_buffer_append_string(buffer, (const char *) source, length);
1369
+ return;
1370
+ }
1371
+
1372
+ while (index < length) {
1373
+ if (source[index] < 0x80) {
1374
+ pm_buffer_append_byte(buffer, source[index]);
1375
+ index++;
1376
+ } else if (encoding->multibyte) {
1377
+ size_t width = encoding->char_width(source + index, (ptrdiff_t) (length - index));
1378
+
1379
+ if (width > 1) {
1380
+ pm_buffer_append_string(buffer, "\\x{", 3);
1381
+ for (size_t i = 0; i < width; i++) {
1382
+ pm_buffer_append_format(buffer, "%02X", source[index + i]);
1383
+ }
1384
+ pm_buffer_append_byte(buffer, '}');
1385
+ index += width;
1386
+ } else {
1387
+ pm_buffer_append_format(buffer, "\\x%02X", source[index]);
1388
+ index++;
1389
+ }
1390
+ } else {
1391
+ pm_buffer_append_format(buffer, "\\x%02X", source[index]);
1392
+ index++;
1393
+ }
1394
+ }
1395
+ }
1396
+
1397
+ /**
1398
+ * Emit an encoding validation error on the regexp node.
1399
+ */
1400
+ #define PM_REGEXP_ENCODING_ERROR(parser, diag_id, ...) \
1401
+ pm_diagnostic_list_append_format( \
1402
+ &(parser)->parser->metadata_arena, \
1403
+ &(parser)->parser->error_list, \
1404
+ (uint32_t) ((parser)->node_start - (parser)->parser->start), \
1405
+ (uint32_t) ((parser)->node_end - (parser)->node_start), \
1406
+ diag_id, __VA_ARGS__)
1407
+
1408
+ /**
1409
+ * Validate encoding for a regexp with an encoding modifier (/e, /s, /u, /n).
1410
+ *
1411
+ * The decision tree is:
1412
+ *
1413
+ * 1. No escape-set encoding (explicit_encoding == NULL):
1414
+ * a. ASCII-only content: validate property escapes, return forced US-ASCII
1415
+ * for /n or the modifier flags for others.
1416
+ * b. US-ASCII source with non-ASCII literals: emit per-byte errors.
1417
+ * c. Source encoding differs from modifier encoding: emit mismatch error.
1418
+ *
1419
+ * 2. Mixed \u and \x escapes: emit the appropriate conflict error depending
1420
+ * on the modifier and which escape type was last.
1421
+ *
1422
+ * 3. \u escape with non-/u modifier: incompatible encoding error.
1423
+ *
1424
+ * 4. Validate that hex escape byte sequences form valid multibyte characters
1425
+ * in the modifier's encoding.
1426
+ */
1427
+ static pm_node_flags_t
1428
+ pm_regexp_validate_encoding_modifier(pm_regexp_parser_t *parser, bool ascii_only, pm_node_flags_t flags, char modifier, const pm_encoding_t *modifier_encoding, const char *source_start, int source_length) {
1429
+
1430
+ if (parser->explicit_encoding == NULL) {
1431
+ if (ascii_only) {
1432
+ // Check property escapes against the modifier's encoding tier.
1433
+ // /n (ASCII-8BIT): only POSIX properties are valid.
1434
+ // /e, /s: POSIX and script properties are valid.
1435
+ // /u: all properties are valid.
1436
+ if (modifier == 'n' && parser->property_name != NULL) {
1437
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INVALID_CHAR_PROPERTY,
1438
+ (int) parser->property_name_length, (const char *) parser->property_name,
1439
+ source_length, source_start);
1440
+ } else if (modifier != 'u' && parser->has_unicode_property_escape) {
1441
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INVALID_CHAR_PROPERTY,
1442
+ (int) parser->unicode_property_name_length, (const char *) parser->unicode_property_name,
1443
+ source_length, source_start);
1444
+ }
1445
+ return modifier == 'n' ? PM_REGULAR_EXPRESSION_FLAGS_FORCED_US_ASCII_ENCODING : flags;
1446
+ }
1447
+
1448
+ if (parser->encoding == PM_ENCODING_US_ASCII_ENTRY) {
1449
+ for (uint32_t i = 0; i < parser->non_ascii_literal_count; i++) {
1450
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
1451
+ }
1452
+ } else if (parser->encoding != modifier_encoding) {
1453
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_ENCODING_OPTION_MISMATCH, modifier, parser->encoding->name);
1454
+
1455
+ if (modifier == 'n' && !ascii_only) {
1456
+ pm_buffer_t formatted = { 0 };
1457
+ pm_regexp_format_for_error(&formatted, parser->encoding, (const uint8_t *) source_start, (size_t) source_length);
1458
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_NON_ESCAPED_MBC, (int) formatted.length, (const char *) formatted.value);
1459
+ pm_buffer_cleanup(&formatted);
1460
+ }
1461
+ }
1462
+
1463
+ return flags;
1464
+ }
1465
+
1466
+ // Mixed unicode + hex escapes.
1467
+ if (parser->has_unicode_escape && parser->has_hex_escape) {
1468
+ if (modifier == 'n') {
1469
+ if (parser->last_escape_was_unicode) {
1470
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_UTF8_CHAR_NON_UTF8_REGEXP, source_length, source_start);
1471
+ } else {
1472
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_ESCAPED_NON_ASCII_IN_UTF8, source_length, source_start);
1473
+ }
1474
+ } else {
1475
+ if (!pm_regexp_validate_hex_escapes(modifier_encoding, &parser->hex_escape_buffer)) {
1476
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1477
+ }
1478
+ }
1479
+
1480
+ return flags;
1481
+ }
1482
+
1483
+ if (modifier != 'u' && parser->explicit_encoding == PM_ENCODING_UTF_8_ENTRY) {
1484
+ if (parser->last_escape_was_unicode) {
1485
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INCOMPAT_CHAR_ENCODING, source_length, source_start);
1486
+ } else if (parser->encoding != PM_ENCODING_UTF_8_ENTRY) {
1487
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INCOMPAT_CHAR_ENCODING, source_length, source_start);
1488
+ }
1489
+ }
1490
+
1491
+ if (modifier != 'n' && !pm_regexp_validate_hex_escapes(modifier_encoding, &parser->hex_escape_buffer)) {
1492
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1493
+ }
1494
+
1495
+ return flags;
1496
+ }
1497
+
1498
+ /**
1499
+ * Validate encoding for a regexp without a modifier and compute the encoding
1500
+ * flags to set on the node.
1501
+ *
1502
+ * The decision tree is:
1503
+ *
1504
+ * 1. If a modifier (/n, /u, /e, /s) is present, delegate to
1505
+ * pm_regexp_validate_encoding_modifier.
1506
+ * 2. Invalid multibyte chars or unicode ranges: suppress further checks (errors
1507
+ * were already emitted during parsing).
1508
+ * 3. US-ASCII source with non-ASCII literals: emit per-byte errors.
1509
+ * 4. ASCII-only content: return forced US-ASCII (or forced UTF-8 if \p{...}).
1510
+ * 5. Escape-set encoding present: validate hex escapes against the target
1511
+ * encoding, handle mixed \u + \x conflicts, and return the appropriate
1512
+ * forced encoding flag.
1513
+ */
1514
+ static pm_node_flags_t
1515
+ pm_regexp_validate_encoding(pm_regexp_parser_t *parser, bool ascii_only, pm_node_flags_t flags, const char *source_start, int source_length) {
1516
+
1517
+ // Invalid multibyte characters suppress further validation.
1518
+ // Errors were already emitted at the byte locations during parsing.
1519
+ if (parser->has_invalid_multibyte) {
1520
+ return flags;
1521
+ }
1522
+
1523
+ if (parser->invalid_unicode_range) {
1524
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INVALID_UNICODE_RANGE, source_length, source_start);
1525
+ return flags;
1526
+ }
1527
+
1528
+ // Check modifier flags first.
1529
+ if (flags & PM_REGULAR_EXPRESSION_FLAGS_ASCII_8BIT) {
1530
+ return pm_regexp_validate_encoding_modifier(parser, ascii_only, flags, 'n', PM_ENCODING_ASCII_8BIT_ENTRY, source_start, source_length);
1531
+ }
1532
+ if (flags & PM_REGULAR_EXPRESSION_FLAGS_UTF_8) {
1533
+ return pm_regexp_validate_encoding_modifier(parser, ascii_only, flags, 'u', PM_ENCODING_UTF_8_ENTRY, source_start, source_length);
1534
+ }
1535
+ if (flags & PM_REGULAR_EXPRESSION_FLAGS_EUC_JP) {
1536
+ return pm_regexp_validate_encoding_modifier(parser, ascii_only, flags, 'e', PM_ENCODING_EUC_JP_ENTRY, source_start, source_length);
1537
+ }
1538
+ if (flags & PM_REGULAR_EXPRESSION_FLAGS_WINDOWS_31J) {
1539
+ return pm_regexp_validate_encoding_modifier(parser, ascii_only, flags, 's', PM_ENCODING_WINDOWS_31J_ENTRY, source_start, source_length);
1540
+ }
1541
+
1542
+ // No modifier — check for non-ASCII literals in US-ASCII encoding.
1543
+ if (parser->encoding == PM_ENCODING_US_ASCII_ENTRY && parser->explicit_encoding == NULL && !ascii_only) {
1544
+ for (uint32_t i = 0; i < parser->non_ascii_literal_count; i++) {
1545
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
1546
+ }
1547
+ }
1548
+
1549
+ // ASCII-only regexps get downgraded to US-ASCII, unless property escapes
1550
+ // force UTF-8.
1551
+ if (ascii_only) {
1552
+ if (parser->has_property_escape) {
1553
+ return PM_REGULAR_EXPRESSION_FLAGS_FORCED_UTF8_ENCODING;
1554
+ }
1555
+ return PM_REGULAR_EXPRESSION_FLAGS_FORCED_US_ASCII_ENCODING;
1556
+ }
1557
+
1558
+ // Check explicit encoding from escape sequences.
1559
+ if (parser->explicit_encoding != NULL) {
1560
+ // Mixed unicode + hex escapes without modifier.
1561
+ if (parser->has_unicode_escape && parser->has_hex_escape && parser->encoding != PM_ENCODING_UTF_8_ENTRY) {
1562
+ if (parser->encoding != PM_ENCODING_US_ASCII_ENTRY &&
1563
+ parser->encoding != PM_ENCODING_ASCII_8BIT_ENTRY &&
1564
+ !pm_regexp_validate_hex_escapes(parser->encoding, &parser->hex_escape_buffer)) {
1565
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1566
+ } else if (parser->last_escape_was_unicode) {
1567
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_UTF8_CHAR_NON_UTF8_REGEXP, source_length, source_start);
1568
+ } else {
1569
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_ESCAPED_NON_ASCII_IN_UTF8, source_length, source_start);
1570
+ }
1571
+
1572
+ return 0;
1573
+ }
1574
+
1575
+ if (parser->explicit_encoding == PM_ENCODING_UTF_8_ENTRY) {
1576
+ if (!pm_regexp_validate_hex_escapes(parser->explicit_encoding, &parser->hex_escape_buffer)) {
1577
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1578
+ }
1579
+
1580
+ return PM_REGULAR_EXPRESSION_FLAGS_FORCED_UTF8_ENCODING;
1581
+ } else if (parser->encoding == PM_ENCODING_US_ASCII_ENTRY) {
1582
+ return PM_REGULAR_EXPRESSION_FLAGS_FORCED_BINARY_ENCODING;
1583
+ } else {
1584
+ if (!pm_regexp_validate_hex_escapes(parser->explicit_encoding, &parser->hex_escape_buffer)) {
1585
+ PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1586
+ }
1587
+ }
1588
+ }
1589
+
1590
+ return 0;
1591
+ }
1592
+
1593
+ /**
1594
+ * Parse a regular expression, validate its encoding, and optionally extract
1595
+ * named capture groups. Encoding validation walks the raw source (content_loc)
1596
+ * to distinguish escape-produced bytes from literal bytes. Named capture
1597
+ * extraction walks the unescaped content since escape sequences in group names
1598
+ * (e.g., line continuations) have already been processed by the lexer.
1599
+ */
1600
+ pm_node_flags_t
1601
+ pm_regexp_parse(pm_parser_t *parser, pm_regular_expression_node_t *node, pm_regexp_name_callback_t name_callback, void *name_data) {
1602
+ const uint8_t *source = parser->start + node->content_loc.start;
1603
+ size_t size = node->content_loc.length;
1604
+ bool extended_mode = PM_NODE_FLAG_P(node, PM_REGULAR_EXPRESSION_FLAGS_EXTENDED);
1605
+ pm_node_flags_t flags = PM_NODE_FLAGS(node);
1606
+
1607
+ const uint8_t *node_start = parser->start + node->base.location.start;
1608
+ const uint8_t *node_end = parser->start + node->base.location.start + node->base.location.length;
1609
+
1610
+ // First pass: walk raw source for encoding validation (no name extraction).
1611
+ pm_regexp_parser_t regexp_parser = {
1612
+ .parser = parser,
1613
+ .start = source,
1614
+ .cursor = source,
1615
+ .end = source + size,
1616
+ .extended_mode = extended_mode,
1617
+ .encoding_changed = parser->encoding_changed,
1618
+ .encoding = parser->encoding,
1619
+ .name_callback = NULL,
1620
+ .name_data = NULL,
1621
+ .report_errors = true,
1622
+ .node_start = node_start,
1623
+ .node_end = node_end,
1624
+ .has_unicode_escape = false,
1625
+ .has_hex_escape = false,
1626
+ .last_escape_was_unicode = false,
1627
+ .explicit_encoding = NULL,
1628
+ .has_property_escape = false,
1629
+ .has_unicode_property_escape = false,
1630
+ .property_name = NULL,
1631
+ .property_name_length = 0,
1632
+ .unicode_property_name = NULL,
1633
+ .unicode_property_name_length = 0,
1634
+ .non_ascii_literal_count = 0,
1635
+ .invalid_unicode_range = false,
1636
+ .hex_escape_buffer = { 0 },
1637
+ .hex_group_active = false,
1638
+ .has_invalid_multibyte = false,
1639
+ };
1640
+
1641
+ pm_regexp_parse_pattern(&regexp_parser);
1642
+
1643
+ // Compute ascii_only from the regexp parser's tracked state. We cannot
1644
+ // use node->unescaped for this because regexp unescaped content preserves
1645
+ // escape text (e.g., \x80 is 4 ASCII chars), not the binary values.
1646
+ bool ascii_only = !regexp_parser.has_hex_escape && !regexp_parser.has_unicode_escape && regexp_parser.non_ascii_literal_count == 0;
1647
+ // Use the unescaped content for error messages to match CRuby's format,
1648
+ // where Ruby escapes like \M-\C-? are resolved to bytes but regexp escapes
1649
+ // like \u{80} are preserved as text.
1650
+ const char *error_source = (const char *) pm_string_source(&node->unescaped);
1651
+ int error_source_length = (int) pm_string_length(&node->unescaped);
1652
+ pm_node_flags_t encoding_flags = pm_regexp_validate_encoding(&regexp_parser, ascii_only, flags, error_source, error_source_length);
1653
+ pm_buffer_cleanup(&regexp_parser.hex_escape_buffer);
1654
+
1655
+ // Second pass: walk unescaped content for named capture extraction.
1656
+ if (name_callback != NULL) {
1657
+ pm_regexp_parse_named_captures(parser, pm_string_source(&node->unescaped), pm_string_length(&node->unescaped), extended_mode, name_callback, name_data);
1658
+ }
1659
+
1660
+ return encoding_flags;
1661
+ }
1662
+
1663
+ /**
1664
+ * Parse an interpolated regular expression for named capture groups only.
1665
+ * This is used for the =~ operator with interpolated regexps where we don't
1666
+ * have a pm_regular_expression_node_t. No encoding validation is performed.
1667
+ *
1668
+ * Note: The encoding-tracking fields (has_unicode_escape, has_hex_escape, etc.)
1669
+ * are initialized but not used for the result. They exist because the parsing
1670
+ * functions (pm_regexp_parse_backslash_escape, etc.) unconditionally update
1671
+ * them as they walk through the content.
774
1672
  */
775
- PRISM_EXPORTED_FUNCTION void
776
- pm_regexp_parse(pm_parser_t *parser, const uint8_t *source, size_t size, bool extended_mode, pm_regexp_name_callback_t name_callback, void *name_data, pm_regexp_error_callback_t error_callback, void *error_data) {
777
- pm_regexp_parse_pattern(&(pm_regexp_parser_t) {
1673
+ void
1674
+ pm_regexp_parse_named_captures(pm_parser_t *parser, const uint8_t *source, size_t size, bool extended_mode, pm_regexp_name_callback_t name_callback, void *name_data) {
1675
+ pm_regexp_parser_t regexp_parser = {
778
1676
  .parser = parser,
779
1677
  .start = source,
780
1678
  .cursor = source,
@@ -784,7 +1682,24 @@ pm_regexp_parse(pm_parser_t *parser, const uint8_t *source, size_t size, bool ex
784
1682
  .encoding = parser->encoding,
785
1683
  .name_callback = name_callback,
786
1684
  .name_data = name_data,
787
- .error_callback = error_callback,
788
- .error_data = error_data
789
- });
1685
+ .report_errors = false,
1686
+ .has_unicode_escape = false,
1687
+ .has_hex_escape = false,
1688
+ .last_escape_was_unicode = false,
1689
+ .explicit_encoding = NULL,
1690
+ .has_property_escape = false,
1691
+ .has_unicode_property_escape = false,
1692
+ .property_name = NULL,
1693
+ .property_name_length = 0,
1694
+ .unicode_property_name = NULL,
1695
+ .unicode_property_name_length = 0,
1696
+ .non_ascii_literal_count = 0,
1697
+ .invalid_unicode_range = false,
1698
+ .hex_escape_buffer = { 0 },
1699
+ .hex_group_active = false,
1700
+ .has_invalid_multibyte = false,
1701
+ };
1702
+
1703
+ pm_regexp_parse_pattern(&regexp_parser);
1704
+ pm_buffer_cleanup(&regexp_parser.hex_escape_buffer);
790
1705
  }