diff --git a/lib/sc/actions/assign_action.ex b/lib/sc/actions/assign_action.ex index 1e1f8dd..e998a75 100644 --- a/lib/sc/actions/assign_action.ex +++ b/lib/sc/actions/assign_action.ex @@ -32,33 +32,53 @@ defmodule SC.Actions.AssignAction do require Logger @enforce_keys [:location, :expr] - defstruct [:location, :expr, :source_location] + defstruct [:location, :expr, :compiled_expr, :source_location] @type t :: %__MODULE__{ location: String.t(), expr: String.t(), + compiled_expr: term() | nil, source_location: map() | nil } @doc """ Create a new AssignAction from parsed attributes. + The expr is compiled for performance during creation. + ## Examples - iex> SC.Actions.AssignAction.new("user.name", "'John'") - %SC.Actions.AssignAction{location: "user.name", expr: "'John'"} + iex> action = SC.Actions.AssignAction.new("user.name", "'John'") + iex> action.location + "user.name" + iex> action.expr + "'John'" + iex> is_list(action.compiled_expr) + true """ @spec new(String.t(), String.t(), map() | nil) :: t() def new(location, expr, source_location \\ nil) when is_binary(location) and is_binary(expr) do + # Pre-compile expression for performance + compiled_expr = compile_safe(expr, :expression) + %__MODULE__{ location: location, expr: expr, + compiled_expr: compiled_expr, source_location: source_location } end + # Safely compile expressions, returning nil on error + defp compile_safe(expr, _type) do + case ValueEvaluator.compile_expression(expr) do + {:ok, compiled} -> compiled + {:error, _reason} -> nil + end + end + @doc """ Execute the assign action by evaluating the expression and assigning to the location. @@ -73,10 +93,12 @@ defmodule SC.Actions.AssignAction do def execute(%__MODULE__{} = assign_action, %StateChart{} = state_chart) do context = build_evaluation_context(state_chart) + # Use ValueEvaluator.evaluate_and_assign with pre-compiled expression if available case ValueEvaluator.evaluate_and_assign( assign_action.location, assign_action.expr, - context + context, + assign_action.compiled_expr ) do {:ok, updated_data_model} -> # Update the state chart with the new data model diff --git a/lib/sc/value_evaluator.ex b/lib/sc/value_evaluator.ex index 1b207e1..391af52 100644 --- a/lib/sc/value_evaluator.ex +++ b/lib/sc/value_evaluator.ex @@ -147,13 +147,21 @@ defmodule SC.ValueEvaluator do Evaluate an expression and assign its result to a location in the data model. This combines expression evaluation with location-based assignment. + If a pre-compiled expression is provided, it will be used for better performance. """ @spec evaluate_and_assign(String.t(), String.t(), map()) :: {:ok, map()} | {:error, term()} def evaluate_and_assign(location_expr, value_expr, context) when is_binary(location_expr) and is_binary(value_expr) and is_map(context) do + evaluate_and_assign(location_expr, value_expr, context, nil) + end + + @spec evaluate_and_assign(String.t(), String.t(), map(), term() | nil) :: + {:ok, map()} | {:error, term()} + def evaluate_and_assign(location_expr, value_expr, context, compiled_expr) + when is_binary(location_expr) and is_binary(value_expr) and is_map(context) do with {:ok, path} <- resolve_location(location_expr, context), - {:ok, compiled_value} <- compile_expression(value_expr), - {:ok, evaluated_value} <- evaluate_value(compiled_value, context), + {:ok, evaluated_value} <- + evaluate_expression_optimized(value_expr, compiled_expr, context), data_model <- extract_data_model(context), {:ok, updated_model} <- assign_value(path, evaluated_value, data_model) do {:ok, updated_model} @@ -162,6 +170,37 @@ defmodule SC.ValueEvaluator do end end + # Use pre-compiled expression if available, otherwise use the string + defp evaluate_expression_optimized(_value_expr, compiled_expr, context) + when not is_nil(compiled_expr) do + # Pass compiled instructions directly to predicator + evaluate_with_predicator(compiled_expr, context) + end + + defp evaluate_expression_optimized(value_expr, nil, context) do + # Pass string directly to predicator for compilation and evaluation + evaluate_with_predicator(value_expr, context) + end + + # Evaluate using predicator with proper SCXML context and functions + defp evaluate_with_predicator(expression_or_instructions, context) do + eval_context = + if has_scxml_context?(context) do + ConditionEvaluator.build_scxml_context(context) + else + context + end + + scxml_functions = ConditionEvaluator.build_scxml_functions(context) + + case Predicator.evaluate(expression_or_instructions, eval_context, functions: scxml_functions) do + {:ok, value} -> {:ok, value} + {:error, reason} -> {:error, reason} + end + rescue + error -> {:error, error} + end + # Private functions # Check if context has SCXML-specific keys diff --git a/test/sc/actions/assign_action_test.exs b/test/sc/actions/assign_action_test.exs index 72c72ac..57456d1 100644 --- a/test/sc/actions/assign_action_test.exs +++ b/test/sc/actions/assign_action_test.exs @@ -184,5 +184,31 @@ defmodule SC.Actions.AssignActionTest do assert %StateChart{data_model: %{"counter" => 0, "stateCount" => 1}} = result end + + test "pre-compiles expressions during creation for performance" do + action = AssignAction.new("user.profile.name", "'John Doe'") + + # Verify that expression is pre-compiled + assert not is_nil(action.compiled_expr) + assert is_list(action.compiled_expr) + + # Verify original strings are preserved + assert action.location == "user.profile.name" + assert action.expr == "'John Doe'" + end + + test "uses pre-compiled expressions for better performance", %{state_chart: state_chart} do + action = AssignAction.new("user.settings.theme", "'dark'") + + # Verify pre-compilation occurred for expression + assert not is_nil(action.compiled_expr) + + # Execute should use pre-compiled expressions internally + result = AssignAction.execute(action, state_chart) + + # Verify result is correct + expected_data = %{"user" => %{"settings" => %{"theme" => "dark"}}} + assert %StateChart{data_model: ^expected_data} = result + end end end