Skip to content
Draft
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
28 changes: 17 additions & 11 deletions src/fpy/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,22 @@ def generate_write_to_port_llvm(builder, args):
return None


WRITE_TO_PORT_MACRO = BuiltinFuncSymbol(
"write_to_port",
NOTHING,
[
("port", SerialPortIndex, None),
("value", SIZED, None),
],
generate_write_to_port,
generate_write_to_port_llvm,
const_arg_indices=frozenset({0}), # port must be compile-time constant
)

# The sentinel constant Svc.Fpy.SerialPortIndex declares one past its last real
# port; the sequencer bounds-checks every port index against it.
MAX_SERIAL_PORTS_NAME = "MAX_SERIAL_PORTS"

TIME_MACRO = BuiltinFuncSymbol(
"time",
TIME,
Expand Down Expand Up @@ -430,15 +446,5 @@ def generate_write_to_port_llvm(builder, args):
const_arg_indices=frozenset({0, 1}),
),
# Serial write: port typed by the dictionary-backed Svc.Fpy.SerialPortIndex enum; value typed SIZED
"write_to_port": BuiltinFuncSymbol(
"write_to_port",
NOTHING,
[
("port", SerialPortIndex, None),
("value", SIZED, None),
],
generate_write_to_port,
generate_write_to_port_llvm,
const_arg_indices=frozenset({0}), # port must be compile-time constant
),
"write_to_port": WRITE_TO_PORT_MACRO,
}
31 changes: 30 additions & 1 deletion src/fpy/semantics.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from typing import Union

from fpy.error import CompileError, diagnostic_context
from fpy.macros import TIME_MACRO
from fpy.macros import MAX_SERIAL_PORTS_NAME, TIME_MACRO, WRITE_TO_PORT_MACRO
from fpy.types import (
pick_binary_op_case,
pick_unary_op_case,
Expand Down Expand Up @@ -2246,6 +2246,31 @@

return struct.unpack(fmt, packed)[0]

@staticmethod
def _check_serial_port_in_range(
port: FpyValue, node: Ast, state: CompileState
) -> bool:
"""Check that a constant serial port index names a port the sequencer
will accept: at or above the MAX_SERIAL_PORTS sentinel (the sentinel
itself included) it always fails at run time with
SERIAL_PORT_INVALID_INDEX. Reports an error and returns False if not.

A dictionary whose enum declares no sentinel gives nothing to check
against, so the index is left to the run-time check."""
max_ports = port.type.enum_dict.get(MAX_SERIAL_PORTS_NAME)
if max_ports is None:
return True
index = port.type.enum_dict[port.val]
if 0 <= index < max_ports:
return True
state.err(
f"Serial port {port.type.display_name}.{port.val} has index {index}, "
f"which is outside the {max_ports} ports the sequencer has "
f"({port.type.display_name}.{MAX_SERIAL_PORTS_NAME} is {max_ports})",
node,
)
return False

@staticmethod
def _parse_time_string(
time_str: str, time_base: str, time_context: int, node: Ast, state: CompileState
Expand Down Expand Up @@ -2618,6 +2643,10 @@
)
)
return
if func is WRITE_TO_PORT_MACRO and not self._check_serial_port_in_range(
arg_values[0], resolved_args[0], state
):
return

if unknown_value:
# we will have to calculate this at runtime
Expand Down
37 changes: 37 additions & 0 deletions test/fpy/test_write_to_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,43 @@ def test_max_port_index(self, fprime_test_api):
assert len(dirs) == 1
assert dirs[0].portIndex == 4

def test_max_serial_ports_sentinel_rejected(self, fprime_test_api):
# MAX_SERIAL_PORTS is the sentinel one past the last real port, so the
# sequencer always rejects it at run time; reject it at compile time
seq = """
value: U32 = 42
write_to_port(Svc.Fpy.SerialPortIndex.MAX_SERIAL_PORTS, value)
"""
assert_compile_failure(fprime_test_api, seq, match="outside the 5 ports")

def test_port_at_or_above_sentinel_rejected(self, fprime_test_api, tmp_path):
# A port constant is out of range whenever its index reaches the
# sentinel, not just when it is the sentinel: here MAX_SERIAL_PORTS is
# lowered to 2, which puts EXAMPLE_PORT_4 out of range.
d = json.loads(Path(default_dictionary).read_text())
for t in d["typeDefinitions"]:
if t.get("qualifiedName") == "Svc.Fpy.SerialPortIndex":
for c in t["enumeratedConstants"]:
if c["name"] == "MAX_SERIAL_PORTS":
c["value"] = 2
custom_dict = tmp_path / "TwoPorts.json"
custom_dict.write_text(json.dumps(d))

seq = """
value: U32 = 42
write_to_port(Svc.Fpy.SerialPortIndex.EXAMPLE_PORT_4, value)
"""
try:
fpy.error.file_name = "<two-port-dict-test>"
fpy.error.input_text = seq
fpy.error.input_lines = seq.splitlines()
_build_global_scopes.cache_clear()
state = get_base_compile_state(str(custom_dict))
with pytest.raises(fpy.error.CompileError, match="outside the 2 ports"):
analyze_ast(text_to_ast(seq), state)
finally:
_build_global_scopes.cache_clear()

def test_non_constant_port_rejected(self, fprime_test_api):
# the port index must be a compile-time constant; an enum-typed variable
# has the right type but is not const, so it is rejected
Expand Down
Loading