KiCad PCB EDA Suite
Loading...
Searching...
No Matches
text_eval_wrapper.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
21#include <fmt/format.h>
22
23// Include the KiCad common functionality
24#include <common.h>
25#include <fast_float/fast_float.h>
26#include <text_eval/text_eval_types.h> // Parser types
28#include <text_eval/text_eval_units.h> // Centralized unit registry
29
30namespace KI_EVAL
31{
32
33#ifdef __GNUC__
34#pragma GCC diagnostic push
35#pragma GCC diagnostic ignored "-Wunused-variable"
36#pragma GCC diagnostic ignored "-Wsign-compare"
37#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
38#endif
39
40#include <text_eval/text_eval.c>
41
42#ifdef __GNUC__
43#pragma GCC diagnostic pop
44#endif
45}
46
47#include <wx/log.h>
48#include <algorithm>
49#include <regex>
50#include <span>
51
52// // Token type enum matching the generated parser
53enum class TextEvalToken : int
54{
55 ENDS = KI_EVAL_LT - 1,
56 LT = KI_EVAL_LT,
57 GT = KI_EVAL_GT,
58 LE = KI_EVAL_LE,
59 GE = KI_EVAL_GE,
60 EQ = KI_EVAL_EQ,
61 NE = KI_EVAL_NE,
62 PLUS = KI_EVAL_PLUS,
63 MINUS = KI_EVAL_MINUS,
64 MULTIPLY = KI_EVAL_MULTIPLY,
65 DIVIDE = KI_EVAL_DIVIDE,
66 MODULO = KI_EVAL_MODULO,
67 UMINUS = KI_EVAL_UMINUS,
68 POWER = KI_EVAL_POWER,
69 COMMA = KI_EVAL_COMMA,
70 TEXT = KI_EVAL_TEXT,
71 AT_OPEN = KI_EVAL_AT_OPEN,
72 CLOSE_BRACE = KI_EVAL_CLOSE_BRACE,
73 LPAREN = KI_EVAL_LPAREN,
74 RPAREN = KI_EVAL_RPAREN,
75 NUMBER = KI_EVAL_NUMBER,
76 STRING = KI_EVAL_STRING,
77 IDENTIFIER = KI_EVAL_IDENTIFIER,
78 DOLLAR_OPEN = KI_EVAL_DOLLAR_OPEN,
79};
80
81// UTF-8 <-> UTF-32 conversion utilities
82namespace utf8_utils {
83
84 // Concept for UTF-8 byte validation
85 template<typename T>
86 concept Utf8Byte = std::same_as<T, char> || std::same_as<T, unsigned char> || std::same_as<T, std::byte>;
87
88 // UTF-8 validation and conversion
90 {
91 private:
92 // UTF-8 byte classification using bit operations
93 static constexpr bool is_ascii( std::byte b ) noexcept
94 {
95 return ( b & std::byte{ 0x80 } ) == std::byte{ 0x00 };
96 }
97
98 static constexpr bool is_continuation( std::byte b ) noexcept
99 {
100 return ( b & std::byte{ 0xC0 } ) == std::byte{ 0x80 };
101 }
102
103 static constexpr int sequence_length( std::byte first ) noexcept
104 {
105 if( is_ascii( first ) )
106 return 1;
107 if( ( first & std::byte{ 0xE0 } ) == std::byte{ 0xC0 } )
108 return 2;
109 if( ( first & std::byte{ 0xF0 } ) == std::byte{ 0xE0 } )
110 return 3;
111 if( ( first & std::byte{ 0xF8 } ) == std::byte{ 0xF0 } )
112 return 4;
113 return 0; // Invalid
114 }
115
116 public:
117 // Convert UTF-8 string to UTF-32 codepoints using C++20 ranges
118 static std::u32string to_utf32( std::string_view utf8 )
119 {
120 std::u32string result;
121 result.reserve( utf8.size() ); // Conservative estimate
122
123 auto bytes = std::as_bytes( std::span{ utf8.data(), utf8.size() } );
124
125 for( size_t i = 0; i < bytes.size(); )
126 {
127 std::byte first = bytes[i];
128 int len = sequence_length( first );
129
130 if( len == 0 || i + len > bytes.size() )
131 {
132 // Invalid sequence - insert replacement character
133 result.push_back( U'\uFFFD' );
134 i++;
135 continue;
136 }
137
138 char32_t codepoint = 0;
139
140 switch( len )
141 {
142 case 1: codepoint = std::to_integer<char32_t>( first ); break;
143
144 case 2:
145 {
146 if( !is_continuation( bytes[i + 1] ) )
147 {
148 result.push_back( U'\uFFFD' );
149 i++;
150 continue;
151 }
152 codepoint = ( std::to_integer<char32_t>( first & std::byte{ 0x1F } ) << 6 )
153 | std::to_integer<char32_t>( bytes[i + 1] & std::byte{ 0x3F } );
154 break;
155 }
156
157 case 3:
158 {
159 if( !is_continuation( bytes[i + 1] ) || !is_continuation( bytes[i + 2] ) )
160 {
161 result.push_back( U'\uFFFD' );
162 i++;
163 continue;
164 }
165 codepoint = ( std::to_integer<char32_t>( first & std::byte{ 0x0F } ) << 12 )
166 | ( std::to_integer<char32_t>( bytes[i + 1] & std::byte{ 0x3F } ) << 6 )
167 | std::to_integer<char32_t>( bytes[i + 2] & std::byte{ 0x3F } );
168 break;
169 }
170
171 case 4:
172 {
173 if( !is_continuation( bytes[i + 1] ) || !is_continuation( bytes[i + 2] )
174 || !is_continuation( bytes[i + 3] ) )
175 {
176 result.push_back( U'\uFFFD' );
177 i++;
178 continue;
179 }
180 codepoint = ( std::to_integer<char32_t>( first & std::byte{ 0x07 } ) << 18 )
181 | ( std::to_integer<char32_t>( bytes[i + 1] & std::byte{ 0x3F } ) << 12 )
182 | ( std::to_integer<char32_t>( bytes[i + 2] & std::byte{ 0x3F } ) << 6 )
183 | std::to_integer<char32_t>( bytes[i + 3] & std::byte{ 0x3F } );
184 break;
185 }
186 }
187
188 // Validate codepoint range
189 if( codepoint > 0x10FFFF || ( codepoint >= 0xD800 && codepoint <= 0xDFFF ) )
190 {
191 result.push_back( U'\uFFFD' ); // Replacement character
192 }
193 else if( len == 2 && codepoint < 0x80 )
194 {
195 result.push_back( U'\uFFFD' ); // Overlong encoding
196 }
197 else if( len == 3 && codepoint < 0x800 )
198 {
199 result.push_back( U'\uFFFD' ); // Overlong encoding
200 }
201 else if( len == 4 && codepoint < 0x10000 )
202 {
203 result.push_back( U'\uFFFD' ); // Overlong encoding
204 }
205 else
206 {
207 result.push_back( codepoint );
208 }
209
210 i += len;
211 }
212
213 return result;
214 }
215
216 // Convert UTF-32 to UTF-8
217 static std::string to_utf8( std::u32string_view utf32 )
218 {
219 std::string result;
220 result.reserve( utf32.size() * 4 ); // Maximum possible size
221
222 for( char32_t cp : utf32 )
223 {
224 if( cp <= 0x7F )
225 {
226 // 1-byte sequence
227 result.push_back( static_cast<char>( cp ) );
228 }
229 else if( cp <= 0x7FF )
230 {
231 // 2-byte sequence
232 result.push_back( static_cast<char>( 0xC0 | ( cp >> 6 ) ) );
233 result.push_back( static_cast<char>( 0x80 | ( cp & 0x3F ) ) );
234 }
235 else if( cp <= 0xFFFF )
236 {
237 // 3-byte sequence
238 if( cp >= 0xD800 && cp <= 0xDFFF )
239 {
240 // Surrogate pair - invalid in UTF-32
241 result.append( "\uFFFD" ); // Replacement character in UTF-8
242 }
243 else
244 {
245 result.push_back( static_cast<char>( 0xE0 | ( cp >> 12 ) ) );
246 result.push_back( static_cast<char>( 0x80 | ( ( cp >> 6 ) & 0x3F ) ) );
247 result.push_back( static_cast<char>( 0x80 | ( cp & 0x3F ) ) );
248 }
249 }
250 else if( cp <= 0x10FFFF )
251 {
252 // 4-byte sequence
253 result.push_back( static_cast<char>( 0xF0 | ( cp >> 18 ) ) );
254 result.push_back( static_cast<char>( 0x80 | ( ( cp >> 12 ) & 0x3F ) ) );
255 result.push_back( static_cast<char>( 0x80 | ( ( cp >> 6 ) & 0x3F ) ) );
256 result.push_back( static_cast<char>( 0x80 | ( cp & 0x3F ) ) );
257 }
258 else
259 {
260 // Invalid codepoint
261 result.append( "\uFFFD" ); // Replacement character in UTF-8
262 }
263 }
264
265 return result;
266 }
267 };
268
269 template<typename T>
270 concept UnicodeCodepoint = std::same_as<T, char32_t>;
271
273 static constexpr bool is_whitespace(UnicodeCodepoint auto cp) noexcept {
274 // Unicode whitespace categories
275 return cp == U' ' || cp == U'\t' || cp == U'\r' || cp == U'\n' ||
276 cp == U'\f' || cp == U'\v' || cp == U'\u00A0' || // Non-breaking space
277 cp == U'\u2000' || cp == U'\u2001' || cp == U'\u2002' || cp == U'\u2003' ||
278 cp == U'\u2004' || cp == U'\u2005' || cp == U'\u2006' || cp == U'\u2007' ||
279 cp == U'\u2008' || cp == U'\u2009' || cp == U'\u200A' || cp == U'\u2028' ||
280 cp == U'\u2029' || cp == U'\u202F' || cp == U'\u205F' || cp == U'\u3000';
281 }
282
283 static constexpr bool is_digit(UnicodeCodepoint auto cp) noexcept {
284 return cp >= U'0' && cp <= U'9';
285 }
286
287 static constexpr bool is_ascii_alpha(UnicodeCodepoint auto cp) noexcept {
288 return (cp >= U'a' && cp <= U'z') || (cp >= U'A' && cp <= U'Z');
289 }
290
291 static constexpr bool is_alpha(UnicodeCodepoint auto cp) noexcept {
292 // Basic Latin + extended Unicode letter ranges
293 return is_ascii_alpha(cp) || (cp >= 0x80 && cp <= 0x10FFFF && cp != 0xFFFD);
294 }
295
296 static constexpr bool is_alnum(UnicodeCodepoint auto cp) noexcept {
297 return is_alpha(cp) || is_digit(cp);
298 }
299 };
300
302 {
303 struct PREFIX
304 {
305 char32_t symbol;
307 };
308
309 static constexpr std::array<PREFIX, 18> prefixes = { {
310 {U'a', 1e-18},
311 {U'f', 1e-15},
312 {U'p', 1e-12},
313 {U'n', 1e-9},
314 {U'u', 1e-6}, {U'µ', 1e-6}, {U'μ', 1e-6}, // Various micro symbols
315 {U'm', 1e-3},
316 {U'k', 1e3}, {U'K', 1e3},
317 {U'M', 1e6},
318 {U'G', 1e9},
319 {U'T', 1e12},
320 {U'P', 1e15},
321 {U'E', 1e18}
322 } };
323
324 static constexpr bool is_si_prefix( UnicodeCodepoint auto cp ) noexcept
325 {
326 return std::ranges::any_of( prefixes,
327 [cp]( const PREFIX& p )
328 {
329 return p.symbol == cp;
330 } );
331 }
332
333 static constexpr double get_multiplier( UnicodeCodepoint auto cp ) noexcept
334 {
335 auto it = std::ranges::find_if( prefixes,
336 [cp]( const PREFIX& p )
337 {
338 return p.symbol == cp;
339 } );
340 return it != prefixes.end() ? it->multiplier : 1.0;
341 }
342 };
343}
344
345// Unit conversion utilities for the text evaluator
347
348 // Internal unit enum matching NUMERIC_EVALUATOR
364
365 // Convert EDA_UNITS to internal Unit enum
367 {
368 switch( aUnits )
369 {
370 case EDA_UNITS::MM: return Unit::MM;
371 case EDA_UNITS::MILS: return Unit::Mil;
372 case EDA_UNITS::INCH: return Unit::Inch;
375 case EDA_UNITS::PS: return Unit::Picoseconds;
379 case EDA_UNITS::UM: return Unit::UM;
380 case EDA_UNITS::CM: return Unit::CM;
381 case EDA_UNITS::UNSCALED: return Unit::SI;
382 default: return Unit::MM;
383 }
384 }
385
386 // Parse unit from string using centralized registry
387 Unit parseUnit( const std::string& aUnitStr )
388 {
389 auto evalUnit = text_eval_units::UnitRegistry::parseUnit( aUnitStr );
390
391 // Convert text_eval_units::Unit to KIEVAL_UNIT_CONV::Unit
392 switch( evalUnit )
393 {
408 default: return Unit::Invalid;
409 }
410 }
411
412 // Get conversion factor from one unit to another (based on numeric_evaluator logic)
413 double getConversionFactor( Unit aFromUnit, Unit aToUnit )
414 {
415 if( aFromUnit == aToUnit )
416 return 1.0;
417
418 // Convert to MM first, then to target unit
419 double toMM = 1.0;
420 switch( aFromUnit )
421 {
422 case Unit::Inch: toMM = 25.4; break;
423 case Unit::Mil: toMM = 25.4 / 1000.0; break;
424 case Unit::UM: toMM = 1.0 / 1000.0; break;
425 case Unit::MM: toMM = 1.0; break;
426 case Unit::CM: toMM = 10.0; break;
427 default: return 1.0; // No conversion for other units
428 }
429
430 double fromMM = 1.0;
431 switch( aToUnit )
432 {
433 case Unit::Inch: fromMM = 1.0 / 25.4; break;
434 case Unit::Mil: fromMM = 1000.0 / 25.4; break;
435 case Unit::UM: fromMM = 1000.0; break;
436 case Unit::MM: fromMM = 1.0; break;
437 case Unit::CM: fromMM = 1.0 / 10.0; break;
438 default: return 1.0; // No conversion for other units
439 }
440
441 return toMM * fromMM;
442 }
443
444 // Convert a value with units to the default units using centralized registry
445 double convertToDefaultUnits( double aValue, const std::string& aUnitStr, EDA_UNITS aDefaultUnits )
446 {
447 return text_eval_units::UnitRegistry::convertToEdaUnits( aValue, aUnitStr, aDefaultUnits );
448 }
449}
450
451
453{
454private:
456 {
457 TEXT, // Regular text content - alphabetic should be TEXT tokens
458 EXPRESSION // Inside @{...} or ${...} - alphabetic should be IDENTIFIER tokens
459 };
460
461 std::u32string m_text;
462 size_t m_pos{ 0 };
463 size_t m_line{ 1 };
464 size_t m_column{ 1 };
466 int m_braceNestingLevel{ 0 }; // Track nesting level of expressions
468 EDA_UNITS m_defaultUnits{ EDA_UNITS::MM }; // Add default units for conversion
469
472
473 [[nodiscard]] char32_t current_char() const noexcept
474 {
475 return m_pos < m_text.size() ? m_text[m_pos] : U'\0';
476 }
477
478 [[nodiscard]] char32_t peek_char( size_t offset = 1 ) const noexcept
479 {
480 size_t peek_pos = m_pos + offset;
481 return peek_pos < m_text.size() ? m_text[peek_pos] : U'\0';
482 }
483
484 void advance_position( size_t count = 1 ) noexcept
485 {
486 for( size_t i = 0; i < count && m_pos < m_text.size(); ++i )
487 {
488 if( m_text[m_pos] == U'\n' )
489 {
490 ++m_line;
491 m_column = 1;
492 }
493 else
494 {
495 ++m_column;
496 }
497 ++m_pos;
498 }
499 }
500
501 void skip_whitespace() noexcept
502 {
503 while( m_pos < m_text.size() && CLASSIFIER::is_whitespace( current_char() ) )
505 }
506
507 void add_error( std::string_view message ) const
508 {
509 if( m_errorCollector )
510 {
511 auto error_msg = fmt::format( "Line {}, Column {}: {}", m_line, m_column, message );
512 m_errorCollector->AddError( error_msg );
513 }
514 }
515
516 [[nodiscard]] static calc_parser::TOKEN_TYPE make_string_token( std::string value ) noexcept
517 {
519 token.isString = true;
520 std::strncpy( token.text, value.c_str(), sizeof( token.text ) - 1 );
521 token.text[sizeof( token.text ) - 1] = '\0';
522 token.dValue = 0.0;
523 return token;
524 }
525
526 [[nodiscard]] static constexpr calc_parser::TOKEN_TYPE make_number_token( double value ) noexcept
527 {
529 token.isString = false;
530 token.dValue = value;
531 return token;
532 }
533
534 [[nodiscard]] calc_parser::TOKEN_TYPE parse_string_literal( char32_t quote_char )
535 {
536 advance_position(); // Skip opening quote
537
538 std::u32string content;
539 content.reserve( 64 ); // Reasonable default
540
541 while( m_pos < m_text.size() && current_char() != quote_char )
542 {
543 char32_t c = current_char();
544
545 if( c == U'\\' && m_pos + 1 < m_text.size() )
546 {
547 char32_t escaped = peek_char();
548 advance_position( 2 );
549
550 switch( escaped )
551 {
552 case U'n': content.push_back( U'\n' ); break;
553 case U't': content.push_back( U'\t' ); break;
554 case U'r': content.push_back( U'\r' ); break;
555 case U'\\': content.push_back( U'\\' ); break;
556 case U'"': content.push_back( U'"' ); break;
557 case U'\'': content.push_back( U'\'' ); break;
558 case U'0': content.push_back( U'\0' ); break;
559 case U'x':
560 {
561 // Hexadecimal escape \xHH
562 std::u32string hex;
563
564 for( int i = 0; i < 2 && m_pos < m_text.size(); ++i )
565 {
566 char32_t hex_char = current_char();
567 if( ( hex_char >= U'0' && hex_char <= U'9' )
568 || ( hex_char >= U'A' && hex_char <= U'F' )
569 || ( hex_char >= U'a' && hex_char <= U'f' ) )
570 {
571 hex.push_back( hex_char );
573 }
574 else
575 {
576 break;
577 }
578 }
579
580 if( !hex.empty() )
581 {
582 try
583 {
584 auto hex_str = utf8_utils::UTF8_CONVERTER::to_utf8( hex );
585 auto value = std::stoul( hex_str, nullptr, 16 );
586
587 if( value <= 0x10FFFF )
588 content.push_back( static_cast<char32_t>( value ) );
589 else
590 content.push_back( U'\uFFFD' );
591 }
592 catch( ... )
593 {
594 content.push_back( U'\uFFFD' );
595 }
596 }
597 else
598 {
599 content.append( U"\\x" );
600 }
601
602 break;
603 }
604 default:
605 content.push_back( U'\\' );
606 content.push_back( escaped );
607 break;
608 }
609 }
610 else if( c == U'\n' )
611 {
612 add_error( "Unterminated string literal" );
613 break;
614 }
615 else
616 {
617 content.push_back( c );
619 }
620 }
621
622 if (m_pos < m_text.size() && current_char() == quote_char) {
623 advance_position(); // Skip closing quote
624 } else {
625 add_error("Missing closing quote in string literal");
626 }
627
629 }
630
632 {
633 std::u32string number_text;
634 number_text.reserve( 32 );
635
636 bool has_decimal = false;
637 double multiplier = 1.0;
638
639 // Parse integer part
640 while( m_pos < m_text.size() && CLASSIFIER::is_digit( current_char() ) )
641 {
642 number_text.push_back( current_char() );
644 }
645
646 // Handle decimal point, SI prefix, or unit suffix
647 if( m_pos < m_text.size() )
648 {
649 char32_t c = current_char();
650
651 // Only treat comma as decimal separator in text context, not expression context
652 // This prevents comma from interfering with function argument separation
653 if( c == U'.' || ( c == U',' && m_context != TOKENIZER_CONTEXT::EXPRESSION ) )
654 {
655 number_text.push_back( U'.' );
656 has_decimal = true;
658 }
660 {
661 // In expression context, check for unit first before SI prefix (unit strings are longer)
662 // Look ahead to see if we have a complete unit string
663 std::u32string potential_unit;
664 size_t temp_pos = m_pos;
665
666 while( temp_pos < m_text.size() )
667 {
668 char32_t unit_char = m_text[temp_pos];
669
670 if( CLASSIFIER::is_alpha( unit_char ) || unit_char == U'"' || unit_char == U'\'' )
671 {
672 potential_unit.push_back( unit_char );
673 temp_pos++;
674 }
675 else
676 {
677 break;
678 }
679 }
680
681 // Check if we have a valid unit
682 if( !potential_unit.empty() )
683 {
684 std::string unit_str = utf8_utils::UTF8_CONVERTER::to_utf8( potential_unit );
685 KIEVAL_UNIT_CONV::Unit parsed_unit = KIEVAL_UNIT_CONV::parseUnit( unit_str );
686
687 if( parsed_unit != KIEVAL_UNIT_CONV::Unit::Invalid )
688 {
689 // This is a valid unit - don't treat the first character as SI prefix
690 // The unit parsing will happen later
691 }
692 else if( SI_HANDLER::is_si_prefix( c ) && !has_decimal )
693 {
694 // Not a valid unit, so treat as SI prefix
695 multiplier = SI_HANDLER::get_multiplier( c );
696 number_text.push_back( U'.' );
697 has_decimal = true;
699 }
700 }
701 else if( SI_HANDLER::is_si_prefix( c ) && !has_decimal )
702 {
703 // No alphabetic characters following, so treat as SI prefix
704 multiplier = SI_HANDLER::get_multiplier( c );
705 number_text.push_back( U'.' );
706 has_decimal = true;
708 }
709 }
710 else if( SI_HANDLER::is_si_prefix( c ) && !has_decimal )
711 {
712 // In text context, treat as SI prefix
713 multiplier = SI_HANDLER::get_multiplier( c );
714 number_text.push_back( U'.' );
715 has_decimal = true;
717 }
718 }
719
720 // Parse fractional part
721 while (m_pos < m_text.size() && CLASSIFIER::is_digit(current_char())) {
722 number_text.push_back(current_char());
724 }
725
726 // Convert to double safely
727 auto number_str = utf8_utils::UTF8_CONVERTER::to_utf8( number_text );
728 double value = 0.0;
729
730 try
731 {
732 if( !number_str.empty() && number_str != "." )
733 {
734 auto result = fast_float::from_chars( number_str.data(), number_str.data() + number_str.size(), value );
735
736 if( result.ec != std::errc() || result.ptr != number_str.data() + number_str.size() )
737 throw std::invalid_argument( fmt::format( "Cannot convert '{}' to number", number_str ) );
738
739 value *= multiplier;
740
741 if( !std::isfinite( value ) )
742 {
743 add_error( "Number out of range" );
744 value = 0.0;
745 }
746 }
747 }
748 catch( const std::exception& e )
749 {
750 add_error( fmt::format( "Invalid number format: {}", e.what() ) );
751 value = 0.0;
752 }
753
754 // Look for unit suffix
756 {
757 // Skip any whitespace between number and unit
758 size_t whitespace_start = m_pos;
759 while( m_pos < m_text.size() && CLASSIFIER::is_whitespace( current_char() ) )
760 {
762 }
763
764 // Parse potential unit suffix
765 std::u32string unit_text;
766
767 // Look ahead to parse potential unit (letters, quotes, etc.)
768 while( m_pos < m_text.size() )
769 {
770 char32_t c = current_char();
771
772 // Unit characters: letters, quotes for inches
773 if( CLASSIFIER::is_alpha( c ) || c == U'"' || c == U'\'' )
774 {
775 unit_text.push_back( c );
777 }
778 else
779 {
780 break;
781 }
782 }
783
784 if( !unit_text.empty() )
785 {
786 // Convert unit text to string and try to parse it
787 std::string unit_str = utf8_utils::UTF8_CONVERTER::to_utf8( unit_text );
788 KIEVAL_UNIT_CONV::Unit parsed_unit = KIEVAL_UNIT_CONV::parseUnit( unit_str );
789
790 if( parsed_unit != KIEVAL_UNIT_CONV::Unit::Invalid )
791 {
792 // Successfully parsed unit - convert value to default units
793 double converted_value = KIEVAL_UNIT_CONV::convertToDefaultUnits( value, unit_str, m_defaultUnits );
794 value = converted_value;
795 }
796 else
797 {
798 // Not a valid unit - backtrack to before the whitespace
799 m_pos = whitespace_start;
800 }
801 }
802 else
803 {
804 // No unit found - backtrack to before the whitespace
805 m_pos = whitespace_start;
806 }
807 }
808
809 return make_number_token(value);
810 }
811
813 std::u32string identifier;
814 identifier.reserve(64);
815
816 while (m_pos < m_text.size() &&
818 identifier.push_back(current_char());
820 }
821
823 }
824
826 std::u32string text;
827 text.reserve(256);
828
829 while (m_pos < m_text.size()) {
830 char32_t current = current_char();
831 char32_t next = peek_char();
832
833 // Stop at special sequences
834 if ((current == U'@' && next == U'{') ||
835 (current == U'$' && next == U'{')) {
836 break;
837 }
838
839 text.push_back(current);
841 }
842
844 }
845
846public:
847 explicit KIEVAL_TEXT_TOKENIZER( std::string_view input, calc_parser::ERROR_COLLECTOR* error_collector = nullptr,
848 EDA_UNITS default_units = EDA_UNITS::MM ) :
849 m_errorCollector( error_collector ),
850 m_defaultUnits( default_units )
851 {
853 }
854
856 {
857 token_value = calc_parser::TOKEN_TYPE{};
858
859 if( m_pos >= m_text.size() )
860 {
861 return TextEvalToken::ENDS;
862 }
863
864 // Only skip whitespace in expression context
866 {
868
869 if( m_pos >= m_text.size() )
870 {
871 return TextEvalToken::ENDS;
872 }
873 }
874
875 char32_t current = current_char();
876 char32_t next = peek_char();
877
878 // Multi-character tokens that switch to expression context
879 if( current == U'@' && next == U'{' )
880 {
881 advance_position( 2 );
882 m_context = TOKENIZER_CONTEXT::EXPRESSION; // Switch to expression context
883 m_braceNestingLevel++; // Increment nesting level
884 token_value = make_string_token( "@{" );
886 }
887
888 if( current == U'$' && next == U'{' )
889 {
890 advance_position( 2 );
891 m_context = TOKENIZER_CONTEXT::EXPRESSION; // Switch to expression context
892 m_braceNestingLevel++; // Increment nesting level
893 token_value = make_string_token( "${" );
895 }
896
897 // Handle closing brace specially to manage context correctly
898 if( current == U'}' )
899 {
901 m_braceNestingLevel--; // Decrement nesting level
902 if( m_braceNestingLevel <= 0 )
903 {
904 m_braceNestingLevel = 0; // Clamp to zero
905 m_context = TOKENIZER_CONTEXT::TEXT; // Switch back to text context only when fully unnested
906 }
907 token_value = make_string_token( "}" );
909 }
910
911 // Multi-character comparison operators
912 if( current == U'<' && next == U'=' )
913 {
914 advance_position( 2 );
915 token_value = make_string_token( "<=" );
916 return TextEvalToken::LE;
917 }
918 if( current == U'>' && next == U'=' )
919 {
920 advance_position( 2 );
921 token_value = make_string_token( ">=" );
922 return TextEvalToken::GE;
923 }
924 if( current == U'=' && next == U'=' )
925 {
926 advance_position( 2 );
927 token_value = make_string_token( "==" );
928 return TextEvalToken::EQ;
929 }
930 if( current == U'!' && next == U'=' )
931 {
932 advance_position( 2 );
933 token_value = make_string_token( "!=" );
934 return TextEvalToken::NE;
935 }
936
937 // Single character tokens using structured binding
938 // Single character tokens (only in expression context)
940 {
941 static constexpr std::array<std::pair<char32_t, TextEvalToken>, 11> single_char_tokens{{
942 {U'(', TextEvalToken::LPAREN},
943 {U')', TextEvalToken::RPAREN},
944 {U'+', TextEvalToken::PLUS},
945 {U'-', TextEvalToken::MINUS},
947 {U'/', TextEvalToken::DIVIDE},
948 {U'%', TextEvalToken::MODULO},
949 {U'^', TextEvalToken::POWER},
950 {U',', TextEvalToken::COMMA},
951 {U'<', TextEvalToken::LT},
952 {U'>', TextEvalToken::GT}
953 }};
954
955 if( auto it = std::ranges::find_if( single_char_tokens,
956 [current]( const auto& pair ) { return pair.first == current; } );
957 it != single_char_tokens.end() )
958 {
960 token_value = make_string_token( utf8_utils::UTF8_CONVERTER::to_utf8( std::u32string{ current } ) );
961 return it->second;
962 }
963 }
964
965 // Complex tokens
966 if( current == U'"' || current == U'\'' )
967 {
968 token_value = parse_string_literal( current );
970 }
971
972 if( CLASSIFIER::is_digit( current ) || ( current == U'.' && CLASSIFIER::is_digit( next ) ) )
973 {
974 token_value = parse_number();
976 }
977
978 // Context-aware handling of alphabetic content
979 if( CLASSIFIER::is_alpha( current ) || current == U'_' )
980 {
982 {
983 // In expression context, alphabetic content is an identifier
984 token_value = parse_identifier();
986 }
987 else
988 {
989 // In text context, alphabetic content is part of regular text
990 token_value = parse_text_content();
991 return TextEvalToken::TEXT;
992 }
993 }
994
995 // Default to text content
996 token_value = parse_text_content();
997 return token_value.text[0] == U'\0' ? TextEvalToken::ENDS : TextEvalToken::TEXT;
998 }
999
1000 [[nodiscard]] bool has_more_tokens() const noexcept { return m_pos < m_text.size(); }
1001 [[nodiscard]] constexpr size_t get_line() const noexcept { return m_line; }
1002 [[nodiscard]] constexpr size_t get_column() const noexcept { return m_column; }
1003};
1004
1005EXPRESSION_EVALUATOR::EXPRESSION_EVALUATOR( bool aClearVariablesOnEvaluate ) :
1006 m_clearVariablesOnEvaluate( aClearVariablesOnEvaluate ),
1007 m_useCustomCallback( false ),
1008 m_defaultUnits( EDA_UNITS::MM ) // Default to millimeters
1009{
1010 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1011}
1012
1014 bool aClearVariablesOnEvaluate ) :
1015 m_clearVariablesOnEvaluate( aClearVariablesOnEvaluate ),
1016 m_customCallback( std::move( aVariableCallback ) ),
1017 m_useCustomCallback( true ),
1018 m_defaultUnits( EDA_UNITS::MM ) // Default to millimeters
1019{
1020 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1021}
1022
1023EXPRESSION_EVALUATOR::EXPRESSION_EVALUATOR( EDA_UNITS aUnits, bool aClearVariablesOnEvaluate ) :
1024 m_clearVariablesOnEvaluate( aClearVariablesOnEvaluate ),
1025 m_useCustomCallback( false ),
1026 m_defaultUnits( aUnits )
1027{
1028 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1029}
1030
1032 bool aClearVariablesOnEvaluate ) :
1033 m_clearVariablesOnEvaluate( aClearVariablesOnEvaluate ),
1034 m_customCallback( std::move( aVariableCallback ) ),
1035 m_useCustomCallback( true ),
1036 m_defaultUnits( aUnits )
1037{
1038 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1039}
1040
1042
1044 m_variables( aOther.m_variables ),
1049{
1050 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1051 if( aOther.m_lastErrors )
1052 {
1053 // Copy error state
1054 for( const auto& error : aOther.m_lastErrors->GetErrors() )
1055 m_lastErrors->AddError( error );
1056 }
1057}
1058
1060{
1061 if( this != &aOther )
1062 {
1063 m_variables = aOther.m_variables;
1068
1069 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1070 if( aOther.m_lastErrors )
1071 {
1072 for( const auto& error : aOther.m_lastErrors->GetErrors() )
1073 m_lastErrors->AddError( error );
1074 }
1075 }
1076 return *this;
1077}
1078
1080 m_variables( std::move( aOther.m_variables ) ),
1081 m_lastErrors( std::move( aOther.m_lastErrors ) ),
1082 m_clearVariablesOnEvaluate( aOther.m_clearVariablesOnEvaluate ),
1083 m_customCallback( std::move( aOther.m_customCallback ) ),
1084 m_useCustomCallback( aOther.m_useCustomCallback ),
1085 m_defaultUnits( aOther.m_defaultUnits )
1086{
1087}
1088
1090{
1091 if( this != &aOther )
1092 {
1093 m_variables = std::move( aOther.m_variables );
1094 m_lastErrors = std::move( aOther.m_lastErrors );
1095 m_clearVariablesOnEvaluate = aOther.m_clearVariablesOnEvaluate;
1096 m_customCallback = std::move( aOther.m_customCallback );
1097 m_useCustomCallback = aOther.m_useCustomCallback;
1098 m_defaultUnits = aOther.m_defaultUnits;
1099 }
1100 return *this;
1101}
1102
1104{
1105 m_customCallback = std::move( aCallback );
1106 m_useCustomCallback = true;
1107}
1108
1114
1119
1121{
1122 m_defaultUnits = aUnits;
1123}
1124
1129
1130void EXPRESSION_EVALUATOR::SetVariable( const wxString& aName, double aValue )
1131{
1132 std::string name = wxStringToStdString( aName );
1133 m_variables[name] = calc_parser::Value{ aValue };
1134}
1135
1136void EXPRESSION_EVALUATOR::SetVariable( const wxString& aName, const wxString& aValue )
1137{
1138 std::string name = wxStringToStdString( aName );
1139 std::string value = wxStringToStdString( aValue );
1141}
1142
1143void EXPRESSION_EVALUATOR::SetVariable( const std::string& aName, const std::string& aValue )
1144{
1145 m_variables[aName] = calc_parser::Value{ aValue };
1146}
1147
1148bool EXPRESSION_EVALUATOR::RemoveVariable( const wxString& aName )
1149{
1150 std::string name = wxStringToStdString( aName );
1151 return m_variables.erase( name ) > 0;
1152}
1153
1155{
1156 m_variables.clear();
1157}
1158
1159bool EXPRESSION_EVALUATOR::HasVariable( const wxString& aName ) const
1160{
1161 std::string name = wxStringToStdString( aName );
1162 return m_variables.find( name ) != m_variables.end();
1163}
1164
1165wxString EXPRESSION_EVALUATOR::GetVariable( const wxString& aName ) const
1166{
1167 std::string name = wxStringToStdString( aName );
1168 auto it = m_variables.find( name );
1169 if( it != m_variables.end() )
1170 {
1171 if( std::holds_alternative<double>( it->second ) )
1172 {
1173 double val = std::get<double>( it->second );
1174 // Smart formatting - whole numbers don't need decimal places
1175 if( val == std::floor( val ) && std::abs( val ) < 1e15 )
1176 return wxString::Format( "%.0f", val );
1177 else
1178 return wxString::Format( "%g", val );
1179 }
1180 else
1181 {
1182 return stdStringToWxString( std::get<std::string>( it->second ) );
1183 }
1184 }
1185 return wxString{};
1186}
1187
1188std::vector<wxString> EXPRESSION_EVALUATOR::GetVariableNames() const
1189{
1190 std::vector<wxString> names;
1191 names.reserve( m_variables.size() );
1192
1193 for( const auto& [name, value] : m_variables )
1194 names.push_back( stdStringToWxString( name ) );
1195
1196 return names;
1197}
1198
1199void EXPRESSION_EVALUATOR::SetVariables( const std::unordered_map<wxString, double>& aVariables )
1200{
1201 for( const auto& [name, value] : aVariables )
1202 SetVariable( name, value );
1203}
1204
1205void EXPRESSION_EVALUATOR::SetVariables( const std::unordered_map<wxString, wxString>& aVariables )
1206{
1207 for( const auto& [name, value] : aVariables )
1208 SetVariable( name, value );
1209}
1210
1211wxString EXPRESSION_EVALUATOR::Evaluate( const wxString& aInput )
1212{
1213 std::unordered_map<wxString, double> emptyNumVars;
1214 std::unordered_map<wxString, wxString> emptyStringVars;
1215 return Evaluate( aInput, emptyNumVars, emptyStringVars );
1216}
1217
1218wxString EXPRESSION_EVALUATOR::Evaluate( const wxString& aInput,
1219 const std::unordered_map<wxString, double>& aTempVariables )
1220{
1221 std::unordered_map<wxString, wxString> emptyStringVars;
1222 return Evaluate( aInput, aTempVariables, emptyStringVars );
1223}
1224
1225wxString EXPRESSION_EVALUATOR::Evaluate( const wxString& aInput,
1226 const std::unordered_map<wxString, double>& aTempNumericVars,
1227 const std::unordered_map<wxString, wxString>& aTempStringVars )
1228{
1229 // Clear previous errors
1230 ClearErrors();
1231
1232 // Expand ${variable} patterns that are OUTSIDE of @{} expressions
1233 wxString processedInput = expandVariablesOutsideExpressions( aInput, aTempNumericVars, aTempStringVars );
1234
1235 // Convert processed input to std::string
1236 std::string input = wxStringToStdString( processedInput ); // Create combined callback for all variable sources
1237 auto combinedCallback = createCombinedCallback( &aTempNumericVars, &aTempStringVars );
1238
1239 // Evaluate using parser
1240 auto [result, hadErrors] = evaluateWithParser( input, combinedCallback );
1241
1242 // Update error state if evaluation had errors
1243 if( hadErrors && !m_lastErrors )
1244 {
1245 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1246 }
1247 if( hadErrors )
1248 {
1249 m_lastErrors->AddError( "Evaluation failed" );
1250 }
1251
1252 // Clear variables if requested
1255
1256 // Convert result back to wxString
1257 return stdStringToWxString( result );
1258}
1259
1261{
1262 return m_lastErrors && m_lastErrors->HasErrors();
1263}
1264
1266{
1267 if( !m_lastErrors )
1268 return wxString{};
1269
1270 return stdStringToWxString( m_lastErrors->GetAllMessages() );
1271}
1272
1274{
1275 if( !m_lastErrors )
1276 return 0;
1277
1278 return m_lastErrors->GetErrors().size();
1279}
1280
1281std::vector<wxString> EXPRESSION_EVALUATOR::GetErrors() const
1282{
1283 std::vector<wxString> result;
1284
1285 if( m_lastErrors )
1286 {
1287 const auto& errors = m_lastErrors->GetErrors();
1288 result.reserve( errors.size() );
1289
1290 for( const auto& error : errors )
1291 result.push_back( stdStringToWxString( error ) );
1292 }
1293
1294 return result;
1295}
1296
1298{
1299 if( m_lastErrors )
1300 m_lastErrors->Clear();
1301}
1302
1307
1312
1313bool EXPRESSION_EVALUATOR::TestExpression( const wxString& aExpression )
1314{
1315 // Create a test input with the expression wrapped in @{}
1316 wxString testInput = "@{" + aExpression + "}";
1317
1318 // Create a minimal callback that returns errors for all variables
1319 auto testCallback = []( const std::string& aVarName ) -> calc_parser::Result<calc_parser::Value>
1320 {
1321 return calc_parser::MakeError<calc_parser::Value>( "Test mode - no variables available" );
1322 };
1323
1324 // Try to parse it
1325 std::string input = wxStringToStdString( testInput );
1326 auto [result, hadErrors] = evaluateWithParser( input, testCallback );
1327
1328 // Check if there were parsing errors (ignore evaluation errors for undefined variables)
1329 if( m_lastErrors )
1330 {
1331 const auto& errors = m_lastErrors->GetErrors();
1332 // Filter out "Test mode - no variables available" errors, look for syntax errors
1333 for( const auto& error : errors )
1334 {
1335 if( error.find( "Syntax error" ) != std::string::npos ||
1336 error.find( "Parser failed" ) != std::string::npos )
1337 {
1338 return false; // Found syntax error
1339 }
1340 }
1341 }
1342
1343 return true; // No syntax errors found
1344}
1345
1346size_t EXPRESSION_EVALUATOR::CountExpressions( const wxString& aInput ) const
1347{
1348 size_t count = 0;
1349 size_t pos = 0;
1350
1351 while( ( pos = aInput.find( "@{", pos ) ) != wxString::npos )
1352 {
1353 count++;
1354 pos += 2; // Move past "@{"
1355 }
1356
1357 return count;
1358}
1359
1360std::vector<wxString> EXPRESSION_EVALUATOR::ExtractExpressions( const wxString& aInput ) const
1361{
1362 std::vector<wxString> expressions;
1363 size_t pos = 0;
1364
1365 while( ( pos = aInput.find( "@{", pos ) ) != wxString::npos )
1366 {
1367 size_t start = pos + 2; // Skip "@{"
1368 size_t end = aInput.find( "}", start );
1369
1370 if( end != wxString::npos )
1371 {
1372 expressions.push_back( aInput.substr( start, end - start ) );
1373 pos = end + 1;
1374 }
1375 else
1376 {
1377 break; // No closing brace found
1378 }
1379 }
1380
1381 return expressions;
1382}
1383
1384std::string EXPRESSION_EVALUATOR::wxStringToStdString( const wxString& aWxStr ) const
1385{
1386 return aWxStr.ToStdString( wxConvUTF8 );
1387}
1388
1389wxString EXPRESSION_EVALUATOR::stdStringToWxString( const std::string& aStdStr ) const
1390{
1391 return wxString( aStdStr.c_str(), wxConvUTF8 );
1392}
1393
1395 const wxString& aInput,
1396 const std::unordered_map<wxString, double>& aTempNumericVars,
1397 const std::unordered_map<wxString, wxString>& aTempStringVars ) const
1398{
1399 wxString result = aInput;
1400 size_t pos = 0;
1401
1402 // Track positions of @{} expressions to avoid substituting inside them
1403 std::vector<std::pair<size_t, size_t>> expressionRanges;
1404
1405 // Find all @{} expression ranges
1406 while( (pos = result.find( "@{", pos )) != std::string::npos )
1407 {
1408 size_t start = pos;
1409 size_t braceCount = 1;
1410 size_t searchPos = start + 2; // Skip "@{"
1411
1412 // Find matching closing brace
1413 while( searchPos < result.length() && braceCount > 0 )
1414 {
1415 if( result[searchPos] == '{' )
1416 braceCount++;
1417 else if( result[searchPos] == '}' )
1418 braceCount--;
1419 searchPos++;
1420 }
1421
1422 if( braceCount == 0 )
1423 {
1424 expressionRanges.emplace_back( start, searchPos ); // searchPos is after '}'
1425 }
1426
1427 pos = searchPos;
1428 }
1429
1430 // Now find and replace ${variable} patterns that are NOT inside @{} expressions
1431 pos = 0;
1432 while( (pos = result.find( "${", pos )) != std::string::npos )
1433 {
1434 // Check if this ${} is inside any @{} expression
1435 bool insideExpression = false;
1436 for( const auto& range : expressionRanges )
1437 {
1438 if( pos >= range.first && pos < range.second )
1439 {
1440 insideExpression = true;
1441 break;
1442 }
1443 }
1444
1445 if( insideExpression )
1446 {
1447 // Special case: if this variable is immediately followed by unit text,
1448 // we should expand it to allow proper unit parsing
1449 size_t closePos = result.find( "}", pos + 2 );
1450 if( closePos != std::string::npos )
1451 {
1452 // Check what comes after the closing brace
1453 size_t afterBrace = closePos + 1;
1454 bool followedByUnit = false;
1455
1456 if( afterBrace < result.length() )
1457 {
1458 // Check if followed by any supported unit strings using centralized registry
1460 for( const auto& unit : units )
1461 {
1462 if( afterBrace + unit.length() <= result.length() &&
1463 result.substr( afterBrace, unit.length() ) == unit )
1464 {
1465 followedByUnit = true;
1466 break;
1467 }
1468 }
1469 }
1470
1471 if( !followedByUnit )
1472 {
1473 pos += 2; // Skip this ${} since it's inside an expression and not followed by units
1474 continue;
1475 }
1476 // If followed by units, continue with variable expansion below
1477 }
1478 else
1479 {
1480 pos += 2; // Invalid pattern, skip
1481 continue;
1482 }
1483 }
1484
1485 // Find the closing brace
1486 size_t closePos = result.find( "}", pos + 2 );
1487 if( closePos == std::string::npos )
1488 {
1489 pos += 2; // Invalid ${} pattern, skip
1490 continue;
1491 }
1492
1493 // Extract variable name
1494 wxString varName = result.substr( pos + 2, closePos - pos - 2 );
1495 wxString replacement;
1496 bool found = false;
1497
1498 // Check temporary string variables first
1499 auto stringIt = aTempStringVars.find( varName );
1500 if( stringIt != aTempStringVars.end() )
1501 {
1502 replacement = stringIt->second;
1503 found = true;
1504 }
1505 else
1506 {
1507 // Check temporary numeric variables
1508 auto numIt = aTempNumericVars.find( varName );
1509 if( numIt != aTempNumericVars.end() )
1510 {
1511 replacement = wxString::FromDouble( numIt->second );
1512 found = true;
1513 }
1514 else
1515 {
1516 // Check instance variables
1517 std::string stdVarName = wxStringToStdString( varName );
1518 auto instIt = m_variables.find( stdVarName );
1519 if( instIt != m_variables.end() )
1520 {
1521 const calc_parser::Value& value = instIt->second;
1522 if( std::holds_alternative<std::string>( value ) )
1523 {
1524 replacement = stdStringToWxString( std::get<std::string>( value ) );
1525 found = true;
1526 }
1527 else if( std::holds_alternative<double>( value ) )
1528 {
1529 replacement = wxString::FromDouble( std::get<double>( value ) );
1530 found = true;
1531 }
1532 }
1533 }
1534 }
1535
1536 if( found )
1537 {
1538 // Replace ${variable} with its value
1539 result.replace( pos, closePos - pos + 1, replacement );
1540 pos += replacement.length();
1541 }
1542 else
1543 {
1544 // Variable not found, record error but leave ${variable} unchanged
1545 if( !m_lastErrors )
1546 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1547 m_lastErrors->AddError( fmt::format( "Undefined variable: {}", wxStringToStdString( varName ) ) );
1548 pos = closePos + 1;
1549 }
1550 }
1551
1552 return result;
1553}
1554
1556 const std::unordered_map<wxString, double>* aTempNumericVars,
1557 const std::unordered_map<wxString, wxString>* aTempStringVars ) const
1558{
1559 return [this, aTempNumericVars, aTempStringVars]( const std::string& aVarName ) -> calc_parser::Result<calc_parser::Value>
1560 {
1561 // Priority 1: Custom callback (if set)
1563 {
1564 auto customResult = m_customCallback( aVarName );
1565 if( customResult.HasValue() )
1566 return customResult;
1567
1568 // If custom callback returned an error, continue to fallback options
1569 // unless the error indicates a definitive "not found" vs "lookup failed"
1570 // For simplicity, we'll always try fallbacks
1571 }
1572
1573 // Priority 2: Temporary string variables
1574 if( aTempStringVars )
1575 {
1576 wxString wxVarName = stdStringToWxString( aVarName );
1577 if( auto it = aTempStringVars->find( wxVarName ); it != aTempStringVars->end() )
1578 {
1579 std::string stdValue = wxStringToStdString( it->second );
1581 }
1582 }
1583
1584 // Priority 3: Temporary numeric variables
1585 if( aTempNumericVars )
1586 {
1587 wxString wxVarName = stdStringToWxString( aVarName );
1588 if( auto it = aTempNumericVars->find( wxVarName ); it != aTempNumericVars->end() )
1589 {
1590 return calc_parser::MakeValue<calc_parser::Value>( it->second );
1591 }
1592 }
1593
1594 // Priority 4: Stored variables
1595 if( auto it = m_variables.find( aVarName ); it != m_variables.end() )
1596 {
1597 return calc_parser::MakeValue<calc_parser::Value>( it->second );
1598 }
1599
1600 // Priority 5: Use KiCad's ExpandTextVars for system/project variables
1601 try
1602 {
1603 wxString varName = stdStringToWxString( aVarName );
1604 wxString testString = wxString::Format( "${%s}", varName );
1605
1606 // Create a resolver that will return true if the variable was found
1607 bool wasResolved = false;
1608 std::function<bool( wxString* )> resolver =
1609 [&wasResolved]( wxString* token ) -> bool
1610 {
1611 // If we get here, ExpandTextVars found the variable and wants to resolve it
1612 // For our purposes, we just want to know if it exists, so return false
1613 // to keep the original ${varname} format, and set our flag
1614 wasResolved = true;
1615 return false; // Don't replace, just detect
1616 };
1617
1618 wxString expandedResult = ExpandTextVars( testString, &resolver );
1619
1620 if( wasResolved )
1621 {
1622 // Variable exists in KiCad's system, now get its actual value
1623 std::function<bool( wxString* )> valueResolver =
1624 []( wxString* token ) -> bool
1625 {
1626 // Let ExpandTextVars resolve this normally
1627 // We'll get the resolved value in token
1628 return false; // Use default resolution
1629 };
1630
1631 wxString resolvedValue = ExpandTextVars( testString, &valueResolver );
1632
1633 // Check if it was actually resolved (not still ${varname})
1634 if( resolvedValue != testString )
1635 {
1636 std::string resolvedStd = wxStringToStdString( resolvedValue );
1637
1638 // Try to parse as number first
1639 try
1640 {
1641 double numValue;
1642 auto result = fast_float::from_chars( resolvedStd.data(), resolvedStd.data() + resolvedStd.size(), numValue );
1643
1644 if( result.ec != std::errc() || result.ptr != resolvedStd.data() + resolvedStd.size() )
1645 throw std::invalid_argument( fmt::format( "Cannot convert '{}' to number", resolvedStd ) );
1646
1648 }
1649 catch( ... )
1650 {
1651 // Not a number, return as string
1652 return calc_parser::MakeValue<calc_parser::Value>( resolvedStd );
1653 }
1654 }
1655 }
1656 }
1657 catch( const std::exception& e )
1658 {
1659 // ExpandTextVars failed, continue to error
1660 }
1661
1662 // Priority 6: If custom callback was tried and failed, return its error
1664 {
1665 return m_customCallback( aVarName ); // Return the original error
1666 }
1667
1668 // No variable found anywhere
1670 fmt::format( "Undefined variable: {}", aVarName ) );
1671 };
1672}
1673
1674std::pair<std::string, bool> EXPRESSION_EVALUATOR::evaluateWithParser(
1675 const std::string& aInput,
1676 VariableCallback aVariableCallback)
1677{
1678 try {
1679 // Try partial error recovery first
1680 auto [partialResult, partialHadErrors] = evaluateWithPartialErrorRecovery(aInput, aVariableCallback);
1681
1682 // If partial recovery made any progress (result differs from input), use it
1683 if (partialResult != aInput) {
1684 // Partial recovery made progress - always report errors collected during partial recovery
1685 return {std::move(partialResult), partialHadErrors};
1686 }
1687
1688 // If no progress was made, try original full parsing approach as fallback
1689 return evaluateWithFullParser(aInput, std::move(aVariableCallback));
1690
1691 } catch (const std::bad_alloc&) {
1692 if (m_lastErrors) {
1693 m_lastErrors->AddError("Out of memory");
1694 }
1695 return {aInput, true};
1696 } catch (const std::exception& e) {
1697 if (m_lastErrors) {
1698 m_lastErrors->AddError(fmt::format("Exception: {}", e.what()));
1699 }
1700 return {aInput, true};
1701 }
1702}
1703
1705 const std::string& aInput,
1706 VariableCallback aVariableCallback)
1707{
1708 std::string result = aInput;
1709 bool hadAnyErrors = false;
1710 size_t pos = 0;
1711
1712 // Process expressions from right to left to avoid position shifts
1713 std::vector<std::pair<size_t, size_t>> expressionRanges;
1714
1715 // Find all expression ranges
1716 while( ( pos = result.find( "@{", pos ) ) != std::string::npos )
1717 {
1718 size_t start = pos;
1719 size_t exprStart = pos + 2; // Skip "@{"
1720 size_t braceCount = 1;
1721 size_t searchPos = exprStart;
1722
1723 // Find matching closing brace, handling nested braces
1724 while( searchPos < result.length() && braceCount > 0 )
1725 {
1726 if( result[searchPos] == '{' )
1727 {
1728 braceCount++;
1729 }
1730 else if( result[searchPos] == '}' )
1731 {
1732 braceCount--;
1733 }
1734 searchPos++;
1735 }
1736
1737 if( braceCount == 0 )
1738 {
1739 size_t end = searchPos; // Position after the '}'
1740 expressionRanges.emplace_back( start, end );
1741 pos = end;
1742 }
1743 else
1744 {
1745 pos = exprStart; // Skip this malformed expression
1746 }
1747 }
1748
1749 // Process expressions from right to left to avoid position shifts
1750 for( auto it = expressionRanges.rbegin(); it != expressionRanges.rend(); ++it )
1751 {
1752 auto [start, end] = *it;
1753 std::string fullExpr = result.substr( start, end - start );
1754 std::string innerExpr = result.substr( start + 2, end - start - 3 ); // Remove @{ and }
1755
1756 // Try to evaluate this single expression
1757 try
1758 {
1759 // Create a simple expression for evaluation
1760 std::string testExpr = "@{" + innerExpr + "}";
1761
1762 // Create a temporary error collector to capture errors for this specific expression
1763 auto tempErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1764 auto oldErrors = std::move( m_lastErrors );
1765 m_lastErrors = std::move( tempErrors );
1766
1767 // Use the full parser for this single expression
1768 auto [evalResult, evalHadErrors] = evaluateWithFullParser( testExpr, aVariableCallback );
1769
1770 if( !evalHadErrors )
1771 {
1772 // Successful evaluation, replace in result
1773 result.replace( start, end - start, evalResult );
1774 }
1775 else
1776 {
1777 // Expression failed - add a specific error for this expression
1778 hadAnyErrors = true;
1779
1780 // Restore main error collector and add error
1781 if( !oldErrors )
1782 oldErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1783
1784 oldErrors->AddError( fmt::format( "Failed to evaluate expression: {}", fullExpr ) );
1785 }
1786
1787 // Restore the main error collector
1788 m_lastErrors = std::move( oldErrors );
1789 }
1790 catch( ... )
1791 {
1792 // Report exception as an error for this expression
1793 if( !m_lastErrors )
1794 m_lastErrors = std::make_unique<calc_parser::ERROR_COLLECTOR>();
1795
1796 m_lastErrors->AddError( fmt::format( "Exception in expression: {}", fullExpr ) );
1797 hadAnyErrors = true;
1798 }
1799 }
1800
1801 return { std::move( result ), hadAnyErrors };
1802}
1803
1804std::pair<std::string, bool> EXPRESSION_EVALUATOR::evaluateWithFullParser( const std::string& aInput,
1805 VariableCallback aVariableCallback )
1806{
1807 if( aInput.empty() )
1808 {
1809 return { std::string{}, false };
1810 }
1811
1812 // RAII guard for error collector cleanup
1813 struct ErrorCollectorGuard
1814 {
1815 ~ErrorCollectorGuard() { calc_parser::g_errorCollector = nullptr; }
1816 } guard;
1817
1818 try
1819 {
1820 // Clear previous errors
1821 if( m_lastErrors )
1822 {
1823 m_lastErrors->Clear();
1824 }
1825
1826 // Set up error collector
1828
1829 // Create tokenizer with default units
1830 KIEVAL_TEXT_TOKENIZER tokenizer{ aInput, m_lastErrors.get(), m_defaultUnits };
1831
1832 // Create parser deleter function
1833 auto parser_deleter = []( void* p )
1834 {
1835 KI_EVAL::ParseFree( p, free );
1836 };
1837
1838 // Allocate parser with RAII cleanup
1839 std::unique_ptr<void, decltype( parser_deleter )> parser{ KI_EVAL::ParseAlloc( malloc ), parser_deleter };
1840
1841 if( !parser )
1842 {
1843 if( m_lastErrors )
1844 {
1845 m_lastErrors->AddError( "Failed to allocate parser" );
1846 }
1847 return { aInput, true };
1848 }
1849
1850 // Parse document
1851 calc_parser::DOC* document = nullptr;
1852
1853 calc_parser::TOKEN_TYPE token_value;
1854 TextEvalToken token_type;
1855
1856 do
1857 {
1858 token_type = tokenizer.get_next_token( token_value );
1859
1860 // Send token to parser
1861 KI_EVAL::Parse( parser.get(), static_cast<int>( token_type ), token_value, &document );
1862
1863 // Early exit on errors
1864 if( m_lastErrors && m_lastErrors->HasErrors() )
1865 {
1866 break;
1867 }
1868
1869 } while( token_type != TextEvalToken::ENDS && tokenizer.has_more_tokens() );
1870
1871 // Finalize parsing
1872 KI_EVAL::Parse( parser.get(), static_cast<int>( TextEvalToken::ENDS ), calc_parser::TOKEN_TYPE{}, &document );
1873
1874 // Process document if parsing succeeded
1875 if( document && ( !m_lastErrors || !m_lastErrors->HasErrors() ) )
1876 {
1878 auto [result, had_errors] = processor.Process( *document, std::move( aVariableCallback ) );
1879
1880 // If processing had any evaluation errors, return original input unchanged
1881 // This preserves the original expression syntax while still reporting errors
1882 if( had_errors )
1883 {
1884 delete document;
1885 return { aInput, true };
1886 }
1887
1888 delete document;
1889 return { std::move( result ), had_errors };
1890 }
1891
1892 // Cleanup and return original on error
1893 delete document;
1894 return { aInput, true };
1895 }
1896 catch( const std::bad_alloc& )
1897 {
1898 if( m_lastErrors )
1899 {
1900 m_lastErrors->AddError( "Out of memory" );
1901 }
1902 return { aInput, true };
1903 }
1904 catch( const std::exception& e )
1905 {
1906 if( m_lastErrors )
1907 {
1908 m_lastErrors->AddError( fmt::format( "Exception: {}", e.what() ) );
1909 }
1910 return { aInput, true };
1911 }
1912}
1913
1918
1920
1922{
1923 m_lastInput.clear();
1924 m_lastResult.clear();
1925 m_lastValid = false;
1926 m_evaluator.ClearErrors();
1927}
1928
1930{
1931 m_evaluator.SetDefaultUnits( aUnits );
1932}
1933
1935{
1936 // No-op: EXPRESSION_EVALUATOR handles locale properly internally
1937}
1938
1940{
1941 return m_lastValid;
1942}
1943
1945{
1946 return m_lastResult;
1947}
1948
1949bool NUMERIC_EVALUATOR_COMPAT::Process( const wxString& aString )
1950{
1951 m_lastInput = aString;
1952 m_evaluator.ClearErrors();
1953
1954 // Convert bare variable names to ${variable} syntax for compatibility
1955 // This allows NUMERIC_EVALUATOR-style variable access to work with EXPRESSION_EVALUATOR
1956 wxString processedExpression = aString;
1957
1958 // Get all variable names that are currently defined
1959 auto varNames = m_evaluator.GetVariableNames();
1960
1961 // Sort variable names by length (longest first) to avoid partial replacements
1962 std::sort( varNames.begin(), varNames.end(),
1963 []( const wxString& a, const wxString& b ) { return a.length() > b.length(); } );
1964
1965 // Replace bare variable names with ${variable} syntax
1966 for( const auto& varName : varNames )
1967 {
1968 // Create a regex to match the variable name as a whole word
1969 // This avoids replacing parts of other words
1970 wxString pattern = "\\b" + varName + "\\b";
1971 wxString replacement = "${" + varName + "}";
1972
1973 // Simple string replacement (not regex for now to avoid complexity)
1974 // Look for the variable name surrounded by non-alphanumeric characters
1975 size_t pos = 0;
1976
1977 while( ( pos = processedExpression.find( varName, pos ) ) != wxString::npos )
1978 {
1979 // Check if this is a whole word (not part of another identifier)
1980 bool isWholeWord = true;
1981
1982 // Check character before
1983 if( pos > 0 )
1984 {
1985 wxChar before = processedExpression[pos - 1];
1986 if( wxIsalnum( before ) || before == '_' || before == '$' )
1987 isWholeWord = false;
1988 }
1989
1990 // Check character after
1991 if( isWholeWord && pos + varName.length() < processedExpression.length() )
1992 {
1993 wxChar after = processedExpression[pos + varName.length()];
1994 if( wxIsalnum( after ) || after == '_' )
1995 isWholeWord = false;
1996 }
1997
1998 if( isWholeWord )
1999 {
2000 processedExpression.replace( pos, varName.length(), replacement );
2001 pos += replacement.length();
2002 }
2003 else
2004 {
2005 pos += varName.length();
2006 }
2007 }
2008 }
2009
2010 // Wrap the processed expression in @{...} syntax for EXPRESSION_EVALUATOR
2011 wxString wrappedExpression = "@{" + processedExpression + "}";
2012
2013 m_lastResult = m_evaluator.Evaluate( wrappedExpression );
2014 m_lastValid = !m_evaluator.HasErrors();
2015
2016 // Additional check: if the result is exactly the wrapped expression,
2017 // it means the expression wasn't evaluated (likely due to errors)
2018 if( m_lastResult == wrappedExpression )
2019 {
2020 m_lastValid = false;
2021 m_lastResult = "NaN";
2022 }
2023
2024 // If there were errors, set result to "NaN" to match NUMERIC_EVALUATOR behavior
2025 if( !m_lastValid )
2026 {
2027 m_lastResult = "NaN";
2028 return false;
2029 }
2030
2031 return true;
2032}
2033
2035{
2036 return m_lastInput;
2037}
2038
2039void NUMERIC_EVALUATOR_COMPAT::SetVar( const wxString& aString, double aValue )
2040{
2041 m_evaluator.SetVariable( aString, aValue );
2042}
2043
2044double NUMERIC_EVALUATOR_COMPAT::GetVar( const wxString& aString )
2045{
2046 if( !m_evaluator.HasVariable( aString ) )
2047 return 0.0;
2048
2049 wxString value = m_evaluator.GetVariable( aString );
2050
2051 // Try to convert to double
2052 double result = 0.0;
2053
2054 if( !value.ToDouble( &result ) )
2055 return 0.0;
2056
2057 return result;
2058}
2059
2060void NUMERIC_EVALUATOR_COMPAT::RemoveVar( const wxString& aString )
2061{
2062 m_evaluator.RemoveVariable( aString );
2063}
2064
2066{
2067 m_evaluator.ClearVariables();
2068}
const char * name
EDA_UNITS GetDefaultUnits() const
Get the current default units.
bool HasVariableCallback() const
Check if a custom variable callback is set.
wxString Evaluate(const wxString &aInput)
Main evaluation function - processes input string and evaluates all} expressions.
bool TestExpression(const wxString &aExpression)
Test if an expression can be parsed without evaluating it.
bool RemoveVariable(const wxString &aName)
Remove a variable from the evaluator.
bool HasErrors() const
Check if the last evaluation had errors.
std::pair< std::string, bool > evaluateWithFullParser(const std::string &aInput, VariableCallback aVariableCallback)
Full parser evaluation (original behavior) - fails completely on any error.
wxString GetErrorSummary() const
Get detailed error information from the last evaluation.
void SetDefaultUnits(EDA_UNITS aUnits)
Set the default units for expressions.
bool GetClearVariablesOnEvaluate() const
Check if automatic variable clearing is enabled.
std::unordered_map< std::string, calc_parser::Value > m_variables
void SetVariables(const std::unordered_map< wxString, double > &aVariables)
Set multiple variables at once from a map.
std::pair< std::string, bool > evaluateWithPartialErrorRecovery(const std::string &aInput, VariableCallback aVariableCallback)
Parse and evaluate with partial error recovery - malformed expressions left unchanged.
std::string wxStringToStdString(const wxString &aWxStr) const
Convert wxString to std::string using UTF-8 encoding.
size_t GetErrorCount() const
Get count of errors from the last evaluation.
VariableCallback createCombinedCallback(const std::unordered_map< wxString, double > *aTempNumericVars=nullptr, const std::unordered_map< wxString, wxString > *aTempStringVars=nullptr) const
Create a callback function that combines all variable sources.
std::vector< wxString > ExtractExpressions(const wxString &aInput) const
Extract all} expressions from input without evaluating.
void ClearErrors()
Clear any stored error information.
std::unique_ptr< calc_parser::ERROR_COLLECTOR > m_lastErrors
wxString stdStringToWxString(const std::string &aStdStr) const
Convert std::string to wxString using UTF-8 encoding.
std::vector< wxString > GetErrors() const
Get individual error messages from the last evaluation.
void ClearVariableCallback()
Clear the custom variable resolver callback.
void ClearVariables()
Clear all stored variables.
bool HasVariable(const wxString &aName) const
Check if a variable exists in stored variables.
std::vector< wxString > GetVariableNames() const
Get all stored variable names currently defined.
~EXPRESSION_EVALUATOR()
Destructor.
EXPRESSION_EVALUATOR & operator=(const EXPRESSION_EVALUATOR &aOther)
std::function< calc_parser::Result< calc_parser::Value >(const std::string &aVariableName)> VariableCallback
std::pair< std::string, bool > evaluateWithParser(const std::string &aInput, VariableCallback aVariableCallback)
Parse and evaluate the input string using the expression parser.
void SetVariable(const wxString &aName, double aValue)
Set a numeric variable for use in expressions.
void SetClearVariablesOnEvaluate(bool aEnable)
Enable or disable automatic variable clearing after evaluation.
void SetVariableCallback(VariableCallback aCallback)
Set a custom variable resolver callback.
wxString GetVariable(const wxString &aName) const
Get the current value of a stored variable.
size_t CountExpressions(const wxString &aInput) const
Count the number of} expressions in input string.
VariableCallback m_customCallback
wxString expandVariablesOutsideExpressions(const wxString &aInput, const std::unordered_map< wxString, double > &aTempNumericVars, const std::unordered_map< wxString, wxString > &aTempStringVars) const
Expand ${variable} patterns that are outside} expressions.
EXPRESSION_EVALUATOR(bool aClearVariablesOnEvaluate=false)
Construct a new Expression Evaluator in static variable mode.
TOKENIZER_CONTEXT m_context
calc_parser::TOKEN_TYPE parse_number()
void add_error(std::string_view message) const
static constexpr calc_parser::TOKEN_TYPE make_number_token(double value) noexcept
void advance_position(size_t count=1) noexcept
utf8_utils::SI_PREFIX_HANDLER SI_HANDLER
constexpr size_t get_column() const noexcept
void skip_whitespace() noexcept
bool has_more_tokens() const noexcept
calc_parser::ERROR_COLLECTOR * m_errorCollector
char32_t peek_char(size_t offset=1) const noexcept
calc_parser::TOKEN_TYPE parse_string_literal(char32_t quote_char)
KIEVAL_TEXT_TOKENIZER(std::string_view input, calc_parser::ERROR_COLLECTOR *error_collector=nullptr, EDA_UNITS default_units=EDA_UNITS::MM)
TextEvalToken get_next_token(calc_parser::TOKEN_TYPE &token_value)
utf8_utils::CHARACTER_CLASSIFIER CLASSIFIER
calc_parser::TOKEN_TYPE parse_text_content()
calc_parser::TOKEN_TYPE parse_identifier()
constexpr size_t get_line() const noexcept
static calc_parser::TOKEN_TYPE make_string_token(std::string value) noexcept
char32_t current_char() const noexcept
void LocaleChanged()
Handle locale changes (for decimal separator)
~NUMERIC_EVALUATOR_COMPAT()
Destructor.
void SetDefaultUnits(EDA_UNITS aUnits)
Set default units for evaluation.
bool Process(const wxString &aString)
Process and evaluate an expression.
wxString Result() const
Get the result of the last evaluation.
void RemoveVar(const wxString &aString)
Remove a single variable.
void SetVar(const wxString &aString, double aValue)
Set a variable value.
void ClearVar()
Remove all variables.
EXPRESSION_EVALUATOR m_evaluator
double GetVar(const wxString &aString)
Get a variable value.
void Clear()
Clear parser state but retain variables.
NUMERIC_EVALUATOR_COMPAT(EDA_UNITS aUnits)
Constructor with default units.
bool IsValid() const
Check if the last evaluation was successful.
wxString OriginalText() const
Get the original input text.
static auto Process(const DOC &aDoc, VariableCallback aVariableCallback) -> std::pair< std::string, bool >
Process document using callback for variable resolution.
auto GetErrors() const -> const std::vector< std::string > &
static constexpr Unit parseUnit(std::string_view unitStr) noexcept
Parse a unit string and return the corresponding Unit enum.
static double convertToEdaUnits(double value, std::string_view unitStr, EDA_UNITS targetUnits)
Convert a value with unit string to target EDA_UNITS.
static std::vector< std::string > getAllUnitStrings()
Get all unit strings in parsing order (longest first)
static constexpr int sequence_length(std::byte first) noexcept
static std::string to_utf8(std::u32string_view utf32)
static constexpr bool is_ascii(std::byte b) noexcept
static std::u32string to_utf32(std::string_view utf8)
static constexpr bool is_continuation(std::byte b) noexcept
wxString ExpandTextVars(const wxString &aSource, const PROJECT *aProject, int aFlags)
Definition common.cpp:59
The common library.
EDA_UNITS
Definition eda_units.h:48
@ PS_PER_INCH
Definition eda_units.h:59
static FILENAME_RESOLVER * resolver
double convertToDefaultUnits(double aValue, const std::string &aUnitStr, EDA_UNITS aDefaultUnits)
Unit parseUnit(const std::string &aUnitStr)
double getConversionFactor(Unit aFromUnit, Unit aToUnit)
Unit edaUnitsToInternal(EDA_UNITS aUnits)
thread_local ERROR_COLLECTOR * g_errorCollector
auto MakeValue(T aVal) -> Result< T >
std::variant< double, std::string > Value
auto MakeError(std::string aMsg) -> Result< T >
STL namespace.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
double fromMM(double aMMValue)
double toMM(double aIUValue)
CITER next(CITER it)
Definition ptree.cpp:124
static constexpr bool is_digit(UnicodeCodepoint auto cp) noexcept
static constexpr bool is_alpha(UnicodeCodepoint auto cp) noexcept
static constexpr bool is_whitespace(UnicodeCodepoint auto cp) noexcept
static constexpr bool is_ascii_alpha(UnicodeCodepoint auto cp) noexcept
static constexpr bool is_alnum(UnicodeCodepoint auto cp) noexcept
static constexpr double get_multiplier(UnicodeCodepoint auto cp) noexcept
static constexpr std::array< PREFIX, 18 > prefixes
static constexpr bool is_si_prefix(UnicodeCodepoint auto cp) noexcept
VECTOR2I end
wxString result
Test unit parsing edge cases and error handling.