/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright The KiCad Developers, see AUTHORS.txt for contributors. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include #include // Include the KiCad common functionality #include #include #include // Parser types #include #include // Centralized unit registry namespace KI_EVAL { #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-variable" #pragma GCC diagnostic ignored "-Wsign-compare" #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" #endif #include #ifdef __GNUC__ #pragma GCC diagnostic pop #endif } #include #include #include // // Token type enum matching the generated parser enum class TextEvalToken : int { ENDS = KI_EVAL_LT - 1, LT = KI_EVAL_LT, GT = KI_EVAL_GT, LE = KI_EVAL_LE, GE = KI_EVAL_GE, EQ = KI_EVAL_EQ, NE = KI_EVAL_NE, PLUS = KI_EVAL_PLUS, MINUS = KI_EVAL_MINUS, MULTIPLY = KI_EVAL_MULTIPLY, DIVIDE = KI_EVAL_DIVIDE, MODULO = KI_EVAL_MODULO, UMINUS = KI_EVAL_UMINUS, POWER = KI_EVAL_POWER, COMMA = KI_EVAL_COMMA, TEXT = KI_EVAL_TEXT, AT_OPEN = KI_EVAL_AT_OPEN, CLOSE_BRACE = KI_EVAL_CLOSE_BRACE, LPAREN = KI_EVAL_LPAREN, RPAREN = KI_EVAL_RPAREN, NUMBER = KI_EVAL_NUMBER, STRING = KI_EVAL_STRING, IDENTIFIER = KI_EVAL_IDENTIFIER, DOLLAR_OPEN = KI_EVAL_DOLLAR_OPEN, }; // UTF-8 <-> UTF-32 conversion utilities namespace utf8_utils { // Concept for UTF-8 byte validation template concept Utf8Byte = std::same_as || std::same_as || std::same_as; // UTF-8 validation and conversion class UTF8_CONVERTER { private: // UTF-8 byte classification using bit operations static constexpr bool is_ascii( std::byte b ) noexcept { return ( b & std::byte{ 0x80 } ) == std::byte{ 0x00 }; } static constexpr bool is_continuation( std::byte b ) noexcept { return ( b & std::byte{ 0xC0 } ) == std::byte{ 0x80 }; } static constexpr int sequence_length( std::byte first ) noexcept { if( is_ascii( first ) ) return 1; if( ( first & std::byte{ 0xE0 } ) == std::byte{ 0xC0 } ) return 2; if( ( first & std::byte{ 0xF0 } ) == std::byte{ 0xE0 } ) return 3; if( ( first & std::byte{ 0xF8 } ) == std::byte{ 0xF0 } ) return 4; return 0; // Invalid } public: // Convert UTF-8 string to UTF-32 codepoints using C++20 ranges static std::u32string to_utf32( std::string_view utf8 ) { std::u32string result; result.reserve( utf8.size() ); // Conservative estimate auto bytes = std::as_bytes( std::span{ utf8.data(), utf8.size() } ); for( size_t i = 0; i < bytes.size(); ) { std::byte first = bytes[i]; int len = sequence_length( first ); if( len == 0 || i + len > bytes.size() ) { // Invalid sequence - insert replacement character result.push_back( U'\uFFFD' ); i++; continue; } char32_t codepoint = 0; switch( len ) { case 1: codepoint = std::to_integer( first ); break; case 2: { if( !is_continuation( bytes[i + 1] ) ) { result.push_back( U'\uFFFD' ); i++; continue; } codepoint = ( std::to_integer( first & std::byte{ 0x1F } ) << 6 ) | std::to_integer( bytes[i + 1] & std::byte{ 0x3F } ); break; } case 3: { if( !is_continuation( bytes[i + 1] ) || !is_continuation( bytes[i + 2] ) ) { result.push_back( U'\uFFFD' ); i++; continue; } codepoint = ( std::to_integer( first & std::byte{ 0x0F } ) << 12 ) | ( std::to_integer( bytes[i + 1] & std::byte{ 0x3F } ) << 6 ) | std::to_integer( bytes[i + 2] & std::byte{ 0x3F } ); break; } case 4: { if( !is_continuation( bytes[i + 1] ) || !is_continuation( bytes[i + 2] ) || !is_continuation( bytes[i + 3] ) ) { result.push_back( U'\uFFFD' ); i++; continue; } codepoint = ( std::to_integer( first & std::byte{ 0x07 } ) << 18 ) | ( std::to_integer( bytes[i + 1] & std::byte{ 0x3F } ) << 12 ) | ( std::to_integer( bytes[i + 2] & std::byte{ 0x3F } ) << 6 ) | std::to_integer( bytes[i + 3] & std::byte{ 0x3F } ); break; } } // Validate codepoint range if( codepoint > 0x10FFFF || ( codepoint >= 0xD800 && codepoint <= 0xDFFF ) ) { result.push_back( U'\uFFFD' ); // Replacement character } else if( len == 2 && codepoint < 0x80 ) { result.push_back( U'\uFFFD' ); // Overlong encoding } else if( len == 3 && codepoint < 0x800 ) { result.push_back( U'\uFFFD' ); // Overlong encoding } else if( len == 4 && codepoint < 0x10000 ) { result.push_back( U'\uFFFD' ); // Overlong encoding } else { result.push_back( codepoint ); } i += len; } return result; } // Convert UTF-32 to UTF-8 static std::string to_utf8( std::u32string_view utf32 ) { std::string result; result.reserve( utf32.size() * 4 ); // Maximum possible size for( char32_t cp : utf32 ) { if( cp <= 0x7F ) { // 1-byte sequence result.push_back( static_cast( cp ) ); } else if( cp <= 0x7FF ) { // 2-byte sequence result.push_back( static_cast( 0xC0 | ( cp >> 6 ) ) ); result.push_back( static_cast( 0x80 | ( cp & 0x3F ) ) ); } else if( cp <= 0xFFFF ) { // 3-byte sequence if( cp >= 0xD800 && cp <= 0xDFFF ) { // Surrogate pair - invalid in UTF-32 result.append( "\uFFFD" ); // Replacement character in UTF-8 } else { result.push_back( static_cast( 0xE0 | ( cp >> 12 ) ) ); result.push_back( static_cast( 0x80 | ( ( cp >> 6 ) & 0x3F ) ) ); result.push_back( static_cast( 0x80 | ( cp & 0x3F ) ) ); } } else if( cp <= 0x10FFFF ) { // 4-byte sequence result.push_back( static_cast( 0xF0 | ( cp >> 18 ) ) ); result.push_back( static_cast( 0x80 | ( ( cp >> 12 ) & 0x3F ) ) ); result.push_back( static_cast( 0x80 | ( ( cp >> 6 ) & 0x3F ) ) ); result.push_back( static_cast( 0x80 | ( cp & 0x3F ) ) ); } else { // Invalid codepoint result.append( "\uFFFD" ); // Replacement character in UTF-8 } } return result; } }; template concept UnicodeCodepoint = std::same_as; struct CHARACTER_CLASSIFIER { static constexpr bool is_whitespace(UnicodeCodepoint auto cp) noexcept { // Unicode whitespace categories return cp == U' ' || cp == U'\t' || cp == U'\r' || cp == U'\n' || cp == U'\f' || cp == U'\v' || cp == U'\u00A0' || // Non-breaking space cp == U'\u2000' || cp == U'\u2001' || cp == U'\u2002' || cp == U'\u2003' || cp == U'\u2004' || cp == U'\u2005' || cp == U'\u2006' || cp == U'\u2007' || cp == U'\u2008' || cp == U'\u2009' || cp == U'\u200A' || cp == U'\u2028' || cp == U'\u2029' || cp == U'\u202F' || cp == U'\u205F' || cp == U'\u3000'; } static constexpr bool is_digit(UnicodeCodepoint auto cp) noexcept { return cp >= U'0' && cp <= U'9'; } static constexpr bool is_ascii_alpha(UnicodeCodepoint auto cp) noexcept { return (cp >= U'a' && cp <= U'z') || (cp >= U'A' && cp <= U'Z'); } static constexpr bool is_alpha(UnicodeCodepoint auto cp) noexcept { // Basic Latin + extended Unicode letter ranges return is_ascii_alpha(cp) || (cp >= 0x80 && cp <= 0x10FFFF && cp != 0xFFFD); } static constexpr bool is_alnum(UnicodeCodepoint auto cp) noexcept { return is_alpha(cp) || is_digit(cp); } }; struct SI_PREFIX_HANDLER { struct PREFIX { char32_t symbol; double multiplier; }; static constexpr std::array prefixes = { { {U'a', 1e-18}, {U'f', 1e-15}, {U'p', 1e-12}, {U'n', 1e-9}, {U'u', 1e-6}, {U'µ', 1e-6}, {U'μ', 1e-6}, // Various micro symbols {U'm', 1e-3}, {U'k', 1e3}, {U'K', 1e3}, {U'M', 1e6}, {U'G', 1e9}, {U'T', 1e12}, {U'P', 1e15}, {U'E', 1e18} } }; static constexpr bool is_si_prefix( UnicodeCodepoint auto cp ) noexcept { return std::ranges::any_of( prefixes, [cp]( const PREFIX& p ) { return p.symbol == cp; } ); } static constexpr double get_multiplier( UnicodeCodepoint auto cp ) noexcept { auto it = std::ranges::find_if( prefixes, [cp]( const PREFIX& p ) { return p.symbol == cp; } ); return it != prefixes.end() ? it->multiplier : 1.0; } }; } // Unit conversion utilities for the text evaluator namespace KIEVAL_UNIT_CONV { // Internal unit enum matching NUMERIC_EVALUATOR enum class Unit { Invalid, UM, MM, CM, Inch, Mil, Degrees, SI, Femtoseconds, Picoseconds, PsPerInch, PsPerCm, PsPerMm }; // Convert EDA_UNITS to internal Unit enum Unit edaUnitsToInternal( EDA_UNITS aUnits ) { switch( aUnits ) { case EDA_UNITS::MM: return Unit::MM; case EDA_UNITS::MILS: return Unit::Mil; case EDA_UNITS::INCH: return Unit::Inch; case EDA_UNITS::DEGREES: return Unit::Degrees; case EDA_UNITS::FS: return Unit::Femtoseconds; case EDA_UNITS::PS: return Unit::Picoseconds; case EDA_UNITS::PS_PER_INCH: return Unit::PsPerInch; case EDA_UNITS::PS_PER_CM: return Unit::PsPerCm; case EDA_UNITS::PS_PER_MM: return Unit::PsPerMm; case EDA_UNITS::UM: return Unit::UM; case EDA_UNITS::CM: return Unit::CM; case EDA_UNITS::UNSCALED: return Unit::SI; default: return Unit::MM; } } // Parse unit from string using centralized registry Unit parseUnit( const std::string& aUnitStr ) { auto evalUnit = text_eval_units::UnitRegistry::parseUnit( aUnitStr ); // Convert text_eval_units::Unit to KIEVAL_UNIT_CONV::Unit switch( evalUnit ) { case text_eval_units::Unit::MM: return Unit::MM; case text_eval_units::Unit::CM: return Unit::CM; case text_eval_units::Unit::INCH: return Unit::Inch; case text_eval_units::Unit::INCH_QUOTE: return Unit::Inch; case text_eval_units::Unit::MIL: return Unit::Mil; case text_eval_units::Unit::THOU: return Unit::Mil; case text_eval_units::Unit::UM: return Unit::UM; case text_eval_units::Unit::DEG: return Unit::Degrees; case text_eval_units::Unit::DEGREE_SYMBOL: return Unit::Degrees; case text_eval_units::Unit::PS: return Unit::Picoseconds; case text_eval_units::Unit::FS: return Unit::Femtoseconds; case text_eval_units::Unit::PS_PER_IN: return Unit::PsPerInch; case text_eval_units::Unit::PS_PER_CM: return Unit::PsPerCm; case text_eval_units::Unit::PS_PER_MM: return Unit::PsPerMm; default: return Unit::Invalid; } } // Get conversion factor from one unit to another (based on numeric_evaluator logic) double getConversionFactor( Unit aFromUnit, Unit aToUnit ) { if( aFromUnit == aToUnit ) return 1.0; // Convert to MM first, then to target unit double toMM = 1.0; switch( aFromUnit ) { case Unit::Inch: toMM = 25.4; break; case Unit::Mil: toMM = 25.4 / 1000.0; break; case Unit::UM: toMM = 1.0 / 1000.0; break; case Unit::MM: toMM = 1.0; break; case Unit::CM: toMM = 10.0; break; default: return 1.0; // No conversion for other units } double fromMM = 1.0; switch( aToUnit ) { case Unit::Inch: fromMM = 1.0 / 25.4; break; case Unit::Mil: fromMM = 1000.0 / 25.4; break; case Unit::UM: fromMM = 1000.0; break; case Unit::MM: fromMM = 1.0; break; case Unit::CM: fromMM = 1.0 / 10.0; break; default: return 1.0; // No conversion for other units } return toMM * fromMM; } // Convert a value with units to the default units using centralized registry double convertToDefaultUnits( double aValue, const std::string& aUnitStr, EDA_UNITS aDefaultUnits ) { return text_eval_units::UnitRegistry::convertToEdaUnits( aValue, aUnitStr, aDefaultUnits ); } } class KIEVAL_TEXT_TOKENIZER { private: enum class TOKENIZER_CONTEXT { TEXT, // Regular text content - alphabetic should be TEXT tokens EXPRESSION // Inside @{...} or ${...} - alphabetic should be IDENTIFIER tokens }; std::u32string m_text; size_t m_pos{ 0 }; size_t m_line{ 1 }; size_t m_column{ 1 }; TOKENIZER_CONTEXT m_context{ TOKENIZER_CONTEXT::TEXT }; int m_braceNestingLevel{ 0 }; // Track nesting level of expressions calc_parser::ERROR_COLLECTOR* m_errorCollector{ nullptr }; EDA_UNITS m_defaultUnits{ EDA_UNITS::MM }; // Add default units for conversion using CLASSIFIER = utf8_utils::CHARACTER_CLASSIFIER; using SI_HANDLER = utf8_utils::SI_PREFIX_HANDLER; [[nodiscard]] constexpr char32_t current_char() const noexcept { return m_pos < m_text.size() ? m_text[m_pos] : U'\0'; } [[nodiscard]] constexpr char32_t peek_char( size_t offset = 1 ) const noexcept { size_t peek_pos = m_pos + offset; return peek_pos < m_text.size() ? m_text[peek_pos] : U'\0'; } constexpr void advance_position( size_t count = 1 ) noexcept { for( size_t i = 0; i < count && m_pos < m_text.size(); ++i ) { if( m_text[m_pos] == U'\n' ) { ++m_line; m_column = 1; } else { ++m_column; } ++m_pos; } } void skip_whitespace() noexcept { while( m_pos < m_text.size() && CLASSIFIER::is_whitespace( current_char() ) ) advance_position(); } void add_error( std::string_view message ) const { if( m_errorCollector ) { auto error_msg = fmt::format( "Line {}, Column {}: {}", m_line, m_column, message ); m_errorCollector->AddError( error_msg ); } } [[nodiscard]] static calc_parser::TOKEN_TYPE make_string_token( std::string value ) noexcept { calc_parser::TOKEN_TYPE token{}; token.isString = true; std::strncpy( token.text, value.c_str(), sizeof( token.text ) - 1 ); token.text[sizeof( token.text ) - 1] = '\0'; token.dValue = 0.0; return token; } [[nodiscard]] static constexpr calc_parser::TOKEN_TYPE make_number_token( double value ) noexcept { calc_parser::TOKEN_TYPE token{}; token.isString = false; token.dValue = value; return token; } [[nodiscard]] calc_parser::TOKEN_TYPE parse_string_literal( char32_t quote_char ) { advance_position(); // Skip opening quote std::u32string content; content.reserve( 64 ); // Reasonable default while( m_pos < m_text.size() && current_char() != quote_char ) { char32_t c = current_char(); if( c == U'\\' && m_pos + 1 < m_text.size() ) { char32_t escaped = peek_char(); advance_position( 2 ); switch( escaped ) { case U'n': content.push_back( U'\n' ); break; case U't': content.push_back( U'\t' ); break; case U'r': content.push_back( U'\r' ); break; case U'\\': content.push_back( U'\\' ); break; case U'"': content.push_back( U'"' ); break; case U'\'': content.push_back( U'\'' ); break; case U'0': content.push_back( U'\0' ); break; case U'x': { // Hexadecimal escape \xHH std::u32string hex; for( int i = 0; i < 2 && m_pos < m_text.size(); ++i ) { char32_t hex_char = current_char(); if( ( hex_char >= U'0' && hex_char <= U'9' ) || ( hex_char >= U'A' && hex_char <= U'F' ) || ( hex_char >= U'a' && hex_char <= U'f' ) ) { hex.push_back( hex_char ); advance_position(); } else { break; } } if( !hex.empty() ) { try { auto hex_str = utf8_utils::UTF8_CONVERTER::to_utf8( hex ); auto value = std::stoul( hex_str, nullptr, 16 ); if( value <= 0x10FFFF ) content.push_back( static_cast( value ) ); else content.push_back( U'\uFFFD' ); } catch( ... ) { content.push_back( U'\uFFFD' ); } } else { content.append( U"\\x" ); } break; } default: content.push_back( U'\\' ); content.push_back( escaped ); break; } } else if( c == U'\n' ) { add_error( "Unterminated string literal" ); break; } else { content.push_back( c ); advance_position(); } } if (m_pos < m_text.size() && current_char() == quote_char) { advance_position(); // Skip closing quote } else { add_error("Missing closing quote in string literal"); } return make_string_token(utf8_utils::UTF8_CONVERTER::to_utf8(content)); } [[nodiscard]] calc_parser::TOKEN_TYPE parse_number() { std::u32string number_text; number_text.reserve( 32 ); bool has_decimal = false; double multiplier = 1.0; // Parse integer part while( m_pos < m_text.size() && CLASSIFIER::is_digit( current_char() ) ) { number_text.push_back( current_char() ); advance_position(); } // Handle decimal point, SI prefix, or unit suffix if( m_pos < m_text.size() ) { char32_t c = current_char(); // Only treat comma as decimal separator in text context, not expression context // This prevents comma from interfering with function argument separation if( c == U'.' || ( c == U',' && m_context != TOKENIZER_CONTEXT::EXPRESSION ) ) { number_text.push_back( U'.' ); has_decimal = true; advance_position(); } else if( m_context == TOKENIZER_CONTEXT::EXPRESSION && CLASSIFIER::is_alpha( c ) ) { // In expression context, check for unit first before SI prefix (unit strings are longer) // Look ahead to see if we have a complete unit string std::u32string potential_unit; size_t temp_pos = m_pos; while( temp_pos < m_text.size() ) { char32_t unit_char = m_text[temp_pos]; if( CLASSIFIER::is_alpha( unit_char ) || unit_char == U'"' || unit_char == U'\'' ) { potential_unit.push_back( unit_char ); temp_pos++; } else { break; } } // Check if we have a valid unit if( !potential_unit.empty() ) { std::string unit_str = utf8_utils::UTF8_CONVERTER::to_utf8( potential_unit ); KIEVAL_UNIT_CONV::Unit parsed_unit = KIEVAL_UNIT_CONV::parseUnit( unit_str ); if( parsed_unit != KIEVAL_UNIT_CONV::Unit::Invalid ) { // This is a valid unit - don't treat the first character as SI prefix // The unit parsing will happen later } else if( SI_HANDLER::is_si_prefix( c ) && !has_decimal ) { // Not a valid unit, so treat as SI prefix multiplier = SI_HANDLER::get_multiplier( c ); number_text.push_back( U'.' ); has_decimal = true; advance_position(); } } else if( SI_HANDLER::is_si_prefix( c ) && !has_decimal ) { // No alphabetic characters following, so treat as SI prefix multiplier = SI_HANDLER::get_multiplier( c ); number_text.push_back( U'.' ); has_decimal = true; advance_position(); } } else if( SI_HANDLER::is_si_prefix( c ) && !has_decimal ) { // In text context, treat as SI prefix multiplier = SI_HANDLER::get_multiplier( c ); number_text.push_back( U'.' ); has_decimal = true; advance_position(); } } // Parse fractional part while (m_pos < m_text.size() && CLASSIFIER::is_digit(current_char())) { number_text.push_back(current_char()); advance_position(); } // Convert to double safely auto number_str = utf8_utils::UTF8_CONVERTER::to_utf8( number_text ); double value = 0.0; try { if( !number_str.empty() && number_str != "." ) { auto result = fast_float::from_chars( number_str.data(), number_str.data() + number_str.size(), value ); if( result.ec != std::errc() || result.ptr != number_str.data() + number_str.size() ) throw std::invalid_argument( fmt::format( "Cannot convert '{}' to number", number_str ) ); value *= multiplier; if( !std::isfinite( value ) ) { add_error( "Number out of range" ); value = 0.0; } } } catch( const std::exception& e ) { add_error( fmt::format( "Invalid number format: {}", e.what() ) ); value = 0.0; } // Look for unit suffix if( m_pos < m_text.size() && m_context == TOKENIZER_CONTEXT::EXPRESSION ) { // Skip any whitespace between number and unit size_t whitespace_start = m_pos; while( m_pos < m_text.size() && CLASSIFIER::is_whitespace( current_char() ) ) { advance_position(); } // Parse potential unit suffix std::u32string unit_text; // Look ahead to parse potential unit (letters, quotes, etc.) while( m_pos < m_text.size() ) { char32_t c = current_char(); // Unit characters: letters, quotes for inches if( CLASSIFIER::is_alpha( c ) || c == U'"' || c == U'\'' ) { unit_text.push_back( c ); advance_position(); } else { break; } } if( !unit_text.empty() ) { // Convert unit text to string and try to parse it std::string unit_str = utf8_utils::UTF8_CONVERTER::to_utf8( unit_text ); KIEVAL_UNIT_CONV::Unit parsed_unit = KIEVAL_UNIT_CONV::parseUnit( unit_str ); if( parsed_unit != KIEVAL_UNIT_CONV::Unit::Invalid ) { // Successfully parsed unit - convert value to default units double converted_value = KIEVAL_UNIT_CONV::convertToDefaultUnits( value, unit_str, m_defaultUnits ); value = converted_value; } else { // Not a valid unit - backtrack to before the whitespace m_pos = whitespace_start; } } else { // No unit found - backtrack to before the whitespace m_pos = whitespace_start; } } return make_number_token(value); } [[nodiscard]] calc_parser::TOKEN_TYPE parse_identifier() { std::u32string identifier; identifier.reserve(64); while (m_pos < m_text.size() && (CLASSIFIER::is_alnum(current_char()) || current_char() == U'_')) { identifier.push_back(current_char()); advance_position(); } return make_string_token(utf8_utils::UTF8_CONVERTER::to_utf8(identifier)); } [[nodiscard]] calc_parser::TOKEN_TYPE parse_text_content() { std::u32string text; text.reserve(256); while (m_pos < m_text.size()) { char32_t current = current_char(); char32_t next = peek_char(); // Stop at special sequences if ((current == U'@' && next == U'{') || (current == U'$' && next == U'{')) { break; } text.push_back(current); advance_position(); } return make_string_token(utf8_utils::UTF8_CONVERTER::to_utf8(text)); } public: explicit KIEVAL_TEXT_TOKENIZER( std::string_view input, calc_parser::ERROR_COLLECTOR* error_collector = nullptr, EDA_UNITS default_units = EDA_UNITS::MM ) : m_errorCollector( error_collector ), m_defaultUnits( default_units ) { m_text = utf8_utils::UTF8_CONVERTER::to_utf32( input ); } [[nodiscard]] TextEvalToken get_next_token( calc_parser::TOKEN_TYPE& token_value ) { token_value = calc_parser::TOKEN_TYPE{}; if( m_pos >= m_text.size() ) { return TextEvalToken::ENDS; } // Only skip whitespace in expression context if( m_context == TOKENIZER_CONTEXT::EXPRESSION ) { skip_whitespace(); if( m_pos >= m_text.size() ) { return TextEvalToken::ENDS; } } char32_t current = current_char(); char32_t next = peek_char(); // Multi-character tokens that switch to expression context if( current == U'@' && next == U'{' ) { advance_position( 2 ); m_context = TOKENIZER_CONTEXT::EXPRESSION; // Switch to expression context m_braceNestingLevel++; // Increment nesting level token_value = make_string_token( "@{" ); return TextEvalToken::AT_OPEN; } if( current == U'$' && next == U'{' ) { advance_position( 2 ); m_context = TOKENIZER_CONTEXT::EXPRESSION; // Switch to expression context m_braceNestingLevel++; // Increment nesting level token_value = make_string_token( "${" ); return TextEvalToken::DOLLAR_OPEN; } // Handle closing brace specially to manage context correctly if( current == U'}' ) { advance_position(); m_braceNestingLevel--; // Decrement nesting level if( m_braceNestingLevel <= 0 ) { m_braceNestingLevel = 0; // Clamp to zero m_context = TOKENIZER_CONTEXT::TEXT; // Switch back to text context only when fully unnested } token_value = make_string_token( "}" ); return TextEvalToken::CLOSE_BRACE; } // Multi-character comparison operators if( current == U'<' && next == U'=' ) { advance_position( 2 ); token_value = make_string_token( "<=" ); return TextEvalToken::LE; } if( current == U'>' && next == U'=' ) { advance_position( 2 ); token_value = make_string_token( ">=" ); return TextEvalToken::GE; } if( current == U'=' && next == U'=' ) { advance_position( 2 ); token_value = make_string_token( "==" ); return TextEvalToken::EQ; } if( current == U'!' && next == U'=' ) { advance_position( 2 ); token_value = make_string_token( "!=" ); return TextEvalToken::NE; } // Single character tokens using structured binding // Single character tokens (only in expression context) if( m_context == TOKENIZER_CONTEXT::EXPRESSION ) { static constexpr std::array, 11> single_char_tokens{{ {U'(', TextEvalToken::LPAREN}, {U')', TextEvalToken::RPAREN}, {U'+', TextEvalToken::PLUS}, {U'-', TextEvalToken::MINUS}, {U'*', TextEvalToken::MULTIPLY}, {U'/', TextEvalToken::DIVIDE}, {U'%', TextEvalToken::MODULO}, {U'^', TextEvalToken::POWER}, {U',', TextEvalToken::COMMA}, {U'<', TextEvalToken::LT}, {U'>', TextEvalToken::GT} }}; if( auto it = std::ranges::find_if( single_char_tokens, [current]( const auto& pair ) { return pair.first == current; } ); it != single_char_tokens.end() ) { advance_position(); token_value = make_string_token( utf8_utils::UTF8_CONVERTER::to_utf8( std::u32string{ current } ) ); return it->second; } } // Complex tokens if( current == U'"' || current == U'\'' ) { token_value = parse_string_literal( current ); return TextEvalToken::STRING; } if( CLASSIFIER::is_digit( current ) || ( current == U'.' && CLASSIFIER::is_digit( next ) ) ) { token_value = parse_number(); return TextEvalToken::NUMBER; } // Context-aware handling of alphabetic content if( CLASSIFIER::is_alpha( current ) || current == U'_' ) { if( m_context == TOKENIZER_CONTEXT::EXPRESSION ) { // In expression context, alphabetic content is an identifier token_value = parse_identifier(); return TextEvalToken::IDENTIFIER; } else { // In text context, alphabetic content is part of regular text token_value = parse_text_content(); return TextEvalToken::TEXT; } } // Default to text content token_value = parse_text_content(); return token_value.text[0] == U'\0' ? TextEvalToken::ENDS : TextEvalToken::TEXT; } [[nodiscard]] constexpr bool has_more_tokens() const noexcept { return m_pos < m_text.size(); } [[nodiscard]] constexpr size_t get_line() const noexcept { return m_line; } [[nodiscard]] constexpr size_t get_column() const noexcept { return m_column; } }; EXPRESSION_EVALUATOR::EXPRESSION_EVALUATOR( bool aClearVariablesOnEvaluate ) : m_clearVariablesOnEvaluate( aClearVariablesOnEvaluate ), m_useCustomCallback( false ), m_defaultUnits( EDA_UNITS::MM ) // Default to millimeters { m_lastErrors = std::make_unique(); } EXPRESSION_EVALUATOR::EXPRESSION_EVALUATOR( VariableCallback aVariableCallback, bool aClearVariablesOnEvaluate ) : m_clearVariablesOnEvaluate( aClearVariablesOnEvaluate ), m_customCallback( std::move( aVariableCallback ) ), m_useCustomCallback( true ), m_defaultUnits( EDA_UNITS::MM ) // Default to millimeters { m_lastErrors = std::make_unique(); } EXPRESSION_EVALUATOR::EXPRESSION_EVALUATOR( EDA_UNITS aUnits, bool aClearVariablesOnEvaluate ) : m_clearVariablesOnEvaluate( aClearVariablesOnEvaluate ), m_useCustomCallback( false ), m_defaultUnits( aUnits ) { m_lastErrors = std::make_unique(); } EXPRESSION_EVALUATOR::EXPRESSION_EVALUATOR( EDA_UNITS aUnits, VariableCallback aVariableCallback, bool aClearVariablesOnEvaluate ) : m_clearVariablesOnEvaluate( aClearVariablesOnEvaluate ), m_customCallback( std::move( aVariableCallback ) ), m_useCustomCallback( true ), m_defaultUnits( aUnits ) { m_lastErrors = std::make_unique(); } EXPRESSION_EVALUATOR::~EXPRESSION_EVALUATOR() = default; EXPRESSION_EVALUATOR::EXPRESSION_EVALUATOR( const EXPRESSION_EVALUATOR& aOther ) : m_variables( aOther.m_variables ), m_clearVariablesOnEvaluate( aOther.m_clearVariablesOnEvaluate ), m_customCallback( aOther.m_customCallback ), m_useCustomCallback( aOther.m_useCustomCallback ), m_defaultUnits( aOther.m_defaultUnits ) { m_lastErrors = std::make_unique(); if( aOther.m_lastErrors ) { // Copy error state for( const auto& error : aOther.m_lastErrors->GetErrors() ) m_lastErrors->AddError( error ); } } EXPRESSION_EVALUATOR& EXPRESSION_EVALUATOR::operator=( const EXPRESSION_EVALUATOR& aOther ) { if( this != &aOther ) { m_variables = aOther.m_variables; m_clearVariablesOnEvaluate = aOther.m_clearVariablesOnEvaluate; m_customCallback = aOther.m_customCallback; m_useCustomCallback = aOther.m_useCustomCallback; m_defaultUnits = aOther.m_defaultUnits; m_lastErrors = std::make_unique(); if( aOther.m_lastErrors ) { for( const auto& error : aOther.m_lastErrors->GetErrors() ) m_lastErrors->AddError( error ); } } return *this; } EXPRESSION_EVALUATOR::EXPRESSION_EVALUATOR( EXPRESSION_EVALUATOR&& aOther ) noexcept : m_variables( std::move( aOther.m_variables ) ), m_lastErrors( std::move( aOther.m_lastErrors ) ), m_clearVariablesOnEvaluate( aOther.m_clearVariablesOnEvaluate ), m_customCallback( std::move( aOther.m_customCallback ) ), m_useCustomCallback( aOther.m_useCustomCallback ), m_defaultUnits( aOther.m_defaultUnits ) { } EXPRESSION_EVALUATOR& EXPRESSION_EVALUATOR::operator=( EXPRESSION_EVALUATOR&& aOther ) noexcept { if( this != &aOther ) { m_variables = std::move( aOther.m_variables ); m_lastErrors = std::move( aOther.m_lastErrors ); m_clearVariablesOnEvaluate = aOther.m_clearVariablesOnEvaluate; m_customCallback = std::move( aOther.m_customCallback ); m_useCustomCallback = aOther.m_useCustomCallback; m_defaultUnits = aOther.m_defaultUnits; } return *this; } void EXPRESSION_EVALUATOR::SetVariableCallback( VariableCallback aCallback ) { m_customCallback = std::move( aCallback ); m_useCustomCallback = true; } void EXPRESSION_EVALUATOR::ClearVariableCallback() { m_customCallback = VariableCallback{}; m_useCustomCallback = false; } bool EXPRESSION_EVALUATOR::HasVariableCallback() const { return m_useCustomCallback && m_customCallback; } void EXPRESSION_EVALUATOR::SetDefaultUnits( EDA_UNITS aUnits ) { m_defaultUnits = aUnits; } EDA_UNITS EXPRESSION_EVALUATOR::GetDefaultUnits() const { return m_defaultUnits; } void EXPRESSION_EVALUATOR::SetVariable( const wxString& aName, double aValue ) { std::string name = wxStringToStdString( aName ); m_variables[name] = calc_parser::Value{ aValue }; } void EXPRESSION_EVALUATOR::SetVariable( const wxString& aName, const wxString& aValue ) { std::string name = wxStringToStdString( aName ); std::string value = wxStringToStdString( aValue ); m_variables[name] = calc_parser::Value{ value }; } void EXPRESSION_EVALUATOR::SetVariable( const std::string& aName, const std::string& aValue ) { m_variables[aName] = calc_parser::Value{ aValue }; } bool EXPRESSION_EVALUATOR::RemoveVariable( const wxString& aName ) { std::string name = wxStringToStdString( aName ); return m_variables.erase( name ) > 0; } void EXPRESSION_EVALUATOR::ClearVariables() { m_variables.clear(); } bool EXPRESSION_EVALUATOR::HasVariable( const wxString& aName ) const { std::string name = wxStringToStdString( aName ); return m_variables.find( name ) != m_variables.end(); } wxString EXPRESSION_EVALUATOR::GetVariable( const wxString& aName ) const { std::string name = wxStringToStdString( aName ); auto it = m_variables.find( name ); if( it != m_variables.end() ) { if( std::holds_alternative( it->second ) ) { double val = std::get( it->second ); // Smart formatting - whole numbers don't need decimal places if( val == std::floor( val ) && std::abs( val ) < 1e15 ) return wxString::Format( "%.0f", val ); else return wxString::Format( "%g", val ); } else { return stdStringToWxString( std::get( it->second ) ); } } return wxString{}; } std::vector EXPRESSION_EVALUATOR::GetVariableNames() const { std::vector names; names.reserve( m_variables.size() ); for( const auto& [name, value] : m_variables ) names.push_back( stdStringToWxString( name ) ); return names; } void EXPRESSION_EVALUATOR::SetVariables( const std::unordered_map& aVariables ) { for( const auto& [name, value] : aVariables ) SetVariable( name, value ); } void EXPRESSION_EVALUATOR::SetVariables( const std::unordered_map& aVariables ) { for( const auto& [name, value] : aVariables ) SetVariable( name, value ); } wxString EXPRESSION_EVALUATOR::Evaluate( const wxString& aInput ) { std::unordered_map emptyNumVars; std::unordered_map emptyStringVars; return Evaluate( aInput, emptyNumVars, emptyStringVars ); } wxString EXPRESSION_EVALUATOR::Evaluate( const wxString& aInput, const std::unordered_map& aTempVariables ) { std::unordered_map emptyStringVars; return Evaluate( aInput, aTempVariables, emptyStringVars ); } wxString EXPRESSION_EVALUATOR::Evaluate( const wxString& aInput, const std::unordered_map& aTempNumericVars, const std::unordered_map& aTempStringVars ) { // Clear previous errors ClearErrors(); // Expand ${variable} patterns that are OUTSIDE of @{} expressions wxString processedInput = expandVariablesOutsideExpressions( aInput, aTempNumericVars, aTempStringVars ); // Convert processed input to std::string std::string input = wxStringToStdString( processedInput ); // Create combined callback for all variable sources auto combinedCallback = createCombinedCallback( &aTempNumericVars, &aTempStringVars ); // Evaluate using parser auto [result, hadErrors] = evaluateWithParser( input, combinedCallback ); // Update error state if evaluation had errors if( hadErrors && !m_lastErrors ) { m_lastErrors = std::make_unique(); } if( hadErrors ) { m_lastErrors->AddError( "Evaluation failed" ); } // Clear variables if requested if( m_clearVariablesOnEvaluate ) ClearVariables(); // Convert result back to wxString return stdStringToWxString( result ); } bool EXPRESSION_EVALUATOR::HasErrors() const { return m_lastErrors && m_lastErrors->HasErrors(); } wxString EXPRESSION_EVALUATOR::GetErrorSummary() const { if( !m_lastErrors ) return wxString{}; return stdStringToWxString( m_lastErrors->GetAllMessages() ); } size_t EXPRESSION_EVALUATOR::GetErrorCount() const { if( !m_lastErrors ) return 0; return m_lastErrors->GetErrors().size(); } std::vector EXPRESSION_EVALUATOR::GetErrors() const { std::vector result; if( m_lastErrors ) { const auto& errors = m_lastErrors->GetErrors(); result.reserve( errors.size() ); for( const auto& error : errors ) result.push_back( stdStringToWxString( error ) ); } return result; } void EXPRESSION_EVALUATOR::ClearErrors() { if( m_lastErrors ) m_lastErrors->Clear(); } void EXPRESSION_EVALUATOR::SetClearVariablesOnEvaluate( bool aEnable ) { m_clearVariablesOnEvaluate = aEnable; } bool EXPRESSION_EVALUATOR::GetClearVariablesOnEvaluate() const { return m_clearVariablesOnEvaluate; } bool EXPRESSION_EVALUATOR::TestExpression( const wxString& aExpression ) { // Create a test input with the expression wrapped in @{} wxString testInput = "@{" + aExpression + "}"; // Create a minimal callback that returns errors for all variables auto testCallback = []( const std::string& aVarName ) -> calc_parser::Result { return calc_parser::MakeError( "Test mode - no variables available" ); }; // Try to parse it std::string input = wxStringToStdString( testInput ); auto [result, hadErrors] = evaluateWithParser( input, testCallback ); // Check if there were parsing errors (ignore evaluation errors for undefined variables) if( m_lastErrors ) { const auto& errors = m_lastErrors->GetErrors(); // Filter out "Test mode - no variables available" errors, look for syntax errors for( const auto& error : errors ) { if( error.find( "Syntax error" ) != std::string::npos || error.find( "Parser failed" ) != std::string::npos ) { return false; // Found syntax error } } } return true; // No syntax errors found } size_t EXPRESSION_EVALUATOR::CountExpressions( const wxString& aInput ) const { size_t count = 0; size_t pos = 0; while( ( pos = aInput.find( "@{", pos ) ) != wxString::npos ) { count++; pos += 2; // Move past "@{" } return count; } std::vector EXPRESSION_EVALUATOR::ExtractExpressions( const wxString& aInput ) const { std::vector expressions; size_t pos = 0; while( ( pos = aInput.find( "@{", pos ) ) != wxString::npos ) { size_t start = pos + 2; // Skip "@{" size_t end = aInput.find( "}", start ); if( end != wxString::npos ) { expressions.push_back( aInput.substr( start, end - start ) ); pos = end + 1; } else { break; // No closing brace found } } return expressions; } std::string EXPRESSION_EVALUATOR::wxStringToStdString( const wxString& aWxStr ) const { return aWxStr.ToStdString( wxConvUTF8 ); } wxString EXPRESSION_EVALUATOR::stdStringToWxString( const std::string& aStdStr ) const { return wxString( aStdStr.c_str(), wxConvUTF8 ); } wxString EXPRESSION_EVALUATOR::expandVariablesOutsideExpressions( const wxString& aInput, const std::unordered_map& aTempNumericVars, const std::unordered_map& aTempStringVars ) const { wxString result = aInput; size_t pos = 0; // Track positions of @{} expressions to avoid substituting inside them std::vector> expressionRanges; // Find all @{} expression ranges while( (pos = result.find( "@{", pos )) != std::string::npos ) { size_t start = pos; size_t braceCount = 1; size_t searchPos = start + 2; // Skip "@{" // Find matching closing brace while( searchPos < result.length() && braceCount > 0 ) { if( result[searchPos] == '{' ) braceCount++; else if( result[searchPos] == '}' ) braceCount--; searchPos++; } if( braceCount == 0 ) { expressionRanges.emplace_back( start, searchPos ); // searchPos is after '}' } pos = searchPos; } // Now find and replace ${variable} patterns that are NOT inside @{} expressions pos = 0; while( (pos = result.find( "${", pos )) != std::string::npos ) { // Check if this ${} is inside any @{} expression bool insideExpression = false; for( const auto& range : expressionRanges ) { if( pos >= range.first && pos < range.second ) { insideExpression = true; break; } } if( insideExpression ) { // Special case: if this variable is immediately followed by unit text, // we should expand it to allow proper unit parsing size_t closePos = result.find( "}", pos + 2 ); if( closePos != std::string::npos ) { // Check what comes after the closing brace size_t afterBrace = closePos + 1; bool followedByUnit = false; if( afterBrace < result.length() ) { // Check if followed by any supported unit strings using centralized registry const auto units = text_eval_units::UnitRegistry::getAllUnitStrings(); for( const auto& unit : units ) { if( afterBrace + unit.length() <= result.length() && result.substr( afterBrace, unit.length() ) == unit ) { followedByUnit = true; break; } } } if( !followedByUnit ) { pos += 2; // Skip this ${} since it's inside an expression and not followed by units continue; } // If followed by units, continue with variable expansion below } else { pos += 2; // Invalid pattern, skip continue; } } // Find the closing brace size_t closePos = result.find( "}", pos + 2 ); if( closePos == std::string::npos ) { pos += 2; // Invalid ${} pattern, skip continue; } // Extract variable name wxString varName = result.substr( pos + 2, closePos - pos - 2 ); wxString replacement; bool found = false; // Check temporary string variables first auto stringIt = aTempStringVars.find( varName ); if( stringIt != aTempStringVars.end() ) { replacement = stringIt->second; found = true; } else { // Check temporary numeric variables auto numIt = aTempNumericVars.find( varName ); if( numIt != aTempNumericVars.end() ) { replacement = wxString::FromDouble( numIt->second ); found = true; } else { // Check instance variables std::string stdVarName = wxStringToStdString( varName ); auto instIt = m_variables.find( stdVarName ); if( instIt != m_variables.end() ) { const calc_parser::Value& value = instIt->second; if( std::holds_alternative( value ) ) { replacement = stdStringToWxString( std::get( value ) ); found = true; } else if( std::holds_alternative( value ) ) { replacement = wxString::FromDouble( std::get( value ) ); found = true; } } } } if( found ) { // Replace ${variable} with its value result.replace( pos, closePos - pos + 1, replacement ); pos += replacement.length(); } else { // Variable not found, record error but leave ${variable} unchanged if( !m_lastErrors ) m_lastErrors = std::make_unique(); m_lastErrors->AddError( fmt::format( "Undefined variable: {}", wxStringToStdString( varName ) ) ); pos = closePos + 1; } } return result; } EXPRESSION_EVALUATOR::VariableCallback EXPRESSION_EVALUATOR::createCombinedCallback( const std::unordered_map* aTempNumericVars, const std::unordered_map* aTempStringVars ) const { return [this, aTempNumericVars, aTempStringVars]( const std::string& aVarName ) -> calc_parser::Result { // Priority 1: Custom callback (if set) if( m_useCustomCallback && m_customCallback ) { auto customResult = m_customCallback( aVarName ); if( customResult.HasValue() ) return customResult; // If custom callback returned an error, continue to fallback options // unless the error indicates a definitive "not found" vs "lookup failed" // For simplicity, we'll always try fallbacks } // Priority 2: Temporary string variables if( aTempStringVars ) { wxString wxVarName = stdStringToWxString( aVarName ); if( auto it = aTempStringVars->find( wxVarName ); it != aTempStringVars->end() ) { std::string stdValue = wxStringToStdString( it->second ); return calc_parser::MakeValue( stdValue ); } } // Priority 3: Temporary numeric variables if( aTempNumericVars ) { wxString wxVarName = stdStringToWxString( aVarName ); if( auto it = aTempNumericVars->find( wxVarName ); it != aTempNumericVars->end() ) { return calc_parser::MakeValue( it->second ); } } // Priority 4: Stored variables if( auto it = m_variables.find( aVarName ); it != m_variables.end() ) { return calc_parser::MakeValue( it->second ); } // Priority 5: Use KiCad's ExpandTextVars for system/project variables try { wxString varName = stdStringToWxString( aVarName ); wxString testString = wxString::Format( "${%s}", varName ); // Create a resolver that will return true if the variable was found bool wasResolved = false; std::function resolver = [&wasResolved]( wxString* token ) -> bool { // If we get here, ExpandTextVars found the variable and wants to resolve it // For our purposes, we just want to know if it exists, so return false // to keep the original ${varname} format, and set our flag wasResolved = true; return false; // Don't replace, just detect }; wxString expandedResult = ExpandTextVars( testString, &resolver ); if( wasResolved ) { // Variable exists in KiCad's system, now get its actual value std::function valueResolver = []( wxString* token ) -> bool { // Let ExpandTextVars resolve this normally // We'll get the resolved value in token return false; // Use default resolution }; wxString resolvedValue = ExpandTextVars( testString, &valueResolver ); // Check if it was actually resolved (not still ${varname}) if( resolvedValue != testString ) { std::string resolvedStd = wxStringToStdString( resolvedValue ); // Try to parse as number first try { double numValue; auto result = fast_float::from_chars( resolvedStd.data(), resolvedStd.data() + resolvedStd.size(), numValue ); if( result.ec != std::errc() || result.ptr != resolvedStd.data() + resolvedStd.size() ) throw std::invalid_argument( fmt::format( "Cannot convert '{}' to number", resolvedStd ) ); return calc_parser::MakeValue( numValue ); } catch( ... ) { // Not a number, return as string return calc_parser::MakeValue( resolvedStd ); } } } } catch( const std::exception& e ) { // ExpandTextVars failed, continue to error } // Priority 6: If custom callback was tried and failed, return its error if( m_useCustomCallback && m_customCallback ) { return m_customCallback( aVarName ); // Return the original error } // No variable found anywhere return calc_parser::MakeError( fmt::format( "Undefined variable: {}", aVarName ) ); }; } std::pair EXPRESSION_EVALUATOR::evaluateWithParser( const std::string& aInput, VariableCallback aVariableCallback) { try { // Try partial error recovery first auto [partialResult, partialHadErrors] = evaluateWithPartialErrorRecovery(aInput, aVariableCallback); // If partial recovery made any progress (result differs from input), use it if (partialResult != aInput) { // Partial recovery made progress - always report errors collected during partial recovery return {std::move(partialResult), partialHadErrors}; } // If no progress was made, try original full parsing approach as fallback return evaluateWithFullParser(aInput, std::move(aVariableCallback)); } catch (const std::bad_alloc&) { if (m_lastErrors) { m_lastErrors->AddError("Out of memory"); } return {aInput, true}; } catch (const std::exception& e) { if (m_lastErrors) { m_lastErrors->AddError(fmt::format("Exception: {}", e.what())); } return {aInput, true}; } } std::pair EXPRESSION_EVALUATOR::evaluateWithPartialErrorRecovery( const std::string& aInput, VariableCallback aVariableCallback) { std::string result = aInput; bool hadAnyErrors = false; size_t pos = 0; // Process expressions from right to left to avoid position shifts std::vector> expressionRanges; // Find all expression ranges while( ( pos = result.find( "@{", pos ) ) != std::string::npos ) { size_t start = pos; size_t exprStart = pos + 2; // Skip "@{" size_t braceCount = 1; size_t searchPos = exprStart; // Find matching closing brace, handling nested braces while( searchPos < result.length() && braceCount > 0 ) { if( result[searchPos] == '{' ) { braceCount++; } else if( result[searchPos] == '}' ) { braceCount--; } searchPos++; } if( braceCount == 0 ) { size_t end = searchPos; // Position after the '}' expressionRanges.emplace_back( start, end ); pos = end; } else { pos = exprStart; // Skip this malformed expression } } // Process expressions from right to left to avoid position shifts for( auto it = expressionRanges.rbegin(); it != expressionRanges.rend(); ++it ) { auto [start, end] = *it; std::string fullExpr = result.substr( start, end - start ); std::string innerExpr = result.substr( start + 2, end - start - 3 ); // Remove @{ and } // Try to evaluate this single expression try { // Create a simple expression for evaluation std::string testExpr = "@{" + innerExpr + "}"; // Create a temporary error collector to capture errors for this specific expression auto tempErrors = std::make_unique(); auto oldErrors = std::move( m_lastErrors ); m_lastErrors = std::move( tempErrors ); // Use the full parser for this single expression auto [evalResult, evalHadErrors] = evaluateWithFullParser( testExpr, aVariableCallback ); if( !evalHadErrors ) { // Successful evaluation, replace in result result.replace( start, end - start, evalResult ); } else { // Expression failed - add a specific error for this expression hadAnyErrors = true; // Restore main error collector and add error if( !oldErrors ) oldErrors = std::make_unique(); oldErrors->AddError( fmt::format( "Failed to evaluate expression: {}", fullExpr ) ); } // Restore the main error collector m_lastErrors = std::move( oldErrors ); } catch( ... ) { // Report exception as an error for this expression if( !m_lastErrors ) m_lastErrors = std::make_unique(); m_lastErrors->AddError( fmt::format( "Exception in expression: {}", fullExpr ) ); hadAnyErrors = true; } } return { std::move( result ), hadAnyErrors }; } std::pair EXPRESSION_EVALUATOR::evaluateWithFullParser( const std::string& aInput, VariableCallback aVariableCallback ) { if( aInput.empty() ) { return { std::string{}, false }; } // RAII guard for error collector cleanup struct ErrorCollectorGuard { ~ErrorCollectorGuard() { calc_parser::g_errorCollector = nullptr; } } guard; try { // Clear previous errors if( m_lastErrors ) { m_lastErrors->Clear(); } // Set up error collector calc_parser::g_errorCollector = m_lastErrors.get(); // Create tokenizer with default units KIEVAL_TEXT_TOKENIZER tokenizer{ aInput, m_lastErrors.get(), m_defaultUnits }; // Create parser deleter function auto parser_deleter = []( void* p ) { KI_EVAL::ParseFree( p, free ); }; // Allocate parser with RAII cleanup std::unique_ptr parser{ KI_EVAL::ParseAlloc( malloc ), parser_deleter }; if( !parser ) { if( m_lastErrors ) { m_lastErrors->AddError( "Failed to allocate parser" ); } return { aInput, true }; } // Parse document calc_parser::DOC* document = nullptr; calc_parser::TOKEN_TYPE token_value; TextEvalToken token_type; do { token_type = tokenizer.get_next_token( token_value ); // Send token to parser KI_EVAL::Parse( parser.get(), static_cast( token_type ), token_value, &document ); // Early exit on errors if( m_lastErrors && m_lastErrors->HasErrors() ) { break; } } while( token_type != TextEvalToken::ENDS && tokenizer.has_more_tokens() ); // Finalize parsing KI_EVAL::Parse( parser.get(), static_cast( TextEvalToken::ENDS ), calc_parser::TOKEN_TYPE{}, &document ); // Process document if parsing succeeded if( document && ( !m_lastErrors || !m_lastErrors->HasErrors() ) ) { calc_parser::DOC_PROCESSOR processor; auto [result, had_errors] = processor.Process( *document, std::move( aVariableCallback ) ); // If processing had any evaluation errors, return original input unchanged // This preserves the original expression syntax while still reporting errors if( had_errors ) { delete document; return { aInput, true }; } delete document; return { std::move( result ), had_errors }; } // Cleanup and return original on error delete document; return { aInput, true }; } catch( const std::bad_alloc& ) { if( m_lastErrors ) { m_lastErrors->AddError( "Out of memory" ); } return { aInput, true }; } catch( const std::exception& e ) { if( m_lastErrors ) { m_lastErrors->AddError( fmt::format( "Exception: {}", e.what() ) ); } return { aInput, true }; } } NUMERIC_EVALUATOR_COMPAT::NUMERIC_EVALUATOR_COMPAT( EDA_UNITS aUnits ) : m_evaluator( aUnits ), m_lastValid( false ) { } NUMERIC_EVALUATOR_COMPAT::~NUMERIC_EVALUATOR_COMPAT() = default; void NUMERIC_EVALUATOR_COMPAT::Clear() { m_lastInput.clear(); m_lastResult.clear(); m_lastValid = false; m_evaluator.ClearErrors(); } void NUMERIC_EVALUATOR_COMPAT::SetDefaultUnits( EDA_UNITS aUnits ) { m_evaluator.SetDefaultUnits( aUnits ); } void NUMERIC_EVALUATOR_COMPAT::LocaleChanged() { // No-op: EXPRESSION_EVALUATOR handles locale properly internally } bool NUMERIC_EVALUATOR_COMPAT::IsValid() const { return m_lastValid; } wxString NUMERIC_EVALUATOR_COMPAT::Result() const { return m_lastResult; } bool NUMERIC_EVALUATOR_COMPAT::Process( const wxString& aString ) { m_lastInput = aString; m_evaluator.ClearErrors(); // Convert bare variable names to ${variable} syntax for compatibility // This allows NUMERIC_EVALUATOR-style variable access to work with EXPRESSION_EVALUATOR wxString processedExpression = aString; // Get all variable names that are currently defined auto varNames = m_evaluator.GetVariableNames(); // Sort variable names by length (longest first) to avoid partial replacements std::sort( varNames.begin(), varNames.end(), []( const wxString& a, const wxString& b ) { return a.length() > b.length(); } ); // Replace bare variable names with ${variable} syntax for( const auto& varName : varNames ) { // Create a regex to match the variable name as a whole word // This avoids replacing parts of other words wxString pattern = "\\b" + varName + "\\b"; wxString replacement = "${" + varName + "}"; // Simple string replacement (not regex for now to avoid complexity) // Look for the variable name surrounded by non-alphanumeric characters size_t pos = 0; while( ( pos = processedExpression.find( varName, pos ) ) != wxString::npos ) { // Check if this is a whole word (not part of another identifier) bool isWholeWord = true; // Check character before if( pos > 0 ) { wxChar before = processedExpression[pos - 1]; if( wxIsalnum( before ) || before == '_' || before == '$' ) isWholeWord = false; } // Check character after if( isWholeWord && pos + varName.length() < processedExpression.length() ) { wxChar after = processedExpression[pos + varName.length()]; if( wxIsalnum( after ) || after == '_' ) isWholeWord = false; } if( isWholeWord ) { processedExpression.replace( pos, varName.length(), replacement ); pos += replacement.length(); } else { pos += varName.length(); } } } // Wrap the processed expression in @{...} syntax for EXPRESSION_EVALUATOR wxString wrappedExpression = "@{" + processedExpression + "}"; m_lastResult = m_evaluator.Evaluate( wrappedExpression ); m_lastValid = !m_evaluator.HasErrors(); // Additional check: if the result is exactly the wrapped expression, // it means the expression wasn't evaluated (likely due to errors) if( m_lastResult == wrappedExpression ) { m_lastValid = false; m_lastResult = "NaN"; } // If there were errors, set result to "NaN" to match NUMERIC_EVALUATOR behavior if( !m_lastValid ) { m_lastResult = "NaN"; return false; } return true; } wxString NUMERIC_EVALUATOR_COMPAT::OriginalText() const { return m_lastInput; } void NUMERIC_EVALUATOR_COMPAT::SetVar( const wxString& aString, double aValue ) { m_evaluator.SetVariable( aString, aValue ); } double NUMERIC_EVALUATOR_COMPAT::GetVar( const wxString& aString ) { if( !m_evaluator.HasVariable( aString ) ) return 0.0; wxString value = m_evaluator.GetVariable( aString ); // Try to convert to double double result = 0.0; if( !value.ToDouble( &result ) ) return 0.0; return result; } void NUMERIC_EVALUATOR_COMPAT::RemoveVar( const wxString& aString ) { m_evaluator.RemoveVariable( aString ); } void NUMERIC_EVALUATOR_COMPAT::ClearVar() { m_evaluator.ClearVariables(); }