From 5a8074d115233a3afa21035b8e169b933952de7c Mon Sep 17 00:00:00 2001 From: sneeker Date: Fri, 26 Apr 2019 18:12:00 +0000 Subject: [PATCH] Verify rewriter and runtime linkage through explicit interfaces --- Makefile | 1 + README.md | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++--- smoke.sh | 33 ++++++++++++++++ 3 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 smoke.sh diff --git a/Makefile b/Makefile index 3213dfd..539a0d5 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ test: all $(OCAMLC) -ppx ./rewrite fieldglass.cma case.ml -o case.byte ./case.byte OCAMLC=$(OCAMLC) sh error.sh + OCAMLC=$(OCAMLC) sh smoke.sh ./rewrite . clean: rm -f *.cmi *.cmo *.cma *.byte rewrite diff --git a/README.md b/README.md index 939cb31..f75cab9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,113 @@ # 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. -The `[@@lens generate]` spelling is also accepted. +```ocaml +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. diff --git a/smoke.sh b/smoke.sh new file mode 100644 index 0000000..c4c0f67 --- /dev/null +++ b/smoke.sh @@ -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"