Bridge separation-logic (SL) specifications to ACSL so that they can be verified by Frama-C / WP.
You write a contract in a compact SL dialect inside a special comment:
/*@[SL]
req p->int*(a);
ens p->int*(a+1);
*/
void inc(int *p) { (*p)++; }…and the tool turns it into the equivalent ACSL contract:
/*@ requires \valid(p);
assigns *p;
ensures *p == \old(*p) + 1; */There are two front ends for the same translation:
- Frama-C plugin (
-sl) — parses the SL in-process and attaches a native ACSL contract, soframa-c -sl -wp file.cverifies directly. (recommended) - Standalone CLI — translates
file.c→file_acsl.con disk, then you run Frama-C on the output (this is whatsl_to_acsl.shautomates).
📖 User manual:
docs/SL_to_ACSL_Manual.md— the SL syntax reference, CLI/GUI walkthrough (with screenshots), and a full appendix of worked translations. For the translation internals, see Hrishiraj Mandal's reportacsl_sl_bridge.pdf.
Everything lives in an opam switch. Install the toolchain once:
opam install dune menhir frama-c alt-ergo
why3 config detect # let WP discover provers (z3/alt-ergo)Before running anything (the helper scripts included), activate the opam switch that has Frama-C so that
dune,frama-c, andocamlfindare on yourPATH:eval $(opam env) # current switch # or, if Frama-C is in another switch: opam switch <name> && eval $(opam env)Verify with
frama-c -version(expect 32.x, Germanium). The scripts do not set this up for you — they assume the toolchain is already onPATH.
src/
sl_lexer.mll, sl_parser.mly SL surface syntax -> Sl_ast
acsl_lexer.mll, acsl_parser.mly ACSL surface syntax (reverse direction, scaffolded)
ast/ sl_ast, core, acsl_ast ASTs + the neutral Core IR
ast_printer/ pretty-printers
translator/ sl_to_core, core_to_acsl, the actual translation
acsl_to_core, core_to_sl
frontend/ sl_extract.ml split a file into code / SL blocks (shared)
main.ml the standalone CLI
frama_sl/ sl_options, sl_attach, sl_register the Frama-C plugin
test/system_test/ example .c inputs + golden _acsl.c outputs
sl_to_acsl.sh standalone: translate + run frama-c
install_sl.sh install the -sl plugin into the Frama-C plugin path
run_sl.sh run the plugin from _build without installing (dev)
./install_sl.shThis builds and dune installs the frama-c-sl package, then writes the discovery file
<libdir>/frama-c/plugins/sl/META so Frama-C auto-loads the plugin.
frama-c -sl file.c # parse SL and attach the ACSL contract
frama-c -sl -print file.c # ...and print the function with its attached contract
frama-c -sl -wp file.c # ...and verify it with WP
frama-c -sl -wp -wp-no-simpl -wp-no-let file.c # more readable WP goalsExample:
frama-c -sl -wp test/system_test/incr_max/incr_max_spatial.c
# [sl] attached SL contract to incr_max
# [wp] Proved goals: 8 / 8To see the original SL next to the generated ACSL, add -sl-show (printed per function):
frama-c -sl -sl-show file.cframa-c-gui -sl -wp file.cThe lower notebook (next to Information / Messages) gains an “SL → ACSL” tab showing, per function, the original separation-logic block (left) beside the generated ACSL contract (right), evenly split, word-wrapped, with keywords highlighted and a Copy button per side.
Each function header carries a proof-status bullet (● green = proved, red = invalid, orange = unknown, ○ grey = not yet attempted). The two views are synced with the source: click a function in the main source view to highlight + scroll to its block here, or click a block here to jump to it in the source. Use Refresh status to re-query after running WP.
Functions whose generated ACSL fails to type/attach (e.g. specs using <limits.h> macros like
INT_MAX) are flagged with ⚠ (not attached) and a red bullet — the generated ACSL is still
shown for inspection, but no contract was attached to the function.
-sl-show remains the console equivalent of this tab. The proof bullets in the source view show
WP results as usual.
run_sl.sh builds the plugin and its libraries in _build and loads them with -load-module:
./run_sl.sh -wp test/system_test/additional_tests/pure/max2/max2.cTranslate only (writes <name>_acsl.c next to the input and echoes it):
eval $(opam env --switch=ocaml5)
dune exec ./src/main.exe -- test/system_test/abs_diff/abs_diff.cTranslate and run Frama-C/WP in one step:
./sl_to_acsl.sh test/system_test/incr_max/incr_max_spatial.c # CLI
./sl_to_acsl.sh --gui test/system_test/incr_max/incr_max_spatial.c # Frama-C GUItest/system_test/ holds example inputs (<name>.c) paired with the expected ACSL
(<name>_acsl.c). additional_tests/ groups extra cases:
additional_tests/
pure/ max2, sign, safe_div # arithmetic / case-splits, no heap
single_cell_heap/ inc, rotate3 # pointer cells, separation
known_gaps/ global_counter # documents a current limitation (expected fail)
Regenerate every _acsl.c and confirm nothing changed versus what is committed:
eval $(opam env --switch=ocaml5)
find test/system_test -name '*.c' ! -name '*_acsl.c' \
-exec dune exec ./src/main.exe -- {} \; >/dev/null 2>&1
git diff --stat -- test/system_test # no output = all outputs match the committed golden filesThe translation chain (the plugin reuses stages ①–④ from the CLI verbatim, then attaches the result instead of writing a file):
SL text
→ Sl_parser/Sl_lexer → Sl_ast.spec (parse SL)
→ Sl_to_core.sl_to_core → Core.spec (neutral IR: phases, frame, aliasing)
→ Core_to_acsl.spec_to_acsl → ACSL text
── CLI: write <name>_acsl.c ; run frama-c
└─ plugin: Logic_lexer.spec → Logic_typing → Annotations.add_* (attach typed funspec)
The Core IR is the hinge: it normalises pre/post (' and @old), turns heap cells into
\valid + assigns, and makes separation (*) explicit as != facts. A second printer
(core_to_sl) renders Core back to SL — the intended hook for future SLEEK integration.
- Loop contracts are supported. A
/*@[SL] ... */block placed directly above afor/whileloop is translated toloop invariant/assigns/variantand attached to the loop statement (e.g.zero_arrayproves 15/15, matching the CLI). A block is routed to a loop or a function contract by whichever (loop statement / function definition) appears nearest below it. '(prime) notation breaks Frama-C preprocessing: Frama-C runs its annotation preprocessor over/*@ ... */comments, and a stray'(used by the*_primespecs andfor.c) is read as an unterminated character literal, aborting before the plugin runs. Use the non-prime variants (\old, alias) with the plugin; the standalone CLI handles'because it reads the raw source.<limits.h>macros (INT_MAX,INT_MIN): not expanded for the plugin, so they fail typing (e.g.abs_diff) — shown as "(not attached)" in the GUI. Same root cause as above; use the CLI.- Recursive/inductive predicates (
x::ll<n>): not supported by the SL dialect; only the flat fragment (single cellsp->int*(v), array ranges, arithmetic, case-splits) is translated.