Skip to content

Commit ccb3b79

Browse files
johnnytclaude
andauthored
Fixes nested if parsing in SAX parser (#94)
* Fixes nested if parsing in SAX parser Adds support for nested if blocks by handling the case where an if element ends while nested inside another if container. Previously, nested if blocks were lost during parsing instead of being created as separate IfAction instances. Changes: - Adds new pattern match case in handle_if_end for nested if containers - Creates IfAction from nested if container and adds to parent block - Enables proper parsing of complex conditional logic with nested if/else Fixes SCION test if_else/test0 which now passes. * Adds tests for nested if parsing * Updates badges and adds active dev notice --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2e76e06 commit ccb3b79

5 files changed

Lines changed: 111 additions & 15 deletions

File tree

README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# Statifier - SCXML State Machines for Elixir
22

3-
[![CI](https://github.com/riddler/statifier/workflows/CI/badge.svg?branch=main)](https://github.com/riddler/statifier/actions)
4-
[![Coverage](https://codecov.io/gh/riddler/statifier/branch/main/graph/badge.svg)](https://codecov.io/gh/riddler/statifier)
3+
[![CI](https://github.com/riddler/statifier/actions/workflows/ci.yml/badge.svg)](https://github.com/riddler/statifier/actions/workflows/ci.yml)
4+
[![codecov](https://codecov.io/gh/riddler/statifier/branch/main/graph/badge.svg)](https://codecov.io/gh/riddler/statifier)
5+
[![Hex.pm Version](https://img.shields.io/hexpm/v/statifier.svg)](https://hex.pm/packages/statifier)
6+
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/statifier/)
7+
8+
> ⚠️ **Active Development Notice**
9+
> This project is still under active development and things may change even though it is passed version 1. APIs, features, and behaviors may evolve as we continue improving SCXML compliance and functionality.
510
611
An Elixir implementation of SCXML (State Chart XML) state charts with a focus on W3C compliance.
712

config/config.exs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,7 @@ if config_env() == :dev do
77

88
config :logger, :console,
99
format: "[$level] $message $metadata\n",
10-
metadata: [
11-
:current_state,
12-
:event,
13-
:action_type,
14-
:state_id,
15-
:phase,
16-
:transition_type,
17-
:source_state,
18-
:condition,
19-
:targets
20-
]
10+
metadata: :all
2111

2212
# Default Statifier logging configuration for dev
2313
config :statifier,

lib/statifier/parser/scxml/state_stack.ex

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ defmodule Statifier.Parser.SCXML.StateStack do
66
and updating parent elements when child elements are completed.
77
"""
88

9+
alias Statifier.Actions.IfAction
10+
911
@doc """
1012
Handle the end of a state element by adding it to its parent.
1113
"""
@@ -584,6 +586,17 @@ defmodule Statifier.Parser.SCXML.StateStack do
584586
{:ok, %{state | stack: [{"transition", updated_transition} | rest]}}
585587
end
586588

589+
# Handle nested if action within parent if container
590+
def handle_if_end(
591+
%{stack: [{_element_name, nested_if_container} | [{"if", parent_if_container} | rest]]} =
592+
state
593+
) do
594+
# Create IfAction from nested if container and add to parent if container
595+
nested_if_action = create_if_action_from_container(nested_if_container)
596+
updated_parent_container = add_action_to_current_block(parent_if_container, nested_if_action)
597+
{:ok, %{state | stack: [{"if", updated_parent_container} | rest]}}
598+
end
599+
587600
def handle_if_end(state) do
588601
# If element not in an onentry/onexit context, just pop it
589602
{:ok, pop_element(state)}
@@ -842,7 +855,6 @@ defmodule Statifier.Parser.SCXML.StateStack do
842855
end)
843856

844857
# Create IfAction with collected blocks
845-
alias Statifier.Actions.IfAction
846858
IfAction.new(conditional_blocks, if_container[:location])
847859
end
848860

test/passing_tests.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"test/statifier/**/*_test.exs",
66
"test/mix/**/*_test.exs"
77
],
8-
"last_updated": "2025-08-31",
8+
"last_updated": "2025-09-02",
99
"scion_tests": [
1010
"test/scion_tests/actionSend/send1_test.exs",
1111
"test/scion_tests/actionSend/send2_test.exs",
@@ -46,6 +46,7 @@
4646
"test/scion_tests/history/history4b_test.exs",
4747
"test/scion_tests/history/history5_test.exs",
4848
"test/scion_tests/history/history6_test.exs",
49+
"test/scion_tests/if_else/test0_test.exs",
4950
"test/scion_tests/misc/deep_initial_test.exs",
5051
"test/scion_tests/more_parallel/test0_test.exs",
5152
"test/scion_tests/more_parallel/test10_test.exs",

test/statifier/parser/scxml/state_stack_if_test.exs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,5 +289,93 @@ defmodule Statifier.Parser.SCXML.StateStackIfTest do
289289
assert length(current_block.actions) == 1
290290
assert hd(current_block.actions) == assign_action
291291
end
292+
293+
test "nested if within parent if container" do
294+
# Test nested if action added to current conditional block within parent if container
295+
nested_if_container = %{
296+
conditional_blocks: [
297+
%{
298+
type: :if,
299+
cond: "x === 40",
300+
actions: [
301+
%AssignAction{location: "x", expr: "x + 10"},
302+
%LogAction{label: "nested", expr: "x"}
303+
]
304+
}
305+
],
306+
current_block_index: 0,
307+
location: %{line: 2, column: 5}
308+
}
309+
310+
parent_if_container = %{
311+
conditional_blocks: [
312+
%{
313+
type: :if,
314+
cond: "x === 28",
315+
actions: [
316+
%AssignAction{location: "x", expr: "x + 12"},
317+
%LogAction{label: "after_assign", expr: "x"}
318+
]
319+
}
320+
],
321+
current_block_index: 0,
322+
location: %{line: 1, column: 1}
323+
}
324+
325+
state = %{
326+
stack: [
327+
{"if", nested_if_container},
328+
{"if", parent_if_container},
329+
{"onentry", :onentry_block},
330+
{"state", %Statifier.State{id: "test_state"}}
331+
]
332+
}
333+
334+
{:ok, result} = StateStack.handle_if_end(state)
335+
336+
# Should have parent if container with nested if added as an action
337+
[{"if", updated_parent_container} | _rest] = result.stack
338+
current_block = Enum.at(updated_parent_container.conditional_blocks, 0)
339+
340+
# Should have 3 actions: assign, log, nested_if_action
341+
assert length(current_block.actions) == 3
342+
343+
# The last action should be the nested IfAction
344+
nested_if_action = List.last(current_block.actions)
345+
assert match?(%Statifier.Actions.IfAction{}, nested_if_action)
346+
347+
# Verify the nested IfAction has correct structure
348+
assert length(nested_if_action.conditional_blocks) == 1
349+
nested_block = hd(nested_if_action.conditional_blocks)
350+
assert nested_block[:type] == :if
351+
assert nested_block[:cond] == "x === 40"
352+
assert length(nested_block[:actions]) == 2
353+
end
354+
355+
test "nested if not in parent if context" do
356+
# Test nested if element not within parent if container (fallback case)
357+
nested_if_container = %{
358+
conditional_blocks: [
359+
%{type: :if, cond: "true", actions: [%LogAction{label: "test", expr: "value"}]}
360+
],
361+
current_block_index: 0,
362+
location: %{line: 1, column: 1}
363+
}
364+
365+
state = %{
366+
stack: [
367+
{"if", nested_if_container},
368+
{"onentry", :onentry_block},
369+
{"state", %Statifier.State{id: "test_state"}}
370+
]
371+
}
372+
373+
{:ok, result} = StateStack.handle_if_end(state)
374+
375+
# Should create IfAction and add to onentry (normal if handling)
376+
[{"onentry", actions} | _rest] = result.stack
377+
assert length(actions) == 1
378+
assert match?(%Statifier.Actions.IfAction{}, hd(actions))
379+
end
292380
end
293381
end

0 commit comments

Comments
 (0)