From ddf80388598e9976c4f2b046e9c5e0846d14a213 Mon Sep 17 00:00:00 2001 From: yottalogical Date: Wed, 21 Sep 2022 15:53:31 -0400 Subject: [PATCH] Prepare for release --- Makefile | 8 +- README.md | 149 ++++++++++++++++++ hazelnut/dune | 2 +- hazelnut/hazelnut.re | 347 +++-------------------------------------- lib/app.re | 99 ++++++------ lib/dune | 2 +- monad/dune | 2 + monad/monad.re | 10 ++ monad/monad.rei | 2 + test/dune | 5 - test/hazelnut_test.re | 98 ------------ test/hazelnut_test.rei | 1 - 12 files changed, 236 insertions(+), 489 deletions(-) create mode 100644 README.md create mode 100644 monad/dune create mode 100644 monad/monad.re create mode 100644 monad/monad.rei delete mode 100644 test/dune delete mode 100644 test/hazelnut_test.re delete mode 100644 test/hazelnut_test.rei diff --git a/Makefile b/Makefile index 1f89ec5..bc7e7eb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,6 @@ -all: fmt build open +HTML_FILE=$(shell pwd)/_build/default/bin/index.html + +all: fmt build fmt: refmt */*.re --in-place @@ -7,8 +9,8 @@ fmt: build: dune build bin/{main.bc.js,index.html} -open: - open _build/default/bin/index.html +url: + @echo "file://$(HTML_FILE)" clean: dune clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1175c0 --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# diy-hazelnut + +A template for you to implement [Hazelnut](https://arxiv.org/pdf/1607.04180) yourself + +## Setup + +### Installing and Setting Up OCaml + +You will need to have the OCaml toolchain installed. If you have not already it installed, I recommend the official [Get Up and Running With OCaml](https://ocaml.org/learn/tutorials/up_and_running.html) tutorial. + +Once you do that, I recommend you setup an opam switch just for this project. You can think of a switch like an isolated environment to install opam packages. The packages you install in one switch won't interfere with the packages installed in another switch. This project has only been tested against OCaml 4.13.1, so we'll be setting up the switch with that version. + +To create the switch: +```sh +opam switch create diy-hazelnut 4.13.1 +``` + +To set this switch as the currently active one: +```sh +opam switch set diy-hazelnut +``` + +To list all the switches you have: +```sh +opam switch list +``` + +To learn more about switches: +```sh +man opam-switch +``` + +### Installing Dependencies + +If you setup a switch for this project, make sure that it's active: +```sh +opam switch set diy-hazelnut +``` + +To install the dependencies to the currently active switch: +```sh +make deps +``` + +### Building, Running, etc. + +All the build commands are managed by the `Makefile`. You're free to modify it to your own liking, but as it is provided, here's what the commands do: + +To autoformat your code: +```sh +make fmt +``` + +To build the webapp: +```sh +make build +``` + +To autoformat and build: +```sh +make +``` + +To get the URL for the webapp: +```sh +make url +``` + +To erase the build: +```sh +make clean +``` + +## Implementing Hazelnut + +Now it's your turn to implement Hazelnut! + +First of all, it's important that you understand what Hazelnut is and how it works. Read *[Hazelnut: A Bidirectionally Typed Structure Editor Calculus][hazelnut_paper]* if you haven't already. You don't have to understand everything right at this moment, but make sure that you have a good overview of how it all works. + +You'll be using the Reason programming language, which essentially just OCaml with a more JavaScript-like syntax. It's the primary language used to implement [Hazel](https://github.com/hazelgrove/hazel). If you're already familiar with OCaml, but not Reason, [this website](https://reasonml.github.io/en/try) can be used to translate between OCaml and Reason. But if you'd really just prefer to use OCaml, you can rename `hazelnut/hazelnut.re` to `hazelnut/hazelnut.ml`, then convert the code in it to OCaml. + +All the code you will write should go in the [`hazelnut/hazelnut.re`](hazelnut/hazelnut.re). The starter code in there has been provided for your convenience, but you're free to modify it however you like. Modifying any other files (especially `hazelnut/hazelnut.rei`) isn't recommended, since it might cause unexpected problems. + +To implement Hazelnut, you will need to complete the following functions: + +| Function | Description | +| :----------- | :-------------------------------------------------------------------------------------------------------------------------------- | +| `erase_exp` | Performs *cursor erasure* described in [Hazelnut Part 3.2][hazelnut_paper]. | +| `syn` | Performs *synthesis* described in [Hazelnut Part 3.1][hazelnut_paper]. Returns `None` if the expression cannot sythesize a type. | +| `syn_action` | Performs a *synthetic action* described in [Hazelnut Part 3.3][hazelnut_paper]. Returns `None` if the action cannot be performed. | + +You are not just welcomed, but encouraged to write your own helper functions. + +To test out your implementation while it's incomplete, you can use `raise(Unimplemented)` to fill the gaps. The webapp will warn you if it ever reaches an unimplemented part, but it won't crash! + +A `typctx` is a map from variable names to types. You can insert/update values with `TypCtx.add`, and read values with `TypCtx.find`. See the [OCaml Map documentation](https://v2.ocaml.org/api/Map.Make.html) for more details. + +## Using the Webapp + +Once you've built the webapp with `make build`, you can open it in your browser. Run the command `make url` to get the URL. + +The first thing you see will be the Hazelnut expression you're building, and the Hazelnut type that it synthesizes. Below that are the buttons that perform actions on the expression. If the button has an input field, you have to fill that in before pressing the button. + +If anything unexpected happens, a warning will appear at the bottom. Here's what each warning means: + +| Warning | Meaning | +| :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | +| Invalid action | According to your implementation, that action cannot performed on the current expression. | +| Invalid input | The provided input isn't valid. | +| Unimplemented | You called upon an operation that you haven't implemented yet. | +| Theorem violation | Your implementation has a bug that caused it to violate a Hazelnut metatheorem. Note: This won't catch every metatheorem violation, only some of them. | + +## Using the Maybe Monad + +Have you ever written code that looks like this? + +```reason +switch (a) { +| Some(b) => + switch (f(b)) { + | Some(c) => + switch (g(c)) { + | Some(d) => Some(h(d)) + | None => None + } + | None => None + } +| None => None +}; +``` + +That code is quite messy, even though it's just trying to express some simple logic: if the value is `Some(_)`, do something to it, but if it's `None`, just return `None`. Luckily there's a thing called the maybe monad. A category theorist could tell you all kinds of cool properties of monads, but for now, all you need to know is that they make it a lot easier to work with `option('a)` types. + +The following is equivalent the block of code above, but much cleaner: + +```reason +let* b = a; +let* c = f(b); +let+ d = g(c); +h(d); +``` + +Use `let*` if the expression below it evaluates to an `option('a)` type. Use `let+` if the expression below it evaluates to a `'a` type. + +To use this monad syntax, uncomment `open Monad_lib.Monad;` at the top of the file. + + + +[hazelnut_paper]: https://arxiv.org/pdf/1607.04180 diff --git a/hazelnut/dune b/hazelnut/dune index 5f2b05c..98e0ea8 100644 --- a/hazelnut/dune +++ b/hazelnut/dune @@ -1,5 +1,5 @@ (library (name hazelnut_lib) - (libraries core incr_dom) + (libraries core incr_dom monad_lib) (preprocess (pps ppx_jane))) diff --git a/hazelnut/hazelnut.re b/hazelnut/hazelnut.re index ab6050a..bcd12b5 100644 --- a/hazelnut/hazelnut.re +++ b/hazelnut/hazelnut.re @@ -1,6 +1,6 @@ open Sexplib.Std; +// open Monad_lib.Monad; // Uncomment this line to use the maybe monad -// TODO: Figure out how to get compare_string and compare_int to exist without manually defining them let compare_string = String.compare; let compare_int = Int.compare; @@ -73,336 +73,31 @@ type typctx = TypCtx.t(htyp); exception Unimplemented; -// Maybe Monad -let ( let* ) = (x: option('a), f: 'a => option('b)): option('b) => - switch (x) { - | Some(x) => f(x) - | None => None - }; +let erase_exp = (e: zexp): hexp => { + // Used to suppress unused variable warnings + // Okay to remove + let _ = e; -// Maybe Monad -let (let+) = (x: option('a), f: 'a => 'b): option('b) => { - let* x = x; - Some(f(x)); + raise(Unimplemented); }; -let matched_arrow_type: htyp => option((htyp, htyp)) = - fun - | Hole => Some((Hole, Hole)) - | Arrow(t1, t2) => Some((t1, t2)) - | _ => None; +let syn = (ctx: typctx, e: hexp): option(htyp) => { + // Used to suppress unused variable warnings + // Okay to remove + let _ = ctx; + let _ = e; -let rec erase_typ: ztyp => htyp = - fun - | Cursor(t) => t - | LArrow(t1, t2) => Arrow(erase_typ(t1), t2) - | RArrow(t1, t2) => Arrow(t1, erase_typ(t2)); - -let rec erase_exp: zexp => hexp = - fun - | Cursor(e) => e - | Lam(x, e) => Lam(x, erase_exp(e)) - | LAp(e1, e2) => Ap(erase_exp(e1), e2) - | RAp(e1, e2) => Ap(e1, erase_exp(e2)) - | LPlus(e1, e2) => Plus(erase_exp(e1), e2) - | RPlus(e1, e2) => Plus(e1, erase_exp(e2)) - | LAsc(e, t) => Asc(erase_exp(e), t) - | RAsc(e, t) => Asc(e, erase_typ(t)) - | NEHole(e) => NEHole(erase_exp(e)); - -let rec consistent = (t1: htyp, t2: htyp): option(unit) => - switch (t1, t2) { - | (Hole, _) - | (_, Hole) => Some() - | (Arrow(t1, t2), Arrow(t1', t2')) => - let* () = consistent(t1, t2); - let+ () = consistent(t1', t2'); - (); - | (t1, t2) => - if (t1 == t2) { - Some(); - } else { - None; - } - }; - -let inconsistent = (t1: htyp, t2: htyp): option(unit) => - switch (consistent(t1, t2)) { - | Some () => None - | None => Some() - }; - -let rec syn = (ctx: typctx, e: hexp): option(htyp) => { - switch (e) { - | Var(x) => - try(Some(TypCtx.find(x, ctx))) { - | Not_found => None - } - | Lam(_, _) => None - | Ap(e1, e2) => - let* t1 = syn(ctx, e1); - let* (t2, t) = matched_arrow_type(t1); - let+ () = ana(ctx, e2, t2); - t; - | Lit(_) => Some(Num) - | Plus(e1, e2) => - let* () = ana(ctx, e1, Num); - let+ () = ana(ctx, e2, Num); - (Num: htyp); - | Asc(e, t) => - let+ () = ana(ctx, e, t); - t; - | EHole => Some(Hole) - | NEHole(e) => - let+ _ = syn(ctx, e); - Hole; - }; -} - -and ana = (ctx: typctx, e: hexp, t: htyp): option(unit) => { - switch (e) { - | Lam(x, e) => - let* (t1, t2) = matched_arrow_type(t); - let ctx' = TypCtx.add(x, t1, ctx); - ana(ctx', e, t2); - | _ => - let* t' = syn(ctx, e); - consistent(t, t'); - }; -}; - -let exp_movement = (e: zexp, d: dir): option(zexp) => - switch (e, d) { - | (Cursor(Asc(e, t)), Child(One)) => Some(LAsc(Cursor(e), t)) - | (Cursor(Asc(e, t)), Child(Two)) => Some(RAsc(e, Cursor(t))) - | (LAsc(Cursor(e), t), Parent) - | (RAsc(e, Cursor(t)), Parent) => Some(Cursor(Asc(e, t))) - - | (Cursor(Lam(x, e)), Child(One)) => Some(Lam(x, Cursor(e))) - | (Lam(x, Cursor(e)), Parent) => Some(Cursor(Lam(x, e))) - - | (Cursor(Plus(e1, e2)), Child(One)) => Some(LPlus(Cursor(e1), e2)) - | (Cursor(Plus(e1, e2)), Child(Two)) => Some(RPlus(e1, Cursor(e2))) - | (LPlus(Cursor(e1), e2), Parent) - | (RPlus(e1, Cursor(e2)), Parent) => Some(Cursor(Plus(e1, e2))) - - | (Cursor(Ap(e1, e2)), Child(One)) => Some(LAp(Cursor(e1), e2)) - | (Cursor(Ap(e1, e2)), Child(Two)) => Some(RAp(e1, Cursor(e2))) - | (LAp(Cursor(e1), e2), Parent) - | (RAp(e1, Cursor(e2)), Parent) => Some(Cursor(Ap(e1, e2))) - - | (Cursor(NEHole(e)), Child(One)) => Some(NEHole(Cursor(e))) - | (NEHole(Cursor(e)), Parent) => Some(Cursor(NEHole(e))) - - | _ => None - }; - -let rec typ_action = (t: ztyp, a: action): option(ztyp) => { - switch (t, a) { - // Movement: Arrow - | (Cursor(Arrow(t1, t2)), Move(Child(One))) => - Some(LArrow(Cursor(t1), t2)) - | (Cursor(Arrow(t1, t2)), Move(Child(Two))) => - Some(RArrow(t1, Cursor(t2))) - | (LArrow(Cursor(t1), t2), Move(Parent)) - | (RArrow(t1, Cursor(t2)), Move(Parent)) => Some(Cursor(Arrow(t1, t2))) - - // Deletion - | (Cursor(_), Del) => Some(Cursor(Hole)) - - // Construction - | (Cursor(t), Construct(Arrow)) => Some(RArrow(t, Cursor(Hole))) - | (Cursor(Hole), Construct(Num)) => Some(Cursor(Num)) - - // Zipper Case: Arrow - | (LArrow(zt, ht), a) => - let+ zt' = typ_action(zt, a); - LArrow(zt', ht); - | (RArrow(ht, zt), a) => - let+ zt' = typ_action(zt, a); - RArrow(ht, zt'); - - | _ => None - }; + raise(Unimplemented); }; -let rec syn_action = - (ctx: typctx, (e: zexp, t: htyp), a: action): option((zexp, htyp)) => { - // Try movement first - let movement = - switch (a) { - | Move(d) => exp_movement(e, d) - | _ => None - }; - - // If movement fails, try synthetic actions - switch (movement) { - | Some(e') => Some((e', t)) - | None => - switch (e, t, a) { - // Construct: Asc - | (Cursor(e1), _, Construct(Asc)) => Some((RAsc(e1, Cursor(t)), t)) - - // Construct: Var - | (Cursor(EHole), Hole, Construct(Var(x))) => - try({ - let ht = TypCtx.find(x, ctx); - Some((Cursor(Var(x)), ht)); - }) { - | Not_found => None - } - - // Construct: Lam - | (Cursor(EHole), Hole, Construct(Lam(x))) => - Some(( - RAsc(Lam(x, EHole), LArrow(Cursor(Hole), Hole)), - Arrow(Hole, Hole), - )) - - // Construct: Ap - | (Cursor(he), _, Construct(Ap)) => - switch (matched_arrow_type(t), inconsistent(t, Arrow(Hole, Hole))) { - | (Some((_, t2)), None) => Some((RAp(he, Cursor(EHole)), t2)) - | (None, Some ()) => Some((RAp(NEHole(he), Cursor(EHole)), Hole)) - | _ => None - } - - // Construct: Lit - | (Cursor(EHole), Hole, Construct(Lit(n))) => - Some((Cursor(Lit(n)), Num)) - - // Construct: Plus - | (Cursor(he), _, Construct(Plus)) => - switch (consistent(t, Num)) { - | Some () => Some((RPlus(he, Cursor(EHole)), Num)) - | None => Some((RPlus(NEHole(he), Cursor(EHole)), Num)) - } - - // Construct: NEHole - | (Cursor(he), _, Construct(NEHole)) => - Some((NEHole(Cursor(he)), Hole)) - - // Deletion - | (Cursor(_), _, Del) => Some((Cursor(EHole), Hole)) - - // Finishing - | (Cursor(NEHole(he)), Hole, Finish) => - let+ t' = syn(ctx, he); - (Cursor(he), t'); - - // Zipper Case: Ap - | (LAp(ze, he), _, _) => - let* t2 = syn(ctx, erase_exp(ze)); - let* (ze', t3) = syn_action(ctx, (ze, t2), a); - let* (t4, t5) = matched_arrow_type(t3); - let+ () = ana(ctx, he, t4); - (LAp(ze', he), t5); - | (RAp(he, ze), _, _) => - let* t2 = syn(ctx, he); - let* (t3, t4) = matched_arrow_type(t2); - let+ ze' = ana_action(ctx, ze, a, t3); - (RAp(he, ze'), t4); - - // Zipper Case: Plus - | (LPlus(ze, he), Num, _) => - let+ ze' = ana_action(ctx, ze, a, Num); - (LPlus(ze', he), Num: htyp); - | (RPlus(he, ze), Num, _) => - let+ ze' = ana_action(ctx, ze, a, Num); - (RPlus(he, ze'), Num: htyp); - - // Zipper Case: Asc - | (LAsc(ze, ht), _, _) => - let+ ze' = ana_action(ctx, ze, a, ht); - (LAsc(ze', ht), ht); - | (RAsc(he, zt), ht, _) => - if (ht == erase_typ(zt)) { - let* zt' = typ_action(zt, a); - let ht' = erase_typ(zt'); - let+ () = ana(ctx, he, ht'); - (RAsc(he, zt'), ht'); - } else { - None; - } - - // Zipper Case: NEHole - | (NEHole(ze), Hole, _) => - let* ht = syn(ctx, erase_exp(ze)); - let+ (ze', _) = syn_action(ctx, (ze, ht), a); - (NEHole(ze'): zexp, Hole); - - | _ => None - } - }; -} - -and ana_action = (ctx: typctx, e: zexp, a: action, t: htyp): option(zexp) => { - // Try movement first - let movement = - switch (a) { - | Move(d) => exp_movement(e, d) - | _ => None - }; - - // If movement fails, try analytic actions - let analysis = - switch (movement) { - | Some(_) => movement - | None => - switch (e, a, t) { - // Construct: Asc - | (Cursor(e1), Construct(Asc), _) => Some(RAsc(e1, Cursor(t))) - - // Construct: Var - | (Cursor(EHole), Construct(Var(x)), _) => - try({ - let t' = TypCtx.find(x, ctx); - let+ () = inconsistent(t, t'); - (NEHole(Cursor(Var(x))): zexp); - }) { - | Not_found => None - } - - // Construct: Lam - | (Cursor(EHole), Construct(Lam(x)), _) => - switch (matched_arrow_type(t), inconsistent(t, Arrow(Hole, Hole))) { - | (Some(_), None) => Some(Lam(x, Cursor(EHole))) - | (None, Some ()) => - Some(NEHole(RAsc(Lam(x, EHole), LArrow(Cursor(Hole), Hole)))) - | _ => None - } - - // Construct: Lit - | (Cursor(EHole), Construct(Lit(n)), _) => - let+ () = inconsistent(t, Num); - (NEHole(Cursor(Lit(n))): zexp); - - // Deletion - | (Cursor(_), Del, _) => Some(Cursor(EHole)) - - // Finishing - | (Cursor(NEHole(he)), Finish, _) => - let+ () = ana(ctx, he, t); - Cursor(he); - - // Zipper Case: Lam - | (Lam(x, ze), _, _) => - let* (t1, t2) = matched_arrow_type(t); - let ctx' = TypCtx.add(x, t1, ctx); - let+ ze' = ana_action(ctx', ze, a, t2); - (Lam(x, ze'): zexp); - - | _ => None - } - }; +let syn_action = + (ctx: typctx, (e: zexp, t: htyp), a: action): option((zexp, htyp)) => { + // Used to suppress unused variable warnings + // Okay to remove + let _ = ctx; + let _ = e; + let _ = t; + let _ = a; - // If analysis fails, try subsumption as a last resort - switch (analysis) { - | Some(_) => analysis - | None => - let* t' = syn(ctx, erase_exp(e)); - let* (e', t'') = syn_action(ctx, (e, t'), a); - let+ () = consistent(t, t''); - e'; - }; + raise(Unimplemented); }; diff --git a/lib/app.re b/lib/app.re index c226846..44566fa 100644 --- a/lib/app.re +++ b/lib/app.re @@ -1,20 +1,8 @@ open Core; open Incr_dom; +open Monad_lib.Monad; module Hazelnut = Hazelnut_lib.Hazelnut; -// Maybe Monad -let ( let* ) = (x: option('a), f: 'a => option('b)): option('b) => - switch (x) { - | Some(x) => f(x) - | None => None - }; - -// Maybe Monad -let (let+) = (x: option('a), f: 'a => 'b): option('b) => { - let* x = x; - Some(f(x)); -}; - // A combination of all Hazelnut types for purposes of printing type pexp = | Cursor(pexp) @@ -144,42 +132,45 @@ let check_for_theorem_violation = e': Hazelnut.zexp, t': Hazelnut.htyp, ) - : option(string) => { - let e = Hazelnut.erase_exp(e); - let e' = Hazelnut.erase_exp(e'); - - let theorem_1 = { - let warning = Some("Theorem 1 violation (Action sensibility)"); - - switch (Hazelnut.syn(Hazelnut.TypCtx.empty, e')) { - | Some(syn_t') => - if (Hazelnut.compare_htyp(t', syn_t') == 0) { - None; - } else { - warning; - } - | None => warning + : option(string) => + try({ + let e = Hazelnut.erase_exp(e); + let e' = Hazelnut.erase_exp(e'); + + let theorem_1 = { + let warning = Some("Theorem 1 violation (Action sensibility)"); + + switch (Hazelnut.syn(Hazelnut.TypCtx.empty, e')) { + | Some(syn_t') => + if (Hazelnut.compare_htyp(t', syn_t') == 0) { + None; + } else { + warning; + } + | None => warning + }; }; - }; - let theorem_2 = - switch (a) { - | Move(_) => - if (Hazelnut.compare_hexp(e, e') == 0 - && Hazelnut.compare_htyp(t, t') == 0) { - None; - } else { - Some("Theorem 2 violation (movement erasure invariance)"); - } - | _ => None - }; + let theorem_2 = + switch (a) { + | Move(_) => + if (Hazelnut.compare_hexp(e, e') == 0 + && Hazelnut.compare_htyp(t, t') == 0) { + None; + } else { + Some("Theorem 2 violation (movement erasure invariance)"); + } + | _ => None + }; - switch (theorem_1, theorem_2) { - | (Some(_) as warning, _) - | (_, Some(_) as warning) => warning - | (None, None) => None + switch (theorem_1, theorem_2) { + | (Some(_) as warning, _) + | (_, Some(_) as warning) => warning + | (None, None) => None + }; + }) { + | Hazelnut.Unimplemented => None }; -}; [@deriving (sexp, fields, compare)] type state = { @@ -197,15 +188,15 @@ module Model = { let set = (s: state): t => {state: s}; - let init = (): t => { - let e: Hazelnut.zexp = Cursor(EHole); - - switch (Hazelnut.syn(Hazelnut.TypCtx.empty, Hazelnut.erase_exp(e))) { - | Some(t) => - set({e, t, warning: None, var_input: "", lam_input: "", lit_input: ""}) - | None => failwith("Invalid initial expression") - }; - }; + let init = (): t => + set({ + e: Cursor(EHole), + t: Hole, + warning: None, + var_input: "", + lam_input: "", + lit_input: "", + }); let cutoff = (t1: t, t2: t): bool => compare(t1, t2) == 0; }; diff --git a/lib/dune b/lib/dune index d4aeefe..f38cd3d 100644 --- a/lib/dune +++ b/lib/dune @@ -1,5 +1,5 @@ (library (name app_lib) - (libraries core hazelnut_lib incr_dom) + (libraries core hazelnut_lib incr_dom monad_lib) (preprocess (pps ppx_jane))) diff --git a/monad/dune b/monad/dune new file mode 100644 index 0000000..dbf60c0 --- /dev/null +++ b/monad/dune @@ -0,0 +1,2 @@ +(library + (name monad_lib)) diff --git a/monad/monad.re b/monad/monad.re new file mode 100644 index 0000000..f445d9f --- /dev/null +++ b/monad/monad.re @@ -0,0 +1,10 @@ +let ( let* ) = (x: option('a), f: 'a => option('b)): option('b) => + switch (x) { + | Some(x) => f(x) + | None => None + }; + +let (let+) = (x: option('a), f: 'a => 'b): option('b) => { + let* x = x; + Some(f(x)); +}; diff --git a/monad/monad.rei b/monad/monad.rei new file mode 100644 index 0000000..f431ebd --- /dev/null +++ b/monad/monad.rei @@ -0,0 +1,2 @@ +let ( let* ): (option('a), 'a => option('b)) => option('b); +let (let+): (option('a), 'a => 'b) => option('b); diff --git a/test/dune b/test/dune deleted file mode 100644 index 6c34aef..0000000 --- a/test/dune +++ /dev/null @@ -1,5 +0,0 @@ -(library - (name hazelnut_test) - (libraries async_kernel incr_dom_testing app_lib js_of_ocaml) - (preprocess - (pps ppx_jane))) diff --git a/test/hazelnut_test.re b/test/hazelnut_test.re deleted file mode 100644 index 5ed0df9..0000000 --- a/test/hazelnut_test.re +++ /dev/null @@ -1,98 +0,0 @@ -open! Core; -open! Incr_dom_testing; -module App = App_lib.App; - -let make_helpers = () => { - let driver = - Driver.create( - ~initial_model=App.initial_model, - ~sexp_of_model=App.Model.sexp_of_t, - ~initial_state=(), - (module App), - ); - - Helpers.make(driver); -}; - -let%expect_test "default model" = { - let (module H) = make_helpers(); - H.show_view(); - %expect - {| - -
No submissions yet
- - - - - |}; -}; - -let%expect_test "submit with default" = { - let (module H) = make_helpers(); - H.click_on(~selector="#submit"); - H.perform_update(); - H.show_view(); - %expect - {| - -
Your latest submission was: Default #0
- - - - - |}; -}; - -let%expect_test "Increment twice and then submit" = { - let (module H) = make_helpers(); - H.click_on(~selector="#increment"); - H.click_on(~selector="#increment"); - H.click_on(~selector="#submit"); - H.perform_update(); - H.show_view(); - %expect - {| - -
Your latest submission was: Default #2
- - - - - |}; -}; - -let%expect_test "Set value and then submit" = { - let (module H) = make_helpers(); - H.input_text(~selector="#input", ~text="hello world"); - H.click_on(~selector="#submit"); - H.perform_update(); - H.show_view(); - %expect - {| - -
Your latest submission was: hello world
- - - - - |}; -}; - -let%expect_test "Set value and then increment" = { - let (module H) = make_helpers(); - H.input_text(~selector="#input", ~text="hello world"); - H.click_on(~selector="#increment"); - H.click_on(~selector="#submit"); - H.perform_update(); - H.show_view(); - %expect - {| - -
Your latest submission was: Default #1
- - - - - |}; -}; diff --git a/test/hazelnut_test.rei b/test/hazelnut_test.rei deleted file mode 100644 index 48b637e..0000000 --- a/test/hazelnut_test.rei +++ /dev/null @@ -1 +0,0 @@ -// This file is intentionally left blank