Module: Dms::Tier1
- Defined in:
- lib/dms/tier1.rb
Defined Under Namespace
Classes: DecoratorCall, DecoratorEntry, DocumentT1, ImportSpec, InlineValueParser, ParamGroup, SingleValueParser, T1Parser
Constant Summary collapse
- RESERVED_SIGIL_CHARS =
Reserved decorator sigil characters (tier-0 set, no underscore). ! @ $ % ^ & * | ~ ‘ . , > < ? ; =
"!@$%^&*|~`.,><?;=".chars.to_set.freeze
- EXTENDED_PICTOGRAPHIC_RANGES =
Extended_Pictographic=Yes ranges (frozen UCD 15.1), sourced from Rust ref.
[ [0x00A9, 0x00A9], [0x00AE, 0x00AE], [0x203C, 0x203C], [0x2049, 0x2049], [0x2122, 0x2122], [0x2139, 0x2139], [0x2194, 0x2199], [0x21A9, 0x21AA], [0x231A, 0x231B], [0x2328, 0x2328], [0x2388, 0x2388], [0x23CF, 0x23CF], [0x23E9, 0x23F3], [0x23F8, 0x23FA], [0x24C2, 0x24C2], [0x25AA, 0x25AB], [0x25B6, 0x25B6], [0x25C0, 0x25C0], [0x25FB, 0x25FE], [0x2600, 0x2605], [0x2607, 0x2612], [0x2614, 0x2685], [0x2690, 0x2705], [0x2708, 0x2712], [0x2714, 0x2714], [0x2716, 0x2716], [0x271D, 0x271D], [0x2721, 0x2721], [0x2728, 0x2728], [0x2733, 0x2734], [0x2744, 0x2744], [0x2747, 0x2747], [0x274C, 0x274C], [0x274E, 0x274E], [0x2753, 0x2755], [0x2757, 0x2757], [0x2763, 0x2767], [0x2795, 0x2797], [0x27A1, 0x27A1], [0x27B0, 0x27B0], [0x27BF, 0x27BF], [0x2934, 0x2935], [0x2B05, 0x2B07], [0x2B1B, 0x2B1C], [0x2B50, 0x2B50], [0x2B55, 0x2B55], [0x3030, 0x3030], [0x303D, 0x303D], [0x3297, 0x3297], [0x3299, 0x3299], [0x1F000, 0x1F0FF], [0x1F10D, 0x1F10F], [0x1F12F, 0x1F12F], [0x1F16C, 0x1F171], [0x1F17E, 0x1F17F], [0x1F18E, 0x1F18E], [0x1F191, 0x1F19A], [0x1F1AD, 0x1F1E5], [0x1F201, 0x1F20F], [0x1F21A, 0x1F21A], [0x1F22F, 0x1F22F], [0x1F232, 0x1F23A], [0x1F23C, 0x1F23F], [0x1F249, 0x1F3FA], [0x1F400, 0x1F53D], [0x1F546, 0x1F64F], [0x1F680, 0x1F6FF], [0x1F774, 0x1F77F], [0x1F7D5, 0x1F7FF], [0x1F80C, 0x1F80F], [0x1F848, 0x1F84F], [0x1F85A, 0x1F85F], [0x1F888, 0x1F88F], [0x1F8AE, 0x1F8FF], [0x1F90C, 0x1F93A], [0x1F93C, 0x1F945], [0x1F947, 0x1FAFF], [0x1FC00, 0x1FFFD], ].freeze
- RANGE_SPECIFIER_PREFIXES =
── Semver helpers ──────────────────────────────────────────────────────
%w[^ ~ >= > < <= =].freeze
Class Method Summary collapse
- .call_to_json(call, tag_fn) ⇒ Object
-
.emit_t1_json(doc_t1, tag_fn) ⇒ Object
── JSON emission ────────────────────────────────────────────────────────.
- .emoji_modifier?(cp) ⇒ Boolean
- .entry_to_json(entry, tag_fn) ⇒ Object
- .extended_pictographic?(cp) ⇒ Boolean
-
.extract_imports(meta) ⇒ Object
── Import extraction and validation ────────────────────────────────────.
- .extract_string_list(val, idx, field, family) ⇒ Object
- .has_range_specifier?(s) ⇒ Boolean
- .import_to_json(imp) ⇒ Object
-
.lex_sigil_atom_at(s, pos) ⇒ Object
Lex one sigil atom at byte offset ‘pos` in UTF-8 string `s`.
- .param_group_to_json(pg, tag_fn) ⇒ Object
-
.parse(src) ⇒ Object
Parse a DMS source string in tier-1 mode.
-
.read_reserved_emoji_atom(s, start) ⇒ Object
Read one extended grapheme cluster of reserved-emoji shape starting at byte offset ‘start` in `s` (UTF-8 string).
- .regional_indicator?(cp) ⇒ Boolean
- .reserved_emoji_codepoint?(cp) ⇒ Boolean
-
.resolve_family(sigil, fn_name, ns, imports) ⇒ Object
Resolve a decorator call’s family from imports.
-
.sigil_atom_start_at?(s, pos) ⇒ Boolean
Returns true if the character at byte offset ‘pos` in UTF-8 string `s` starts a sigil atom (ASCII reserved char OR reserved emoji codepoint).
- .valid_semver?(s) ⇒ Boolean
-
.validate_sigil_atoms(sigil, idx) ⇒ Object
Validate that a sigil string consists of valid sigil atoms only.
Class Method Details
.call_to_json(call, tag_fn) ⇒ Object
1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 |
# File 'lib/dms/tier1.rb', line 1727 def self.call_to_json(call, tag_fn) params_json = call.params.map { |pg| param_group_to_json(pg, tag_fn) } { "family" => call.family, "fn" => call.fn_name, "ns" => call.ns, "position" => call.position.to_s, "params" => params_json, "params_dec" => [] } end |
.emit_t1_json(doc_t1, tag_fn) ⇒ Object
── JSON emission ────────────────────────────────────────────────────────
1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 |
# File 'lib/dms/tier1.rb', line 1667 def self.emit_t1_json(doc_t1, tag_fn) imports_json = doc_t1.imports.map { |imp| import_to_json(imp) } body_tagged = tag_fn.call(doc_t1.t0.body) decorators_json = doc_t1.decorators.map { |entry| entry_to_json(entry, tag_fn) } { "tier" => doc_t1.observed_tier, "imports" => imports_json, "body" => body_tagged, "decorators" => decorators_json } end |
.emoji_modifier?(cp) ⇒ Boolean
75 76 77 |
# File 'lib/dms/tier1.rb', line 75 def self.emoji_modifier?(cp) cp >= 0x1F3FB && cp <= 0x1F3FF end |
.entry_to_json(entry, tag_fn) ⇒ Object
1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 |
# File 'lib/dms/tier1.rb', line 1706 def self.entry_to_json(entry, tag_fn) path_json = entry.path.map do |seg| if seg.key?("key") { "key" => seg["key"] } else { "index" => seg["index"] } end end calls_json = {} entry.calls.each do |sigil, calls| calls_json[sigil] = calls.map { |c| call_to_json(c, tag_fn) } end { "path" => path_json, "calls" => calls_json, "comments" => [] } end |
.extended_pictographic?(cp) ⇒ Boolean
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/dms/tier1.rb', line 46 def self.extended_pictographic?(cp) return false if cp < 0xA9 lo = 0 hi = EXTENDED_PICTOGRAPHIC_RANGES.length - 1 while lo <= hi mid = (lo + hi) / 2 range_lo, range_hi = EXTENDED_PICTOGRAPHIC_RANGES[mid] if cp < range_lo hi = mid - 1 elsif cp > range_hi lo = mid + 1 else return true end end false end |
.extract_imports(meta) ⇒ Object
── Import extraction and validation ────────────────────────────────────
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 |
# File 'lib/dms/tier1.rb', line 276 def self.extract_imports() raw_list = ["_dms_imports"] return [] if raw_list.nil? unless raw_list.is_a?(Array) raise DecodeError.new(0, 0, "_dms_imports must be a list") end specs = [] # Seen (sigil, ns_repr, family) triples for collision detection seen_triples = {} raw_list.each_with_index do |entry, idx| unless entry.is_a?(Hash) raise DecodeError.new(0, 0, "_dms_imports[#{idx}] must be a table") end # dialect (required string) dialect = entry["dialect"] case dialect when nil raise DecodeError.new(0, 0, "_dms_imports[#{idx}] is missing required field 'dialect'") when String raise DecodeError.new(0, 0, "_dms_imports[#{idx}].dialect must be a non-empty string") if dialect.empty? else raise DecodeError.new(0, 0, "_dms_imports[#{idx}].dialect must be a string") end # version (required string, semver, no range specifiers) version = entry["version"] case version when nil raise DecodeError.new(0, 0, "_dms_imports[#{idx}] is missing required field 'version'") when String raise DecodeError.new(0, 0, "_dms_imports[#{idx}].version must be a non-empty string") if version.empty? if has_range_specifier?(version) raise DecodeError.new(0, 0, "range-specifier syntax in version not supported " \ "(_dms_imports[#{idx}].version \"#{version}\"): write a plain semver string") end unless valid_semver?(version) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].version \"#{version}\" is not a valid semver string " \ "(expected MAJOR.MINOR.PATCH with optional -pre and +build)") end else raise DecodeError.new(0, 0, "_dms_imports[#{idx}].version must be a string") end # ns (optional string) ns_val = entry["ns"] ns = case ns_val when nil then nil when String raise DecodeError.new(0, 0, "_dms_imports[#{idx}].ns must be a non-empty string when present") if ns_val.empty? ns_val else raise DecodeError.new(0, 0, "_dms_imports[#{idx}].ns must be a string") end # bind (optional table: sigil → list of family names) bind = {} if (bind_val = entry["bind"]) unless bind_val.is_a?(Hash) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].bind must be a table") end bind_val.each do |sigil, families_val| if sigil.empty? raise DecodeError.new(0, 0, "_dms_imports[#{idx}].bind has an empty sigil key") end err = Tier1.validate_sigil_atoms(sigil, idx) raise DecodeError.new(0, 0, err) if err unless families_val.is_a?(Array) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].bind[\"#{sigil}\"] must be a list " \ "(use list form even for a single family)") end names = families_val.map do |item| unless item.is_a?(String) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].bind[\"#{sigil}\"] must be a list of strings") end item end bind[sigil] = names end end # allow (optional table: family → list of names) allow_map = {} if (allow_val = entry["allow"]) unless allow_val.is_a?(Hash) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].allow must be a table") end allow_val.each do |family, names_val| allow_map[family] = extract_string_list(names_val, idx, "allow", family) end end # deny (optional table: family → list of names) deny_map = {} if (deny_val = entry["deny"]) unless deny_val.is_a?(Hash) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].deny must be a table") end deny_val.each do |family, names_val| deny_map[family] = extract_string_list(names_val, idx, "deny", family) end end # allow/deny mutual exclusion allow_map.each_key do |family| if deny_map.key?(family) raise DecodeError.new(0, 0, "_dms_imports[#{idx}]: family \"#{family}\" appears in both " \ "'allow' and 'deny' — they are mutually exclusive for the same family") end end # alias (optional table: family → table: alias → canonical) alias_map = {} if (alias_val = entry["alias"]) unless alias_val.is_a?(Hash) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].alias must be a table") end alias_val.each do |family, inner_val| unless inner_val.is_a?(Hash) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].alias[\"#{family}\"] must be a table (alias → canonical)") end inner_map = {} inner_val.each do |alias_name, canonical_val| unless canonical_val.is_a?(String) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].alias[\"#{family}\"][\"#{alias_name}\"] must be a string") end inner_map[alias_name] = canonical_val end alias_map[family] = inner_map end end # Cross-import collision detection ns_repr = ns || "<unset>" bind.each do |sigil, families| families.each do |family| triple_key = "#{sigil}|#{ns_repr}|#{family}" if (prev_idx = seen_triples[triple_key]) prev = specs[prev_idx] raise DecodeError.new(0, 0, "Decorator binding collision on (sigil='#{sigil}', ns=#{ns_repr}, " \ "family='#{family}'): " \ "import ##{prev_idx} dialect '#{prev.dialect}' v#{prev.version} and " \ "import ##{idx} dialect '#{dialect}' v#{version} both bind " \ "'#{sigil}' → '#{family}'. Resolve by remapping one.") end seen_triples[triple_key] = idx end end specs << ImportSpec.new( dialect: dialect, version: version, ns: ns, bind: bind, allow: allow_map, deny: deny_map, alias_map: alias_map ) end specs end |
.extract_string_list(val, idx, field, family) ⇒ Object
449 450 451 452 453 454 455 456 457 458 459 |
# File 'lib/dms/tier1.rb', line 449 def self.extract_string_list(val, idx, field, family) unless val.is_a?(Array) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].#{field}[\"#{family}\"] must be a list") end val.map do |item| unless item.is_a?(String) raise DecodeError.new(0, 0, "_dms_imports[#{idx}].#{field}[\"#{family}\"] must be a list of strings") end item end end |
.has_range_specifier?(s) ⇒ Boolean
269 270 271 272 |
# File 'lib/dms/tier1.rb', line 269 def self.has_range_specifier?(s) trimmed = s.lstrip RANGE_SPECIFIER_PREFIXES.any? { |p| trimmed.start_with?(p) } end |
.import_to_json(imp) ⇒ Object
1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 |
# File 'lib/dms/tier1.rb', line 1680 def self.import_to_json(imp) bind_json = {} imp.bind.each { |sigil, fams| bind_json[sigil] = fams } allow_json = {} imp.allow.each { |family, names| allow_json[family] = names } deny_json = {} imp.deny.each { |family, names| deny_json[family] = names } alias_json = {} imp.alias_map.each do |family, inner| alias_json[family] = inner end { "dialect" => imp.dialect, "version" => imp.version, "ns" => imp.ns, "bind" => bind_json, "allow" => allow_json, "deny" => deny_json, "alias" => alias_json } end |
.lex_sigil_atom_at(s, pos) ⇒ Object
Lex one sigil atom at byte offset ‘pos` in UTF-8 string `s`. Returns byte-length of the atom (1 for ASCII, cluster len for emoji), or nil if no sigil atom here.
163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/dms/tier1.rb', line 163 def self.lex_sigil_atom_at(s, pos) return nil if pos >= s.bytesize b = s.getbyte(pos) return nil if b.nil? # ASCII reserved sigil char if b < 0x80 return RESERVED_SIGIL_CHARS.include?(b.chr) ? 1 : nil end # Multi-byte: try emoji cluster end_pos = read_reserved_emoji_atom(s, pos) return nil if end_pos.nil? end_pos - pos end |
.param_group_to_json(pg, tag_fn) ⇒ Object
1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 |
# File 'lib/dms/tier1.rb', line 1739 def self.param_group_to_json(pg, tag_fn) case pg.kind when :named tagged_val = {} pg.value.each { |k, v| tagged_val[k] = tag_fn.call(v) } { "kind" => "named", "value" => tagged_val } when :positional { "kind" => "positional", "value" => pg.value.map { |v| tag_fn.call(v) } } end end |
.parse(src) ⇒ Object
Parse a DMS source string in tier-1 mode. Returns DocumentT1.
547 548 549 |
# File 'lib/dms/tier1.rb', line 547 def self.parse(src) T1Parser.new(src).parse end |
.read_reserved_emoji_atom(s, start) ⇒ Object
Read one extended grapheme cluster of reserved-emoji shape starting at byte offset ‘start` in `s` (UTF-8 string). Returns the exclusive end byte offset, or nil if no emoji cluster starts here. Algorithm mirrors Rust read_reserved_emoji_atom exactly.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/dms/tier1.rb', line 83 def self.read_reserved_emoji_atom(s, start) return nil if start >= s.bytesize # Decode first codepoint sub = s.byteslice(start, s.bytesize - start) return nil if sub.nil? || sub.empty? sub = sub.force_encoding("UTF-8") c0 = sub[0] return nil if c0.nil? cp0 = c0.ord return nil unless reserved_emoji_codepoint?(cp0) len0 = c0.bytesize end_pos = start + len0 # Regional-indicator pair (GB12/GB13) if regional_indicator?(cp0) rest = s.byteslice(end_pos, s.bytesize - end_pos) if rest && !rest.empty? rest = rest.force_encoding("UTF-8") c1 = rest[0] if c1 && regional_indicator?(c1.ord) end_pos += c1.bytesize end end return end_pos end # GB9/GB9a/GB11 loop loop do rest = s.byteslice(end_pos, s.bytesize - end_pos) break if rest.nil? || rest.empty? rest = rest.force_encoding("UTF-8") c = rest[0] break if c.nil? cp = c.ord if emoji_modifier?(cp) || cp == 0xFE0F || cp == 0x20E3 # GB9/GB9a - Extend or SpacingMark end_pos += c.bytesize next end if cp == 0x200D # GB11 - ZWJ x Extended_Pictographic after_zwj = end_pos + c.bytesize after = s.byteslice(after_zwj, s.bytesize - after_zwj) if after && !after.empty? after = after.force_encoding("UTF-8") nc = after[0] if nc && extended_pictographic?(nc.ord) end_pos = after_zwj + nc.bytesize next end end # ZWJ not followed by E_P: cluster ends before ZWJ break end break end end_pos end |
.regional_indicator?(cp) ⇒ Boolean
71 72 73 |
# File 'lib/dms/tier1.rb', line 71 def self.regional_indicator?(cp) cp >= 0x1F1E6 && cp <= 0x1F1FF end |
.reserved_emoji_codepoint?(cp) ⇒ Boolean
64 65 66 67 68 69 |
# File 'lib/dms/tier1.rb', line 64 def self.reserved_emoji_codepoint?(cp) return true if cp >= 0x1F1E6 && cp <= 0x1F1FF # regional indicator return true if cp >= 0x1F3FB && cp <= 0x1F3FF # skin-tone modifier return true if cp == 0x20E3 # keycap combiner extended_pictographic?(cp) end |
.resolve_family(sigil, fn_name, ns, imports) ⇒ Object
Resolve a decorator call’s family from imports. Returns [family_name, canonical_fn_name] or raises. Also applies deny-list check.
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 |
# File 'lib/dms/tier1.rb', line 466 def self.resolve_family(sigil, fn_name, ns, imports) # Filter imports by ns if specified candidate_imports = if ns filtered = imports.select { |imp| imp.ns == ns } if filtered.empty? raise DecodeError.new(0, 0, "unknown namespace '#{ns}'") end filtered else imports end # For each import, check families bound to this sigil # Apply aliases and allow/deny rules accepted = [] # [family_name, canonical_fn_name] candidate_imports.each do |imp| families = imp.bind[sigil] next unless families families.each do |family| # Apply alias: fn_name might be an alias canonical = if (family_aliases = imp.alias_map[family]) family_aliases[fn_name] || fn_name else fn_name end # Apply deny list if (deny_list = imp.deny[family]) next if deny_list.include?(canonical) end # Apply allow list if (allow_list = imp.allow[family]) next unless allow_list.include?(canonical) end accepted << [family, canonical] end end if accepted.empty? # Check if sigil is bound at all in any import sigil_bound = candidate_imports.any? { |imp| imp.bind.key?(sigil) } unless sigil_bound raise DecodeError.new(0, 0, "name '#{fn_name}' not found in any family bound to sigil '#{sigil}'") end # Sigil IS bound but fn_name was filtered out (denied or not allowed). # Check deny against all imports for the deny_rejected test. candidate_imports.each do |imp| families = imp.bind[sigil] next unless families families.each do |family| canonical = fn_name if (family_aliases = imp.alias_map[family]) canonical = family_aliases[fn_name] || fn_name end if (deny_list = imp.deny[family]) if deny_list.include?(canonical) raise DecodeError.new(0, 0, "decorator '#{fn_name}' is denied by family '#{family}' deny list") end end end end # Filtered by allow list — also an error raise DecodeError.new(0, 0, "name '#{fn_name}' not found in any family bound to sigil '#{sigil}'") end # Use first accepted accepted.first end |
.sigil_atom_start_at?(s, pos) ⇒ Boolean
Returns true if the character at byte offset ‘pos` in UTF-8 string `s` starts a sigil atom (ASCII reserved char OR reserved emoji codepoint).
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/dms/tier1.rb', line 144 def self.sigil_atom_start_at?(s, pos) return false if pos >= s.bytesize b = s.getbyte(pos) return false if b.nil? # ASCII reserved sigil return true if RESERVED_SIGIL_CHARS.include?(b.chr) # Multi-byte: check if it's a reserved emoji codepoint return false if b < 0x80 sub = s.byteslice(pos, s.bytesize - pos) return false if sub.nil? || sub.empty? sub = sub.force_encoding("UTF-8") c = sub[0] return false if c.nil? reserved_emoji_codepoint?(c.ord) end |
.valid_semver?(s) ⇒ Boolean
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/dms/tier1.rb', line 243 def self.valid_semver?(s) # Drop build metadata s = s.split("+", 2).first # Split pre-release core_str, pre_str = s.split("-", 2) parts = core_str.split(".", -1) return false unless parts.length == 3 parts.each do |p| return false if p.empty? return false if p.length > 1 && p.start_with?("0") return false unless p.match?(/\A\d+\z/) end if pre_str return false if pre_str.empty? pre_str.split(".").each do |id| return false if id.empty? if id.match?(/\A\d+\z/) return false if id.length > 1 && id.start_with?("0") else return false unless id.match?(/\A[A-Za-z0-9\-]+\z/) end end end true end |
.validate_sigil_atoms(sigil, idx) ⇒ Object
Validate that a sigil string consists of valid sigil atoms only. Returns nil on success, error message string on failure.
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/dms/tier1.rb', line 179 def self.validate_sigil_atoms(sigil, idx) s = sigil.encode("UTF-8") rescue sigil pos = 0 while pos < s.bytesize atom_len = lex_sigil_atom_at(s, pos) if atom_len.nil? # Decode the char at pos for the error message sub = s.byteslice(pos, s.bytesize - pos).force_encoding("UTF-8") c = sub[0] || "?" if c == "_" return "_dms_imports[#{idx}].bind key \"#{sigil}\" (or containing '_') " \ "is invalid: underscore is not in the tier-0 reserved decorator sigil set" end return "_dms_imports[#{idx}].bind key \"#{sigil}\" contains '#{c}' " \ "which is not in the tier-0 reserved decorator sigil set " \ "nor in the Reserved Emoji Set" end pos += atom_len end nil end |