FastLED 3.10.8
Loading...
Searching...
No Matches
json.cpp.hpp
Go to the documentation of this file.
1
2#include "fl/stl/json.h"
3#include "fl/stl/json/types.h"
4#include "fl/system/sketch_macros.h" // FL_PLATFORM_HAS_LARGE_MEMORY -- gates ieee754_format_decimal
5#include "fl/stl/string.h"
6#include "fl/stl/vector.h"
7#include "fl/stl/deque.h"
8#include "fl/stl/span.h"
9#include "fl/stl/charconv.h"
10#include "fl/stl/ieee754_string.h" // FastLED #3022 phase 2 -- integer-only IEEE 754 codec
11// IWYU pragma: begin_keep
12#include "fl/stl/bit_cast.h" // bit_cast<float>(u32) / bit_cast<u32>(float)
13// IWYU pragma: end_keep
14#include "fl/math/math.h" // For floor function
16#include "fl/stl/stdint.h"
17#include "fl/stl/optional.h"
18#include "fl/stl/flat_map.h"
19#include "fl/log/log.h"
20#include "fl/math/math.h"
21#include "fl/stl/cstddef.h"
22#include "fl/stl/cstring.h"
23#include "fl/stl/limits.h"
24#include "fl/stl/move.h"
25#include "fl/stl/strstream.h"
26#include "fl/stl/shared_ptr.h"
28#include "fl/stl/string_view.h"
29#include "fl/stl/noexcept.h"
30
31// fl::numeric_limits<i16>::min(), fl::numeric_limits<i16>::max(), and fl::numeric_limits<u8>::max() should come from the platform's
32// <stdint.h> or <cstdint> headers (via fl/stl/stdint.h).
33// FastLED no longer defines these macros to avoid conflicts with system headers.
34
35
36
37namespace fl {
38
39
40
41
42// Returns true iff `bits` represents a finite IEEE-754 single-precision value
43// whose magnitude exceeds 2**24 (the float-integer-precision boundary). Used to
44// decide whether an array of numbers can survive packing into `fl::vector<float>`
45// without integer-precision loss. Integer-only -- operates on the bit pattern.
46//
47// Math: |value| > 2**24 iff
48// biased_exp > 151 (exponent >= 25, magnitude >= 2**25), OR
49// biased_exp == 151 AND mantissa != 0 (in (2**24, 2**25), e.g. 16777217.0), OR
50// biased_exp == 0xFF (inf / NaN).
51// Pure `biased_exp > 151u` would miss the (2**24, 2**25) decade -- those values
52// still carry exponent 151 but have non-zero mantissa bits.
53// We treat NaN / inf as "beyond precision" so the caller's safe-fallback path
54// is taken -- matches the previous `canBeRepresentedAsFloat(double)` semantics
55// before this code was bit-twiddled (FastLED #3022 phase 2).
56static inline bool float_bits_magnitude_exceeds_2_24(u32 bits) FL_NO_EXCEPT {
57 const u32 biased_exp = (bits >> 23) & 0xFFu;
58 const u32 mantissa = bits & 0x7FFFFFu;
59 return biased_exp == 0xFFu || biased_exp > 151u ||
60 (biased_exp == 151u && mantissa != 0u);
61}
62
63
65 static json_value null_value;
66 return null_value;
67}
68
70 static json_object empty_object;
71 return empty_object;
72}
73
74// ArduinoJson parser removed -- use json::parse() (native parser) instead.
75
76// ============================================================================
77// CUSTOM JSON PARSER - VISITOR PATTERN (Milestones 2-4)
78// ============================================================================
79
80namespace { // Anonymous namespace for internal implementation
81
82// Recursion limit protection
83constexpr int MAX_JSON_DEPTH = 32;
84
85// Token types
86// Windows headers define TRUE/FALSE macros that conflict with our enum values
87#ifdef TRUE
88#undef TRUE
89#endif
90#ifdef FALSE
91#undef FALSE
92#endif
93enum class JsonToken : u8 {
96
97 // Array lookahead optimization tokens
98 ARRAY_UINT8, // [0-255] -> vector<u8>
99 ARRAY_INT8, // [-128 to 127] (unused, covered by INT16)
100 ARRAY_INT16, // [-32768 to 32767] -> vector<i16>
101 ARRAY_INT32, // (unused, falls back to slow path)
102 ARRAY_INT64, // (unused, falls back to slow path)
103 ARRAY_FLOAT, // Floats or mixed int/float -> vector<float>
104 ARRAY_DOUBLE, // (unused, uses ARRAY_FLOAT)
105 ARRAY_STRING, // (unused, falls back to slow path)
106 ARRAY_BOOL, // (unused, falls back to slow path)
107 ARRAY_MIXED // (unused, falls back to slow path)
108};
109
110// Visitor return value
111enum class ParseState : u8 { KEEP_GOING = 0, ERROR = 1 };
112
113// Base visitor interface
115public:
117 virtual ~JsonVisitor() FL_NO_EXCEPT = default;
118};
119
120// Character-by-character tokenizer
122private:
123 const char* mInput;
124 size_t mLen;
125 size_t mPos;
127
129 while (mPos < mLen && (mInput[mPos] == ' ' || mInput[mPos] == '\t' ||
130 mInput[mPos] == '\n' || mInput[mPos] == '\r')) {
131 mPos++;
132 }
133 }
134
135 // Array lookahead scanner - scans array to determine if it can be optimized
136 // Returns specialized token (ARRAY_UINT8, etc.) or LBRACKET for slow path
137 // Assumes mPos is positioned after '[' character
139 if (!mEnableLookahead) {
140 return JsonToken::LBRACKET; // Lookahead disabled
141 }
142
143 size_t start_pos = mPos; // Position after '['
144
145 // Track element types and ranges
146 bool has_int = false;
147 bool has_float = false;
148 bool has_float_beyond_precision = false; // Track floats > 2^24
149 bool has_string = false;
150 bool has_bool = false;
151 bool has_null = false;
154
156
157 // Empty array - use slow path
158 if (mPos < mLen && mInput[mPos] == ']') {
159 out_span = fl::span<const char>();
160 return JsonToken::LBRACKET;
161 }
162
163 // Scan elements
164 while (mPos < mLen) {
166
167 // Check for array end
168 if (mInput[mPos] == ']') {
169 break;
170 }
171
172 char c = mInput[mPos];
173
174 // Abort on nested structures
175 if (c == '[' || c == '{') {
176 return JsonToken::LBRACKET;
177 }
178
179 // STRING
180 if (c == '"') {
181 has_string = true;
182 mPos++;
183 while (mPos < mLen) {
184 if (mInput[mPos] == '\\') {
185 mPos += 2; // Skip escape sequence
186 } else if (mInput[mPos] == '"') {
187 mPos++;
188 break;
189 } else {
190 mPos++;
191 }
192 }
193 }
194 // NUMBER
195 else if (c == '-' || (c >= '0' && c <= '9')) {
196 size_t num_start = mPos;
197 bool is_float = false;
198
199 if (c == '-') mPos++;
200 while (mPos < mLen && mInput[mPos] >= '0' && mInput[mPos] <= '9') mPos++;
201
202 if (mPos < mLen && mInput[mPos] == '.') {
203 is_float = true;
204 mPos++;
205 while (mPos < mLen && mInput[mPos] >= '0' && mInput[mPos] <= '9') mPos++;
206 }
207
208 if (mPos < mLen && (mInput[mPos] == 'e' || mInput[mPos] == 'E')) {
209 is_float = true;
210 mPos++;
211 if (mPos < mLen && (mInput[mPos] == '+' || mInput[mPos] == '-')) mPos++;
212 while (mPos < mLen && mInput[mPos] >= '0' && mInput[mPos] <= '9') mPos++;
213 }
214
215 if (is_float) {
216 has_float = true;
217#if FL_PLATFORM_HAS_LARGE_MEMORY
218 // Bit-twiddling parse -- never reaches libgcc soft-FP. The
219 // resulting u32 carries the same IEEE 754 single-precision
220 // bit pattern strtof would produce (+/-1 ULP). The magnitude
221 // check rides on the biased exponent so we avoid `fl::abs`.
222 const u32 f_bits = fl::ieee754_parse_decimal(&mInput[num_start], mPos - num_start);
224 has_float_beyond_precision = true;
225 }
226#else
227 // Low-memory gate per FastLED #3082: assume float values
228 // exceed 2^24 (forces slow path / generic array fallback).
229 // Drops `ieee754_parse_decimal` (598 B) + `kPow10Mant` LUT
230 // (824 B) + `kPow10BExp` (206 B) from the link.
231 has_float_beyond_precision = true;
232#endif
233 } else {
234 has_int = true;
235 // Parse actual value to get accurate range
236 // fl::parseInt doesn't allocate (Phase 1 test confirms zero allocations)
237 i64 val = fl::parseInt(&mInput[num_start], mPos - num_start);
238 if (val < int_min) int_min = val;
239 if (val > int_max) int_max = val;
240 }
241 }
242 // TRUE
243 else if (c == 't' && mPos + 3 < mLen &&
244 mInput[mPos+1] == 'r' && mInput[mPos+2] == 'u' && mInput[mPos+3] == 'e') {
245 has_bool = true;
246 mPos += 4;
247 }
248 // FALSE
249 else if (c == 'f' && mPos + 4 < mLen &&
250 mInput[mPos+1] == 'a' && mInput[mPos+2] == 'l' &&
251 mInput[mPos+3] == 's' && mInput[mPos+4] == 'e') {
252 has_bool = true;
253 mPos += 5;
254 }
255 // NULL
256 else if (c == 'n' && mPos + 3 < mLen &&
257 mInput[mPos+1] == 'u' && mInput[mPos+2] == 'l' && mInput[mPos+3] == 'l') {
258 has_null = true;
259 mPos += 4;
260 }
261 else {
262 return JsonToken::LBRACKET; // Unknown token
263 }
264
266
267 // Check for comma or end
268 if (mPos < mLen) {
269 if (mInput[mPos] == ',') {
270 mPos++;
271 } else if (mInput[mPos] == ']') {
272 break;
273 } else {
274 return JsonToken::LBRACKET; // Invalid syntax
275 }
276 }
277 }
278
279 // Determine array type
280 int type_count = (has_int ? 1 : 0) + (has_float ? 1 : 0) +
281 (has_string ? 1 : 0) + (has_bool ? 1 : 0) + (has_null ? 1 : 0);
282
283 out_span = fl::span<const char>(&mInput[start_pos], mPos - start_pos);
284
285 // Only emit specialized tokens for types json_value supports
286 if (type_count == 1 || (type_count == 2 && has_int && has_float)) {
287 if (has_float || (has_int && has_float)) {
288 // Don't optimize floats beyond integer precision
289 if (has_float_beyond_precision) {
290 return JsonToken::LBRACKET; // Use slow path
291 }
292 return JsonToken::ARRAY_FLOAT; // vector<float>
293 }
294 if (has_int) {
295 // Determine optimal integer type
296 if (int_min >= 0 && int_max <= 255) {
297 return JsonToken::ARRAY_UINT8; // vector<u8>
298 }
299 if (int_min >= -32768 && int_max <= 32767) {
300 return JsonToken::ARRAY_INT16; // vector<i16>
301 }
302 // Larger ints fall back to slow path
303 return JsonToken::LBRACKET;
304 }
305 // Strings, bools - use slow path
306 return JsonToken::LBRACKET;
307 }
308
309 // Mixed types - use slow path
310 return JsonToken::LBRACKET;
311 }
312
315 if (mPos >= mLen) {
316 out_value = fl::span<const char>();
318 }
319
320 char c = mInput[mPos];
321
322 // Structural tokens
323 switch (c) {
324 case '{':
325 out_value = fl::span<const char>(&mInput[mPos], 1);
326 mPos++;
327 return JsonToken::LBRACE;
328 case '}':
329 out_value = fl::span<const char>(&mInput[mPos], 1);
330 mPos++;
331 return JsonToken::RBRACE;
332 case '[': {
333#if FL_PLATFORM_HAS_LARGE_MEMORY
334 size_t saved_pos = mPos;
335 mPos++; // Skip '['
336 JsonToken array_token = scan_array_lookahead(out_value);
337 if (array_token != JsonToken::LBRACKET) {
338 // Specialized array token - mPos now points at ']'
339 mPos++; // Skip ']'
340 return array_token;
341 }
342 // Slow path - restore position
343 mPos = saved_pos;
344 out_value = fl::span<const char>(&mInput[mPos], 1);
345 mPos++;
346 return JsonToken::LBRACKET;
347#else
348 // Low-memory gate per #3224 Tier 2E: skip the array-packing
349 // lookahead. The packed-array tokens (ARRAY_UINT8 / INT16 /
350 // FLOAT) are gated out of JsonBuilder::on_token on Low-memory,
351 // so scanning ahead to detect them anyway is dead work. Drops
352 // `scan_array_lookahead` + `classify_array_token` + friends
353 // (~700 B of state-machine code).
354 out_value = fl::span<const char>(&mInput[mPos], 1);
355 mPos++;
356 return JsonToken::LBRACKET;
357#endif
358 }
359 case ']':
360 out_value = fl::span<const char>(&mInput[mPos], 1);
361 mPos++;
362 return JsonToken::RBRACKET;
363 case ':':
364 out_value = fl::span<const char>(&mInput[mPos], 1);
365 mPos++;
366 return JsonToken::COLON;
367 case ',':
368 out_value = fl::span<const char>(&mInput[mPos], 1);
369 mPos++;
370 return JsonToken::COMMA;
371 default:
372 break; // Fall through to other token types
373 }
374
375 // STRING tokenization
376 if (c == '"') {
377 size_t start = mPos + 1; // After opening quote
378 mPos++;
379
380 while (mPos < mLen) {
381 if (mInput[mPos] == '\\') {
382 // Skip escape sequence
383 mPos++;
384 if (mPos >= mLen) {
385 out_value = fl::span<const char>();
386 return JsonToken::ERROR; // Unclosed string
387 }
388 mPos++; // Skip escaped character
389 } else if (mInput[mPos] == '"') {
390 // Found closing quote
391 out_value = fl::span<const char>(&mInput[start], mPos - start);
392 mPos++; // Move past closing quote
393 return JsonToken::STRING;
394 } else {
395 mPos++;
396 }
397 }
398
399 // Unclosed string
400 out_value = fl::span<const char>();
401 return JsonToken::ERROR;
402 }
403
404 // NUMBER tokenization
405 if (c == '-' || (c >= '0' && c <= '9')) {
406 size_t start = mPos;
407
408 // Optional minus
409 if (c == '-') mPos++;
410
411 // Require at least one digit
412 if (mPos >= mLen || !(mInput[mPos] >= '0' && mInput[mPos] <= '9')) {
413 out_value = fl::span<const char>();
414 return JsonToken::ERROR;
415 }
416
417 // Digits before decimal
418 while (mPos < mLen && mInput[mPos] >= '0' && mInput[mPos] <= '9') {
419 mPos++;
420 }
421
422 // Optional decimal point
423 if (mPos < mLen && mInput[mPos] == '.') {
424 mPos++;
425 // Require digits after decimal
426 if (mPos >= mLen || !(mInput[mPos] >= '0' && mInput[mPos] <= '9')) {
427 out_value = fl::span<const char>();
428 return JsonToken::ERROR;
429 }
430 while (mPos < mLen && mInput[mPos] >= '0' && mInput[mPos] <= '9') {
431 mPos++;
432 }
433 }
434
435 // Optional exponent
436 if (mPos < mLen && (mInput[mPos] == 'e' || mInput[mPos] == 'E')) {
437 mPos++;
438 // Optional sign
439 if (mPos < mLen && (mInput[mPos] == '+' || mInput[mPos] == '-')) {
440 mPos++;
441 }
442 // Require exponent digits
443 if (mPos >= mLen || !(mInput[mPos] >= '0' && mInput[mPos] <= '9')) {
444 out_value = fl::span<const char>();
445 return JsonToken::ERROR;
446 }
447 while (mPos < mLen && mInput[mPos] >= '0' && mInput[mPos] <= '9') {
448 mPos++;
449 }
450 }
451
452 out_value = fl::span<const char>(&mInput[start], mPos - start);
453 return JsonToken::NUMBER;
454 }
455
456 // Keyword: true
457 if (c == 't' && mPos + 3 < mLen &&
458 mInput[mPos+1] == 'r' && mInput[mPos+2] == 'u' && mInput[mPos+3] == 'e') {
459 out_value = fl::span<const char>(&mInput[mPos], 4);
460 mPos += 4;
461 return JsonToken::TRUE;
462 }
463
464 // Keyword: false
465 if (c == 'f' && mPos + 4 < mLen &&
466 mInput[mPos+1] == 'a' && mInput[mPos+2] == 'l' &&
467 mInput[mPos+3] == 's' && mInput[mPos+4] == 'e') {
468 out_value = fl::span<const char>(&mInput[mPos], 5);
469 mPos += 5;
470 return JsonToken::FALSE;
471 }
472
473 // Keyword: null
474 if (c == 'n' && mPos + 3 < mLen &&
475 mInput[mPos+1] == 'u' && mInput[mPos+2] == 'l' && mInput[mPos+3] == 'l') {
476 out_value = fl::span<const char>(&mInput[mPos], 4);
477 mPos += 4;
479 }
480
481 // Unknown token
482 out_value = fl::span<const char>();
483 return JsonToken::ERROR;
484 }
485
486public:
487 JsonTokenizer(bool enable_lookahead = true) FL_NO_EXCEPT : mInput(nullptr), mLen(0), mPos(0), mEnableLookahead(enable_lookahead) {}
488
489 bool parse(const fl::string& input, JsonVisitor& visitor) FL_NO_EXCEPT {
490 return parse(fl::string_view(input.c_str(), input.length()), visitor);
491 }
492
494 mInput = input.data();
495 mLen = input.size();
496 mPos = 0;
497
498 while (true) {
499 fl::span<const char> token_value;
500 JsonToken token = next_token(token_value);
501
502 if (token == JsonToken::ERROR) {
503 return false;
504 }
505
506 ParseState result = visitor.on_token(token, token_value);
507 if (result == ParseState::ERROR) {
508 return false;
509 }
510
511 if (token == JsonToken::END_OF_INPUT) {
512 break;
513 }
514 }
515
516 return true;
517 }
518};
519
520// Validator - validates JSON structure with bracket matching (Milestone 8)
522private:
524 bool mExpectKey; // In object, expecting key next
525 bool mExpectValue; // Expecting value next
526 bool mExpectColon; // Expecting colon after key
528
529public:
531
533 (void)value; // Suppress unused parameter warning
534
535 // Recursion depth check
536 if (mDepth > MAX_JSON_DEPTH) {
537 FL_ERROR("JSON parser: FATAL - recursion depth exceeded " << MAX_JSON_DEPTH);
538 return ParseState::ERROR;
539 }
540
541 switch (token) {
543 mBracketStack.push_back('{');
544 mDepth++;
545 mExpectKey = true;
546 mExpectValue = false;
548
550 if (mBracketStack.empty() || mBracketStack.back() != '{') {
551 return ParseState::ERROR; // Unmatched closing brace
552 }
553 mBracketStack.pop_back();
554 mDepth--;
555 mExpectKey = false;
557
559 mBracketStack.push_back('[');
560 mDepth++;
561 mExpectValue = true;
563
565 if (mBracketStack.empty() || mBracketStack.back() != '[') {
566 return ParseState::ERROR; // Unmatched closing bracket
567 }
568 mBracketStack.pop_back();
569 mDepth--;
570 mExpectValue = false;
572
573 case JsonToken::COLON:
574 if (!mExpectColon) {
575 return ParseState::ERROR; // Unexpected colon
576 }
577 mExpectColon = false;
578 mExpectValue = true;
580
582 if (!mBracketStack.empty() && mBracketStack.back() == '{' && mExpectKey) {
583 // This is an object key
584 mExpectKey = false;
585 mExpectColon = true;
586 } else if (mExpectValue || mBracketStack.empty()) {
587 // This is a value
588 mExpectValue = false;
589 } else {
590 return ParseState::ERROR;
591 }
593
595 case JsonToken::TRUE:
596 case JsonToken::FALSE:
598 // Specialized array tokens (complete arrays, treat as values)
602 if (mExpectValue || mBracketStack.empty()) {
603 mExpectValue = false;
605 }
606 return ParseState::ERROR;
607
608 case JsonToken::COMMA:
609 // Set expectations for next element
610 if (!mBracketStack.empty()) {
611 if (mBracketStack.back() == '{') {
612 mExpectKey = true;
613 } else {
614 mExpectValue = true;
615 }
616 }
618
621
622 default:
623 return ParseState::ERROR;
624 }
625 }
626
627 bool is_valid() const FL_NO_EXCEPT {
628 return mBracketStack.empty() && !mExpectColon && !mExpectKey;
629 }
630};
631
632// Helper: Check if string contains escape sequences
634 for (size_t i = 0; i < span.size(); i++) {
635 if (span[i] == '\\') {
636 return true;
637 }
638 }
639 return false;
640}
641
642// Helper: Unescape JSON string (only call if has_escape_sequences() returns true)
645 result.reserve(span.size());
646
647 for (size_t i = 0; i < span.size(); i++) {
648 if (span[i] == '\\' && i + 1 < span.size()) {
649 char next = span[i + 1];
650 switch (next) {
651 case '"': result += '"'; i++; break;
652 case '\\': result += '\\'; i++; break;
653 case '/': result += '/'; i++; break;
654 case 'b': result += '\b'; i++; break;
655 case 'f': result += '\f'; i++; break;
656 case 'n': result += '\n'; i++; break;
657 case 'r': result += '\r'; i++; break;
658 case 't': result += '\t'; i++; break;
659 default: result += span[i]; break; // Pass through unknown escapes
660 }
661 } else {
662 result += span[i];
663 }
664 }
665
666 return result;
667}
668
669// Array optimization helpers (Milestone 9)
671
673 if (arr.empty()) return GENERIC_ARRAY;
674
675 bool all_numeric = true;
678 bool has_float = false;
679 bool has_float_beyond_precision = false;
680
681 for (const auto& elem : arr) {
682 if (!elem) {
683 all_numeric = false;
684 break;
685 }
686
687 if (elem->is_int()) {
688 auto val = elem->as_int();
689 if (val) {
690 i64 v = *val;
691 min_val = (v < min_val) ? v : min_val;
692 max_val = (v > max_val) ? v : max_val;
693 } else {
694 all_numeric = false;
695 break;
696 }
697 } else if (elem->is_float()) {
698 has_float = true;
699 auto val = elem->as_float();
700 if (!val) {
701 all_numeric = false;
702 break;
703 }
704 // Check if float is beyond integer precision (>2^24 or <-2^24).
705 // Integer biased-exp comparison -- no FP arithmetic (#3022 phase 2).
706 const u32 fbits = fl::bit_cast<u32>(*val);
708 has_float_beyond_precision = true;
709 }
710 } else {
711 all_numeric = false;
712 break;
713 }
714 }
715
716 if (!all_numeric) return GENERIC_ARRAY;
717
718 // Classification
719 if (has_float) {
720 // If floats are beyond integer precision, don't optimize
721 if (has_float_beyond_precision) {
722 return GENERIC_ARRAY;
723 }
724 return ALL_FLOATS;
725 }
726
727 // Integer arrays
728 if (min_val >= 0 && max_val <= 255) {
729 return ALL_UINT8;
730 }
731
732 if (min_val >= -32768 && max_val <= 32767) {
733 return ALL_INT16;
734 }
735
736 // Large integers (>32767 or <-32768) that don't fit in int16 but can be represented as float
737 // Convert to float if within float's integer precision range (+/-2^24)
738 if (min_val >= -16777216 && max_val <= 16777216) {
739 return ALL_FLOATS;
740 }
741
742 return GENERIC_ARRAY;
743}
744
746 auto arr = array_val->data.ptr<json_array>();
747 if (!arr) return array_val;
748
750
751 switch (type) {
752 case ALL_UINT8: {
753 fl::vector<u8> vec;
754 vec.reserve(arr->size());
755 for (const auto& elem : *arr) {
756 auto val = elem->as_int();
757 if (val) vec.push_back(static_cast<u8>(*val));
758 }
760 }
761
762 case ALL_INT16: {
763 fl::vector<i16> vec;
764 vec.reserve(arr->size());
765 for (const auto& elem : *arr) {
766 auto val = elem->as_int();
767 if (val) vec.push_back(static_cast<i16>(*val));
768 }
770 }
771
772 case ALL_FLOATS: {
774 vec.reserve(arr->size());
775 for (const auto& elem : *arr) {
776 if (elem->is_float()) {
777 auto val = elem->as_float();
778 if (val) vec.push_back(*val);
779 } else if (elem->is_int()) {
780 auto val = elem->as_int();
781 if (val) {
782 // Route i64 -> float via i32 to avoid the direct cast
783 // pulling libgcc-nofp's `_floatdisf.o` (~5 KB of soft-FP
784 // double cascade) on no-FPU targets. ALL_FLOATS
785 // classification only fires when every int is within
786 // +/-2^24 anyway (classify_array line 717), so the int32
787 // narrowing is lossless. See FastLED #3076.
788 vec.push_back(static_cast<float>(static_cast<fl::i32>(*val)));
789 }
790 }
791 }
793 }
794
795 default:
796 return array_val; // Keep as generic array
797 }
798}
799
800// Builder - handles nested objects/arrays with stack management (Milestone 6)
801class JsonBuilder : public JsonVisitor {
802private:
803 struct StackFrame {
806 fl::string pending_key; // For objects: key waiting for value
807 };
808
809 fl::vector_inlined<StackFrame, 8> mStack; // Inline first 8 frames (most JSON is shallow)
812 // String interning enabled for large strings (> 64 bytes) that overflow SSO
814
815 // Parse integer array directly from span into vector (zero allocations)
816 template<typename T>
818 const char* p = span.data();
819 const char* end = p + span.size();
820
821 while (p < end) {
822 // Skip whitespace
823 while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
824 if (p >= end) break;
825
826 // Parse number
827 const char* num_start = p;
828 if (*p == '-') p++;
829 while (p < end && *p >= '0' && *p <= '9') p++;
830
831 i64 val = fl::parseInt(num_start, p - num_start);
832 out_vec.push_back(static_cast<T>(val));
833
834 // Skip whitespace
835 while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
836
837 // Skip comma
838 if (p < end && *p == ',') p++;
839 }
840
841 return true;
842 }
843
844 // Parse float array directly from span into vector (zero allocations)
845 template<typename T>
847 const char* p = span.data();
848 const char* end = p + span.size();
849
850 while (p < end) {
851 // Skip whitespace
852 while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
853 if (p >= end) break;
854
855 // Parse number
856 const char* num_start = p;
857 bool is_float = false;
858
859 if (*p == '-') p++;
860 while (p < end && *p >= '0' && *p <= '9') p++;
861
862 if (p < end && *p == '.') {
863 is_float = true;
864 p++;
865 while (p < end && *p >= '0' && *p <= '9') p++;
866 }
867
868 if (p < end && (*p == 'e' || *p == 'E')) {
869 is_float = true;
870 p++;
871 if (p < end && (*p == '+' || *p == '-')) p++;
872 while (p < end && *p >= '0' && *p <= '9') p++;
873 }
874
875 T val;
876 if (is_float) {
877#if FL_PLATFORM_HAS_LARGE_MEMORY
878 // Bit-twiddling parse -> IEEE 754 bit pattern -> bit-cast to float.
879 // The `static_cast<T>` from `float` is the caller's responsibility
880 // (e.g. T=float costs nothing; T=int pulls one __aeabi_f2iz at the
881 // call site, which we accept per the "user opts in" mandate).
882 const u32 bits = fl::ieee754_parse_decimal(num_start, p - num_start);
883 val = static_cast<T>(fl::bit_cast<float>(bits));
884#else
885 // Low-memory gate per FastLED #3082: float JSON literals in
886 // packed-array parse path saturate to zero. The integer-only
887 // RPC contract on LPC8xx / AVR never emits floats here.
888 val = static_cast<T>(0);
889#endif
890 } else {
891 val = static_cast<T>(fl::parseInt(num_start, p - num_start));
892 }
893 out_vec.push_back(val);
894
895 // Skip whitespace
896 while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
897
898 // Skip comma
899 if (p < end && *p == ',') p++;
900 }
901
902 return true;
903 }
904
906 if (mStack.empty()) {
907 mRoot = val;
908 } else {
909 StackFrame& top = mStack.back();
910
911 if (top.type == StackFrame::OBJECT) {
912 // Attach to object with pending key
913 auto obj = top.value->data.ptr<json_object>();
914 if (obj && !top.pending_key.empty()) {
915 (*obj)[top.pending_key] = val;
916 top.pending_key.clear();
917 }
918 } else {
919 // Attach to array
920 auto arr = top.value->data.ptr<json_array>();
921 if (arr) {
922 arr->push_back(val);
923 }
924 }
925 }
926 }
927
928public:
930
932 // Recursion depth check
933 if (mDepth > MAX_JSON_DEPTH) {
934 FL_ERROR("JSON parser: FATAL - recursion depth exceeded " << MAX_JSON_DEPTH);
935 return ParseState::ERROR;
936 }
937
938 switch (token) {
939 // Specialized array tokens - parse directly into typed vectors.
940 // Gated on Low-memory per #3224 Tier 2D: these packed-array tokens
941 // only fire when the tokenizer's `scan_array_lookahead` decides an
942 // array is uniformly-typed and small. LowMemory targets don't use
943 // the array-packing optimization (the integer-only RPC contract on
944 // LPC8xx never sends packed vectors), so dropping these cases
945 // collapses 3 large make_shared<json_value>(vector<T>) variant
946 // emplace instantiations + parse_int_array + parse_float_array.
947#if FL_PLATFORM_HAS_LARGE_MEMORY
949 fl::vector<u8> vec;
950 if (!parse_int_array(value, vec)) return ParseState::ERROR;
951 auto arr_val = fl::make_shared<json_value>(fl::move(vec));
952 push_value(arr_val);
954 }
955
957 fl::vector<i16> vec;
958 if (!parse_int_array(value, vec)) return ParseState::ERROR;
959 auto arr_val = fl::make_shared<json_value>(fl::move(vec));
960 push_value(arr_val);
962 }
963
966 if (!parse_float_array(value, vec)) return ParseState::ERROR;
967 auto arr_val = fl::make_shared<json_value>(fl::move(vec));
968 push_value(arr_val);
970 }
971#endif
972
973 case JsonToken::LBRACE: {
975 // Don't push to parent yet - will push in RBRACE
976 mStack.push_back({StackFrame::OBJECT, obj_val, ""});
977 mDepth++;
979 }
980
981 case JsonToken::RBRACE: {
982 if (!mStack.empty() && mStack.back().type == StackFrame::OBJECT) {
983 auto obj_val = mStack.back().value;
984 mStack.pop_back();
985 mDepth--;
986 // Push to parent after object is complete
987 push_value(obj_val);
988 }
990 }
991
992 case JsonToken::LBRACKET: {
993 auto arr_val = fl::make_shared<json_value>(json_array{});
994 // Don't push to parent yet - will push in RBRACKET after optimization
995 mStack.push_back({StackFrame::ARRAY, arr_val, ""});
996 mDepth++;
998 }
999
1000 case JsonToken::RBRACKET: {
1001 if (!mStack.empty() && mStack.back().type == StackFrame::ARRAY) {
1002 // Optimize array before pushing to parent (Milestone 9)
1003 auto arr_val = optimize_array(mStack.back().value);
1004 mStack.pop_back();
1005 mDepth--;
1006 // Push optimized array to parent
1007 push_value(arr_val);
1008 }
1010 }
1011
1012 case JsonToken::STRING: {
1013 // Check if this is an object key (next token should be COLON)
1014 if (!mStack.empty() && mStack.back().type == StackFrame::OBJECT &&
1015 mStack.back().pending_key.empty()) {
1016 // Key: handle escape sequences, then intern (StringInterner handles SSO internally)
1018 mStack.back().pending_key = mInterner.intern(unescape_string(value));
1019 } else {
1020 mStack.back().pending_key = mInterner.intern(value);
1021 }
1022 } else {
1023 // Value: handle escape sequences, then intern (StringInterner handles SSO internally)
1024 fl::string str;
1026 str = mInterner.intern(unescape_string(value));
1027 } else {
1028 str = mInterner.intern(value);
1029 }
1030 auto str_val = fl::make_shared<json_value>(str);
1031 push_value(str_val);
1032 }
1033
1035 }
1036
1037 case JsonToken::NUMBER: {
1038 // Determine if int or float by checking for decimal/exponent
1039 bool is_float = false;
1040 for (size_t i = 0; i < value.size(); i++) {
1041 if (value[i] == '.' || value[i] == 'e' || value[i] == 'E') {
1042 is_float = true;
1043 break;
1044 }
1045 }
1046
1048#if FL_PLATFORM_HAS_LARGE_MEMORY
1049 if (is_float) {
1050 // Bit-twiddling parse -> IEEE 754 bits -> store as float in the
1051 // variant. The `bit_cast<float>` is a `memcpy` -- no libgcc
1052 // soft-FP helper is reached on the parser hot path
1053 // (FastLED #3022 phase 2).
1054 const u32 bits = fl::ieee754_parse_decimal(value.data(), value.size());
1056 } else {
1057 int i = fl::parseInt(value.data(), value.size());
1058 i64 i64_val = static_cast<i64>(i);
1059 num_val = fl::make_shared<json_value>(i64_val);
1060 }
1061#else
1062 // Low-memory gate per FastLED #3224 Tier 2D: drop the float
1063 // store entirely. JSON float literals on LowMemory parse to
1064 // i64(0), avoiding the variant `float` emplace instantiation
1065 // that #3082's `0.0f` store used to anchor. Combined with
1066 // the ARRAY_FLOAT case gate above, LowMemory's JsonBuilder
1067 // never instantiates a `make_shared<json_value>(float)` or
1068 // `make_shared<json_value>(vector<float>)` path.
1069 (void)is_float;
1070 int i = fl::parseInt(value.data(), value.size());
1071 i64 i64_val = static_cast<i64>(i);
1072 num_val = fl::make_shared<json_value>(i64_val);
1073#endif
1074
1075 push_value(num_val);
1077 }
1078
1079 case JsonToken::TRUE:
1082
1083 case JsonToken::FALSE:
1086
1090
1091 case JsonToken::COLON:
1092 case JsonToken::COMMA:
1093 // Structural tokens - no action needed
1095
1098
1099 default:
1100 return ParseState::ERROR;
1101 }
1102 }
1103
1107};
1108
1109} // namespace
1110
1111// PARSE2 IMPLEMENTATION - Milestone 8: Two-phase parser with validation
1113 JsonTokenizer tokenizer;
1114
1115 // Phase 1: Validate
1116 JsonValidator validator;
1117 if (!tokenizer.parse(txt, validator) || !validator.is_valid()) {
1118 return fl::make_shared<json_value>(nullptr);
1119 }
1120
1121 // Phase 2: Build
1122 JsonBuilder builder;
1123 if (!tokenizer.parse(txt, builder)) {
1124 return fl::make_shared<json_value>(nullptr);
1125 }
1126
1127 return builder.get_result();
1128}
1129
1130// Phase 1 validation only (for testing - MUST allocate zero heap memory)
1132 return parse2_validate_only(fl::string_view(txt.c_str(), txt.length()));
1133}
1134
1136 JsonTokenizer tokenizer;
1137 JsonValidator validator;
1138 return tokenizer.parse(txt, validator) && validator.is_valid();
1139}
1140
1142 // Parse the JSON value to a string, then parse it back to a json object,
1143 // and use the working to_string_native method
1144 // This is a workaround to avoid reimplementing the serialization logic
1145
1146 // First, create a temporary json document with this value
1147 json temp;
1148 // Use the public method to set the value
1150 // Use the working implementation
1151 return temp.to_string_native();
1152}
1153
1154// Recursive serializer visitor - declared and defined at file scope
1155// Receives direct references to variant data (no copies) and handles serialization recursively
1158
1159 void append(const char* str) FL_NO_EXCEPT {
1160 while (*str) { out.push_back(*str++); }
1161 }
1162
1164 for (size_t i = 0; i < str.size(); ++i) {
1165 out.push_back(str[i]);
1166 }
1167 }
1168
1170 out.push_back('"');
1171 for (size_t i = 0; i < str.size(); ++i) {
1172 char c = str[i];
1173 switch (c) {
1174 case '"': append("\\\""); break;
1175 case '\\': append("\\\\"); break;
1176 case '\n': append("\\n"); break;
1177 case '\r': append("\\r"); break;
1178 case '\t': append("\\t"); break;
1179 case '\b': append("\\b"); break;
1180 case '\f': append("\\f"); break;
1181 default: out.push_back(c); break;
1182 }
1183 }
1184 out.push_back('"');
1185 }
1186
1187 // Serialize a json_value recursively
1189 if (!value) {
1190 append("null");
1191 return;
1192 }
1193 value->data.visit(*this);
1194 }
1195
1196 // accept() overloads - called by variant::visit() with direct references
1197 void accept(const fl::nullptr_t&) FL_NO_EXCEPT { append("null"); }
1198
1199 void accept(const bool& b) FL_NO_EXCEPT { append(b ? "true" : "false"); }
1200
1201 void accept(const i64& i) FL_NO_EXCEPT {
1202 fl::string num_str;
1203 num_str.append(i);
1204 append_str(num_str);
1205 }
1206
1207 void accept(const float& f) FL_NO_EXCEPT {
1208#if FL_PLATFORM_HAS_LARGE_MEMORY
1209 // Integer-only bits -> decimal (FastLED #3022 phase 2). Default 3-digit
1210 // precision matches the previous `num_str.append(f, 3)` contract.
1212#else
1213 // Low-memory targets gate the float decimal codec to drop
1214 // `ieee754_format_decimal` (594 B) + `kPow10Mant` LUT (824 B) from the
1215 // link. The integer-only RPC contract on LPC8xx / AVR-class targets
1216 // never serializes float values; this writes a stub for the rare cases
1217 // where a `json(float)` round-trip happens to be exercised in test
1218 // code that also runs on Low-memory. See FastLED #3082.
1219 (void)f;
1220 append_str(fl::string("0"));
1221#endif
1222 }
1223
1225
1226 void accept(const json_array& arr) FL_NO_EXCEPT {
1227 if (arr.empty()) {
1228 append("[]");
1229 return;
1230 }
1231 out.push_back('[');
1232 bool first = true;
1233 for (const auto& item : arr) {
1234 if (!first) out.push_back(',');
1235 first = false;
1236 serialize_value(item ? item.get() : &get_null_json_value());
1237 }
1238 out.push_back(']');
1239 }
1240
1242 if (obj.empty()) {
1243 append("{}");
1244 return;
1245 }
1246 out.push_back('{');
1247 bool first = true;
1248 for (const auto& kv : obj) {
1249 if (!first) out.push_back(',');
1250 first = false;
1251 append_escaped(kv.first);
1252 out.push_back(':');
1253 serialize_value(kv.second ? kv.second.get() : &get_null_json_value());
1254 }
1255 out.push_back('}');
1256 }
1257
1259 out.push_back('[');
1260 bool first = true;
1261 for (const auto& item : audio) {
1262 if (!first) out.push_back(',');
1263 first = false;
1264 fl::string num_str;
1265 num_str.append(static_cast<int>(item));
1266 append_str(num_str);
1267 }
1268 out.push_back(']');
1269 }
1270
1272 out.push_back('[');
1273 bool first = true;
1274 for (const auto& item : bytes) {
1275 if (!first) out.push_back(',');
1276 first = false;
1277 fl::string num_str;
1278 num_str.append(static_cast<int>(item));
1279 append_str(num_str);
1280 }
1281 out.push_back(']');
1282 }
1283
1285 out.push_back('[');
1286 bool first = true;
1287 for (const auto& item : floats) {
1288 if (!first) out.push_back(',');
1289 first = false;
1290#if FL_PLATFORM_HAS_LARGE_MEMORY
1291 // Integer-only bits -> decimal per element (FastLED #3022 phase 2).
1292 // 6-digit precision preserves the previous output contract.
1294#else
1295 // Low-memory gate per FastLED #3082; see scalar `accept(float)`.
1296 (void)item;
1297 append_str(fl::string("0"));
1298#endif
1299 }
1300 out.push_back(']');
1301 }
1302};
1303
1305 if (!mValue) {
1306 return "null";
1307 }
1308
1309 // Use fl::deque for memory-efficient JSON serialization
1310 fl::deque<char> json_chars;
1311
1312 // Use the visitor to serialize the value recursively
1313 SerializerVisitor visitor{json_chars};
1314 visitor.serialize_value(mValue.get());
1315
1316 // Convert deque to fl::string. With FastLED #3270 the deque is chunked,
1317 // so its storage is no longer contiguous -- treating `&json_chars[0]`
1318 // as a buffer would walk off the first chunk. Reserve + per-element
1319 // append is correct for the chunked layout (and was correct for the
1320 // pre-#3270 layout too whenever a wraparound occurred).
1322 if (!json_chars.empty()) {
1323 result.reserve(json_chars.size());
1324 for (fl::size i = 0; i < json_chars.size(); ++i) {
1325 result.push_back(json_chars[i]);
1326 }
1327 }
1328
1329 return result;
1330}
1331
1332// Forward declaration for the serializeValue function
1334
1335
1338 if (!jsonStr) {
1339 return result;
1340 }
1341 size_t len = strlen(jsonStr);
1342 for (size_t i = 0; i < len; ++i) {
1343 char c = jsonStr[i];
1344 if (c != ' ' && c != '\t' && c != '\n' && c != '\r') {
1345 result += c;
1346 }
1347 }
1348 return result;
1349}
1350
1351} // namespace fl
bool parse_float_array(const fl::span< const char > &span, fl::vector< T > &out_vec) FL_NO_EXCEPT
Definition json.cpp.hpp:846
void push_value(const fl::shared_ptr< json_value > &val) FL_NO_EXCEPT
Definition json.cpp.hpp:905
bool parse_int_array(const fl::span< const char > &span, fl::vector< T > &out_vec) FL_NO_EXCEPT
Definition json.cpp.hpp:817
fl::shared_ptr< json_value > get_result() FL_NO_EXCEPT
ParseState on_token(JsonToken token, const fl::span< const char > &value) FL_NO_EXCEPT override
Definition json.cpp.hpp:931
fl::vector_inlined< StackFrame, 8 > mStack
Definition json.cpp.hpp:809
bool parse(fl::string_view input, JsonVisitor &visitor) FL_NO_EXCEPT
Definition json.cpp.hpp:493
JsonTokenizer(bool enable_lookahead=true) FL_NO_EXCEPT
Definition json.cpp.hpp:487
bool parse(const fl::string &input, JsonVisitor &visitor) FL_NO_EXCEPT
Definition json.cpp.hpp:489
JsonToken next_token(fl::span< const char > &out_value) FL_NO_EXCEPT
Definition json.cpp.hpp:313
JsonToken scan_array_lookahead(fl::span< const char > &out_span) FL_NO_EXCEPT
Definition json.cpp.hpp:138
ParseState on_token(JsonToken token, const fl::span< const char > &value) FL_NO_EXCEPT override
Definition json.cpp.hpp:532
virtual ~JsonVisitor() FL_NO_EXCEPT=default
virtual ParseState on_token(JsonToken token, const fl::span< const char > &value) FL_NO_EXCEPT=0
void clear(bool freeMemory=false) FL_NO_EXCEPT
bool empty() const FL_NO_EXCEPT
fl::size size() const FL_NO_EXCEPT
Definition deque.h:517
bool empty() const FL_NO_EXCEPT
Definition deque.h:516
void set_value(const fl::shared_ptr< json_value > &value) FL_NO_EXCEPT
Definition json.h:828
fl::shared_ptr< json_value > mValue
Definition json.h:263
fl::string to_string_native() const FL_NO_EXCEPT
static fl::string normalize_json_string(const char *jsonStr) FL_NO_EXCEPT
const T * data() const FL_NO_EXCEPT
Definition span.h:486
constexpr fl::size size() const FL_NO_EXCEPT
Definition span.h:483
string & append(const bitset_fixed< N > &bs) FL_NO_EXCEPT
Definition string.h:293
void reserve(fl::size n) FL_NO_EXCEPT
Definition vector.h:591
void push_back(const T &value) FL_NO_EXCEPT
Definition vector.h:624
FastLED's Elegant JSON Library: fl::json
#define FL_ERROR(...)
Definition log.h:373
Centralized logging categories for FastLED hardware interfaces and subsystems.
constexpr T * end(T(&array)[N]) FL_NO_EXCEPT
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:30
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
ArrayType classify_array(const json_array &arr) FL_NO_EXCEPT
Definition json.cpp.hpp:672
bool has_escape_sequences(const fl::span< const char > &span) FL_NO_EXCEPT
Definition json.cpp.hpp:633
fl::shared_ptr< json_value > optimize_array(fl::shared_ptr< json_value > array_val) FL_NO_EXCEPT
Definition json.cpp.hpp:745
fl::string unescape_string(const fl::span< const char > &span) FL_NO_EXCEPT
Definition json.cpp.hpp:643
decltype(nullptr) nullptr_t
Definition s16x16x4.h:13
unsigned char u8
Definition stdint.h:131
json_value & get_null_json_value() FL_NO_EXCEPT
Definition json.cpp.hpp:64
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr int type_rank< T >::value
u32 ieee754_parse_decimal(const char *s, fl::size len, fl::size *consumed) FL_NO_EXCEPT
Parse a decimal floating-point number into IEEE 754 single-precision bits.
json_object & get_empty_json_obj() FL_NO_EXCEPT
Definition json.cpp.hpp:69
int parseInt(const char *str, fl::size len) FL_NO_EXCEPT
Parse an integer from a character buffer.
static bool float_bits_magnitude_exceeds_2_24(u32 bits) FL_NO_EXCEPT
Definition json.cpp.hpp:56
fl::vector< fl::shared_ptr< json_value > > json_array
Definition types.h:54
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
To bit_cast(const From &from) FL_NO_EXCEPT
Definition bit_cast.h:48
fl::flat_map< fl::string, fl::shared_ptr< json_value >, fl::StringFastLess > json_object
Definition types.h:55
fl::string ieee754_format_decimal(u32 bits, int precision) FL_NO_EXCEPT
Format IEEE 754 single-precision bits as decimal text.
VectorN< T, INLINED_SIZE > vector_inlined
Definition vector.h:1137
fl::string serializeValue(const json_value &value) FL_NO_EXCEPT
FASTLED_FORCE_INLINE size_t strlen(const char *s) FL_NO_EXCEPT
Definition cstring.h:56
Base definition for an LED controller.
Definition crgb.hpp:179
fl::i64 i64
Definition stdint.h:221
void accept(const json_object &obj) FL_NO_EXCEPT
void accept(const json_array &arr) FL_NO_EXCEPT
void accept(const fl::vector< i16 > &audio) FL_NO_EXCEPT
void accept(const i64 &i) FL_NO_EXCEPT
void serialize_value(const json_value *value) FL_NO_EXCEPT
void accept(const fl::vector< float > &floats) FL_NO_EXCEPT
void append_escaped(const fl::string &str) FL_NO_EXCEPT
void accept(const bool &b) FL_NO_EXCEPT
void accept(const fl::string &s) FL_NO_EXCEPT
fl::deque< char > & out
void accept(const fl::nullptr_t &) FL_NO_EXCEPT
void append(const char *str) FL_NO_EXCEPT
void accept(const fl::vector< u8 > &bytes) FL_NO_EXCEPT
void append_str(const fl::string &str) FL_NO_EXCEPT
void accept(const float &f) FL_NO_EXCEPT
enum fl::anonymous_namespace{json.cpp.hpp}::JsonBuilder::StackFrame::Type type
static bool parse2_validate_only(const fl::string &txt) FL_NO_EXCEPT
static fl::shared_ptr< json_value > parse2(const fl::string &txt) FL_NO_EXCEPT
friend class json
Definition types.h:741
fl::string to_string() const FL_NO_EXCEPT
static constexpr T min() FL_NO_EXCEPT
Definition limits.h:107
static constexpr T max() FL_NO_EXCEPT
Definition limits.h:108