Code generation › Quality of the code

Code generation

The quality of the generated code

archec gen writes code that should build clean, that a reviewer can trace back to the model, and that is tested by the model's own test plans. This page is how to check each of these, from the model:

  • Build and lint — archec gen --build --lint, each finding at the model element it comes from
  • C unit tests from the test plans — archec gen --target c-tests, make test, coverage of states and transitions
  • Defensive code — --defensive: ranges, stale inputs and end-to-end checks (CRC, counter) on receive
  • The traceability matrix — archec gen --target trace-matrix: requirement → element → generated function → test case → result

Each tool archec runs that is not installed (cppcheck, clang-tidy, cargo clippy, gcov) is said when the command runs, and what it would check is left out — never pretended. test/codegen-quality.test.js runs what is installed for real.

Build and lint#

archec gen main.arche --target c -o generated --build            # build it
archec gen main.arche --target c -o generated --build --lint     # and lint it
archec gen main.arche --target embassy -o generated --lint --sarif gen.sarif

--build builds what was just written:

Target Build --lint adds
c each binary's Makefile (make -B), or with no deployment every module on its own (-fsyntax-only), with -std=c99 -Wall -Wextra -Werror -pedantic cppcheck with its MISRA C:2012 addon (--addon=misra), and clang-tidy (bugprone, cert, clang-analyzer, performance; your .clang-tidy in the output folder instead when there is one)
embassy cargo build in each crate cargo clippy instead of the build

The generated C is meant to build clean, so with -Werror a warning is a build error: the command exits 1 when the build fails, 0 otherwise (a linter's finding alone does not fail it — read them, or gate on the SARIF in CI).

Each finding is said at the model element its generated line comes from, then where it is in the generated code:

main.arche:57 error [cc-unused-variable] unused variable 'x' — c/controller_ports.c:6 in controller_send_cmd (port cmd of Controller)
components.arche:172 warning [clippy-blocks_in_conditions] … — embassy/brake_fw/src/app.rs:26 in main (fn main of brakes)
components.arche:118 warning [misra-10.4] … — c/adcu.c:61 in adcu_fire_with (transition Manual -> Ready on self_test_ok of Adcu)

The rule is cc-<warning> for the compiler, misra-<rule> for a MISRA C:2012 rule, cppcheck-<id> for cppcheck's other checks, clang-tidy's check name, and clippy-<lint> for clippy. The element is found from the names the generators give and the comments they write:

Generated Model element
<part>_init, any other line of c/<part>.c the component
<part>_<fn> its function
<part>_on_receive_<port>, _on_deadline_, _command_, _output_, _send_ the port
<part>_fire_with, _timeout, below /* transition A -> B on M */ that transition
<part>_enter, _exit, below case <PART>_<STATE>: that state
<msg>_encode, _decode in c/arche_types.c the message
thread_<id>, a send function in c/<binary>/wiring.c the thread, the port
fn x in embassy/<binary>/src/app.rs, after /// <part path> the function (or port) of that part

A line that maps to nothing (a runtime file, main.c) is said at the generated file and line only.

--sarif <file> writes the findings as SARIF 2.1.0, as archec check --sarif does: each at the model element's file and line (so code scanning shows it on the .arche file), the generated place in its message, a rule per code.

C unit tests from the test plans#

archec gen main.arche --target c-tests -o generated            # write them
make -C generated/c-tests test                                  # build and run them
archec gen main.arche --target c-tests -o generated --run       # both
archec gen main.arche --target c-tests -o generated --coverage  # and gcov's coverage

--target c-tests writes, under c-tests/, the C of each component (as --target c writes it), arche_tests.c and a Makefile. arche_tests.c is the harness archec difftest drives the generated C with — the system's parts on a virtual clock, each message along its links with their latency, one thing at a time per part, in the order the specification gives — and a function per test case. No test framework: the harness is the test runner, and make test exits 1 when a case fails. make test CASES="TC-1 TP-2" runs some cases (by id or plan); ARCHE_TRACE=1 prints what the parts do, as archec difftest traces it.

A case runs as Arche's test runner runs it: from a fresh start, each step at its at, its do, its wait, then its expectations — an expression once the step has settled; within 40ms: true no later than 40 ms after the step began; at d:; eventually: within the case's timeout (100 ms); never: and always: watched after each event until the case ends, 10 ms after its last step.

What a case says in Python is translated where C can say it:

In the case In C
send(ctrl.engage, ENGAGE(True)) — a port that receives the part handles it now, as a message from outside
send(hmi.bus, ENGAGE_REQ(True)) — a port that sends sent along the port's links
MSG(points=1000, nearest=30.0), MSG(1000, 30.0) the message's fields by name or in order
adcu.speed = 20.0, +=, -= the part's variable
brakes.state == "Braking" the part is in Braking (or in a state inside it)
sent("BRAKE_CMD") >= 1 that message's sends since the case began
numbers, constants, True/False, and/or/not, comparisons, arithmetic the same

A case with anything else (print, received(…), a fault, a keyframe, a function test) is generated as SKIP, with what C could not say; nothing is guessed. The output is the one archec test reads from Arche:

PASS TP-BRK/TC-BRK-01 Brakes when fast
FAIL TP-BRK/TC-BRK-03 A wrong expectation
    ctrl.state == "Braking" — false at 1.000 ms (main.arche:111)
SKIP TP-BRK/TC-BRK-04 Python the C tests cannot say — do print("hello") (main.arche:114)
arche-tests: 2 passed, 1 failed, 1 skipped

--run builds and runs them, prints that and writes c-tests/results.json — the verdicts by PLAN/CASE and what they say of the requirements the cases verify, as archec test writes .archec/test-results.json. The traceability matrix reads it.

A body that waits (await wait(…)) runs in the harness, but its wait takes no time; one that loops forever on its waits (while True: await wait(WATCHDOG)) runs past 100000 events: its case is an ERROR (and the next case still runs). The car's watchdog loop does this today: in C it needs a thread, which the harness does not give a body.

Coverage#

With gcov (--coverage, or make coverage), each module is built with --coverage and its lines read back as the model:

gcov counts covered in the model
the line after /* transition A -> B on M */ that transition was taken
case <PART>_<STATE>: in <part>_enter ran, the initial state at init, or a taken transition's target that state was entered
a line of <part>_<fn> that function ran

and reported as archec test --coverage reports Arche's (also in c-tests/coverage.json):

Coverage (the generated C (gcov)):
  states           5/5     100 %
  transitions      3/4      75 %
      not covered: brakes: Braking -> Released on after 100ms
  functions        0/1       0 %
      not covered: sensor.scan

A part with one instance is named by its path (brakes), a module several instances share by its component (Brakes).

Defensive code#

archec gen main.arche --target c --defensive -o generated

--defensive adds to the C what the model says a message may be, checked where it comes in:

The model says The C does on receive
a field's range (or its type's), a value message's <msg>_in_range() in arche_types.c; a message with a value out of it is rejected — not clamped — and counted in arche_errors.range
a port's stale-after, else the message's period an input that has not come for that long (the period × 1.5, the tolerance archec replay gives it) is flagged stale: <binary>_input_stale(<BINARY>_IN_k) says so now, arche_errors.stale counts each time it goes stale
a wire format with a checksum a frame whose checksum is wrong does not decode; it is counted in arche_errors.crc
a wire format with a counter in the style of AUTOSAR E2E: a counter that did not move is a duplicate, rejected (arche_errors.repeat); one that jumped means messages were lost: the message is kept, the loss counted (arche_errors.jump) and logged

Why reject and not clamp: a value out of its range is a fault upstream — a sensor, a bus, a sender's bug — and a clamped value hides it as a plausible one. A rejected message never reaches the part, which keeps acting on the last good one; the stale flag says when that has gone on too long.

With a deployment, each binary's wiring.c checks every message it dispatches to a part (arche_accept_<link>_<msg>, its inputs listed in wiring.h), checks for stale inputs at each period of a periodic thread and at each message, and logs each rejection once until the input is accepted again (a stuck sensor does not flood the log). Without a deployment, <msg>_in_range() and arche_errors are there for your receive code. A thread that waits for messages and has no period only sees an input go stale at its next message, or when asked (<binary>_input_stale).

It is opt-in rather than on by default because it changes what the parts see: a rejected message never reaches them, so archec difftest would no longer compare like with like against the simulator, which checks none of this; and each checked input costs a few bytes of RAM and a few compares, which a small target should choose to pay. The checks are in C only: the Embassy crates do not have them yet.

The traceability matrix#

archec test main.arche                                          # Arche's simulator: arche-out/.archec/test-results.json
archec gen main.arche --target c-tests -o generated --run       # the C: generated/c-tests/results.json
archec gen main.arche --target trace-matrix -o generated        # generated/trace-matrix/matrix.md, .csv

For each requirement: the model elements that satisfy it (satisfies, satisfied-by, a trace), the C generated for each (--target c's file, line and function, read back as in build and lint), the test cases that verify it (a plan's or a case's verifies, verified-by, trace verifies, verify { tests … }), and what the last run of each case said:

Requirement Model element Generated code Test case Last result
REQ-BRAKE Brake when fast component Controller (main.arche:54) c/controller.c:9 controller_init TP-BRK/TC-BRK-01
TP-BRK/TC-BRK-03
pass (C), pass (sim)
fail (C)
REQ-SCAN Report the speed fn scan (main.arche:46) c/sensor.c:11 sensor_scan — —

A component is said by where its module starts (its init); a function, a state, a transition or a port traced to a requirement by its own generated function; a part by its own and its component's. The results are those of --results <file> (each a results JSON, repeatable), else those found: arche-out/.archec/test-results.json (sim) and <out>/c-tests/results.json (C); a case in neither is not run.

Below the table, the gaps: a requirement no element satisfies, whose elements have no generated C, that no case verifies, with a case that failed, or whose cases have not run. matrix.csv has a row per requirement, element and case (requirement,title,element,element_at,generated,test_case,result), for a spreadsheet or a certification tool.