"""
C runtime code-generation helpers for built-in native templates.
This module contains small, deterministic emitters used by the built-in
``c`` and ``c_poll`` templates. The helpers render lifecycle action,
transition-effect, and guard bodies with explicit runtime diagnostics around
DSL expression failures that would otherwise surface as native crashes or C
compile errors.
The module contains:
* :func:`render_c_reset_vars_body` - Render default persistent-variable setup.
* :func:`render_c_action_body` - Render operation statements as a fallible C body.
* :func:`render_c_condition_body` - Render a fallible C guard/condition body.
The generated code remains C99-only and uses only helpers already emitted by
``machine.c``; it does not add third-party runtime dependencies.
Example::
>>> from pyfcstm.dsl.node import Integer, OperationAssignment
>>> code = render_c_action_body(
... [OperationAssignment("x", Integer("1"))],
... {"x": "int"},
... "DemoMachine",
... "DEMO_MACHINE",
... )
>>> "scope->x = 1;" in code
True
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
from ..dsl import node as dsl_nodes
from ..model import IfBlock, Operation, OperationStatement
from ..utils import to_c_identifier
@dataclass(frozen=True)
class _ExprRenderResult:
"""
Rendered C expression plus coarse DSL type metadata.
:param text: C expression text.
:type text: str
:param value_type: Coarse DSL value type, such as ``"int"`` or ``"float"``.
:type value_type: str, optional
Example::
>>> result = _ExprRenderResult("scope->x", "int")
>>> result.text
'scope->x'
"""
text: str
value_type: Optional[str]
@dataclass(frozen=True)
class _CNames:
"""
Generated C naming context.
:param machine_class_name: Generated machine class prefix.
:type machine_class_name: str
:param machine_macro_name: Generated macro prefix.
:type machine_macro_name: str
Example::
>>> names = _CNames("RootMachine", "ROOT_MACHINE")
>>> names.failure
'ROOT_MACHINE_FAILURE'
"""
machine_class_name: str
machine_macro_name: str
@property
def success(self) -> str:
"""
Return the generated success macro.
:return: Success macro name.
:rtype: str
Example::
>>> _CNames("Demo", "DEMO").success
'DEMO_SUCCESS'
"""
return "%s_SUCCESS" % self.machine_macro_name
@property
def failure(self) -> str:
"""
Return the generated failure macro.
:return: Failure macro name.
:rtype: str
Example::
>>> _CNames("Demo", "DEMO").failure
'DEMO_FAILURE'
"""
return "%s_FAILURE" % self.machine_macro_name
@property
def set_error(self) -> str:
"""
Return the generated internal error setter name.
:return: Error-setter function name.
:rtype: str
Example::
>>> _CNames("Demo", "DEMO").set_error
'_Demo_set_error'
"""
return "_%s_set_error" % self.machine_class_name
OperationalNode = Union[OperationStatement, dsl_nodes.OperationalStatement]
_MATH_FUNC_NAMES = {
"sin",
"cos",
"tan",
"asin",
"acos",
"atan",
"sinh",
"cosh",
"tanh",
"asinh",
"acosh",
"atanh",
"sqrt",
"cbrt",
"exp",
"log",
"log10",
"log2",
"log1p",
"ceil",
"floor",
"round",
"trunc",
}
_INT_OPERATORS = {"<<", ">>", "&", "^", "|"}
def _quote_c_string(value: str) -> str:
"""
Quote a string for generated C source.
:param value: Text to quote.
:type value: str
:return: C string literal text.
:rtype: str
Example::
>>> _quote_c_string("a'b")
'"a\\'b"'
"""
return json.dumps(value)
def _line(lines: List[str], indent: str, level: int, text: str) -> None:
lines.append("%s%s" % (indent * level, text))
def _normalise_var_types(var_types: Mapping[str, Any]) -> Dict[str, str]:
"""
Convert model define metadata to a name/type mapping.
:param var_types: Mapping of variable names to type strings or define objects.
:type var_types: typing.Mapping[str, typing.Any]
:return: Normalized variable type map.
:rtype: dict
Example::
>>> _normalise_var_types({"x": "int"})
{'x': 'int'}
"""
result = {}
for name, value in var_types.items():
if isinstance(value, str):
result[name] = value
elif hasattr(value, "type"):
result[name] = str(value.type)
else:
result[name] = str(value)
return result
def _coerce_expr(expr: Any) -> dsl_nodes.Expr:
if isinstance(expr, dsl_nodes.Expr):
return expr
if hasattr(expr, "to_ast_node"):
return expr.to_ast_node()
raise TypeError("Unsupported C expression node: %r" % (type(expr),))
def _coerce_statement(statement: OperationalNode) -> dsl_nodes.OperationalStatement:
if isinstance(statement, dsl_nodes.OperationalStatement):
return statement
if isinstance(statement, (Operation, IfBlock)):
return statement.to_ast_node()
if hasattr(statement, "to_ast_node"):
node = statement.to_ast_node()
if isinstance(node, dsl_nodes.OperationalStatement):
return node
raise TypeError("Unsupported C operation statement: %r" % (type(statement),))
def _merge_types(type_a: Optional[str], type_b: Optional[str]) -> Optional[str]:
known = {type_a, type_b} - {None}
if not known:
return None
if "float" in known:
return "float"
if known == {"int"}:
return "int"
return next(iter(known))
def _infer_expr_type(
expr: dsl_nodes.Expr, known_types: Mapping[str, str]
) -> Optional[str]:
if isinstance(expr, (dsl_nodes.Integer, dsl_nodes.HexInt, dsl_nodes.Boolean)):
return "int"
if isinstance(expr, (dsl_nodes.Float, dsl_nodes.Constant)):
return "float"
if isinstance(expr, dsl_nodes.Name):
return known_types.get(expr.name)
if isinstance(expr, dsl_nodes.Paren):
return _infer_expr_type(expr.expr, known_types)
if isinstance(expr, dsl_nodes.UnaryOp):
return _infer_expr_type(expr.expr, known_types)
if isinstance(expr, dsl_nodes.UFunc):
if expr.func in {"floor", "ceil", "round", "trunc", "int", "sign"}:
return "int"
if expr.func == "abs":
return _infer_expr_type(expr.expr, known_types)
return "float"
if isinstance(expr, dsl_nodes.BinaryOp):
if expr.op in _INT_OPERATORS:
return "int"
if expr.op in {
"&&",
"||",
"=>",
"xor",
"iff",
"==",
"!=",
"<",
"<=",
">",
">=",
}:
return "int"
if expr.op == "/":
return "float"
return _merge_types(
_infer_expr_type(expr.expr1, known_types),
_infer_expr_type(expr.expr2, known_types),
)
if isinstance(expr, dsl_nodes.ConditionalOp):
return _merge_types(
_infer_expr_type(expr.value_true, known_types),
_infer_expr_type(expr.value_false, known_types),
)
return None
def _is_static_zero(expr: dsl_nodes.Expr) -> bool:
"""
Return whether an expression is syntactically a zero literal.
The C runtime emitter uses this narrow check to keep generated code
compileable on toolchains that reject constant division or modulo by zero at
compile time. It deliberately does not fold arbitrary expressions: broader
arithmetic reasoning belongs in the inspect/verify layers, while code
generation only needs to mask denominator literals that are already guarded
by generated runtime diagnostics.
:param expr: Expression to inspect.
:type expr: pyfcstm.dsl.node.Expr
:return: ``True`` when the expression is a parenthesized or signed zero
literal.
:rtype: bool
Example::
>>> _is_static_zero(dsl_nodes.Integer("0"))
True
>>> _is_static_zero(dsl_nodes.BinaryOp(dsl_nodes.Integer("1"), "-", dsl_nodes.Integer("1")))
False
"""
expr = _coerce_expr(expr)
if isinstance(expr, dsl_nodes.Paren):
return _is_static_zero(expr.expr)
if isinstance(expr, dsl_nodes.UnaryOp) and expr.op in {"+", "-"}:
return _is_static_zero(expr.expr)
if isinstance(expr, (dsl_nodes.Integer, dsl_nodes.HexInt)):
return expr.value == 0
if isinstance(expr, dsl_nodes.Float):
return expr.value == 0.0
return False
def _safe_static_zero_result(value_type: Optional[str]) -> str:
"""
Return a harmless placeholder for a statically failing expression.
:param value_type: Coarse DSL value type of the expression being replaced.
:type value_type: str, optional
:return: C literal with the requested coarse type.
:rtype: str
Example::
>>> _safe_static_zero_result("float")
'0.0'
>>> _safe_static_zero_result("int")
'0'
"""
if value_type == "float":
return "0.0"
return "0"
def _render_expr(
expr: dsl_nodes.Expr,
known_types: Mapping[str, str],
state_names: Optional[Iterable[str]] = None,
) -> _ExprRenderResult:
expr = _coerce_expr(expr)
state_name_set = set(state_names if state_names is not None else known_types.keys())
if isinstance(expr, dsl_nodes.Integer):
return _ExprRenderResult(repr(expr.value), "int")
if isinstance(expr, dsl_nodes.HexInt):
return _ExprRenderResult(hex(expr.value), "int")
if isinstance(expr, dsl_nodes.Float):
return _ExprRenderResult(repr(expr.value), "float")
if isinstance(expr, dsl_nodes.Boolean):
return _ExprRenderResult("1" if expr.value else "0", "int")
if isinstance(expr, dsl_nodes.Constant):
return _ExprRenderResult(repr(expr.value), "float")
if isinstance(expr, dsl_nodes.Name):
if expr.name in known_types:
text = (
"scope->%s" % to_c_identifier(expr.name)
if expr.name in state_name_set
else to_c_identifier(expr.name)
)
return _ExprRenderResult(text, known_types.get(expr.name))
return _ExprRenderResult(to_c_identifier(expr.name), None)
if isinstance(expr, dsl_nodes.Paren):
inner = _render_expr(expr.expr, known_types, state_name_set)
return _ExprRenderResult("(%s)" % inner.text, inner.value_type)
if isinstance(expr, dsl_nodes.UnaryOp):
inner = _render_expr(expr.expr, known_types, state_name_set)
op = "!" if expr.op == "not" else expr.op
return _ExprRenderResult(
"(%s%s)" % (op, inner.text), _infer_expr_type(expr, known_types)
)
if isinstance(expr, dsl_nodes.UFunc):
inner = _render_expr(expr.expr, known_types, state_name_set)
if expr.func == "sign":
text = "(((%s) > 0) - ((%s) < 0))" % (inner.text, inner.text)
elif expr.func == "abs":
text = (
"fabs(%s)" % inner.text
if inner.value_type == "float"
else "llabs(%s)" % inner.text
)
elif expr.func == "cbrt":
text = "cbrt(%s)" % inner.text
elif expr.func in _MATH_FUNC_NAMES:
text = "%s(%s)" % (expr.func, inner.text)
else:
text = "%s(%s)" % (expr.func, inner.text)
return _ExprRenderResult(text, _infer_expr_type(expr, known_types))
if isinstance(expr, dsl_nodes.BinaryOp):
left = _render_expr(expr.expr1, known_types, state_name_set)
right = _render_expr(expr.expr2, known_types, state_name_set)
value_type = _infer_expr_type(expr, known_types)
if expr.op in {"/", "%"} and _is_static_zero(expr.expr2):
text = _safe_static_zero_result(value_type)
elif expr.op in _INT_OPERATORS and (
left.value_type == "float" or right.value_type == "float"
):
text = "0"
elif expr.op == "**":
text = "pow(%s, %s)" % (left.text, right.text)
elif expr.op == "%" and (
left.value_type == "float" or right.value_type == "float"
):
text = "fmod(%s, %s)" % (left.text, right.text)
elif expr.op == "/":
text = "(((double)(%s)) / (%s))" % (left.text, right.text)
elif expr.op == "=>":
text = "((!(%s)) || (%s))" % (left.text, right.text)
elif expr.op == "xor":
text = "((%s) != (%s))" % (left.text, right.text)
elif expr.op == "iff":
text = "((%s) == (%s))" % (left.text, right.text)
else:
text = "((%s) %s (%s))" % (left.text, expr.op, right.text)
return _ExprRenderResult(text, value_type)
if isinstance(expr, dsl_nodes.ConditionalOp):
cond = _render_expr(expr.cond, known_types, state_name_set)
value_true = _render_expr(expr.value_true, known_types, state_name_set)
value_false = _render_expr(expr.value_false, known_types, state_name_set)
text = "((%s) ? (%s) : (%s))" % (cond.text, value_true.text, value_false.text)
return _ExprRenderResult(text, _infer_expr_type(expr, known_types))
raise TypeError("Unsupported C expression node: %r" % (type(expr),))
def _python_type_name(value_type: Optional[str]) -> str:
if value_type == "float":
return "float"
return "int"
def _zero_division_message(
operator_text: str, value_type: Optional[str], right_text: str
) -> str:
if operator_text == "%":
if value_type == "float":
return "float modulo"
return "integer modulo by zero"
if value_type == "float" or "." in right_text:
return "float division by zero"
return "division by zero"
def _emit_error(
lines: List[str], names: _CNames, indent: str, level: int, message: str
) -> None:
_line(
lines,
indent,
level,
"%s(machine, %s);" % (names.set_error, _quote_c_string(message)),
)
_line(lines, indent, level, "return %s;" % names.failure)
def _emit_expr_checks(
lines: List[str],
expr: dsl_nodes.Expr,
known_types: Mapping[str, str],
state_names: Iterable[str],
names: _CNames,
usage: str,
indent: str,
level: int,
) -> bool:
expr = _coerce_expr(expr)
state_name_set = set(state_names)
if isinstance(expr, dsl_nodes.Paren):
return _emit_expr_checks(
lines, expr.expr, known_types, state_name_set, names, usage, indent, level
)
if isinstance(expr, dsl_nodes.UnaryOp):
return _emit_expr_checks(
lines, expr.expr, known_types, state_name_set, names, usage, indent, level
)
if isinstance(expr, dsl_nodes.UFunc):
return _emit_expr_checks(
lines, expr.expr, known_types, state_name_set, names, usage, indent, level
)
if isinstance(expr, dsl_nodes.ConditionalOp):
cond = _render_expr(expr.cond, known_types, state_name_set).text
safe = _emit_expr_checks(
lines, expr.cond, known_types, state_name_set, names, usage, indent, level
)
_line(lines, indent, level, "if (%s) {" % cond)
safe = (
_emit_expr_checks(
lines,
expr.value_true,
known_types,
state_name_set,
names,
usage,
indent,
level + 1,
)
and safe
)
_line(lines, indent, level, "} else {")
safe = (
_emit_expr_checks(
lines,
expr.value_false,
known_types,
state_name_set,
names,
usage,
indent,
level + 1,
)
and safe
)
_line(lines, indent, level, "}")
return safe
if isinstance(expr, dsl_nodes.BinaryOp):
left = _render_expr(expr.expr1, known_types, state_name_set)
right = _render_expr(expr.expr2, known_types, state_name_set)
if expr.op == "&&":
safe = _emit_expr_checks(
lines,
expr.expr1,
known_types,
state_name_set,
names,
usage,
indent,
level,
)
_line(lines, indent, level, "if (%s) {" % left.text)
safe = (
_emit_expr_checks(
lines,
expr.expr2,
known_types,
state_name_set,
names,
usage,
indent,
level + 1,
)
and safe
)
_line(lines, indent, level, "}")
return safe
if expr.op == "||":
safe = _emit_expr_checks(
lines,
expr.expr1,
known_types,
state_name_set,
names,
usage,
indent,
level,
)
_line(lines, indent, level, "if (!(%s)) {" % left.text)
safe = (
_emit_expr_checks(
lines,
expr.expr2,
known_types,
state_name_set,
names,
usage,
indent,
level + 1,
)
and safe
)
_line(lines, indent, level, "}")
return safe
safe = _emit_expr_checks(
lines, expr.expr1, known_types, state_name_set, names, usage, indent, level
)
safe = (
_emit_expr_checks(
lines,
expr.expr2,
known_types,
state_name_set,
names,
usage,
indent,
level,
)
and safe
)
if expr.op in {"/", "%"}:
_line(lines, indent, level, "if ((%s) == 0) {" % right.text)
_emit_error(
lines,
names,
indent,
level + 1,
"%s evaluation failed: %s"
% (
usage,
_zero_division_message(
expr.op,
_merge_types(left.value_type, right.value_type),
right.text,
),
),
)
_line(lines, indent, level, "}")
if expr.op in _INT_OPERATORS:
invalid = left.value_type == "float" or right.value_type == "float"
if invalid:
message = (
"%s evaluation failed: unsupported operand type(s) for %s: '%s' and '%s'"
% (
usage,
expr.op,
_python_type_name(left.value_type),
_python_type_name(right.value_type),
)
)
_emit_error(lines, names, indent, level, message)
return False
return safe
return True
def _state_target(name: str, state_vars: Mapping[str, str]) -> str:
if name in state_vars:
return "scope->%s" % to_c_identifier(name)
return to_c_identifier(name)
def _c_type(value_type: Optional[str]) -> str:
if value_type == "int":
return "PYFCSTM_GENERATED_INT64"
return "double"
def _render_statement_sequence(
statements: Sequence[dsl_nodes.OperationalStatement],
state_types: Mapping[str, str],
visible_types: Mapping[str, str],
names: _CNames,
indent: str,
level: int,
) -> Tuple[List[str], Dict[str, str]]:
lines: List[str] = []
current_types = dict(visible_types)
for statement in statements:
known_types = {**state_types, **current_types}
if isinstance(statement, dsl_nodes.OperationAssignment):
target = _state_target(statement.name, state_types)
expr = _render_expr(statement.expr, known_types, state_types.keys())
checks: List[str] = []
safe = _emit_expr_checks(
checks,
statement.expr,
known_types,
state_types.keys(),
names,
"operation assignment to '%s'" % statement.name,
indent,
level,
)
lines.extend(checks)
if not safe:
continue
if statement.name not in state_types:
inferred_type = _infer_expr_type(statement.expr, known_types)
if statement.name not in current_types:
_line(
lines,
indent,
level,
"%s %s;"
% (_c_type(inferred_type), to_c_identifier(statement.name)),
)
if inferred_type is not None:
current_types[statement.name] = inferred_type
if state_types.get(statement.name) == "int" and expr.value_type == "float":
temp_name = "__pyfcstm_value_%d" % len(lines)
_line(lines, indent, level, "double %s = %s;" % (temp_name, expr.text))
_line(
lines,
indent,
level,
"if (%s != (double)((PYFCSTM_GENERATED_INT64)%s)) {"
% (temp_name, temp_name),
)
_line(
lines,
indent,
level + 1,
(
"%s(machine, "
"\"Variable '%s' is int type, cannot assign float %%.15g; "
'non-integer float from operation block writeback", '
"%s);"
)
% (names.set_error, statement.name, temp_name),
)
_line(lines, indent, level + 1, "return %s;" % names.failure)
_line(lines, indent, level, "}")
_line(
lines,
indent,
level,
"%s = (PYFCSTM_GENERATED_INT64)%s;" % (target, temp_name),
)
continue
_line(lines, indent, level, "%s = %s;" % (target, expr.text))
continue
if isinstance(statement, dsl_nodes.OperationIf):
def emit_branch_body(
branch: dsl_nodes.OperationIfBranch, body_level: int
) -> None:
"""Emit one branch body with simulator-compatible local scope."""
body, _ = _render_statement_sequence(
tuple(branch.statements),
state_types,
dict(current_types),
names,
indent,
body_level,
)
if body:
lines.extend(body)
else:
_line(lines, indent, body_level, "/* no-op */")
def emit_branch_chain(index: int, chain_level: int) -> None:
"""Nest later branch checks so earlier matched branches stay lazy."""
branch = statement.branches[index]
if branch.condition is None:
emit_branch_body(branch, chain_level)
return
branch_known = {**state_types, **current_types}
_emit_expr_checks(
lines,
branch.condition,
branch_known,
state_types.keys(),
names,
"if-block condition",
indent,
chain_level,
)
_line(
lines,
indent,
chain_level,
"if (%s) {"
% _render_expr(
branch.condition, branch_known, state_types.keys()
).text,
)
emit_branch_body(branch, chain_level + 1)
if index + 1 < len(statement.branches):
_line(lines, indent, chain_level, "} else {")
emit_branch_chain(index + 1, chain_level + 1)
_line(lines, indent, chain_level, "}")
else:
_line(lines, indent, chain_level, "}")
emit_branch_chain(0, level)
continue
raise TypeError("Unsupported C operation statement: %r" % (type(statement),))
return lines, current_types
[docs]
def render_c_action_body(
statements: Iterable[OperationalNode],
var_types: Mapping[str, Any],
machine_class_name: str,
machine_macro_name: str,
indent: str = " ",
) -> str:
"""
Render a fallible generated C body for operation statements.
:param statements: Operation statements from an action or transition effect.
:type statements: typing.Iterable[typing.Union[pyfcstm.model.OperationStatement, pyfcstm.dsl.node.OperationalStatement]]
:param var_types: Persistent variable type mapping or model defines mapping.
:type var_types: typing.Mapping[str, typing.Any]
:param machine_class_name: Generated machine class name, such as
``"RootMachine"``.
:type machine_class_name: str
:param machine_macro_name: Generated macro prefix, such as
``"ROOT_MACHINE"``.
:type machine_macro_name: str
:param indent: Indentation unit used for generated C code, defaults to four
spaces.
:type indent: str, optional
:return: C statements ending in a generated success or failure return.
:rtype: str
Example::
>>> from pyfcstm.dsl.node import Integer, OperationAssignment
>>> body = render_c_action_body(
... [OperationAssignment("x", Integer("1"))], {"x": "int"}, "M", "M"
... )
>>> body.strip().endswith("return M_SUCCESS;")
True
"""
state_types = _normalise_var_types(var_types)
nodes = tuple(_coerce_statement(statement) for statement in statements)
names = _CNames(machine_class_name, machine_macro_name)
lines = [
"%s(void)machine;" % indent,
"%s(void)scope;" % indent,
]
body, _ = _render_statement_sequence(nodes, state_types, {}, names, indent, 1)
lines.extend(body)
lines.append("%sreturn %s;" % (indent, names.success))
return "\n".join(lines)
[docs]
def render_c_reset_vars_body(
var_defines: Mapping[str, Any],
machine_class_name: str,
machine_macro_name: str,
indent: str = " ",
) -> str:
"""
Render C statements for default persistent-variable initialization.
The emitted body evaluates ``def`` initializers in declaration order and
applies the same persistent ``int`` writeback boundary used by operation
blocks. Integer defaults therefore accept integer-valued floats but report a
generated runtime error for non-integer floats instead of relying on C's
implicit narrowing conversion.
:param var_defines: Model variable definitions keyed by DSL variable name.
:type var_defines: typing.Mapping[str, typing.Any]
:param machine_class_name: Generated machine class name.
:type machine_class_name: str
:param machine_macro_name: Generated macro prefix.
:type machine_macro_name: str
:param indent: Indentation unit used for generated C code, defaults to four
spaces.
:type indent: str, optional
:return: C statements ending in a generated success return.
:rtype: str
Example::
>>> body = render_c_reset_vars_body({}, "Demo", "DEMO")
>>> body.strip().endswith("return DEMO_SUCCESS;")
True
"""
state_types = _normalise_var_types(var_defines)
names = _CNames(machine_class_name, machine_macro_name)
lines = [
"%s(void)machine;" % indent,
"%s(void)scope;" % indent,
]
for name, define in var_defines.items():
expr = _render_expr(define.init, state_types, state_types.keys())
checks: List[str] = []
safe = _emit_expr_checks(
checks,
define.init,
state_types,
state_types.keys(),
names,
"variable '%s' initializer" % name,
indent,
1,
)
lines.extend(checks)
if not safe:
continue
target = "scope->%s" % to_c_identifier(name)
if state_types.get(name) == "int" and expr.value_type == "float":
temp_name = "__pyfcstm_init_%d" % len(lines)
_line(lines, indent, 1, "double %s = %s;" % (temp_name, expr.text))
_line(
lines,
indent,
1,
"if (%s != (double)((PYFCSTM_GENERATED_INT64)%s)) {"
% (temp_name, temp_name),
)
_line(
lines,
indent,
2,
(
"%s(machine, "
"\"Variable '%s' is int type, cannot assign float %%.15g; "
"non-integer float from variable '%s' initializer\", "
"%s);"
)
% (names.set_error, name, name, temp_name),
)
_line(lines, indent, 2, "return %s;" % names.failure)
_line(lines, indent, 1, "}")
_line(
lines,
indent,
1,
"%s = (PYFCSTM_GENERATED_INT64)%s;" % (target, temp_name),
)
continue
_line(lines, indent, 1, "%s = %s;" % (target, expr.text))
lines.append("%sreturn %s;" % (indent, names.success))
return "\n".join(lines)
[docs]
def render_c_condition_body(
expr: Any,
var_types: Mapping[str, Any],
machine_class_name: str,
machine_macro_name: str,
usage: str,
result_name: str = "result",
indent: str = " ",
) -> str:
"""
Render a fallible generated C body for a boolean condition.
:param expr: Guard or condition expression.
:type expr: typing.Any
:param var_types: Persistent variable type mapping or model defines mapping.
:type var_types: typing.Mapping[str, typing.Any]
:param machine_class_name: Generated machine class name.
:type machine_class_name: str
:param machine_macro_name: Generated macro prefix.
:type machine_macro_name: str
:param usage: Diagnostic usage prefix, for example
``"transition guard"``.
:type usage: str
:param result_name: Pointer variable receiving the truth value, defaults to
``"result"``.
:type result_name: str, optional
:param indent: Indentation unit used for generated C code, defaults to four
spaces.
:type indent: str, optional
:return: C condition body ending in generated success or failure return.
:rtype: str
Example::
>>> from pyfcstm.dsl.node import Integer
>>> body = render_c_condition_body(Integer("1"), {}, "M", "M", "guard")
>>> "*result = !!(1);" in body
True
"""
state_types = _normalise_var_types(var_types)
expr_node = _coerce_expr(expr)
names = _CNames(machine_class_name, machine_macro_name)
lines = [
"%s(void)machine;" % indent,
"%s(void)scope;" % indent,
]
_emit_expr_checks(
lines, expr_node, state_types, state_types.keys(), names, usage, indent, 1
)
rendered = _render_expr(expr_node, state_types, state_types.keys())
lines.append("%s*%s = !!(%s);" % (indent, result_name, rendered.text))
lines.append("%sreturn %s;" % (indent, names.success))
return "\n".join(lines)