@@ -102,6 +102,27 @@ t_computed_expression::t_computed_expression(
102102 m_column_ids (column_ids),
103103 m_dtype (dtype) {}
104104
105+ struct t_computed_expression_cache {
106+ t_computed_expression_cache () = default ;
107+ t_computed_expression_cache (const t_computed_expression_cache&) = delete ;
108+ t_computed_expression_cache& operator =(const t_computed_expression_cache&) =
109+ delete ;
110+ t_computed_expression_cache (t_computed_expression_cache&&) = delete ;
111+ t_computed_expression_cache& operator =(t_computed_expression_cache&&) =
112+ delete ;
113+
114+ std::shared_ptr<t_data_table> m_current_source_table;
115+ t_uindex m_row_idx{0 };
116+ std::vector<std::pair<std::string, t_tscalar>> m_values;
117+ std::vector<std::shared_ptr<t_column>> m_columns;
118+ std::vector<t_dtype> m_input_dtypes;
119+ exprtk::symbol_table<t_tscalar> m_sym_table;
120+ exprtk::expression<t_tscalar> m_expr;
121+ std::unique_ptr<t_computed_function_store> m_function_store;
122+ };
123+
124+ t_computed_expression::~t_computed_expression () = default ;
125+
105126void
106127t_computed_expression::compute (
107128 const std::shared_ptr<t_data_table>& source_table,
@@ -110,53 +131,94 @@ t_computed_expression::compute(
110131 t_expression_vocab& vocab,
111132 t_regex_mapping& regex_mapping
112133) const {
113- // TODO: share symtables across pre/re/compute
114- exprtk::symbol_table<t_tscalar> sym_table;
134+ auto num_input_columns = m_column_ids.size ();
115135
116- // pi, infinity, etc.
117- sym_table.add_constants ();
136+ bool needs_build = !m_cache;
137+ if (!needs_build) {
138+ for (t_uindex cidx = 0 ; cidx < num_input_columns; ++cidx) {
139+ const std::string& column_name = m_column_ids[cidx].second ;
140+ if (source_table->get_column (column_name)->get_dtype ()
141+ != m_cache->m_input_dtypes [cidx]) {
142+ needs_build = true ;
143+ break ;
144+ }
145+ }
146+ }
118147
119- t_uindex row_idx = 0 ;
148+ if (needs_build) {
149+ m_cache = std::make_unique<t_computed_expression_cache>();
150+ auto & cache = *m_cache;
151+
152+ cache.m_current_source_table = source_table;
153+ cache.m_sym_table .add_constants (); // pi, infinity, etc.
154+
155+ // is_type_validator = false: we are computing values, not type-checking.
156+ cache.m_function_store = std::make_unique<t_computed_function_store>(
157+ vocab,
158+ regex_mapping,
159+ false ,
160+ cache.m_current_source_table ,
161+ pkey_map,
162+ cache.m_row_idx
163+ );
164+ cache.m_function_store ->register_computed_functions (cache.m_sym_table );
120165
121- // Create a function store, with is_type_validator set to false as we
122- // are calculating values, not type-checking.
123- t_computed_function_store function_store (
124- vocab, regex_mapping, false , source_table, pkey_map, row_idx
125- );
126- function_store.register_computed_functions (sym_table);
166+ // Size exactly once; the symbol table binds a T* into each
167+ // m_values[cidx].second, so this storage must never be reallocated.
168+ cache.m_values .resize (num_input_columns);
169+ cache.m_columns .resize (num_input_columns);
170+ cache.m_input_dtypes .resize (num_input_columns);
127171
128- exprtk::expression<t_tscalar> expr_definition;
129- std::vector<std::pair<std::string, t_tscalar>> values;
130- tsl::hopscotch_map<std::string, std::shared_ptr<t_column>> columns;
172+ for (t_uindex cidx = 0 ; cidx < num_input_columns; ++cidx) {
173+ const std::string& column_id = m_column_ids[cidx].first ;
174+ const std::string& column_name = m_column_ids[cidx].second ;
175+ t_dtype dtype = source_table->get_column (column_name)->get_dtype ();
131176
132- auto num_input_columns = m_column_ids.size ();
133- values.resize (num_input_columns);
134- columns.reserve (num_input_columns);
177+ cache.m_input_dtypes [cidx] = dtype;
135178
136- for (t_uindex cidx = 0 ; cidx < num_input_columns; ++cidx) {
137- const std::string& column_id = m_column_ids[cidx].first ;
138- const std::string& column_name = m_column_ids[cidx].second ;
139- columns[column_id] = source_table->get_column (column_name);
179+ t_tscalar rval;
180+ rval.clear ();
181+ rval.m_type = dtype;
140182
141- t_tscalar rval;
142- rval.clear ();
143- rval.m_type = columns[column_id]->get_dtype ();
183+ cache.m_values [cidx] =
184+ std::pair<std::string, t_tscalar>(column_id, rval);
185+ cache.m_sym_table .add_variable (
186+ column_id, cache.m_values [cidx].second
187+ );
188+ }
144189
145- values[cidx] = std::pair<std::string, t_tscalar>(column_id, rval);
146- sym_table.add_variable (column_id, values[cidx].second );
190+ cache.m_expr .register_symbol_table (cache.m_sym_table );
191+
192+ // Load-bearing for compile-once: exprtk constant-folds an all-literal
193+ // function call (e.g. intern('x'), concat('a','b')) to a literal node at
194+ // compile time ONLY when the function reports no side effects. Our
195+ // functions inherit the igeneric_function/ifunction default
196+ // has_side_effects() == true and never call disable_has_side_effects(),
197+ // so such calls are not folded and re-evaluate on every value(). If a
198+ // future exprtk bump flips that default, the first call's result would
199+ // be frozen into the cached AST -- revisit this cache if so.
200+ if (!m_computed_expression_parser.m_parser ->compile (
201+ m_parsed_expression_string, cache.m_expr
202+ )) {
203+ std::stringstream ss;
204+ ss << " [t_computed_expression::compute] Failed to parse "
205+ " expression: `"
206+ << m_parsed_expression_string << " `, failed with error: "
207+ << m_computed_expression_parser.m_parser ->error () << ' \n ' ;
208+
209+ PSP_COMPLAIN_AND_ABORT (ss.str ());
210+ }
147211 }
148212
149- expr_definition.register_symbol_table (sym_table);
150-
151- if (!m_computed_expression_parser.m_parser ->compile (
152- m_parsed_expression_string, expr_definition
153- )) {
154- std::stringstream ss;
155- ss << " [t_computed_expression::compute] Failed to parse expression: `"
156- << m_parsed_expression_string << " `, failed with error: "
157- << m_computed_expression_parser.m_parser ->error () << ' \n ' ;
213+ auto & cache = *m_cache;
158214
159- PSP_COMPLAIN_AND_ABORT (ss.str ());
215+ // Re-point the per-call inputs the compiled function objects read through
216+ // (the source table differs across the master/flattened/delta/prev/current
217+ // tables within a single update), and re-fetch this call's source columns.
218+ cache.m_current_source_table = source_table;
219+ for (t_uindex cidx = 0 ; cidx < num_input_columns; ++cidx) {
220+ cache.m_columns [cidx] =
221+ source_table->get_column (m_column_ids[cidx].second );
160222 }
161223
162224 // create or get output column using m_expression_alias
@@ -167,12 +229,13 @@ t_computed_expression::compute(
167229
168230 for (t_uindex ridx = 0 ; ridx < num_rows; ++ridx) {
169231 for (t_uindex cidx = 0 ; cidx < num_input_columns; ++cidx) {
170- const std::string& column_id = m_column_ids[cidx].first ;
171- values[cidx].second .set (columns[column_id]->get_scalar (ridx));
232+ cache.m_values [cidx].second .set (
233+ cache.m_columns [cidx]->get_scalar (ridx)
234+ );
172235 }
173- row_idx = ridx;
236+ cache. m_row_idx = ridx;
174237
175- t_tscalar value = expr_definition .value ();
238+ t_tscalar value = cache. m_expr .value ();
176239
177240 if (!value.is_valid () || value.is_none ()) {
178241 output_column->clear (ridx);
@@ -182,7 +245,8 @@ t_computed_expression::compute(
182245 output_column->set_scalar (ridx, value);
183246 }
184247
185- function_store.clear_computed_function_state ();
248+ // order()'s accumulator must still be reset at the end of every call.
249+ cache.m_function_store ->clear_computed_function_state ();
186250};
187251
188252const std::string&
0 commit comments