Developer & Contributor Guide¶
Welcome to the fsmc Developer & Contributor Guide. This guide details how to develop, test, and contribute to fsmc, including the architectural invariants, codebase organization, and step-by-step recipes for adding new frontend parsers, middle-end analysis passes, backend code emitters, and runtime enhancements.
1. Architectural Foundations¶
- Zero-Heap, Zero-Exception Embedded Runtime:
The C++ runtime library (
include/fsm/backend/cpp/runtime/) and emitted standalone state machines operate with 0 bytes dynamic heap allocation (malloc/new), no virtual tables, and no C++ exceptions. Stack storage for advanced features (UML History, deferred event queues) is strictly bounded at compile-time. - MBSE 4-Domain Segregated Datapath:
State machines do not rely on monolithic context objects. Transitions interact through 4 strictly segregated memory domains:
InPorts: Read-only input snapshot with formal contract ranges.OutPorts: Single-assignment actuator command buffer.Registers: Internal persistent memory with \(z^{-1}\) delay semantics.Services: External hardware drivers, OS abstractions, and logging interfaces.
- Lossless Canonical Intermediate Representation (
FsmIr): Every frontend model (SysML v2, Cameo XMI, SCXML, JSON, PlantUML, Mermaid, DOT, SMV) is parsed into the unified, strongly-typedFsmIrAST before verification, optimization, or code emission. - Compile-Time Static Verification: Before target code generation, the middle-end pipeline runs abstract interpretation (interval lattice analysis), SMT invariants verification, and temporal model checking.
2. Repository Structure¶
fsmc/
├── include/fsm/
│ ├── ir/ # Unified AST (FsmIr, StateNode, TransitionEdge, PortDefinition)
│ ├── diagnostic/ # DiagnosticEngine with ANSI colors, SourceSpan, and carets
│ ├── frontend/ # Parser interfaces & implementations
│ │ ├── formal/ # High-semantics formal parsers (SysML v2, Cameo XMI, SCXML, SMV)
│ │ ├── diagram/ # Diagram sketch parsers (PlantUML, Mermaid, DOT, JSON)
│ │ └── directive/ # Annotation, guard, and LTL/INVAR parsers
│ ├── middleend/ # PassManager, transformation passes, and model checkers
│ │ ├── passes/ # Optimization & dead-state elimination passes
│ │ └── analysis/ # Interval arithmetic & GuardSatisfiabilityPass
│ └── backend/ # Code generators and serializers
│ ├── cpp/ # C++ target generator & standalone bundles
│ │ └── runtime/ # Canonical C++17/C++20 zero-heap runtime engine
│ ├── diagram/ # Diagram format serializers
│ ├── formal/ # Formal model serializers
│ └── rtm/ # Requirement Traceability Matrix exporter
├── tools/
│ ├── fsmc/ # Primary compiler driver CLI
│ └── fsm-opt/ # Standalone formal IR optimizer & linter CLI
├── tests/ # Modular GoogleTest suites (54 suites, 100% green)
├── scripts/ # Maintenance tools (generate_standalone_runtime.py)
└── docs/ # MkDocs documentation source
3. Developer Recipes¶
Recipe A: Implementing a New Frontend Parser¶
To add support for a new modeling format (e.g. SimulinkStateflowParser or AutosarArxmlParser):
1. Inherit from IParser¶
Create your parser class under include/fsm/frontend/formal/ (for formal models) or include/fsm/frontend/diagram/ (for visual notations):
#pragma once
#include <string_view>
#include <string>
#include "fsm/frontend/common/parser_interface.hpp"
namespace fsm::frontend {
class CustomModelParser : public IParser {
public:
[[nodiscard]] std::string_view name() const noexcept override {
return "custom_format";
}
[[nodiscard]] FrontendKind kind() const noexcept override {
return FrontendKind::Formal;
}
bool parse(std::string_view source_code, fsm::ir::FsmIr& out_model, std::string& out_error) override {
out_model.name = "ParsedStateMachine";
// Step 1: Parse states and transitions
// out_model.states.push_back(StateNode{"Idle"});
// out_model.transitions.push_back(TransitionEdge{"Idle", "Active", "EvStart"});
// Step 2: Extract port and register contracts if present
// out_model.ports.push_back(PortDefinition("sensor_val", "float", PortDirection::In));
// Step 3: Always normalize hierarchy before returning
out_model.normalize_hierarchy();
return true;
}
};
} // namespace fsm::frontend
2. Register in ParserFactory¶
Register your new parser in include/fsm/frontend/common/parser_factory.hpp.
3. Add Unit Tests¶
Add test cases in tests/frontend/ using the Arrange-Act-Assert (AAA) pattern to verify state hierarchy, event triggers, and error reporting.
Recipe B: Writing a Middle-End Analysis or Optimization Pass¶
Middle-end passes operate on the canonical FsmIr AST and emit structured warnings or errors via DiagnosticEngine.
1. Inherit from IPass¶
Create your pass in include/fsm/middleend/passes/ or include/fsm/middleend/analysis/:
#pragma once
#include "fsm/middleend/pass_manager.hpp"
#include "fsm/diagnostic/diagnostic_engine.hpp"
namespace fsm::middleend {
class RedundantTransitionPass : public IPass {
public:
[[nodiscard]] std::string_view name() const noexcept override {
return "RedundantTransitionPass";
}
PassResult run(fsm::ir::FsmIr& model, fsm::diagnostic::DiagnosticEngine& diag) override {
bool modified = false;
for (const auto& trans : model.transitions) {
if (trans.source == trans.target && !trans.guard.has_value() && trans.event.empty()) {
diag.report(fsm::diagnostic::Diagnostic::warning(
"W0401",
"Spontaneous unguarded self-transition on state '" + trans.source + "' causes infinite livelocks."
));
}
}
return modified ? PassResult::Modified : PassResult::Preserved;
}
};
} // namespace fsm::middleend
2. Register in PassManager¶
Register the pass in include/fsm/middleend/pass_manager.hpp within the default or optimizing pipeline.
Recipe C: Writing a Backend Serializer¶
To emit a new textual diagram or formal MBSE format:
1. Implement a Format Serializer¶
Create your serializer in include/fsm/backend/diagram/ or include/fsm/backend/formal/:
#pragma once
#include <sstream>
#include <string>
#include "fsm/ir/fsm_ir.hpp"
namespace fsm::backend::diagram {
class CustomGraphSerializer {
public:
static std::string serialize(const fsm::ir::FsmIr& model) {
std::ostringstream ss;
ss << "# State Machine: " << model.name << "\n";
for (const auto& t : model.transitions) {
ss << t.source << " -> " << t.target << " [" << t.event << "]\n";
}
return ss.str();
}
};
} // namespace fsm::backend::diagram
2. Register in EmitterFactory¶
Dispatch the new serializer in EmitterFactory::emit_diagram in src/backend/emitter_factory.cpp and register its format identifier in EmitterFactory::supported_formats().
Recipe D: Modifying the Zero-Heap C++ Runtime¶
When enhancing the C++ runtime engine:
- Edit Canonical Headers: Make changes inside
include/fsm/backend/cpp/runtime/. Keep concerns separated indetail/(history_manager.hpp,deferred_manager.hpp,transition_executor.hpp,reentrancy_tracker.hpp,notification_dispatcher.hpp). - Synchronize Standalone Bundles: Run the standalone generation script to update the single-header distribution files:
- Ensure Zero Heap Allocations: Verify that all containers use fixed-capacity static storage (
fsm::static_vector,static_ring_buffer) and that zeromalloc/newcalls or dynamic allocations are introduced.
4. Testing & Verification Guidelines¶
Unit Testing Standards (GoogleTest)¶
All unit tests follow strict conventions:
- AAA Pattern: Explicit
Arrange,Act, andAssertphases. - Doxygen Documentation: Every test file and test case includes
@file,@brief Test Intent, andScenario:blocks. - Clean Assertions: No fix annotations (
// FIX), scratch comments, or unhandled warnings.
/**
* @brief Test Intent: Verify parser extracts guarded transitions with range contracts.
*
* Scenario:
* - Ingest model string with input port contract [0, 100].
* - Verify FsmIr AST structure and port contract values.
*/
TEST(CustomParserTest, IngestGuardedTransition) {
// Arrange
const std::string source = "state Idle -> Active on EvStart if in.temp > 50;";
fsm::ir::FsmIr model;
std::string error;
CustomParser parser;
// Act
bool ok = parser.parse(source, model, error);
// Assert
ASSERT_TRUE(ok) << error;
EXPECT_EQ(model.transitions.size(), 1u);
}
Full Validation Suite¶
Before opening a pull request, run the complete verification pipeline:
# 1. Verify standalone runtime synchronization
python3 scripts/generate_standalone_runtime.py --check
# 2. Build and run all CTest suites (must pass 100%)
cmake --build build -j$(nproc)
ctest --test-dir build --output-on-failure
# 3. Setup doc environment (if not already done) and verify documentation build
pip install -r requirements-docs.txt
mkdocs build
5. Coding Standards & Conventions¶
| Rule | Convention | Example |
|---|---|---|
| Types & Classes | PascalCase |
Sysml2Parser, TransitionEdge, HistoryManager |
| Methods & Functions | snake_case |
dispatch(), step(), normalize_hierarchy() |
| Member Variables | snake_case_ (trailing underscore) |
registers_, current_state_ |
| Public Struct Fields | snake_case |
source, target, min_value |
| Constants & Enums | snake_case / kCamelCase |
dispatch_status::success, channel_index_event |
| Compiler Warnings | Zero Warnings (-Wall -Wextra -Wpedantic -Werror) |
Must compile cleanly across GCC, Clang, and MSVC |