Verify rewriter and runtime linkage through explicit interfaces

This commit is contained in:
milner committed 2019-04-26 18:12:00 +00:00
1 parent 16fc1d6cee
commit ef4c7899d5
3 files changed
+142 -5

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ test: all
$(OCAMLC) -ppx ./rewrite fieldglass.cma case.ml -o case.byte $(OCAMLC) -ppx ./rewrite fieldglass.cma case.ml -o case.byte
./case.byte ./case.byte
OCAMLC=$(OCAMLC) sh error.sh OCAMLC=$(OCAMLC) sh error.sh
OCAMLC=$(OCAMLC) sh smoke.sh ./rewrite .
clean: clean:
rm -f *.cmi *.cmo *.cma *.byte rewrite rm -f *.cmi *.cmo *.cma *.byte rewrite
+108 -5
View File
@@ -1,10 +1,113 @@
# fieldglass # fieldglass
Lenses for reading and updating immutable data in OCaml. Generates accessors and composable lenses from OCaml record
declarations, allowing a caller to read or replace a nested field through
ordinary functions while retaining the surrounding record structure.
Annotate a record with `[@@fieldglass generate]` to generate field accessors. ```ocaml
The `[@@lens generate]` spelling is also accepted. type swatch = { colour : string; label : string }
[@@fieldglass generate]
Build with `make`; run the checks with `make test`. let grey = { colour = "grey"; label = "sample" }
let blue = set_colour "blue" grey
let labelled = update_label ~f:String.uppercase_ascii grey
let colour = Fieldglass.view _colour blue
```
The `_hd` and `_tl` lenses raise `Invalid_argument` on empty lists. For each field, the rewriter emits a getter, a setter prefixed with `set_`, an
updater prefixed with `update_`, and a getter/setter pair whose name begins
with `_`. A setter takes the replacement value followed by the source record,
while an updater applies its labelled `~f` argument to the existing field
value and constructs a replacement record containing the result.
Generated updates retain the unfocussed fields and perform no assignment to
the source record, including when the declaration contains mutable fields.
Both `[@@fieldglass generate]` and `[@@lens generate]` select this expansion,
which produces ordinary OCaml functions that can be used independently of
the `Fieldglass` runtime's composition operations.
## Build
Run `make` to compile the runtime archive and PPX executable, or `make test`
to also check lens operations, generated record accessors, configuration
errors, and linkage against an explicit module interface.
To compile a consumer, pass the rewriter through `-ppx` and link the runtime
archive before the source file, as in the following invocation:
```sh
ocamlc -ppx ./rewrite fieldglass.cma example.ml -o example
```
The command `OPAMBUILDTEST=true opam pin add fieldglass .` builds and tests
the package before installing the `fieldglass` rewriter and runtime library.
Use the same OCaml compiler for the rewriter and its consumers, since the
PPX exchanges compiler syntax trees and links against the compiler libraries.
## Lenses
`Fieldglass.lens ~view ~set` constructs a pair with representation
`('s -> 'a) * ('b -> 's -> 't)`, where the getter extracts a value from the
source and the setter combines a replacement with that source to produce
the result. The separate type parameters permit a lens to change the type
of its focussed value when the enclosing structure admits that change.
`view` applies the getter, `set` applies the setter, and `over ~f` passes the
getter's result through `f` before supplying it to the setter alongside the
original source. With `compose outer inner`, reading follows the outer getter
and then the inner getter; replacement updates the inner structure before
passing it back through the outer setter, preserving the surrounding data
according to the supplied setters.
`Fieldglass.Infix` exposes these operations as `^.`, `^~`, and `^%`, with
`^>` composing an outer lens with an inner lens and `^<` accepting the same
operands in reverse order. The runtime also supplies tuple projections and
the identity lens `_id`, together with `_hd` and `_tl` for focussing on a
list's head or tail; either list lens raises `Invalid_argument` when asked
to read or update an empty list.
## Configuration
Supply labelled options after `generate` to control the generated names,
argument order, and selection of operations, using identifiers or strings
for prefixes and argument names. Boolean options accept a bare label as an
enabled flag, or an explicit `true` or `false` value when the configuration
needs to specify the setting directly.
| Option | Behaviour |
| --- | --- |
| `~field_prefix:name` | Add a prefix to each field name. |
| `~field_prefix_from_type` | Use the record type name as the prefix. |
| `~get_prefix:name` | Prefix getter names. |
| `~set_prefix:name` | Replace the `set` prefix. |
| `~update_prefix:name` | Replace the `update` prefix. |
| `~self_arg_first` | Put the record first in setters and updaters. |
| `~func_named_arg:name` | Rename the updater's `f` argument. |
| `~func_no_named_arg` | Make the updater's function argument positional. |
| `~no_get` | Omit getters. |
| `~no_set` | Omit setters. |
| `~no_update` | Omit updaters. |
| `~no_lens` | Omit lens pairs. |
| `~just_lens` | Generate lens pairs without named accessors. |
Before applying naming options, the rewriter removes the longest shared
field prefix ending at an underscore boundary, so fields such as
`paint_colour` and `paint_label` produce the base names `colour` and `label`.
A record containing only one field keeps that field's complete name, while
generated lens pairs retain replacement-first setters even when
`~self_arg_first` changes the argument order of the named accessors.
An annotation attached to a mutually recursive record declaration applies
to the entire declaration group, whose options the rewriter processes from
left to right with later settings taking precedence. When records share
field labels, `~field_prefix_from_type` gives their accessors distinct names;
the rewriter rejects duplicate generated bindings and repeated options
within a single annotation with a compilation error.
Place generation annotations on record declarations in implementations and
declare the exported accessor types explicitly in module signatures, since
the rewriter emits value bindings and rejects generation annotations in
signatures. For private records or records containing universally quantified
fields, select `~no_set ~no_update ~no_lens` to generate getters without
requesting replacement operations that the rewriter does not generate for
those declarations.
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
set -eu
compiler=${OCAMLC:-ocamlc}
rewriter=$(cd "$(dirname "$1")" && pwd)/$(basename "$1")
library=$(cd "$2" && pwd)
directory=$(mktemp -d)
trap 'rm -rf "$directory"' EXIT HUP INT TERM
cat > "$directory/unit.mli" <<'ML'
type t = { colour : string; label : string }
val colour : t -> string
val set_colour : string -> t -> t
val _colour : (t -> string) * (string -> t -> t)
ML
cat > "$directory/unit.ml" <<'ML'
type t = { colour : string; label : string } [@@fieldglass generate]
ML
cat > "$directory/client.ml" <<'ML'
let () =
let original = Unit.{ colour = "grey"; label = "sample" } in
let changed = Fieldglass.over Unit._colour ~f:String.uppercase_ascii original in
assert (Unit.colour changed = "GREY");
assert (changed.Unit.label = original.Unit.label);
assert (Unit.colour original = "grey")
ML
"$compiler" -c "$directory/unit.mli"
"$compiler" -I "$directory" -ppx "$rewriter" -c "$directory/unit.ml"
"$compiler" -I "$library" -I "$directory" "$library/fieldglass.cma" \
"$directory/unit.cmo" "$directory/client.ml" -o "$directory/client.byte"
"$directory/client.byte"