Skip to content

Commit fe1fbfb

Browse files
committed
Add black-box quotation preprocessor
Introduce {% name body %} quotations, expanded during lexing by an external tool over stdin/stdout. A quotation expands to a sentence fragment that is spliced into the surrounding sentence, so it may replace only part of a sentence (and several may appear in one). The terminating '.' is always written by the user. Locations in the expansion are remapped back to the original source, so errors point at the original quoted text. Usage Write a quotation as {% name body %}, where name selects a handler and body is arbitrary text (the {% %} delimiters nest). The expansion is a fragment, so the surrounding sentence and its '.' are written outside the quotation: op forty_two = {% calc 6 * 7 %}. Quotations compose with ordinary source and with each other in one sentence: op mixed = {% calc 6 * 7 %} + ({% calc 2 + 3 %} * 10). Enabling quotations Quotations run external programs, so the feature is OFF by default and a quotation encountered while it is off is a hard error (never a silent skip or a silent execution). Enable it for a run in one of two ways: easycrypt -enable-quotations compile foo.ec EC_ENABLE_QUOTATIONS=1 easycrypt compile foo.ec It cannot be enabled from easycrypt.project, which ships inside a checked-out tree -- letting it turn the feature on would defeat the safeguard. Only enable quotations for sources you trust. Binding handlers Once enabled, a name is resolved to a shell command in order: a 'quote = name:command' entry in the [general] section of easycrypt.project (relative paths resolved against the project directory); then EC_QUOTE_<NAME>; then EC_QUOTE_CMD; then an executable handlers/<name> (also .py/.sh) next to the source file. Handler protocol The handler reads the body (after a header line) on stdin and writes a single JSON object on stdout: { "expanded": <source>, "segments": [...] }, where segments is the source map used for error-location remapping. A non-zero exit reports an error at the quotation. See doc/quotations.rst for the full reference.
1 parent e2bb4eb commit fe1fbfb

23 files changed

Lines changed: 1038 additions & 22 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ uninstall:
4343
$(DUNE) uninstall
4444

4545
unit: build
46-
$(CHECK) unit
46+
$(CHECK) unit quotations-disabled
4747

4848
stdlib: build
4949
$(CHECK) prelude stdlib

config/tests.config

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,11 @@ exclude = examples/MEE-CBC examples/old examples/old/list-ddh !examples/incomple
1414
okdirs = examples/MEE-CBC
1515

1616
[test-unit]
17-
okdirs = tests tests/exception
17+
args = -enable-quotations
18+
okdirs = tests tests/exception tests/quotations tests/quotations-project
19+
20+
# Quotations are disabled by default (they run external programs); a file
21+
# using one must fail when -enable-quotations is NOT given. This scenario
22+
# deliberately omits the flag.
23+
[test-quotations-disabled]
24+
kodirs = tests/quotations-disabled

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ EasyCrypt reference manual
55
:maxdepth: 2
66

77
tactics
8+
quotations

doc/quotations.rst

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
========================================================================
2+
Quotations (external preprocessor)
3+
========================================================================
4+
5+
A *quotation* lets you embed, directly in an EasyCrypt source file, a
6+
fragment written in some other surface syntax, and have EasyCrypt expand it
7+
into ordinary EasyCrypt code by delegating to an **external tool**. The tool
8+
is a black box: EasyCrypt communicates with it over standard input and
9+
standard output, so it can be written in any language.
10+
11+
.. warning::
12+
13+
Quotations run external programs, so the feature is **disabled by
14+
default**. Enable it explicitly with the command-line flag
15+
``-enable-quotations`` or the environment variable
16+
``EC_ENABLE_QUOTATIONS=1``. It cannot be enabled from ``easycrypt.project``
17+
(that file ships inside a checked-out tree, so allowing it to turn the
18+
feature on would defeat the safeguard). While disabled, encountering a
19+
quotation is a hard error, never a silent skip or a silent execution. Only
20+
enable quotations for sources you trust.
21+
22+
Quotations are processed during lexing, before parsing. A quotation expands
23+
to a **sentence fragment**: its tokens are spliced into the surrounding
24+
sentence, so a quotation may stand for only *part* of a sentence and several
25+
quotations may appear in one sentence. The sentence terminator (``.``) is
26+
always written by the user and never produced by a quotation. When the
27+
external tool — or EasyCrypt's handling of its output — produces an error, the
28+
location reported by EasyCrypt is mapped back to the **original** quoted text,
29+
not to the generated code.
30+
31+
------------------------------------------------------------------------
32+
Syntax
33+
------------------------------------------------------------------------
34+
35+
A quotation is delimited by ``{%`` and ``%}``:
36+
37+
.. admonition:: Syntax
38+
39+
``{% {name} {body} %}``
40+
41+
Here:
42+
43+
- ``{name}`` is a lowercase identifier selecting which external *handler*
44+
expands the quotation (see `Configuring handlers`_).
45+
46+
- ``{body}`` is arbitrary text. It runs from the character following
47+
``{name}`` up to the matching ``%}``. The delimiters nest: a ``{% ... %}``
48+
pair occurring inside the body is kept verbatim and does not close the
49+
outer quotation, so a body may itself contain quotation delimiters.
50+
51+
A quotation expands to a sentence *fragment*, so the ``.`` that ends the
52+
sentence is written outside the quotation. For example, a ``calc`` handler
53+
that returns the value of an arithmetic expression::
54+
55+
op forty_two = {% calc 6 * 7 %}.
56+
57+
expands to ``op forty_two = 42.``. Because the expansion is only a fragment,
58+
quotations compose with ordinary source and with each other within one
59+
sentence::
60+
61+
op mixed = {% calc 6 * 7 %} + ({% calc 2 + 3 %} * 10).
62+
63+
It is an error for a quotation's expansion to contain a sentence terminator
64+
(``.``): the fragment must not close the sentence itself.
65+
66+
------------------------------------------------------------------------
67+
Configuring handlers
68+
------------------------------------------------------------------------
69+
70+
A quotation ``name`` is resolved to a shell command in this order:
71+
72+
- a binding in ``easycrypt.project`` (see below);
73+
74+
- ``EC_QUOTE_<NAME>`` — where ``<NAME>`` is ``name`` uppercased — gives the
75+
command for that specific quotation name;
76+
77+
- ``EC_QUOTE_CMD`` — a fallback command used for any quotation whose specific
78+
variable is unset;
79+
80+
- otherwise, an executable ``handlers/<name>`` (also tried with the ``.py``
81+
and ``.sh`` extensions) sitting next to the source file. This lets a
82+
directory of files be self-contained, needing no environment to set up — it
83+
is how the test suite binds its handlers.
84+
85+
The recommended way is the project file. In the ``[general]`` section of
86+
``easycrypt.project``, add one repeatable ``quote`` entry per handler, of the
87+
form ``name:command``::
88+
89+
[general]
90+
quote = calc:handlers/calc.py
91+
quote = verbatim:python3 tools/verbatim.py
92+
93+
The ``command`` is a shell command (so it may include an interpreter and
94+
arguments). When it is, verbatim, a relative path to an existing file, it is
95+
resolved against the directory containing ``easycrypt.project``; otherwise it
96+
is passed to the shell unchanged. Project-file bindings take precedence over
97+
the environment, so the committed configuration is authoritative.
98+
99+
To bind a quotation ad hoc through the environment instead::
100+
101+
export EC_QUOTE_CALC=/path/to/calc-handler
102+
103+
A quotation whose name resolves to no command raises an error located at the
104+
quotation.
105+
106+
------------------------------------------------------------------------
107+
The handler protocol
108+
------------------------------------------------------------------------
109+
110+
For each quotation, EasyCrypt launches the bound command, writes a request to
111+
its standard input, and reads the expansion from its standard output.
112+
113+
Request (sent by EasyCrypt)
114+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
115+
116+
A single header line, followed by the raw body::
117+
118+
#ec-quote v1 name=<name> file=<orig-file> line=<L> col=<C> off=<O>
119+
<body bytes...>
120+
121+
where ``line``/``col`` are the 1-based line and 0-based column of the body's
122+
first character in the original file, and ``off`` is its absolute character
123+
offset.
124+
125+
Response (returned by the handler)
126+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
127+
128+
A single JSON object on standard output::
129+
130+
{ "expanded": "<expanded EasyCrypt source>",
131+
"segments": [ { "out": [ob, oe], "in": [ib, ie], "kind": "verbatim" },
132+
... ] }
133+
134+
``"expanded"`` (a string, required) is the replacement source. ``"segments"``
135+
(optional) is the *source map*: each segment maps a half-open character range
136+
``[ob, oe)`` of the **expanded** source back to a range ``[ib, ie)`` of the
137+
**body** (offsets relative to the start of each, the body being the handler's
138+
own stdin payload).
139+
140+
The two ``"kind"`` values answer one question: given an error at some offset
141+
*inside* a segment's output range, can EasyCrypt compute the *exact*
142+
corresponding input offset, or only point at the region as a whole? It depends
143+
on whether the handler copied the text or invented it.
144+
145+
- ``"kind": "verbatim"`` — the output range is a **character-for-character
146+
copy** of the input range, so the two ranges have the same length
147+
(``oe - ob == ie - ib``). Output character *k* is input character *k*, so an
148+
error at output offset ``o`` maps to input offset ``ib + (o - ob)`` —
149+
**column-precise**. Mark a segment verbatim exactly when you pass input
150+
through unchanged.
151+
152+
- ``"kind": "synthesized"`` — the output range was **generated** by the
153+
handler (a computed value, boilerplate, glue), so it has no
154+
character-to-character relationship with the input and the ranges typically
155+
differ in length. There is no meaningful per-character offset to compute, so
156+
the whole output range is attributed to the whole input range: an error
157+
there points at the responsible region of the body rather than at a
158+
misleading column. Mark a segment synthesized whenever the output is not a
159+
literal copy.
160+
161+
Why distinguish them: collapsing *everything* to the region (as synthesized
162+
does) is always safe but throws away precision; the ``+ (o - ob)`` arithmetic
163+
is only valid when the copy is exact, which is what ``verbatim`` asserts. A
164+
real handler mixes both — for example, expanding ``{% sugar foo %}`` into
165+
``lemma foo_lemma : <user text>`` would mark the boilerplate
166+
``lemma foo_lemma :`` synthesized and the copied ``<user text>`` verbatim, so
167+
errors in the user's own text get exact columns while errors in the generated
168+
glue fall back to the region. A handler that does not care about precision may
169+
mark everything synthesized (coarse, but never wrong).
170+
171+
As a safety check, EasyCrypt treats a segment as verbatim only if it is
172+
labelled ``"verbatim"`` **and** the two ranges actually have equal length; a
173+
mislabelled (length-mismatched) verbatim segment is downgraded to the safe
174+
collapse rather than producing bogus offsets.
175+
176+
If the response carries no (or an unparsable) ``"segments"`` field, the entire
177+
expansion is attributed to the entire quotation (coarse mapping). Output that
178+
is not a JSON object, or that lacks a string ``"expanded"`` field, is reported
179+
as an error at the quotation.
180+
181+
Errors
182+
~~~~~~
183+
184+
A handler that exits with a non-zero status makes EasyCrypt raise an error
185+
located at the quotation, using the handler's standard-error output as the
186+
message.
187+
188+
------------------------------------------------------------------------
189+
Location mapping
190+
------------------------------------------------------------------------
191+
192+
Because the expanded code is lexed and parsed in a separate buffer, the
193+
positions EasyCrypt computes for it would, naively, refer to the generated
194+
text. Using the source map and the body's original offset, EasyCrypt rewrites
195+
those positions so that **every** location it reports — parse errors, type
196+
errors, and printed locations alike — refers to the original source file.
197+
198+
For a ``verbatim`` segment this is exact down to the column; for a
199+
``synthesized`` segment the location collapses to the originating region of
200+
the body. Two examples make the difference concrete.
201+
202+
*Verbatim.* A handler that copies its body through unchanged reports::
203+
204+
{ "expanded": "op broken : int = no_such_op + 1",
205+
"segments": [ { "out": [0, 32], "in": [0, 32], "kind": "verbatim" } ] }
206+
207+
The error on ``no_such_op`` (output offset 18) maps to input offset 18, then
208+
to the original file, so EasyCrypt points at the exact columns of
209+
``no_such_op`` in the source — not at the quotation as a whole.
210+
211+
*Synthesized.* The ``calc`` handler turns the body ``6 * 7`` into ``42``::
212+
213+
{ "expanded": "42",
214+
"segments": [ { "out": [0, 2], "in": [0, 5], "kind": "synthesized" } ] }
215+
216+
Output and input have different lengths and ``42`` came from no particular
217+
character of ``6 * 7``, so an error on ``42`` cannot be attributed to a column;
218+
it points at the whole ``6 * 7`` region instead.
219+
220+
This is also why offset-range segments are used rather than line markers such
221+
as ``#line``: a line marker can only say "this generated line came from input
222+
line *N*", whereas reporting an error at the exact columns of ``no_such_op``
223+
needs the character-level mapping a verbatim segment provides.
224+
225+
------------------------------------------------------------------------
226+
Examples
227+
------------------------------------------------------------------------
228+
229+
A ``calc`` handler that evaluates an integer expression returns the resulting
230+
literal as a fragment, so::
231+
232+
op forty_two = {% calc 6 * 7 %}.
233+
234+
expands to ``op forty_two = 42.``.
235+
236+
A ``verbatim`` handler that copies its body through with a single ``verbatim``
237+
segment lets EasyCrypt point at the exact original character on error. Given::
238+
239+
{% verbatim op broken : int = no_such_op + 1 %}.
240+
241+
EasyCrypt reports the unknown-identifier error at the column of ``no_such_op``
242+
inside the quotation, even though that identifier sits at a different offset in
243+
the generated buffer.
244+
245+
.. note::
246+
247+
The result of expanding a quotation is stored in the compiled ``.eco``
248+
file. When iterating on a handler, remove the stale ``.eco`` so the
249+
quotation is expanded afresh.

src/ec.ml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,14 @@ let main () =
398398
ldropts.ldro_idirs;
399399
end;
400400

401+
(* Quotations: enabled only via CLI/env (never easycrypt.project), and the
402+
handlers declared in easycrypt.project are registered only then. *)
403+
EcQuotation.set_enabled options.o_options.o_enable_quotes;
404+
if options.o_options.o_enable_quotes then
405+
List.iter
406+
(fun (name, command) -> EcQuotation.register ~name ~command)
407+
ldropts.ldro_quotes;
408+
401409
(* Initialize printer *)
402410
EcCorePrinting.Registry.register (module EcPrinting);
403411

@@ -780,6 +788,14 @@ let main () =
780788
List.iter
781789
(fun p ->
782790
let loc = p.EP.gl_action.EcLocation.pl_loc in
791+
(* Mechanism B: a location should never escape quotation
792+
position-remapping (EcIo) carrying the synthetic buffer
793+
filename. If one does, collapse it rather than print
794+
meaningless <quotation:...> coordinates. *)
795+
let loc =
796+
if EcQuotation.is_sentinel loc.EcLocation.loc_fname
797+
then EcLocation._dummy else loc
798+
in
783799

784800
(* -upto: if this command starts past the target, print goals and exit *)
785801
if past_upto loc then begin

0 commit comments

Comments
 (0)