Module: Dommy::Js::Quickjs::SourceGuard
- Defined in:
- lib/dommy/js/quickjs/source_guard.rb
Overview
Source-level workaround for a QuickJS bytecode-generation bug: a ‘for…of` whose ITERABLE expression contains a `yield` fails to COMPILE with an internal “stack underflow” error (the for-of iterator-close finally region miscomputes the operand stack across the generator suspend). V8 / real browsers compile it fine, so SPA bundles ship it — e.g. note.com’s modern build has ‘for (var f of (yield O(), _)) v(f)`, and the whole code-split chunk fails to load, so the app never mounts.
The fix hoists the iterable into a temp ‘var` so the `yield` leaves the for-of operand position — `for (x of (yield a, b)) …` becomes `var t = (yield a, b); for (x of t) …` — semantically identical, and it compiles. Applied ONLY as a retry after a genuine “stack underflow” compile failure (see Backend), so working scripts are never rewritten and an imperfect transform cannot regress anything (the source already failed).
Defined Under Namespace
Classes: Rewriter
Constant Summary collapse
- ERROR_MARKER =
The marker QuickJS raises for this codegen bug.
"stack underflow"- IDENT =
/[A-Za-z0-9_$]/.freeze
Class Method Summary collapse
-
.atom_end(src, i, prev_sig) ⇒ Object
If ‘src` begins a string, template, regex literal, or comment, return the index just after it; otherwise nil.
- .contains_yield?(expr) ⇒ Boolean
-
.fix_for_of_yield(source) ⇒ Object
Rewrite every ‘for…of` whose iterable contains a `yield`, hoisting the iterable into a preceding `var`.
- .ident_char?(ch) ⇒ Boolean
-
.match_bracket(src, open, limit: nil) ⇒ Object
Index of the close bracket matching the (/[/{ at ‘open`, skipping nested brackets, strings, templates, regexes and comments. `limit` bounds the scan (a for-head is short; bounding keeps the whole pass linear and avoids a runaway scan when a mis-lexed token unbalances brackets)..
-
.regex_allowed?(prev_sig) ⇒ Boolean
A ‘/` begins a regex (not division) unless the previous significant code char can end an expression (ident / `)` / `]` / a literal).
- .relevant_error?(error) ⇒ Boolean
- .scan_quote(src, i, quote) ⇒ Object
-
.scan_regex(src, i) ⇒ Object
End of a regex literal at ‘i` (past its flags), or nil if it isn’t one (an unescaped newline before the closing ‘/` → it was division).
- .scan_template(src, i) ⇒ Object
-
.top_level_of(head) ⇒ Object
Position of the for-of ‘of` keyword at the top level of a for-head, or nil for a for-in / C-style for (a top-level `;` rules out for-of).
Class Method Details
.atom_end(src, i, prev_sig) ⇒ Object
If ‘src` begins a string, template, regex literal, or comment, return the index just after it; otherwise nil. `prev_sig` (previous significant code char) disambiguates a regex `/` from division.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 62 def atom_end(src, i, prev_sig) case src[i] when "/" if src[i + 1] == "/" src.index("\n", i) || src.length elsif src[i + 1] == "*" (idx = src.index("*/", i + 2)) ? idx + 2 : src.length elsif regex_allowed?(prev_sig) scan_regex(src, i) end when "'", '"' scan_quote(src, i, src[i]) when "`" scan_template(src, i) end end |
.contains_yield?(expr) ⇒ Boolean
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 202 def contains_yield?(expr) i = 0 n = expr.length prev = nil while i < n stop = atom_end(expr, i, prev) if stop prev = expr[stop - 1] i = stop next end if expr[i] == "y" && expr[i, 5] == "yield" && !ident_char?(i.zero? ? nil : expr[i - 1]) && !ident_char?(expr[i + 5]) return true end prev = expr[i] unless expr[i] =~ /\s/ i += 1 end false end |
.fix_for_of_yield(source) ⇒ Object
Rewrite every ‘for…of` whose iterable contains a `yield`, hoisting the iterable into a preceding `var`. Returns the source unchanged when there is nothing to do.
33 34 35 36 37 38 39 40 41 42 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 33 def fix_for_of_yield(source) return source unless source.include?("yield") # Scan at the BYTE level (binary encoding): on a multibyte (UTF-8) source # — note's bundle has Japanese strings — Ruby's character indexing is # O(index), making a char-by-char pass O(n^2). All tokens we look for are # ASCII, and UTF-8 continuation bytes (>= 0x80) never collide with them, # so byte scanning is both safe and O(1) per position. Rewriter.new(source.b).run.force_encoding(source.encoding) end |
.ident_char?(ch) ⇒ Boolean
46 47 48 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 46 def ident_char?(ch) !ch.nil? && IDENT.match?(ch) end |
.match_bracket(src, open, limit: nil) ⇒ Object
Index of the close bracket matching the (/[/{ at ‘open`, skipping nested brackets, strings, templates, regexes and comments. `limit` bounds the scan (a for-head is short; bounding keeps the whole pass linear and avoids a runaway scan when a mis-lexed token unbalances brackets).
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 143 def match_bracket(src, open, limit: nil) pairs = {"(" => ")", "[" => "]", "{" => "}"} want = [pairs[src[open]]] i = open + 1 n = src.length n = [n, open + limit].min if limit prev = src[open] while i < n stop = atom_end(src, i, prev) if stop prev = src[stop - 1] i = stop next end c = src[i] if pairs.key?(c) want << pairs[c] elsif ")]}".include?(c) return i if want.size == 1 && want.last == c want.pop if want.last == c end prev = c unless c =~ /\s/ i += 1 end nil end |
.regex_allowed?(prev_sig) ⇒ Boolean
A ‘/` begins a regex (not division) unless the previous significant code char can end an expression (ident / `)` / `]` / a literal).
52 53 54 55 56 57 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 52 def regex_allowed?(prev_sig) return true if prev_sig.nil? return false if ident_char?(prev_sig) !")]'\"`".include?(prev_sig) end |
.relevant_error?(error) ⇒ Boolean
26 27 28 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 26 def relevant_error?(error) error.respond_to?(:message) && error..to_s.include?(ERROR_MARKER) end |
.scan_quote(src, i, quote) ⇒ Object
79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 79 def scan_quote(src, i, quote) j = i + 1 n = src.length while j < n c = src[j] return j + 1 if c == quote j += c == "\\" ? 2 : 1 end n end |
.scan_regex(src, i) ⇒ Object
End of a regex literal at ‘i` (past its flags), or nil if it isn’t one (an unescaped newline before the closing ‘/` → it was division).
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 115 def scan_regex(src, i) j = i + 1 n = src.length in_class = false while j < n c = src[j] case c when "\\" then j += 2; next when "[" then in_class = true when "]" then in_class = false when "/" unless in_class j += 1 j += 1 while j < n && src[j] =~ /[a-z]/i return j end when "\n" return nil end j += 1 end nil end |
.scan_template(src, i) ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 91 def scan_template(src, i) j = i + 1 n = src.length while j < n c = src[j] return j + 1 if c == "`" if c == "\\" j += 2 next end if c == "$" && src[j + 1] == "{" close = match_bracket(src, j + 1) return n unless close j = close + 1 next end j += 1 end n end |
.top_level_of(head) ⇒ Object
Position of the for-of ‘of` keyword at the top level of a for-head, or nil for a for-in / C-style for (a top-level `;` rules out for-of).
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/dommy/js/quickjs/source_guard.rb', line 173 def top_level_of(head) depth = 0 i = 0 n = head.length prev = nil while i < n stop = atom_end(head, i, prev) if stop prev = head[stop - 1] i = stop next end c = head[i] case c when "(", "[", "{" then depth += 1 when ")", "]", "}" then depth -= 1 when ";" then return nil if depth.zero? when "o" if depth.zero? && head[i, 2] == "of" && !ident_char?(i.zero? ? nil : head[i - 1]) && !ident_char?(head[i + 2]) return i end end prev = c unless c =~ /\s/ i += 1 end nil end |