|
| 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. |
0 commit comments