Skip to content
This repository was archived by the owner on Sep 12, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions lib/sc/actions/assign_action.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
43 changes: 41 additions & 2 deletions lib/sc/value_evaluator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions test/sc/actions/assign_action_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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