very_fast_yaml 0.1.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b1692e3ff72ddcb470bd2aacf948130320973863685317ada5eabccbcb4811d7
4
+ data.tar.gz: 2a763988384b4f273190a0f03b882b00fe649922c5011605fa74edfdf1074808
5
+ SHA512:
6
+ metadata.gz: 2b643c0974004ca486564625088956cb69909c3d2ddbf144e169250df7fe20d00efde85eb73c1e5e8fd0a271100dbb62693e5a30a47b52d1929eb12e41c0d473
7
+ data.tar.gz: 659f5f80f09ae9ce5fca24304e4cb6ab048c43dae589a9fdb6a2cfeb981d0c9d253061f662f7aacc6ca8a299a73b2955c07c5813d76d4965d3608ae6d43a624e
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Arato
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,207 @@
1
+ /* Conservative ASCII block-YAML recognizer. Any uncertainty restarts in libyaml.
2
+ * Not a separate YAML dialect: acceptance is deliberately narrower than YAML.
3
+ * Source slices refer to the native copy, never movable Ruby string storage. */
4
+ #include "scan.h"
5
+
6
+ typedef struct {
7
+ const unsigned char *s, *end;
8
+ size_t indent;
9
+ } block_line;
10
+ typedef struct {
11
+ load_ctx *load;
12
+ const unsigned char *next, *end;
13
+ block_line line;
14
+ int failed;
15
+ } block_ctx;
16
+
17
+ static int indicator(block_line l, unsigned char ch) {
18
+ return l.s < l.end && l.s[0] == ch && (l.s + 1 == l.end || l.s[1] == ' ');
19
+ }
20
+
21
+ static void next_line(block_ctx *b) {
22
+ while (b->next < b->end) {
23
+ if ((++b->load->operations & 4095) == 0) rb_thread_check_ints();
24
+ const unsigned char *start = b->next;
25
+ const unsigned char *end = memchr(start, '\n', (size_t)(b->end - start));
26
+ if (!end) end = b->end;
27
+ b->next = end < b->end ? end + 1 : end;
28
+ const unsigned char *s = start;
29
+ while (s < end && *s == ' ') s++;
30
+ while (end > s && end[-1] == ' ') end--;
31
+ if (s == end) continue;
32
+ b->line = (block_line){s, end, (size_t)(s - start)};
33
+ return;
34
+ }
35
+ b->line = (block_line){b->end, b->end, 0};
36
+ }
37
+
38
+ static void after_indicator(block_ctx *b) {
39
+ const unsigned char *start = b->line.s;
40
+ b->line.s++;
41
+ while (b->line.s < b->line.end && *b->line.s == ' ') b->line.s++;
42
+ b->line.indent += (size_t)(b->line.s - start);
43
+ }
44
+
45
+ /* Locate a block map delimiter; quotes are accepted only without escaping. */
46
+ static const unsigned char *map_colon(block_line l) {
47
+ const unsigned char *p = l.s;
48
+ if (p < l.end && (*p == '\'' || *p == '"')) {
49
+ unsigned char quote = *p++;
50
+ while (p < l.end && *p != quote) { if (*p == '\\') return NULL; p++; }
51
+ if (p == l.end) return NULL;
52
+ p++;
53
+ while (p < l.end && *p == ' ') p++;
54
+ return p < l.end && *p == ':' && (p + 1 == l.end || p[1] == ' ') ? p : NULL;
55
+ }
56
+ for (; p < l.end; p++)
57
+ if (*p == ':' && (p + 1 == l.end || p[1] == ' ')) return p;
58
+ return NULL;
59
+ }
60
+
61
+ #if defined(__GNUC__) || defined(__clang__)
62
+ #define RY_UNLIKELY(x) __builtin_expect(!!(x), 0)
63
+ #else
64
+ #define RY_UNLIKELY(x) (x)
65
+ #endif
66
+ static const unsigned char reserved_initial[128] = {
67
+ [','] = 1, ['!'] = 1, ['&'] = 1, ['*'] = 1, ['#'] = 1,
68
+ ['|'] = 1, ['>'] = 1, ['@'] = 1, ['"'] = 1, [39] = 1,
69
+ ['%'] = 1, [96] = 1, ['['] = 1, [']'] = 1, ['{'] = 1, ['}'] = 1
70
+ };
71
+
72
+ static VALUE block_inline(block_ctx *b, const unsigned char *s, const unsigned char *end) {
73
+ while (end > s && end[-1] == ' ') end--;
74
+ size_t n = (size_t)(end - s);
75
+ if (!n) return Qnil;
76
+ if (n == 2 && s[0] == '[' && s[1] == ']') return rb_ary_new();
77
+ if (n == 2 && s[0] == '{' && s[1] == '}') return rb_hash_new();
78
+ if (RY_UNLIKELY(s[0] == '\'' || s[0] == '"')) {
79
+ unsigned char quote = s[0];
80
+ if (n < 2 || end[-1] != quote) { b->failed = 1; return Qnil; }
81
+ for (const unsigned char *p = s + 1; p < end - 1; p++)
82
+ if (*p == quote || (quote == '"' && *p == '\\')) { b->failed = 1; return Qnil; }
83
+ return yaml_string(b->load, s + 1, n - 2);
84
+ }
85
+ /* Fail closed for properties, directives, comments, block/flow scalars,
86
+ * document boundaries, and reserved indicators. */
87
+ if (reserved_initial[s[0]] ||
88
+ ((s[0] == '-' || s[0] == '?' || s[0] == ':') && (n == 1 || s[1] == ' ')) ||
89
+ (n >= 3 && (!memcmp(s, "---", 3) || !memcmp(s, "...", 3)))) {
90
+ b->failed = 1; return Qnil;
91
+ }
92
+ for (const unsigned char *p = s; p < end; p++) {
93
+ if ((*p == '#' && p > s && p[-1] == ' ') ||
94
+ (*p == ':' && (p + 1 == end || p[1] == ' '))) { b->failed = 1; return Qnil; }
95
+ }
96
+ return scalar(b->load, s, n, 1);
97
+ }
98
+
99
+ static int generic_mapping_tag(const unsigned char *s, size_t n) {
100
+ if (n < 2 || s[0] != '!' || s[1] == '!' || s[1] == '<') return 0;
101
+ /* Reserved Ruby and shorthand tags are always delegated. */
102
+ if ((n >= 6 && !memcmp(s, "!ruby/", 6)) || (n >= 5 && !memcmp(s, "!map:", 5)) || (n >= 4 && !memcmp(s, "!str", 4)) ||
103
+ (n == 4 && !memcmp(s, "!set", 4)) || (n == 5 && !memcmp(s, "!omap", 5))) return 0;
104
+ for (size_t i = 1; i < n; i++) {
105
+ unsigned char ch = s[i];
106
+ if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
107
+ (ch >= '0' && ch <= '9') || ch == '_' || ch == ':' || ch == '-' || ch == '/' || ch == '.')) return 0;
108
+ }
109
+ return 1;
110
+ }
111
+
112
+ static VALUE block_node(block_ctx *b);
113
+
114
+ static VALUE block_value(block_ctx *b, const unsigned char *s, size_t parent_indent) {
115
+ while (s < b->line.end && *s == ' ') s++;
116
+ if (s < b->line.end) {
117
+ if (generic_mapping_tag(s, (size_t)(b->line.end - s))) {
118
+ next_line(b);
119
+ if (b->line.s == b->line.end || b->line.indent <= parent_indent) { b->failed = 1; return Qnil; }
120
+ VALUE result = block_node(b);
121
+ if (!RB_TYPE_P(result, T_HASH)) b->failed = 1;
122
+ return result;
123
+ }
124
+ VALUE value = block_inline(b, s, b->line.end);
125
+ next_line(b);
126
+ return value;
127
+ }
128
+ next_line(b);
129
+ if (b->line.s < b->line.end && (b->line.indent > parent_indent ||
130
+ (b->line.indent == parent_indent && indicator(b->line, '-')))) return block_node(b);
131
+ return Qnil;
132
+ }
133
+
134
+ static VALUE block_node(block_ctx *b) {
135
+ load_ctx *c = b->load;
136
+ if (b->failed || c->depth >= 128) { b->failed = 1; return Qnil; }
137
+ block_line first = b->line;
138
+ if (first.s == first.end) return Qnil;
139
+ int sequence = indicator(first, '-');
140
+ if (!sequence && !indicator(first, '?') && !map_colon(first)) {
141
+ VALUE value = block_inline(b, first.s, first.end);
142
+ next_line(b);
143
+ return value;
144
+ }
145
+ VALUE container = sequence ? rb_ary_new() : rb_hash_new();
146
+ rb_ary_push(c->stack, container);
147
+ rb_ary_push(c->stack, Qnil);
148
+ c->wants_value[c->depth++] = 0;
149
+ while (!b->failed && b->line.s < b->line.end && b->line.indent == first.indent) {
150
+ if (b->line.indent == 0 && b->line.end - b->line.s == 3 && !memcmp(b->line.s, "...", 3)) break;
151
+ if (sequence) {
152
+ if (!indicator(b->line, '-')) break;
153
+ after_indicator(b);
154
+ VALUE value;
155
+ if (b->line.s < b->line.end) {
156
+ if (generic_mapping_tag(b->line.s, (size_t)(b->line.end - b->line.s)))
157
+ value = block_value(b, b->line.s, first.indent);
158
+ else
159
+ value = block_node(b);
160
+ } else {
161
+ next_line(b);
162
+ value = b->line.s < b->line.end && b->line.indent > first.indent ? block_node(b) : Qnil;
163
+ }
164
+ rb_ary_push(container, value);
165
+ } else {
166
+ VALUE key, value;
167
+ if (indicator(b->line, '?')) {
168
+ after_indicator(b);
169
+ if (b->line.s < b->line.end) key = block_node(b);
170
+ else {
171
+ next_line(b);
172
+ key = b->line.s < b->line.end && b->line.indent > first.indent ? block_node(b) : Qnil;
173
+ }
174
+ if (b->failed || b->line.indent != first.indent || !indicator(b->line, ':')) { b->failed = 1; break; }
175
+ if (!attach(c, key)) { b->failed = 1; break; }
176
+ value = block_value(b, b->line.s + 1, first.indent);
177
+ } else {
178
+ const unsigned char *colon = map_colon(b->line);
179
+ if (!colon || colon == b->line.s) { b->failed = 1; break; }
180
+ /* YAML limits implicit keys to 1024 Unicode characters. */
181
+ if ((size_t)(colon - b->line.s) > 1024) { b->failed = 1; break; }
182
+ key = block_inline(b, b->line.s, colon);
183
+ if (b->failed || !attach(c, key)) { b->failed = 1; break; }
184
+ value = block_value(b, colon + 1, first.indent);
185
+ }
186
+ if (b->failed || !attach(c, value)) { b->failed = 1; break; }
187
+ }
188
+ }
189
+ c->depth--;
190
+ rb_ary_pop(c->stack);
191
+ rb_ary_pop(c->stack);
192
+ RB_GC_GUARD(container);
193
+ return container;
194
+ }
195
+
196
+ static VALUE block_load(load_ctx *c, size_t length) {
197
+ const unsigned char *end = c->input + length;
198
+ if (!block_ascii(c->input, end)) return unsupported;
199
+ block_ctx b = {c, c->input, end, {NULL, NULL, 0}, 0};
200
+ next_line(&b);
201
+ if (b.line.indent == 0 && b.line.end - b.line.s == 3 && !memcmp(b.line.s, "---", 3)) next_line(&b);
202
+ if (b.line.s == b.line.end) return unsupported;
203
+ VALUE result = block_node(&b);
204
+ if (!b.failed && b.line.indent == 0 && b.line.end - b.line.s == 3 && !memcmp(b.line.s, "...", 3)) next_line(&b);
205
+ if (b.failed || b.line.s != b.line.end) return unsupported;
206
+ return result;
207
+ }
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ require "mkmf"
3
+ if with_config("yaml-dir") || with_config("yaml-include") || with_config("yaml-lib")
4
+ dir_config("yaml")
5
+ elsif !pkg_config("yaml-0.1")
6
+ prefix = ["/opt/homebrew/opt/libyaml", "/usr/local/opt/libyaml"].find { |p| File.file?("#{p}/include/yaml.h") }
7
+ dir_config("yaml", "#{prefix}/include", "#{prefix}/lib") if prefix
8
+ end
9
+ yaml_available = have_header("yaml.h") &&
10
+ (have_func("yaml_parser_initialize", "yaml.h") || have_library("yaml", "yaml_parser_initialize"))
11
+ abort "libyaml headers required (brew install libyaml / apt install libyaml-dev)" unless yaml_available
12
+ $CFLAGS << " -O3" unless $CFLAGS.match?(/(?:^|\s)-O(?:[0-3sgz]|fast)(?:\s|$)/)
13
+ $CFLAGS << " -std=c99 -Wall -Wextra -Wno-unused-parameter"
14
+ $defs << "-DRY_DISABLE_SIMD" unless enable_config("simd", true)
15
+ create_makefile("rapid_yaml/rapid_yaml")
@@ -0,0 +1,225 @@
1
+ #include <ruby.h>
2
+ #include <ruby/encoding.h>
3
+ #include <yaml.h>
4
+ #include <string.h>
5
+
6
+ #define MAX_DEPTH 512
7
+ static VALUE unsupported;
8
+ static ID id_tokenize;
9
+ static rb_encoding *utf8;
10
+
11
+ typedef struct {
12
+ yaml_parser_t parser;
13
+ yaml_event_t event;
14
+ int initialized, live_event, depth, mode;
15
+ size_t length;
16
+ unsigned int operations;
17
+ unsigned char wants_value[MAX_DEPTH];
18
+ unsigned char *input;
19
+ VALUE stack, scanner, result, key_cache;
20
+ } load_ctx;
21
+
22
+ static int attach(load_ctx *c, VALUE value) {
23
+ if (!c->depth) { c->result = value; return 1; }
24
+ int d = c->depth - 1;
25
+ VALUE parent = rb_ary_entry(c->stack, d * 2);
26
+ if (RB_TYPE_P(parent, T_ARRAY)) {
27
+ rb_ary_push(parent, value);
28
+ } else if (!c->wants_value[d]) {
29
+ if (RB_TYPE_P(value, T_STRING) && RSTRING_LEN(value) == 2 && !memcmp(RSTRING_PTR(value), "<<", 2)) return 0;
30
+ rb_ary_store(c->stack, d * 2 + 1, value);
31
+ c->wants_value[d] = 1;
32
+ } else {
33
+ rb_hash_aset(parent, rb_ary_entry(c->stack, d * 2 + 1), value);
34
+ /* The next key overwrites this slot; no write barrier is needed to clear it. */
35
+ c->wants_value[d] = 0;
36
+ }
37
+ return 1;
38
+ }
39
+
40
+ static int ascii_equal_ci(const unsigned char *s, const char *word, size_t n) {
41
+ for (size_t i = 0; i < n; i++) if ((s[i] | 32) != (unsigned char)word[i]) return 0;
42
+ return 1;
43
+ }
44
+
45
+ static VALUE yaml_string(load_ctx *c, const unsigned char *s, size_t n) {
46
+ if (c->depth && !c->wants_value[c->depth - 1] && RB_TYPE_P(rb_ary_entry(c->stack, (c->depth - 1) * 2), T_HASH)) {
47
+ if (n && n <= 64) {
48
+ unsigned int slot = (s[0] * 3 + s[n - 1] * 17 + n * 7) & 127;
49
+ VALUE key = rb_ary_entry(c->key_cache, slot);
50
+ if (!NIL_P(key) && (size_t)RSTRING_LEN(key) == n && !memcmp(RSTRING_PTR(key), s, n)) return key;
51
+ key = rb_enc_interned_str((const char *)s, (long)n, utf8);
52
+ rb_ary_store(c->key_cache, slot, key);
53
+ return key;
54
+ }
55
+ return rb_enc_interned_str((const char *)s, (long)n, utf8);
56
+ }
57
+ return rb_utf8_str_new((const char *)s, (long)n);
58
+ }
59
+
60
+ /* Reject impossible numeric candidates before invoking Psych's regex scanner.
61
+ * This is a superset recognizer, not an alternative scalar schema: timestamps,
62
+ * radix numbers, symbols and all ambiguous candidates still use Psych. */
63
+ static int possibly_typed(const unsigned char *s, size_t n) {
64
+ size_t start = (n && (s[0] == '-' || s[0] == '+')) ? 1 : 0;
65
+ if (!n || s[0] == ':' || start == n) return 1;
66
+ if (n > start + 4 && s[start + 4] == '-') return 1;
67
+ if (n > start + 1 && s[start] == '0' && (s[start + 1] == 'x' || s[start + 1] == 'b')) return 1;
68
+ if (s[start] == '.' && n - start == 4) return 1;
69
+ unsigned int dots = 0, colons = 0, exponents = 0;
70
+ for (size_t i = start; i < n; i++) {
71
+ unsigned char ch = s[i];
72
+ if ((ch >= '0' && ch <= '9') || ch == ',' || ch == '_') continue;
73
+ if (ch == '.' && ++dots == 1) continue;
74
+ if (ch == ':' && ++colons <= 2) continue;
75
+ if ((ch == 'e' || ch == 'E') && ++exponents == 1) continue;
76
+ if ((ch == '-' || ch == '+') && i > start && (s[i-1] == 'e' || s[i-1] == 'E')) continue;
77
+ return 0;
78
+ }
79
+ return 1;
80
+ }
81
+
82
+ /* Plain shortcuts require single-line ASCII; callers must establish this. */
83
+ static VALUE scalar(load_ctx *c, const unsigned char *s, size_t n, int plain) {
84
+ if (plain) {
85
+ if (!n || (n == 1 && s[0] == '~')) return Qnil;
86
+ unsigned char first = s[0] | 32;
87
+ if ((first >= 'a' && first <= 'z') || s[0] == '_') {
88
+ if ((n == 4 && ascii_equal_ci(s, "null", 4))) return Qnil;
89
+ if ((n == 4 && ascii_equal_ci(s, "true", 4)) || (n == 3 && ascii_equal_ci(s, "yes", 3)) || (n == 2 && ascii_equal_ci(s, "on", 2))) return Qtrue;
90
+ if ((n == 5 && ascii_equal_ci(s, "false", 5)) || (n == 2 && ascii_equal_ci(s, "no", 2)) || (n == 3 && ascii_equal_ci(s, "off", 3))) return Qfalse;
91
+ return yaml_string(c, s, n);
92
+ }
93
+ size_t i = (s[0] == '-' || s[0] == '+') ? 1 : 0;
94
+ if (i < n && n - i <= 18 && (s[i] != '0' || n - i == 1)) {
95
+ unsigned long long number = 0;
96
+ for (; i < n && s[i] >= '0' && s[i] <= '9'; i++) number = number * 10 + s[i] - '0';
97
+ if (i == n) return s[0] == '-' ? LL2NUM(-(long long)number) : ULL2NUM(number);
98
+ }
99
+ }
100
+ VALUE string = yaml_string(c, s, n);
101
+ return plain && possibly_typed(s, n) ? rb_funcall(c->scanner, id_tokenize, 1, string) : string;
102
+ }
103
+
104
+ #include "block.h"
105
+
106
+ static VALUE event_scalar(load_ctx *c, yaml_event_t *e) {
107
+ const unsigned char *s = e->data.scalar.value;
108
+ size_t n = e->data.scalar.length;
109
+ int plain = e->data.scalar.style == YAML_PLAIN_SCALAR_STYLE;
110
+ if (plain) {
111
+ /* Psych uses Unicode case folding and line-anchored scalar regexes.
112
+ * Preserve their long-s/ligature and multiline behavior. The block
113
+ * tier already proves single-line ASCII; this tier must check. */
114
+ for (size_t i = 0; i < n; i++) {
115
+ if (s[i] >= 128 || s[i] == '\n' || s[i] == '\r')
116
+ return rb_funcall(c->scanner, id_tokenize, 1, yaml_string(c, s, n));
117
+ }
118
+ }
119
+ return scalar(c, s, n, plain);
120
+ }
121
+
122
+ static VALUE load_body(VALUE arg) {
123
+ load_ctx *c = (load_ctx *)arg;
124
+ if (c->mode != 1) {
125
+ VALUE fast = block_load(c, c->length);
126
+ if (fast != unsupported || c->mode == 2) return fast;
127
+ rb_ary_clear(c->stack);
128
+ c->depth = 0;
129
+ }
130
+ if (!yaml_parser_initialize(&c->parser)) rb_memerror();
131
+ c->initialized = 1;
132
+ /* Psych treats UTF-8/ASCII/binary Strings as UTF-8, unlike binary IO. */
133
+ yaml_parser_set_encoding(&c->parser, YAML_UTF8_ENCODING);
134
+ yaml_parser_set_input_string(&c->parser, c->input, c->length);
135
+ for (;;) {
136
+ if ((++c->operations & 4095) == 0) rb_thread_check_ints();
137
+ if (!yaml_parser_parse(&c->parser, &c->event)) return unsupported;
138
+ c->live_event = 1;
139
+ yaml_event_t *e = &c->event;
140
+ VALUE value;
141
+ switch (e->type) {
142
+ case YAML_STREAM_START_EVENT: case YAML_DOCUMENT_START_EVENT: break;
143
+ case YAML_STREAM_END_EVENT: return unsupported; /* empty stream: use Psych's fallback */
144
+ case YAML_DOCUMENT_END_EVENT: return c->result;
145
+ case YAML_ALIAS_EVENT: return unsupported;
146
+ case YAML_SCALAR_EVENT:
147
+ if (e->data.scalar.anchor || e->data.scalar.tag) return unsupported;
148
+ value = event_scalar(c, e);
149
+ if (!attach(c, value)) return unsupported;
150
+ break;
151
+ case YAML_SEQUENCE_START_EVENT: case YAML_MAPPING_START_EVENT:
152
+ if (c->depth == MAX_DEPTH) return unsupported;
153
+ if (e->type == YAML_SEQUENCE_START_EVENT) {
154
+ if (e->data.sequence_start.anchor || e->data.sequence_start.tag) return unsupported;
155
+ value = rb_ary_new();
156
+ } else {
157
+ if (e->data.mapping_start.anchor) return unsupported;
158
+ const char *tag = (const char *)e->data.mapping_start.tag;
159
+ /* Psych treats unregistered, non-special mapping tags as hashes.
160
+ * !str is a special Ruby string shorthand, not a generic tag. */
161
+ if (tag && (strstr(tag, "ruby/") || !strncmp(tag, "!map:", 5) || !strncmp(tag, "!str", 4) ||
162
+ !strncmp(tag, "!set", 4) || !strncmp(tag, "!omap", 5) ||
163
+ !strncmp(tag, "tag:yaml.org,2002:", 18))) return unsupported;
164
+ value = rb_hash_new();
165
+ }
166
+ rb_ary_push(c->stack, value);
167
+ rb_ary_push(c->stack, Qnil);
168
+ c->wants_value[c->depth++] = 0;
169
+ break;
170
+ case YAML_SEQUENCE_END_EVENT: case YAML_MAPPING_END_EVENT:
171
+ c->depth--;
172
+ rb_ary_pop(c->stack);
173
+ value = rb_ary_pop(c->stack);
174
+ if (!attach(c, value)) return unsupported;
175
+ break;
176
+ default: return unsupported;
177
+ }
178
+ yaml_event_delete(&c->event);
179
+ c->live_event = 0;
180
+ }
181
+ }
182
+
183
+ static VALUE load_cleanup(VALUE arg) {
184
+ load_ctx *c = (load_ctx *)arg;
185
+ if (c->live_event) yaml_event_delete(&c->event);
186
+ if (c->initialized) yaml_parser_delete(&c->parser);
187
+ xfree(c->input);
188
+ return Qnil;
189
+ }
190
+
191
+ static VALUE native_load_impl(VALUE source, VALUE scanner, int mode) {
192
+ Check_Type(source, T_STRING);
193
+ load_ctx c = {0};
194
+ c.stack = rb_ary_new();
195
+ c.key_cache = rb_ary_new_capa(128);
196
+ c.scanner = scanner;
197
+ c.result = Qnil;
198
+ size_t length = (size_t)RSTRING_LEN(source);
199
+ c.length = length;
200
+ c.mode = mode;
201
+ c.input = ALLOC_N(unsigned char, length ? length : 1);
202
+ memcpy(c.input, RSTRING_PTR(source), length);
203
+ VALUE result = rb_ensure(load_body, (VALUE)&c, load_cleanup, (VALUE)&c);
204
+ RB_GC_GUARD(c.stack);
205
+ RB_GC_GUARD(c.key_cache);
206
+ RB_GC_GUARD(c.scanner);
207
+ RB_GC_GUARD(c.result);
208
+ return result;
209
+ }
210
+
211
+ static VALUE native_load(VALUE self, VALUE source, VALUE scanner) { return native_load_impl(source, scanner, 0); }
212
+ static VALUE native_events_load(VALUE self, VALUE source, VALUE scanner) { return native_load_impl(source, scanner, 1); }
213
+ static VALUE native_block_load(VALUE self, VALUE source, VALUE scanner) { return native_load_impl(source, scanner, 2); }
214
+
215
+ void Init_rapid_yaml(void) {
216
+ VALUE mod = rb_define_module("RapidYAML");
217
+ unsupported = rb_obj_freeze(rb_obj_alloc(rb_cObject));
218
+ rb_define_const(mod, "UNSUPPORTED", unsupported);
219
+ rb_global_variable(&unsupported);
220
+ id_tokenize = rb_intern("tokenize");
221
+ utf8 = rb_utf8_encoding();
222
+ rb_define_singleton_method(mod, "native_load", native_load, 2);
223
+ rb_define_singleton_method(mod, "native_events_load", native_events_load, 2);
224
+ rb_define_singleton_method(mod, "native_block_load", native_block_load, 2);
225
+ }
@@ -0,0 +1,29 @@
1
+ /* Bounded unaligned vector loads; no padding, aliasing, or endian assumptions.
2
+ * Advanced SIMD is baseline on AArch64, SSE2 on x86-64. Other CPUs use C. */
3
+ #if !defined(RY_DISABLE_SIMD) && defined(__aarch64__)
4
+ #include <arm_neon.h>
5
+ #elif !defined(RY_DISABLE_SIMD) && defined(__SSE2__)
6
+ #include <emmintrin.h>
7
+ #endif
8
+
9
+ static int block_ascii(const unsigned char *p, const unsigned char *end) {
10
+ #if !defined(RY_DISABLE_SIMD) && defined(__aarch64__)
11
+ while (end - p >= 16) {
12
+ uint8x16_t v = vld1q_u8(p);
13
+ uint8x16_t valid = vorrq_u8(vceqq_u8(v, vdupq_n_u8(10)),
14
+ vandq_u8(vcgeq_u8(v, vdupq_n_u8(32)), vcleq_u8(v, vdupq_n_u8(126))));
15
+ if (vminvq_u8(valid) != 255) return 0;
16
+ p += 16;
17
+ }
18
+ #elif !defined(RY_DISABLE_SIMD) && defined(__SSE2__)
19
+ while (end - p >= 16) {
20
+ __m128i v = _mm_loadu_si128((const __m128i *)p);
21
+ __m128i valid = _mm_or_si128(_mm_cmpeq_epi8(v, _mm_set1_epi8(10)),
22
+ _mm_andnot_si128(_mm_cmpgt_epi8(v, _mm_set1_epi8(126)), _mm_cmpgt_epi8(v, _mm_set1_epi8(31))));
23
+ if (_mm_movemask_epi8(valid) != 65535) return 0;
24
+ p += 16;
25
+ }
26
+ #endif
27
+ for (; p < end; p++) if ((*p < 32 && *p != 10) || *p > 126) return 0;
28
+ return 1;
29
+ }
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module RapidYAML
3
+ VERSION = "0.1.3"
4
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ require "rapid_yaml"
3
+ RapidYAML.install!
data/lib/rapid_yaml.rb ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+ require "yaml"
3
+ require "forwardable"
4
+ require_relative "rapid_yaml/version"
5
+ require "rapid_yaml/rapid_yaml"
6
+
7
+ # Requiring rapid_yaml does not patch Psych. See rapid_yaml/yaml for opt-in.
8
+ module RapidYAML
9
+ PSYCH_METHODS = Psych.singleton_methods(false).to_h { |name| [name, Psych.method(name)] }.freeze
10
+ FAST_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::ASCII_8BIT].freeze
11
+ NATIVE_LOADER = ENV["RAPID_YAML_ENGINE"] == "events" ? :native_events_load : :native_load
12
+ ACCELERATED_METHODS = %i[load safe_load unsafe_load load_file safe_load_file unsafe_load_file].freeze
13
+ MAX_FILE_BYTES = 16 * 1024 * 1024
14
+ LOAD_KEYWORDS = %i[aliases filename fallback].freeze
15
+ UNSAFE_KEYWORDS = %i[filename fallback].freeze
16
+ private_constant :PSYCH_METHODS, :FAST_ENCODINGS, :NATIVE_LOADER, :ACCELERATED_METHODS, :MAX_FILE_BYTES, :LOAD_KEYWORDS, :UNSAFE_KEYWORDS
17
+
18
+ # Real method definitions, rather than captured Proc bodies, can run in Ractors.
19
+ extend SingleForwardable
20
+ def_single_delegators "::Psych", *(PSYCH_METHODS.keys - ACCELERATED_METHODS)
21
+
22
+ def self.secondary_ractor?
23
+ defined?(Ractor) && Ractor.current != Ractor.main
24
+ end
25
+
26
+ def self.load(source, **options)
27
+ return Psych.load(source, **options) if secondary_ractor?
28
+ accelerated_load(:load, source, options)
29
+ end
30
+
31
+ def self.safe_load(source, **options)
32
+ return Psych.safe_load(source, **options) if secondary_ractor?
33
+ accelerated_load(:safe_load, source, options)
34
+ end
35
+
36
+ def self.unsafe_load(source, **options)
37
+ return Psych.unsafe_load(source, **options) if secondary_ractor?
38
+ accelerated_load(:unsafe_load, source, options)
39
+ end
40
+
41
+ def self.load_file(filename, **options)
42
+ return Psych.load_file(filename, **options) if secondary_ractor?
43
+ accelerated_file(:load, filename, options)
44
+ end
45
+
46
+ def self.safe_load_file(filename, **options)
47
+ return Psych.safe_load_file(filename, **options) if secondary_ractor?
48
+ accelerated_file(:safe_load, filename, options)
49
+ end
50
+
51
+ def self.unsafe_load_file(filename, **options)
52
+ return Psych.unsafe_load_file(filename, **options) if secondary_ractor?
53
+ accelerated_file(:unsafe_load, filename, options)
54
+ end
55
+
56
+ def self.accelerated_load(name, source, options)
57
+ original = PSYCH_METHODS.fetch(name)
58
+ fast_keywords = name == :unsafe_load ? UNSAFE_KEYWORDS : LOAD_KEYWORDS
59
+ tags = Psych.load_tags
60
+ unless source.instance_of?(String) && FAST_ENCODINGS.include?(source.encoding) &&
61
+ (options.keys - fast_keywords).empty? &&
62
+ (options[:filename].nil? || options[:filename].instance_of?(String)) &&
63
+ tags.empty? && tags.default.nil? && tags.default_proc.nil? && Psych.domain_types.empty?
64
+ return original.call(source, **options)
65
+ end
66
+ loader = name == :unsafe_load ? Psych::ClassLoader.new : Psych::ClassLoader::Restricted.new(name == :load ? ["Symbol"] : [], [])
67
+ begin
68
+ value = public_send(NATIVE_LOADER, source, Psych::ScalarScanner.new(loader))
69
+ rescue StandardError
70
+ # Psych parses before resolving scalars: retain its error precedence.
71
+ return original.call(source, **options)
72
+ end
73
+ value.equal?(UNSUPPORTED) ? original.call(source, **options) : value
74
+ end
75
+ private_class_method :accelerated_load
76
+
77
+ def self.accelerated_file(name, filename, options)
78
+ File.open(filename, "r:bom|utf-8") do |file|
79
+ stat = file.stat
80
+ source = if stat.file? && stat.size.positive? && stat.size <= MAX_FILE_BYTES && Encoding.default_internal.nil?
81
+ # Bound reads of changing files; preserve the BOM-selected encoding.
82
+ (file.read(stat.size) || "").force_encoding(file.external_encoding)
83
+ else
84
+ # Preserve incremental IO and transcoding for devices and large files.
85
+ file
86
+ end
87
+ public_send(name, source, filename: filename, **options)
88
+ end
89
+ end
90
+ private_class_method :accelerated_file
91
+
92
+ def self.const_missing(name)
93
+ Psych.const_get(name)
94
+ end
95
+
96
+ # Explicit, process-wide and idempotent; call during application boot.
97
+ def self.install!
98
+ Psych.singleton_class.prepend(YAMLAcceleration) unless Psych.singleton_class < YAMLAcceleration
99
+ self
100
+ end
101
+
102
+ module YAMLAcceleration
103
+ # Do not enter Proc-backed methods or read captured Method objects outside
104
+ # the main Ractor. `super` retains Psych's own Ractor-local configuration.
105
+ def load(...)
106
+ return super if RapidYAML.secondary_ractor?
107
+ RapidYAML.load(...)
108
+ end
109
+
110
+ def safe_load(...)
111
+ return super if RapidYAML.secondary_ractor?
112
+ RapidYAML.safe_load(...)
113
+ end
114
+
115
+ def unsafe_load(...)
116
+ return super if RapidYAML.secondary_ractor?
117
+ RapidYAML.unsafe_load(...)
118
+ end
119
+
120
+ def load_file(...)
121
+ return super if RapidYAML.secondary_ractor?
122
+ RapidYAML.load_file(...)
123
+ end
124
+
125
+ def safe_load_file(...)
126
+ return super if RapidYAML.secondary_ractor?
127
+ RapidYAML.safe_load_file(...)
128
+ end
129
+
130
+ def unsafe_load_file(...)
131
+ return super if RapidYAML.secondary_ractor?
132
+ RapidYAML.unsafe_load_file(...)
133
+ end
134
+ end
135
+ private_constant :YAMLAcceleration
136
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: very_fast_yaml
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Peter Arato
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: psych
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '4.0'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '6'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '4.0'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '6'
32
+ - !ruby/object:Gem::Dependency
33
+ name: forwardable
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '1.3'
39
+ - - "<"
40
+ - !ruby/object:Gem::Version
41
+ version: '2'
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '1.3'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: '2'
52
+ executables: []
53
+ extensions:
54
+ - ext/rapid_yaml/extconf.rb
55
+ extra_rdoc_files: []
56
+ files:
57
+ - LICENSE
58
+ - ext/rapid_yaml/block.h
59
+ - ext/rapid_yaml/extconf.rb
60
+ - ext/rapid_yaml/rapid_yaml.c
61
+ - ext/rapid_yaml/scan.h
62
+ - lib/rapid_yaml.rb
63
+ - lib/rapid_yaml/version.rb
64
+ - lib/rapid_yaml/yaml.rb
65
+ licenses:
66
+ - MIT
67
+ metadata: {}
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.1'
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.6.9
83
+ specification_version: 4
84
+ summary: Native YAML loading with a Psych-compatible Ruby interface
85
+ test_files: []