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