Skip to content
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
69 changes: 55 additions & 14 deletions lib/statifier/actions/foreach_action.ex
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,68 @@ defmodule Statifier.Actions.ForeachAction do
"""
@spec execute(StateChart.t(), t()) :: StateChart.t()
def execute(%StateChart{} = state_chart, %__MODULE__{} = foreach_action) do
# Step 1: Evaluate array expression
case evaluate_array(foreach_action, state_chart) do
{:ok, collection} when is_list(collection) ->
# Step 2: Execute iteration with proper variable scoping
execute_iteration(foreach_action, collection, state_chart)

{:ok, _non_list} ->
# Not an iterable collection - raise error.execution
raise_execution_error(
state_chart,
"Array expression did not evaluate to an iterable collection"
)
# Step 1: Validate item and index parameters are legal variable names
case validate_variable_names(foreach_action) do
:ok ->
# Step 2: Evaluate array expression
case evaluate_array(foreach_action, state_chart) do
{:ok, collection} when is_list(collection) ->
# Step 3: Execute iteration with proper variable scoping
execute_iteration(foreach_action, collection, state_chart)

{:ok, _non_list} ->
# Not an iterable collection - raise error.execution
raise_execution_error(
state_chart,
"Array expression did not evaluate to an iterable collection"
)

{:error, reason} ->
# Array evaluation failed - raise error.execution
raise_execution_error(state_chart, "Array evaluation failed: #{inspect(reason)}")
end

{:error, reason} ->
# Array evaluation failed - raise error.execution
raise_execution_error(state_chart, "Array evaluation failed: #{inspect(reason)}")
# Invalid variable names - raise error.execution
raise_execution_error(state_chart, reason)
end
end

# Private functions

# Validate that item and index parameters are legal variable names
defp validate_variable_names(foreach_action) do
# Validate item parameter (required)
case validate_single_variable_name(foreach_action.item, "item") do
:ok ->
# Validate index parameter (optional)
if foreach_action.index do
validate_single_variable_name(foreach_action.index, "index")
else
:ok
end

error ->
error
end
end

# Validate a single variable name using the Evaluator location validation
defp validate_single_variable_name(variable_name, param_type) do
case Evaluator.resolve_location(variable_name) do
{:ok, _location} ->
:ok

{:error, %{type: :not_assignable}} ->
{:error,
"#{param_type} parameter '#{variable_name}' is not a legal variable name - cannot assign to literals"}

{:error, reason} ->
{:error,
"#{param_type} parameter '#{variable_name}' is not a legal variable name: #{inspect(reason)}"}
end
end

# Evaluate the array expression to get the collection
defp evaluate_array(%{compiled_array: compiled_array, array: array_expr}, state_chart) do
case Evaluator.evaluate_value(compiled_array || array_expr, state_chart) do
Expand Down
11 changes: 8 additions & 3 deletions lib/statifier/data.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,35 @@ defmodule Statifier.Data do
Represents a data element in an SCXML datamodel.

Corresponds to SCXML `<data>` elements which define variables in the state machine's datamodel.
Each data element has an `id` (required) and optional `expr` or `src` for initialization.
Each data element has an `id` (required) and optional `expr`, `src`, or child content for initialization.
Per SCXML specification, precedence is: `expr` attribute > child content > `src` attribute.
"""

defstruct [
:id,
:expr,
:src,
:child_content,
# Document order for deterministic processing
document_order: nil,
# Location information for validation
source_location: nil,
id_location: nil,
expr_location: nil,
src_location: nil
src_location: nil,
child_content_location: nil
]

@type t :: %__MODULE__{
id: String.t(),
expr: String.t() | nil,
src: String.t() | nil,
child_content: String.t() | nil,
document_order: integer() | nil,
source_location: map() | nil,
id_location: map() | nil,
expr_location: map() | nil,
src_location: map() | nil
src_location: map() | nil,
child_content_location: map() | nil
}
end
139 changes: 98 additions & 41 deletions lib/statifier/datamodel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ defmodule Statifier.Datamodel do
%Statifier.Data{id: "counter", expr: "0"},
%Statifier.Data{id: "name", expr: "'John'"}
]
datamodel = Statifier.Datamodel.initialize(data_elements, state_chart)
state_chart = Statifier.Datamodel.initialize(state_chart, data_elements)

# Access variables
value = Statifier.Datamodel.get(datamodel, "counter")
value = Statifier.Datamodel.get(state_chart.datamodel, "counter")
# => 0

# Update variables
datamodel = Statifier.Datamodel.set(datamodel, "counter", 1)
"""

alias Statifier.{Configuration, Evaluator}
alias Statifier.{Configuration, Evaluator, Event, StateChart}
alias Statifier.Logging.LogManager
require LogManager

Expand All @@ -45,13 +45,19 @@ defmodule Statifier.Datamodel do
Initialize a datamodel from a list of data elements.

Processes each `<data>` element, evaluates its expression (if any),
and stores the result in the datamodel.
and stores the result in the datamodel. Returns the updated StateChart
with the initialized datamodel and any error events that were generated.
"""
@spec initialize(list(Statifier.Data.t()), Statifier.StateChart.t()) :: t()
def initialize(data_elements, state_chart) when is_list(data_elements) do
Enum.reduce(data_elements, new(), fn data_element, model ->
initialize_variable(data_element, model, state_chart)
end)
@spec initialize(Statifier.StateChart.t(), list(Statifier.Data.t())) ::
Statifier.StateChart.t()
def initialize(state_chart, data_elements) when is_list(data_elements) do
{datamodel, updated_state_chart} =
Enum.reduce(data_elements, {new(), state_chart}, fn data_element, {model, sc} ->
initialize_variable(sc, data_element, model)
end)

# Return StateChart with updated datamodel
%{updated_state_chart | datamodel: datamodel}
end

@doc """
Expand Down Expand Up @@ -132,11 +138,11 @@ defmodule Statifier.Datamodel do
@doc """
Build evaluation context for Predicator expressions.

Takes the datamodel and state_chart, returns a context ready for evaluation.
Takes the state_chart, extracts its datamodel, and returns a context ready for evaluation.
This is the single source of truth for context preparation.
"""
@spec build_evaluation_context(t(), Statifier.StateChart.t()) :: map()
def build_evaluation_context(datamodel, state_chart) do
@spec build_evaluation_context(Statifier.StateChart.t()) :: map()
def build_evaluation_context(%Statifier.StateChart{datamodel: datamodel} = state_chart) do
%{}
# Start with datamodel variables as base
|> Map.merge(datamodel)
Expand Down Expand Up @@ -194,56 +200,107 @@ defmodule Statifier.Datamodel do
|> Map.put("_ioprocessors", [])
end

defp initialize_variable(%{id: id, expr: expr}, model, state_chart)
defp initialize_variable(state_chart, %{id: id} = data_element, model)
when is_binary(id) do
# Build context for expression evaluation using the simplified approach
# Create a temporary state chart with current datamodel for evaluation
temp_state_chart = %{state_chart | datamodel: model}

# Evaluate the expression or use nil as default
value = evaluate_initial_expression(expr, temp_state_chart)
# Evaluate the data value using SCXML precedence: expr > child_content > src
case determine_data_value(temp_state_chart, data_element) do
{:ok, value, updated_state_chart} ->
# Store in model and return updated state chart
{Map.put(model, id, value), updated_state_chart}

{:error, reason, updated_state_chart} ->
# Per SCXML spec: create empty variable and generate error.execution event
LogManager.debug(updated_state_chart, "Data element initialization failed", %{
action_type: "datamodel_error",
data_id: id,
error: inspect(reason)
})

# Create error.execution event
error_event = %Event{
name: "error.execution",
data: %{"reason" => reason, "type" => "datamodel.initialization", "data_id" => id},
origin: :internal
}

# Store in model
Map.put(model, id, value)
# Add event to internal queue and create empty variable
final_state_chart = StateChart.enqueue_event(updated_state_chart, error_event)
{Map.put(model, id, nil), final_state_chart}
end
end

defp initialize_variable(_data_element, model, _state_chart) do
defp initialize_variable(state_chart, _data_element, model) do
# Skip data elements without valid id
model
{model, state_chart}
end

defp evaluate_initial_expression(nil, _state_chart), do: nil
defp evaluate_initial_expression("", _state_chart), do: nil
# Implement SCXML data value precedence: expr attribute > child content > src attribute
defp determine_data_value(state_chart, %{expr: expr, child_content: child_content, src: src}) do
cond do
# 1. expr attribute has highest precedence
expr && expr != "" ->
evaluate_initial_expression_with_errors(state_chart, expr)

# 2. child content has second precedence
child_content && child_content != "" ->
evaluate_child_content_with_errors(state_chart, child_content)

# 3. src attribute has lowest precedence (not implemented yet)
src && src != "" ->
# NOTE: src loading planned for future implementation phase
LogManager.debug(state_chart, "src attribute not yet supported for data elements", %{
action_type: "datamodel_src_loading",
src: src
})

{:ok, nil, state_chart}

defp evaluate_initial_expression(expr_string, state_chart) do
# 4. Default to nil if no value source specified
true ->
{:ok, nil, state_chart}
end
end

# Error-aware version of evaluate_initial_expression for proper error event generation
defp evaluate_initial_expression_with_errors(state_chart, expr_string) do
case Evaluator.compile_expression(expr_string) do
{:ok, compiled} ->
case Evaluator.evaluate_value(compiled, state_chart) do
{:ok, val} ->
val
{:ok, value} ->
{:ok, value, state_chart}

{:error, reason} ->
# Log the error but continue with fallback
LogManager.debug(state_chart, "Failed to evaluate datamodel expression", %{
action_type: "datamodel_evaluation",
expr_string: expr_string,
error: inspect(reason)
})

# For now, default to the literal string if evaluation fails
# This handles cases like object literals that Predicator can't parse
expr_string
# Expression evaluation failed - should generate error.execution
{:error, "Expression evaluation failed: #{inspect(reason)}", state_chart}
end

{:error, reason} ->
LogManager.debug(state_chart, "Failed to compile datamodel expression", %{
action_type: "datamodel_compilation",
expr_string: expr_string,
error: inspect(reason)
})
# Compilation failed - should generate error.execution
{:error, "Expression compilation failed: #{inspect(reason)}", state_chart}
end
end

# Error-aware version of evaluate_child_content using Predicator/Evaluator
defp evaluate_child_content_with_errors(state_chart, child_content) do
# Use Evaluator to handle all expression types including literals, arrays, objects
case Evaluator.compile_expression(child_content) do
{:ok, compiled} ->
case Evaluator.evaluate_value(compiled, state_chart) do
{:ok, value} ->
{:ok, value, state_chart}

# Default to literal string if compilation fails
expr_string
{:error, reason} ->
# Expression evaluation failed - this is an error for child content
{:error, "Child content evaluation failed: #{inspect(reason)}", state_chart}
end

{:error, reason} ->
# Compilation failed - this is an error for child content
{:error, "Child content compilation failed: #{inspect(reason)}", state_chart}
end
end

Expand Down
5 changes: 4 additions & 1 deletion lib/statifier/document.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Statifier.Document do
:datamodel,
:version,
:xmlns,
:binding,
initial: [],
states: [],
datamodel_elements: [],
Expand All @@ -25,14 +26,16 @@ defmodule Statifier.Document do
name_location: nil,
initial_location: nil,
datamodel_location: nil,
version_location: nil
version_location: nil,
binding_location: nil
]

@type t :: %__MODULE__{
name: String.t() | nil,
datamodel: String.t() | nil,
version: String.t() | nil,
xmlns: String.t() | nil,
binding: String.t() | nil,
initial: [String.t()],
states: [Statifier.State.t()],
datamodel_elements: [Statifier.Data.t()],
Expand Down
8 changes: 4 additions & 4 deletions lib/statifier/evaluator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ defmodule Statifier.Evaluator do

def evaluate_condition(compiled_cond, state_chart) do
# Build context once using the unified approach
context = Datamodel.build_evaluation_context(state_chart.datamodel, state_chart)
context = Datamodel.build_evaluation_context(state_chart)
functions = Datamodel.build_predicator_functions(state_chart.configuration)

case Predicator.evaluate(compiled_cond, context, functions: functions) do
Expand All @@ -88,7 +88,7 @@ defmodule Statifier.Evaluator do

def evaluate_value(compiled_expr, state_chart) do
# Build context once using the unified approach
context = Datamodel.build_evaluation_context(state_chart.datamodel, state_chart)
context = Datamodel.build_evaluation_context(state_chart)
functions = Datamodel.build_predicator_functions(state_chart.configuration)

case Predicator.evaluate(compiled_expr, context, functions: functions) do
Expand Down Expand Up @@ -129,7 +129,7 @@ defmodule Statifier.Evaluator do
{:ok, [String.t()]} | {:error, term()}
def resolve_location(location_expr, state_chart) when is_binary(location_expr) do
# Build evaluation context for location resolution
context = Datamodel.build_evaluation_context(state_chart.datamodel, state_chart)
context = Datamodel.build_evaluation_context(state_chart)

# Note: context_location doesn't need functions parameter
case Predicator.context_location(location_expr, context) do
Expand Down Expand Up @@ -195,7 +195,7 @@ defmodule Statifier.Evaluator do

# Evaluate using predicator with proper SCXML context and functions
defp evaluate_with_predicator(expression_or_instructions, state_chart) do
context = Datamodel.build_evaluation_context(state_chart.datamodel, state_chart)
context = Datamodel.build_evaluation_context(state_chart)
functions = Datamodel.build_predicator_functions(state_chart.configuration)

case Predicator.evaluate(expression_or_instructions, context, functions: functions) do
Expand Down
Loading