|
| 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 | +The expanded EasyCrypt source, then a form-feed byte (``\f``, ``0x0C``), then |
| 129 | +a JSON *source map*:: |
| 130 | + |
| 131 | + <expanded EasyCrypt source> |
| 132 | + \f |
| 133 | + { "segments": [ { "out": [ob, oe], "in": [ib, ie], "kind": "verbatim" }, |
| 134 | + ... ] } |
| 135 | + |
| 136 | +Each segment maps a half-open character range ``[ob, oe)`` of the **output** |
| 137 | +back to a range ``[ib, ie)`` of the **body** (offsets relative to the start of |
| 138 | +each, the body being the handler's own stdin payload): |
| 139 | + |
| 140 | +- ``"kind": "verbatim"`` — the output range is a character-for-character copy |
| 141 | + of the input range (``oe - ob == ie - ib``); reverse mapping is |
| 142 | + column-precise. |
| 143 | + |
| 144 | +- ``"kind": "synthesized"`` — the output range was generated by the handler |
| 145 | + and has no one-to-one origin; the whole output range is attributed to the |
| 146 | + whole input range, so an error there points at the responsible region of |
| 147 | + the body rather than at a misleading column. |
| 148 | + |
| 149 | +If the response contains no source-map section, the entire expansion is |
| 150 | +attributed to the entire quotation (coarse mapping). |
| 151 | + |
| 152 | +Errors |
| 153 | +~~~~~~ |
| 154 | + |
| 155 | +A handler that exits with a non-zero status makes EasyCrypt raise an error |
| 156 | +located at the quotation, using the handler's standard-error output as the |
| 157 | +message. |
| 158 | + |
| 159 | +------------------------------------------------------------------------ |
| 160 | +Location mapping |
| 161 | +------------------------------------------------------------------------ |
| 162 | + |
| 163 | +Because the expanded code is lexed and parsed in a separate buffer, the |
| 164 | +positions EasyCrypt computes for it would, naively, refer to the generated |
| 165 | +text. Using the source map and the body's original offset, EasyCrypt rewrites |
| 166 | +those positions so that **every** location it reports — parse errors, type |
| 167 | +errors, and printed locations alike — refers to the original source file. |
| 168 | + |
| 169 | +For a ``verbatim`` segment this is exact down to the column; for a |
| 170 | +``synthesized`` segment the location collapses to the originating region of |
| 171 | +the body. |
| 172 | + |
| 173 | +------------------------------------------------------------------------ |
| 174 | +Examples |
| 175 | +------------------------------------------------------------------------ |
| 176 | + |
| 177 | +A ``calc`` handler that evaluates an integer expression returns the resulting |
| 178 | +literal as a fragment, so:: |
| 179 | + |
| 180 | + op forty_two = {% calc 6 * 7 %}. |
| 181 | + |
| 182 | +expands to ``op forty_two = 42.``. |
| 183 | + |
| 184 | +A ``verbatim`` handler that copies its body through with a single ``verbatim`` |
| 185 | +segment lets EasyCrypt point at the exact original character on error. Given:: |
| 186 | + |
| 187 | + {% verbatim op broken : int = no_such_op + 1 %}. |
| 188 | + |
| 189 | +EasyCrypt reports the unknown-identifier error at the column of ``no_such_op`` |
| 190 | +inside the quotation, even though that identifier sits at a different offset in |
| 191 | +the generated buffer. |
| 192 | + |
| 193 | +.. note:: |
| 194 | + |
| 195 | + The result of expanding a quotation is stored in the compiled ``.eco`` |
| 196 | + file. When iterating on a handler, remove the stale ``.eco`` so the |
| 197 | + quotation is expanded afresh. |
0 commit comments