smarter_json 1.2.3 → 1.2.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edd2cbc389a44f0714898f2a2942032632ca5f13a83d93acd708988866974893
4
- data.tar.gz: 5cfa8719265797fd0fcd59f0060a5dd255aeba67ff229c78ab5696ad86f8b8e3
3
+ metadata.gz: 004e9e158ab0ec9ac6f7f3e532e01fac5ca525ed0f7409794052b6debbdeaa4e
4
+ data.tar.gz: deaa95b2c671d93db12c3059e79e4ab5908c44f009d5bc843b42b7561c030f7d
5
5
  SHA512:
6
- metadata.gz: b0306cb7eab1db78053e8620f119b7debc54d836c38369b328bc0e0b9e8798d23eb4bbdb82281d036f62cb8cd296a3c8d7667f41a49d7e643a955830e08caf99
7
- data.tar.gz: 0a5f052e115141014bcf2f0e031d43fb40521dd01da0f1ed5c6d4edeea785ead09f8ebcdfb76b580b4b8157391087ff2a79c5cd548ae7d8a0be96d0527a9f8fb
6
+ metadata.gz: 6b09e63686e0e37ecd25f8a1d511e865bb8f44df2fa95b213e3f5a34d4083ad5b30e0b15ce33c764cdaf960d7f2ebe6a859c8acb3ecdaa49c87118015ef34d88
7
+ data.tar.gz: 3ab34cf983f13f9b5479c69f4764c2cf44ba8da5290c1879b79c9de9338103025a67ab20bd6d6b25a1a5f8029b97868b53e8b8baae4daa430be370c046a222ad
data/CHANGELOG.md CHANGED
@@ -13,6 +13,26 @@
13
13
  > ⚠️ We discourage the use of `process(input).first` / `process(input)[0]` because it silently drops potential additional documents
14
14
  > Please use `process_one` if you are expecting only one JSON doc, e.g. in API payloads, because it emits on_warning if it finds multiple docs.
15
15
 
16
+ ## 1.2.5 (2026-07-01)
17
+
18
+ RSpec tests: 1,282 → 1,308
19
+
20
+ ### New Features
21
+
22
+ - **RFC 7464 JSON Text Sequences are now read natively.** The record separator (`0x1E`) that frames each record is a first-class document separator, so an RS-framed stream parses into its documents without a warning, and bare-scalar records (a number / keyword / string on its own) are read correctly.
23
+
24
+ ## 1.2.4 (2026-07-01)
25
+
26
+ RSpec tests: 1,268 → 1,282
27
+
28
+ ### Bug Fixes
29
+
30
+ - **Portable builds by default — fixes the "Illegal instruction" crash on mixed-hardware fleets.** The C extension was compiled with `-march=native` on every platform except Apple Silicon, baking in the build host's CPU instructions (e.g. AVX-512). A binary built on one machine could then crash with `Illegal instruction` when run on a CPU lacking those instructions — common when the build host differs from the run host (CI/build servers, Docker images, mixed-hardware fleets). The C extension is now built **portable** by default (no host-specific instructions).
31
+
32
+ ### New Features
33
+
34
+ - **`SMARTER_JSON_PERFORMANCE` build option** (`portable` default, `tuned`, or `max`) to opt into host-tuned or host-specific instructions at install time. See the [README](README.md#cpu-optimization-smarter_json_performance) / [Introduction](docs/_introduction.md#build-time-performance-tuning-smarter_json_performance) for details.
35
+
16
36
  ## 1.2.3 (2026-06-28)
17
37
 
18
38
  RSpec tests: 1,167 → 1,268
data/README.md CHANGED
@@ -114,6 +114,26 @@ gem install smarter_json
114
114
 
115
115
  The C extension is built on install and used automatically. On platforms where it can't build, the pure-Ruby implementation runs instead and produces identical results.
116
116
 
117
+ ### CPU Optimization (`SMARTER_JSON_PERFORMANCE`)
118
+
119
+ The C extension is compiled when the gem is installed. By default it is built **portable**: it uses no CPU-specific instructions, so a binary built on one machine runs on any other CPU of the same architecture. Set `SMARTER_JSON_PERFORMANCE` at install time to trade portability for speed:
120
+
121
+ | Level | Flags added | Portable? | Use when |
122
+ |----------------------|-------------------------------------------|----------------------------------|---------------------------------------|
123
+ | `portable` (default) | none | Yes, any CPU of the arch | Build host may differ from run host |
124
+ | `tuned` | `-mtune=native` | Yes, instruction scheduling only | Build and run hosts share a microarch |
125
+ | `max` | `-march=native`, or `-mcpu=native` on ARM | No, host instruction optimization| Build host and run host are the same |
126
+
127
+ `max` enables host-specific instructions, so a binary built with it can crash with `Illegal instruction` if it later runs on a CPU that lacks them (for example, built on an AVX-512 machine and run on one without). `tuned` only changes instruction scheduling, never the instruction set, so it stays portable. Every flag is probed against your compiler at build time and skipped if unsupported, so an unavailable flag never breaks the build.
128
+
129
+ ```bash
130
+ SMARTER_JSON_PERFORMANCE=tuned gem install smarter_json # portable, tuned for this machine's microarchitecture
131
+ SMARTER_JSON_PERFORMANCE=max gem install smarter_json # fastest, NOT portable — only when you build on the machine you run on
132
+ SMARTER_JSON_PERFORMANCE=tuned bundle install # same, under Bundler
133
+ ```
134
+
135
+ For a fixed baseline instead of `native` (e.g. a portable-but-newer instruction set), pass flags directly via `CFLAGS`, which the build also honors: `CFLAGS="-march=x86-64-v2" gem install smarter_json`.
136
+
117
137
  ## Usage
118
138
 
119
139
  Pass a String of JSON content or an IO; you get back the extracted data. The same call handles strict JSON, JSON5, and HJSON-style config — there are no modes or flags.
@@ -37,6 +37,28 @@ It raises only on genuinely unreadable input (unterminated string, mismatched br
37
37
 
38
38
  Both the C extension and the pure-Ruby engine are **iterative, not recursive** — they track nesting on an explicit, heap-allocated stack rather than the call stack. So deeply nested input **cannot overflow the call stack or segfault**: nesting is bounded only by available memory, the same posture as Oj (the stdlib `json` caps at 100). The trade-off: there is currently **no fixed nesting or input-size limit**, so size-limit untrusted input upstream.
39
39
 
40
+ ## Build-Time Performance Tuning (`SMARTER_JSON_PERFORMANCE`)
41
+
42
+ The C extension is compiled when the gem is installed. By default it is built **portable**: it uses no CPU-specific instructions, so a binary compiled on one machine runs on any other CPU of the same architecture. This matters whenever the machine that builds the gem differs from the machine that runs it — a CI or build server, a Docker image moved between hosts, or a mixed-hardware fleet. A build that bakes in instructions the run host lacks (such as AVX-512) would otherwise crash with `Illegal instruction`.
43
+
44
+ Set `SMARTER_JSON_PERFORMANCE` at install time to trade portability for speed:
45
+
46
+ | Level | Flags added | Portable? | Use when |
47
+ |----------------------|-------------------------------------------|----------------------------------|---------------------------------------|
48
+ | `portable` (default) | none | Yes, any CPU of the arch | Build host may differ from run host |
49
+ | `tuned` | `-mtune=native` | Yes, instruction scheduling only | Build and run hosts share a microarch |
50
+ | `max` | `-march=native`, or `-mcpu=native` on ARM | No, host instruction optimization| Build host and run host are the same |
51
+
52
+ `tuned` only changes instruction scheduling, never the instruction set, so it stays portable — and it pays off when the build and run hosts share a microarchitecture (the same chip, or a fleet of identical instances). `max` enables host-specific instructions and is the fastest, but a binary built with it can crash on a different CPU. Every flag is probed against your compiler at build time and skipped if unsupported, so an unavailable flag never breaks the build.
53
+
54
+ ```bash
55
+ SMARTER_JSON_PERFORMANCE=tuned gem install smarter_json # portable, tuned for this machine's microarchitecture
56
+ SMARTER_JSON_PERFORMANCE=max gem install smarter_json # fastest, NOT portable — only when you build on the machine you run on
57
+ SMARTER_JSON_PERFORMANCE=tuned bundle install # same, under Bundler
58
+ ```
59
+
60
+ For a fixed baseline instead of `native` (e.g. a portable-but-newer instruction set), pass flags directly via `CFLAGS`, which the build also honors: `CFLAGS="-march=x86-64-v2" gem install smarter_json`.
61
+
40
62
  ---------------
41
63
 
42
64
  NEXT: [The Basic Read API](./basic_read_api.md) | UP: [README](../README.md)
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmarterJSON
4
+ # Pure (mkmf-free) selection of CPU-optimization flags from the
5
+ # SMARTER_JSON_PERFORMANCE environment variable. Kept separate from extconf.rb
6
+ # so the logic can be unit-tested without invoking a compiler.
7
+ #
8
+ # Levels:
9
+ # portable - no host-specific flags. The binary runs on any CPU of the same
10
+ # architecture. The safe default: a binary built here will not
11
+ # crash with "Illegal instruction" on an older/different CPU.
12
+ # tuned - -mtune=native: tunes instruction scheduling for the build host's
13
+ # microarchitecture WITHOUT changing the instruction set, so the
14
+ # binary stays portable. A real win when build and run hosts share
15
+ # a microarchitecture (same chip or a homogeneous fleet).
16
+ # max - host-specific instructions: -march=native, or -mcpu=native on
17
+ # ARM/Clang where -march=native is rejected. Fastest, but NOT
18
+ # portable -- may crash on a CPU lacking the build host's
19
+ # instructions. Use only when build host and run host match.
20
+ #
21
+ # `accepts` is a predicate (in the real build, a wrapper over mkmf's
22
+ # try_compile) returning true when the compiler accepts a given flag; each
23
+ # candidate is probed so an unsupported flag is skipped rather than breaking
24
+ # the build.
25
+ module CpuFlags
26
+ LEVELS = %w[portable tuned max].freeze
27
+
28
+ # Candidate flags per level, in preference order. The first one the compiler
29
+ # accepts wins. `max` degrades march -> mcpu -> mtune; tuned only ever
30
+ # considers -mtune=native (never an instruction-set flag).
31
+ CANDIDATES = {
32
+ 'portable' => [].freeze,
33
+ 'tuned' => ['-mtune=native'].freeze,
34
+ 'max' => ['-march=native', '-mcpu=native', '-mtune=native'].freeze,
35
+ }.freeze
36
+
37
+ # Returns a Hash: { level: String, flags: Array<String>, warning: String|nil }.
38
+ def self.select(raw_level, accepts:)
39
+ level, warning = normalize(raw_level)
40
+ chosen = CANDIDATES[level].find { |flag| accepts.call(flag) }
41
+ { level: level, flags: chosen ? [chosen] : [], warning: warning }
42
+ end
43
+
44
+ # Normalizes the env value to a known level. Unknown values fall back to
45
+ # 'portable' (a typo can then only ever be slower, never non-portable) and
46
+ # return a warning naming the bad value and the fallback.
47
+ def self.normalize(raw_level)
48
+ value = raw_level.to_s.strip.downcase
49
+ return ['portable', nil] if value.empty?
50
+ return [value, nil] if LEVELS.include?(value)
51
+
52
+ ['portable', "SMARTER_JSON_PERFORMANCE=#{raw_level.inspect} is not one of #{LEVELS.join('|')}; using 'portable'."]
53
+ end
54
+ end
55
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "mkmf"
4
4
  require "rbconfig"
5
+ require_relative "cpu_flags"
5
6
 
6
7
  # Ruby sometimes ships CFLAGS with "-g -O3"; drop the debug half so the
7
8
  # extension is built optimized, not with debug info.
@@ -9,11 +10,31 @@ if RbConfig::MAKEFILE_CONFIG["CFLAGS"].include?("-g -O3")
9
10
  RbConfig::MAKEFILE_CONFIG["CFLAGS"] = RbConfig::MAKEFILE_CONFIG["CFLAGS"].sub("-g -O3", "-O3 $(cflags)")
10
11
  end
11
12
 
13
+ # Probe whether the compiler accepts a flag by compiling a trivial program with
14
+ # it. Lets us skip flags the toolchain rejects (e.g. -march=native on Clang/ARM,
15
+ # or GCC-only flags on MSVC) instead of breaking the build. Replaces the old
16
+ # RUBY_PLATFORM string guesses: ask the actual compiler, don't infer from the OS.
17
+ def compiler_accepts?(flag)
18
+ try_compile("int main(void){return 0;}", flag)
19
+ end
20
+
12
21
  optflags = "-O3 -flto -fomit-frame-pointer -DNDEBUG".dup
13
- # -march=native is skipped on arm64-darwin (Apple clang already targets the host).
14
- optflags << " -march=native" unless RUBY_PLATFORM.start_with?("arm64-darwin")
15
- # -fno-semantic-interposition: GCC/Clang only (not MSVC).
16
- optflags << " -fno-semantic-interposition" unless RUBY_PLATFORM.include?("mswin")
22
+
23
+ # CPU optimization level, set via SMARTER_JSON_PERFORMANCE (default: portable).
24
+ # See cpu_flags.rb for the full description of each level.
25
+ #
26
+ # portable (default) - no host-specific flags; runs on any CPU of the same arch.
27
+ # tuned - -mtune=native; host scheduling tuning, still portable.
28
+ # max - host instruction set (-march/-mcpu native); fastest, but
29
+ # NOT portable -- may crash on a CPU lacking those instructions.
30
+ cpu = SmarterJSON::CpuFlags.select(ENV["SMARTER_JSON_PERFORMANCE"], accepts: method(:compiler_accepts?))
31
+ warn(cpu[:warning]) if cpu[:warning]
32
+ cpu[:flags].each { |flag| optflags << " #{flag}" }
33
+ puts("SmarterJSON performance level: #{cpu[:level]} -- optflags: #{optflags}")
34
+
35
+ # -fno-semantic-interposition: GCC/Clang only (not MSVC). Allows intra-library
36
+ # calls to bypass the PLT on Linux and enables more aggressive LTO inlining.
37
+ optflags << " -fno-semantic-interposition" if compiler_accepts?("-fno-semantic-interposition")
17
38
 
18
39
  CONFIG["optflags"] = optflags
19
40
  CONFIG["debugflags"] = ""
@@ -1624,14 +1624,17 @@ static int fj_implicit_root_ahead(fj_state *st) {
1624
1624
  return result;
1625
1625
  }
1626
1626
 
1627
- /* Between top-level documents, whitespace, comments, AND commas all separate
1628
- * (commas collapse like the in-container lenient-comma rule). A space alone never
1629
- * separates that is handled inside the document by the quoteless run. Mirrors
1630
- * the Ruby Parser#skip_document_separators. */
1627
+ /* Between top-level documents, whitespace, comments, commas, AND the RFC 7464
1628
+ * record separator (0x1E) all separate (commas collapse like the in-container
1629
+ * lenient-comma rule; 0x1E frames each JSON Text Sequence record). A space alone
1630
+ * never separates — that is handled inside the document by the quoteless run.
1631
+ * Mirrors the Ruby Parser#skip_document_separators. */
1631
1632
  static void fj_skip_document_separators(fj_state *st) {
1632
1633
  for (;;) {
1634
+ int b;
1633
1635
  fj_skip_ws_comments(st);
1634
- if (fj_byte(st) != ',') break;
1636
+ b = fj_byte(st);
1637
+ if (b != ',' && b != 0x1E) break;
1635
1638
  fj_advance(st, 1);
1636
1639
  }
1637
1640
  }
@@ -1640,8 +1643,9 @@ static int fj_is_hws(int b) { return b == ' ' || b == '\t' || b == 0x0B || b ==
1640
1643
 
1641
1644
  /* After a top-level value: a self-delimiting value (object / array / string) may be
1642
1645
  * followed by anything, but a bare scalar (number / keyword) must be followed by a
1643
- * real separator — a newline, ',', a comment, or EOF. A space is NOT a separator, so
1644
- * `1 2 3` and `42 "x" true` raise. Mirrors the Ruby Parser#enforce_scalar_boundary. */
1646
+ * real separator — a newline, ',', the RFC 7464 record separator (0x1E), a comment,
1647
+ * or EOF. A space is NOT a separator, so `1 2 3` and `42 "x" true` raise. Mirrors the
1648
+ * Ruby Parser#enforce_scalar_boundary. */
1645
1649
  static void fj_enforce_scalar_boundary(fj_state *st, VALUE value) {
1646
1650
  int b, nx;
1647
1651
  if (RB_TYPE_P(value, T_STRING) || RB_TYPE_P(value, T_HASH) || RB_TYPE_P(value, T_ARRAY)) return;
@@ -1655,7 +1659,7 @@ static void fj_enforce_scalar_boundary(fj_state *st, VALUE value) {
1655
1659
  break;
1656
1660
  }
1657
1661
  b = fj_byte(st);
1658
- if (b == -1 || b == 0x0A || b == 0x0D || b == ',') return;
1662
+ if (b == -1 || b == 0x0A || b == 0x0D || b == ',' || b == 0x1E) return;
1659
1663
  if (b == '#') return;
1660
1664
  if (b == '/') { nx = fj_byte_at(st, 1); if (nx == '/' || nx == '*') return; }
1661
1665
  fj_error(st, "a top-level number or keyword must be followed by a newline, ',', or end of input");
@@ -384,6 +384,7 @@ module SmarterJSON
384
384
  TAB = 0x09
385
385
  LF = 0x0A
386
386
  CR = 0x0D
387
+ RS = 0x1E # RFC 7464 record separator — frames each JSON Text Sequence record
387
388
  end
388
389
 
389
390
  module Framer
@@ -1052,13 +1053,14 @@ module SmarterJSON
1052
1053
  parse_iter(implicit_root_object_ahead?)
1053
1054
  end
1054
1055
 
1055
- # Between top-level documents, whitespace, comments, AND commas all separate
1056
- # (commas collapse like the in-container lenient-comma rule). A space alone never
1057
- # separates that is handled inside the document by the quoteless run, so
1056
+ # Between top-level documents, whitespace, comments, commas, AND the RFC 7464
1057
+ # record separator (0x1E) all separate (commas collapse like the in-container
1058
+ # lenient-comma rule; 0x1E frames each JSON Text Sequence record). A space alone
1059
+ # never separates — that is handled inside the document by the quoteless run, so
1058
1060
  # `1 2 3` is one document (the string "1 2 3") while `1, 2, 3` is three.
1059
1061
  def skip_document_separators
1060
1062
  skip_whitespace_and_comments
1061
- while byte == COMMA
1063
+ while (b = byte) == COMMA || b == RS
1062
1064
  advance(1)
1063
1065
  skip_whitespace_and_comments
1064
1066
  end
@@ -1066,15 +1068,16 @@ module SmarterJSON
1066
1068
 
1067
1069
  # After a top-level value: a self-delimiting value (object / array / quoted string)
1068
1070
  # may be followed by anything (the next document self-delimits), but a bare scalar
1069
- # (number / keyword) must be followed by a real separator — a newline, ',', a
1070
- # comment, or EOF. A space is NOT a separator, so `1 2 3` and `42 "x" true` raise
1071
- # rather than silently splitting; bare top-level words raise in parse_value itself.
1071
+ # (number / keyword) must be followed by a real separator — a newline, ',', the RFC
1072
+ # 7464 record separator (0x1E), a comment, or EOF. A space is NOT a separator, so
1073
+ # `1 2 3` and `42 "x" true` raise rather than silently splitting; bare top-level
1074
+ # words raise in parse_value itself.
1072
1075
  def enforce_scalar_boundary(value)
1073
1076
  return if value.is_a?(String) || value.is_a?(Hash) || value.is_a?(Array)
1074
1077
 
1075
1078
  skip_horizontal_whitespace
1076
1079
  b = byte
1077
- return if b.nil? || b == LF || b == CR || b == COMMA
1080
+ return if b.nil? || b == LF || b == CR || b == COMMA || b == RS
1078
1081
  return if b == HASH || (b == SLASH && ((c = byte_at(1)) == SLASH || c == STAR))
1079
1082
 
1080
1083
  raise error("a top-level number or keyword must be followed by a newline, ',', or end of input")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SmarterJSON
4
- VERSION = "1.2.3"
4
+ VERSION = "1.2.5"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smarter_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tilo Sloboda
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-06-28 00:00:00.000000000 Z
10
+ date: 2026-07-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -53,6 +53,7 @@ files:
53
53
  - docs/basic_write_api.md
54
54
  - docs/examples.md
55
55
  - docs/options.md
56
+ - ext/smarter_json/cpu_flags.rb
56
57
  - ext/smarter_json/extconf.rb
57
58
  - ext/smarter_json/smarter_json.c
58
59
  - ext/smarter_json/smarter_json.h
@@ -91,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
92
  - !ruby/object:Gem::Version
92
93
  version: '0'
93
94
  requirements: []
94
- rubygems_version: 3.6.9
95
+ rubygems_version: 4.0.15
95
96
  specification_version: 4
96
97
  summary: A lenient, fast JSON processor for Ruby — reads strict JSON, NDJSON, JSONL,
97
98
  JSON5, HJSON, and the messy JSON humans and LLMs actually write.