diff --git a/dune-project b/dune-project index 335194ae48..1194593485 100644 --- a/dune-project +++ b/dune-project @@ -29,13 +29,12 @@ reason ppx_yojson_conv_lib ppx_yojson_conv - incr_dom (omd (>= 2.0.0~alpha4)) ezjs_idb - virtual_dom + bonsai ppx_deriving ptmap - uuidm + (uuidm (= 0.9.8)) ; 0.9.9 has breaking deprecated changes unionFind ocamlformat (junit_alcotest :with-test) diff --git a/dune-workspace b/dune-workspace new file mode 100644 index 0000000000..93d6e50904 --- /dev/null +++ b/dune-workspace @@ -0,0 +1,10 @@ +(lang dune 3.16) + +; List of warning codes found at https://ocaml.org/manual/5.2/comp.html#s:comp-options +(env + (dev + (flags + (:standard -warn-error +A-26-27-K-58))) ; Disable some unused warnings. + (release + (flags + (:standard -warn-error +A-58)))) diff --git a/hazel.opam b/hazel.opam index 36a81b933a..e78b188325 100644 --- a/hazel.opam +++ b/hazel.opam @@ -14,13 +14,12 @@ depends: [ "reason" "ppx_yojson_conv_lib" "ppx_yojson_conv" - "incr_dom" "omd" {>= "2.0.0~alpha4"} "ezjs_idb" - "virtual_dom" + "bonsai" "ppx_deriving" "ptmap" - "uuidm" + "uuidm" {= "0.9.8"} "unionFind" "ocamlformat" "junit_alcotest" {with-test} diff --git a/hazel.opam.locked b/hazel.opam.locked index 31fd8b2c15..b2cfce2cea 100644 --- a/hazel.opam.locked +++ b/hazel.opam.locked @@ -1,7 +1,7 @@ opam-version: "2.0" name: "hazel" -version: "~dev" +version: "dev" synopsis: "Hazel, a live functional programming environment with typed holes" maintainer: "Hazel Development Team" authors: "Hazel Development Team" @@ -11,11 +11,19 @@ bug-reports: "https://github.com/hazelgrove/hazel/issues" depends: [ "abstract_algebra" {= "v0.16.0"} "alcotest" {= "1.8.0" & with-test} - "angstrom" {= "0.16.0"} + "angstrom" {= "0.16.1"} "astring" {= "0.8.5"} + "async" {= "v0.16.0"} + "async_durable" {= "v0.16.0"} + "async_extra" {= "v0.16.0"} "async_js" {= "v0.16.0"} "async_kernel" {= "v0.16.0"} "async_rpc_kernel" {= "v0.16.0"} + "async_rpc_websocket" {= "v0.16.0"} + "async_ssl" {= "v0.16.1"} + "async_unix" {= "v0.16.0"} + "async_websocket" {= "v0.16.0"} + "babel" {= "v0.16.0"} "base" {= "v0.16.3"} "base-bigarray" {= "base"} "base-bytes" {= "base"} @@ -26,85 +34,117 @@ depends: [ "base64" {= "3.5.1"} "base_bigstring" {= "v0.16.0"} "base_quickcheck" {= "v0.16.0"} + "bigarray-compat" {= "1.1.0"} "bignum" {= "v0.16.0"} - "bigstringaf" {= "0.9.1"} + "bigstringaf" {= "0.10.0"} "bin_prot" {= "v0.16.0"} + "bonsai" {= "v0.16.0"} "camlp-streams" {= "5.0.1"} "chrome-trace" {= "3.16.0"} "cmdliner" {= "1.3.0"} + "cohttp" {= "5.3.1"} + "cohttp-async" {= "5.3.0"} + "cohttp_async_websocket" {= "v0.16.0"} + "conduit" {= "7.1.0"} + "conduit-async" {= "7.1.0"} "conf-bash" {= "1"} "conf-gmp" {= "4"} + "conf-gmp-powm-sec" {= "3"} + "conf-libffi" {= "2.0.0"} + "conf-libssl" {= "4"} + "conf-pkg-config" {= "3"} + "conf-zlib" {= "1"} "core" {= "v0.16.2"} + "core_bench" {= "v0.16.0"} "core_kernel" {= "v0.16.0"} - "cppo" {= "1.6.9"} + "core_unix" {= "v0.16.0"} + "cppo" {= "1.7.0"} "crunch" {= "3.3.1" & with-doc} + "cryptokit" {= "1.16.1"} "csexp" {= "1.5.2"} + "ctypes" {= "0.23.0"} + "ctypes-foreign" {= "0.23.0"} "diffable" {= "v0.16.0"} + "domain-name" {= "0.4.0"} "dune" {= "3.16.0"} "dune-build-info" {= "3.16.0"} "dune-configurator" {= "3.16.0"} "dune-rpc" {= "3.16.0"} "dyn" {= "3.16.0"} "either" {= "1.0.0"} + "expect_test_helpers_core" {= "v0.16.0"} "ezjs_idb" {= "0.1.1"} "ezjs_min" {= "0.3.0"} "fiber" {= "3.7.0"} "fieldslib" {= "v0.16.0"} "fix" {= "20230505"} - "fmt" {= "0.9.0" & with-test} + "fmt" {= "0.9.0"} "fpath" {= "0.7.3"} + "fuzzy_match" {= "v0.16.0"} "gen" {= "1.1"} - "gen_js_api" {= "1.1.2"} + "gen_js_api" {= "1.1.3"} "incr_dom" {= "v0.16.0"} "incr_map" {= "v0.16.0"} "incr_select" {= "v0.16.0"} "incremental" {= "v0.16.1"} + "indentation_buffer" {= "v0.16.0"} "int_repr" {= "v0.16.0"} + "integers" {= "0.7.0"} + "ipaddr" {= "5.6.0"} + "ipaddr-sexp" {= "5.6.0"} "jane-street-headers" {= "v0.16.0"} "janestreet_lru_cache" {= "v0.16.1"} "js_of_ocaml" {= "5.8.2"} "js_of_ocaml-compiler" {= "5.8.2"} "js_of_ocaml-ppx" {= "5.8.2"} "js_of_ocaml_patches" {= "v0.16.0"} + "jsonm" {= "1.0.2"} "jsonrpc" {= "1.19.0"} "jst-config" {= "v0.16.0"} "junit" {= "2.0.2" & with-test} "junit_alcotest" {= "2.0.2" & with-test} - "lambdasoup" {= "1.0.0"} + "lambdasoup" {= "1.1.1"} + "logs" {= "0.7.0"} "lsp" {= "1.19.0"} - "lwt" {= "5.7.0"} + "macaddr" {= "5.6.0"} + "magic-mime" {= "1.3.1"} "markup" {= "1.0.3"} - "menhir" {= "20231231"} - "menhirCST" {= "20231231"} - "menhirLib" {= "20231231"} - "menhirSdk" {= "20231231"} + "menhir" {= "20240715"} + "menhirCST" {= "20240715"} + "menhirLib" {= "20240715"} + "menhirSdk" {= "20240715"} "merlin-extend" {= "0.6.1"} - "merlin-lib" {= "5.1-502"} - "num" {= "1.5"} + "merlin-lib" {= "5.2.1-502"} + "num" {= "1.5-1"} "ocaml" {= "5.2.0"} "ocaml-base-compiler" {= "5.2.0"} "ocaml-compiler-libs" {= "v0.17.0"} "ocaml-config" {= "3"} - "ocaml-index" {= "1.0"} + "ocaml-embed-file" {= "v0.16.0"} + "ocaml-index" {= "1.1"} "ocaml-lsp-server" {= "1.19.0"} "ocaml-options-vanilla" {= "1"} "ocaml-syntax-shims" {= "1.0.0"} - "ocaml-version" {= "3.6.7"} - "ocamlbuild" {= "0.14.3"} + "ocaml-version" {= "3.6.9"} + "ocaml_intrinsics" {= "v0.16.1"} + "ocamlbuild" {= "0.15.0"} "ocamlc-loc" {= "3.16.0"} "ocamlfind" {= "1.9.6"} "ocamlformat" {= "0.26.2"} "ocamlformat-lib" {= "0.26.2"} "ocamlformat-rpc-lib" {= "0.26.2"} "ocp-indent" {= "1.8.1"} - "ocplib-endian" {= "1.2"} "octavius" {= "1.2.2"} - "odoc" {= "2.4.2" & with-doc} - "odoc-parser" {= "2.4.2" & with-doc} - "ojs" {= "1.1.2"} + "odoc" {= "2.4.3" & with-doc} + "odoc-parser" {= "2.4.3" & with-doc} + "ojs" {= "1.1.3"} "omd" {= "2.0.0~alpha4"} "ordering" {= "3.16.0"} + "ordinal_abbreviation" {= "v0.16.0"} "parsexp" {= "v0.16.0"} + "patdiff" {= "v0.16.1"} + "patience_diff" {= "v0.16.0"} + "polling_state_rpc" {= "v0.16.0"} "pp" {= "1.2.0"} "ppx_assert" {= "v0.16.0"} "ppx_base" {= "v0.16.0"} @@ -112,6 +152,7 @@ depends: [ "ppx_bin_prot" {= "v0.16.0"} "ppx_cold" {= "v0.16.0"} "ppx_compare" {= "v0.16.0"} + "ppx_css" {= "v0.16.0"} "ppx_custom_printf" {= "v0.16.0"} "ppx_derivers" {= "1.2.1"} "ppx_deriving" {= "6.0.2"} @@ -141,19 +182,24 @@ depends: [ "ppx_stable_witness" {= "v0.16.0"} "ppx_string" {= "v0.16.0"} "ppx_tydi" {= "v0.16.0"} + "ppx_typed_fields" {= "v0.16.0"} "ppx_typerep_conv" {= "v0.16.0"} "ppx_variants_conv" {= "v0.16.0"} "ppx_yojson_conv" {= "v0.16.0"} "ppx_yojson_conv_lib" {= "v0.16.0"} - "ppxlib" {= "0.32.1"} + "ppxlib" {= "0.33.0"} + "profunctor" {= "v0.16.0"} "protocol_version_header" {= "v0.16.0"} - "ptime" {= "1.1.0" & with-test} + "ptime" {= "1.2.0" & with-test} "ptmap" {= "2.0.5"} - "re" {= "1.11.0"} + "re" {= "1.12.0"} "reason" {= "3.12.0"} + "record_builder" {= "v0.16.0"} "result" {= "1.5"} "sedlex" {= "3.2"} "seq" {= "base"} + "sexp_grammar" {= "v0.16.0"} + "sexp_pretty" {= "v0.16.0"} "sexplib" {= "v0.16.0"} "sexplib0" {= "v0.16.0"} "spawn" {= "v0.15.1"} @@ -164,7 +210,11 @@ depends: [ "stored_reversed" {= "v0.16.0"} "streamable" {= "v0.16.1"} "stringext" {= "1.6.0"} + "textutils" {= "v0.16.0"} + "textutils_kernel" {= "v0.16.0"} + "tilde_f" {= "v0.16.0"} "time_now" {= "v0.16.0"} + "timezone" {= "v0.16.0"} "topkg" {= "1.0.7"} "typerep" {= "v0.16.0"} "tyxml" {= "4.6.0"} @@ -172,16 +222,16 @@ depends: [ "unionFind" {= "20220122"} "uri" {= "4.4.0"} "uri-sexp" {= "4.4.0"} - "uucp" {= "15.1.0"} + "uucp" {= "16.0.0"} "uuidm" {= "0.9.8"} - "uunf" {= "15.1.0"} - "uuseg" {= "15.1.0"} + "uunf" {= "16.0.0"} + "uuseg" {= "16.0.0"} "uutf" {= "1.0.3"} "variantslib" {= "v0.16.0"} "virtual_dom" {= "v0.16.0"} "xdg" {= "3.16.0"} "yojson" {= "2.2.2"} - "zarith" {= "1.13"} + "zarith" {= "1.14"} "zarith_stubs_js" {= "v0.16.1"} ] build: [ diff --git a/src/haz3lcore/lang/term/IdTagged.re b/src/haz3lcore/lang/term/IdTagged.re index 084a252de4..3812b0e83f 100644 --- a/src/haz3lcore/lang/term/IdTagged.re +++ b/src/haz3lcore/lang/term/IdTagged.re @@ -14,6 +14,11 @@ type t('a) = { term: 'a, }; +// To be used if you want to remove the id from the debug output +// let pp: ((Format.formatter, 'a) => unit, Format.formatter, t('a)) => unit = +// (fmt_a, formatter, ta) => { +// fmt_a(formatter, ta.term); +// }; let fresh = term => { {ids: [Id.mk()], copied: false, term}; }; diff --git a/src/haz3lschool/Exercise.re b/src/haz3lschool/Exercise.re index 2bd7cfc53e..eff5b3b69a 100644 --- a/src/haz3lschool/Exercise.re +++ b/src/haz3lschool/Exercise.re @@ -55,7 +55,6 @@ module F = (ExerciseEnv: ExerciseEnv) => { type p('code) = { id: Id.t, title: string, - version: int, module_name: string, prompt: string, point_distribution, @@ -68,6 +67,8 @@ module F = (ExerciseEnv: ExerciseEnv) => { syntax_tests, }; + type record = p(Zipper.t); + let id_of = p => { p.id; }; @@ -96,7 +97,6 @@ module F = (ExerciseEnv: ExerciseEnv) => { { id: p.id, title: p.title, - version: p.version, module_name: p.module_name, prompt: p.prompt, point_distribution: p.point_distribution, @@ -134,14 +134,32 @@ module F = (ExerciseEnv: ExerciseEnv) => { }; [@deriving (show({with_path: false}), sexp, yojson)] - type persistent_state = ( - pos, - list((pos, PersistentZipper.t)), - string, + type persistent_state = { + focus: pos, + editors: list((pos, PersistentZipper.t)), + title: string, + hidden_bugs: list(wrong_impl(PersistentZipper.t)), + prompt: string, point_distribution, - int, - string, - ); + required: int, + module_name: string, + // NOTE: Add new fields to record here as new instructor editable features are + // implemented (eg. prelude: PersistentZipper.t when adding the feature + // to edit the prelude). After adding these field(s), we will need to + // go into persistent_state_of_state and unpersist_state to implement + // how these fields are saved and loaded to and from local memory + // respectively. + // NOTE: It may be helpful to look at changes made in the mutant-add-delete and title-editor + // branches in the Hazel repo to see and understand where changes + // were made. It is likely that new implementations of editble features + // will follow a similar route. + }; + + let clamp_idx = (eds: eds, idx: int) => { + let length = List.length(eds.hidden_bugs); + let idx = idx > length - 1 ? idx - 1 : idx; + idx >= 0 ? Some(idx) : None; + }; let editor_of_state: state => Editor.t = ({pos, eds, _}) => @@ -151,7 +169,11 @@ module F = (ExerciseEnv: ExerciseEnv) => { | YourTestsValidation => eds.your_tests.tests | YourTestsTesting => eds.your_tests.tests | YourImpl => eds.your_impl - | HiddenBugs(i) => List.nth(eds.hidden_bugs, i).impl + | HiddenBugs(i) => + switch (clamp_idx(eds, i)) { + | Some(idx) => List.nth(eds.hidden_bugs, idx).impl + | None => eds.your_impl + } | HiddenTests => eds.hidden_tests.tests }; @@ -290,7 +312,6 @@ module F = (ExerciseEnv: ExerciseEnv) => { { id, title, - version, module_name, prompt, point_distribution, @@ -327,7 +348,6 @@ module F = (ExerciseEnv: ExerciseEnv) => { { id, title, - version, module_name, prompt, point_distribution, @@ -346,7 +366,6 @@ module F = (ExerciseEnv: ExerciseEnv) => { { id, title, - version, module_name, prompt, point_distribution, @@ -383,7 +402,6 @@ module F = (ExerciseEnv: ExerciseEnv) => { { id, title, - version, module_name, prompt, point_distribution, @@ -474,6 +492,84 @@ module F = (ExerciseEnv: ExerciseEnv) => { }, }; + let set_editing_title = ({eds, _} as state: state, editing: bool) => { + ...state, + eds: { + ...eds, + prelude: Editor.set_read_only(eds.prelude, editing), + correct_impl: Editor.set_read_only(eds.correct_impl, editing), + your_tests: { + let tests = Editor.set_read_only(eds.your_tests.tests, editing); + { + tests, + required: eds.your_tests.required, + provided: eds.your_tests.provided, + }; + }, + hidden_bugs: + eds.hidden_bugs + |> List.map(({impl, hint}) => { + let impl = Editor.set_read_only(impl, editing); + {impl, hint}; + }), + hidden_tests: { + let tests = Editor.set_read_only(eds.hidden_tests.tests, editing); + {tests, hints: eds.hidden_tests.hints}; + }, + your_impl: Editor.set_read_only(eds.your_impl, editing), + }, + }; + + let update_exercise_title = ({eds, _} as state: state, new_title: string) => { + ...state, + eds: { + ...eds, + title: new_title, + }, + }; + + let add_buggy_impl = + (~settings: CoreSettings.t, state: state, ~editing_title) => { + let new_buggy_impl = { + impl: Editor.init(Zipper.init(), ~settings), + hint: "no hint available", + }; + let new_state = { + pos: HiddenBugs(List.length(state.eds.hidden_bugs)), + eds: { + ...state.eds, + hidden_bugs: state.eds.hidden_bugs @ [new_buggy_impl], + }, + }; + let new_state = set_editing_title(new_state, editing_title); + put_editor(new_state, new_buggy_impl.impl); + }; + + let delete_buggy_impl = (state: state, index: int) => { + let length = List.length(state.eds.hidden_bugs); + let editor_on = + length > 1 + ? List.nth( + state.eds.hidden_bugs, + index < length - 1 ? index + 1 : index - 1, + ). + impl + : state.eds.your_tests.tests; + let pos = + length > 1 + ? HiddenBugs(index < length - 1 ? index : index - 1) + : YourTestsValidation; + let new_state = { + pos, + eds: { + ...state.eds, + hidden_bugs: + List.filteri((i, _) => i != index, state.eds.hidden_bugs), + }, + }; + put_editor(new_state, editor_on); + }; + let set_editing_prompt = ({eds, _} as state: state, editing: bool) => { ...state, eds: { @@ -488,6 +584,16 @@ module F = (ExerciseEnv: ExerciseEnv) => { provided: eds.your_tests.provided, }; }, + hidden_bugs: + eds.hidden_bugs + |> List.map(({impl, hint}) => { + let impl = Editor.set_read_only(impl, editing); + {impl, hint}; + }), + hidden_tests: { + let tests = Editor.set_read_only(eds.hidden_tests.tests, editing); + {tests, hints: eds.hidden_tests.hints}; + }, your_impl: Editor.set_read_only(eds.your_impl, editing), }, }; @@ -514,6 +620,16 @@ module F = (ExerciseEnv: ExerciseEnv) => { provided: eds.your_tests.provided, }; }, + hidden_bugs: + eds.hidden_bugs + |> List.map(({impl, hint}) => { + let impl = Editor.set_read_only(impl, editing); + {impl, hint}; + }), + hidden_tests: { + let tests = Editor.set_read_only(eds.hidden_tests.tests, editing); + {tests, hints: eds.hidden_tests.hints}; + }, your_impl: Editor.set_read_only(eds.your_impl, editing), }, }; @@ -554,6 +670,16 @@ module F = (ExerciseEnv: ExerciseEnv) => { provided: eds.your_tests.provided, }; }, + hidden_bugs: + eds.hidden_bugs + |> List.map(({impl, hint}) => { + let impl = Editor.set_read_only(impl, editing); + {impl, hint}; + }), + hidden_tests: { + let tests = Editor.set_read_only(eds.hidden_tests.tests, editing); + {tests, hints: eds.hidden_tests.hints}; + }, your_impl: Editor.set_read_only(eds.your_impl, editing), }, }; @@ -583,6 +709,16 @@ module F = (ExerciseEnv: ExerciseEnv) => { provided: eds.your_tests.provided, }; }, + hidden_bugs: + eds.hidden_bugs + |> List.map(({impl, hint}) => { + let impl = Editor.set_read_only(impl, editing); + {impl, hint}; + }), + hidden_tests: { + let tests = Editor.set_read_only(eds.hidden_tests.tests, editing); + {tests, hints: eds.hidden_tests.hints}; + }, your_impl: Editor.set_read_only(eds.your_impl, editing), }, }; @@ -612,6 +748,16 @@ module F = (ExerciseEnv: ExerciseEnv) => { provided: eds.your_tests.provided, }; }, + hidden_bugs: + eds.hidden_bugs + |> List.map(({impl, hint}) => { + let impl = Editor.set_read_only(impl, editing); + {impl, hint}; + }), + hidden_tests: { + let tests = Editor.set_read_only(eds.hidden_tests.tests, editing); + {tests, hints: eds.hidden_tests.hints}; + }, your_impl: Editor.set_read_only(eds.your_impl, editing), }, }; @@ -643,36 +789,45 @@ module F = (ExerciseEnv: ExerciseEnv) => { set_instructor_mode({pos: YourImpl, eds}, instructor_mode); }; - let persistent_state_of_state = - ({pos, eds} as state: state, ~instructor_mode: bool) => { + let persistent_state_of_state = (state: state, ~instructor_mode: bool) => { let zippers = positioned_editors(state) |> List.filter(((pos, _)) => visible_in(pos, ~instructor_mode)) |> List.map(((pos, editor)) => { (pos, PersistentZipper.persist(Editor.(editor.state.zipper))) }); - ( - pos, - zippers, - eds.prompt, - eds.point_distribution, - eds.your_tests.required, - eds.module_name, - ); + let persistent_hidden_bugs = + state.eds.hidden_bugs + |> List.map(({impl, hint}) => { + {impl: PersistentZipper.persist(Editor.(impl.state.zipper)), hint} + }); + { + focus: state.pos, + editors: zippers, + title: state.eds.title, + hidden_bugs: persistent_hidden_bugs, + prompt: state.eds.prompt, + point_distribution: state.eds.point_distribution, + required: state.eds.your_tests.required, + module_name: state.eds.module_name, + }; }; let unpersist_state = ( - ( - pos, - positioned_zippers, + { + focus, + editors, + title, + hidden_bugs, prompt, point_distribution, required, module_name, - ): persistent_state, + }: persistent_state, ~spec: spec, ~instructor_mode: bool, + ~editing_title: bool, ~editing_prompt: bool, ~editing_test_val_rep: bool, ~editing_mut_test_rep: bool, @@ -683,7 +838,7 @@ module F = (ExerciseEnv: ExerciseEnv) => { : state => { let lookup = (pos, default) => if (visible_in(pos, ~instructor_mode)) { - let persisted_zipper = List.assoc(pos, positioned_zippers); + let persisted_zipper = List.assoc(pos, editors); let zipper = PersistentZipper.unpersist(persisted_zipper); Editor.init(zipper, ~settings); } else { @@ -693,25 +848,21 @@ module F = (ExerciseEnv: ExerciseEnv) => { let correct_impl = lookup(CorrectImpl, spec.correct_impl); let your_tests_tests = lookup(YourTestsValidation, spec.your_tests.tests); let your_impl = lookup(YourImpl, spec.your_impl); - let (_, hidden_bugs) = - List.fold_left( - ((i, hidden_bugs: list(wrong_impl(Editor.t))), {impl, hint}) => { - let impl = lookup(HiddenBugs(i), impl); - (i + 1, hidden_bugs @ [{impl, hint}]); - }, - (0, []), - spec.hidden_bugs, - ); + let hidden_bugs = + hidden_bugs + |> List.map(({impl, hint}) => { + let impl = + Editor.init(PersistentZipper.unpersist(impl), ~settings); + {impl, hint}; + }); let hidden_tests_tests = lookup(HiddenTests, spec.hidden_tests.tests); - let state = set_instructor_mode( { - pos, + pos: focus, eds: { id: spec.id, - title: spec.title, - version: spec.version, + title, module_name, prompt, point_distribution, @@ -733,6 +884,7 @@ module F = (ExerciseEnv: ExerciseEnv) => { }, instructor_mode, ); + let state = set_editing_title(state, editing_title); let state = set_editing_prompt(state, editing_prompt); let state = set_editing_test_val_rep(state, editing_test_val_rep); let state = set_editing_mut_test_rep(state, editing_mut_test_rep); @@ -909,7 +1061,11 @@ module F = (ExerciseEnv: ExerciseEnv) => { | YourTestsValidation => s.test_validation.statics | YourTestsTesting => s.user_tests.statics | YourImpl => s.user_impl.statics - | HiddenBugs(idx) => List.nth(s.hidden_bugs, idx).statics + | HiddenBugs(idx) => + switch (clamp_idx(state.eds, idx)) { + | Some(idx) => List.nth(s.hidden_bugs, idx).statics + | None => s.user_impl.statics + } | HiddenTests => s.hidden_tests.statics }; @@ -1083,7 +1239,6 @@ module F = (ExerciseEnv: ExerciseEnv) => { { id: Id.mk(), title, - version: 1, module_name, prompt: "", point_distribution, @@ -1123,6 +1278,7 @@ module F = (ExerciseEnv: ExerciseEnv) => { data, ~spec, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, @@ -1135,6 +1291,7 @@ module F = (ExerciseEnv: ExerciseEnv) => { |> unpersist_state( ~spec, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, diff --git a/src/haz3lschool/Gradescope.re b/src/haz3lschool/Gradescope.re index c8a8305995..aedf6ea127 100644 --- a/src/haz3lschool/Gradescope.re +++ b/src/haz3lschool/Gradescope.re @@ -117,6 +117,7 @@ module Main = { ~settings, ~spec, ~instructor_mode=true, + ~editing_title=false, ~editing_prompt=false, ~editing_test_val_rep=false, ~editing_mut_test_rep=false, diff --git a/src/haz3lweb/Editors.re b/src/haz3lweb/Editors.re index a2ede71e4e..229e411f17 100644 --- a/src/haz3lweb/Editors.re +++ b/src/haz3lweb/Editors.re @@ -57,7 +57,6 @@ let perform_action = CoreSettings.on | _ => settings }; - print_endline("action: " ++ Action.show(a)); switch (Perform.go(~settings, a, get_editor(editors))) { | Error(err) => Error(FailedToPerform(err)) | Ok(ed) => Ok(put_editor(ed, editors)) @@ -117,6 +116,44 @@ let set_instructor_mode = (editors: t, instructor_mode: bool): t => ) }; +let set_editing_title = (editors: t, editing: bool): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.set_editing_title(exercise, editing)) + }; + +let update_exercise_title = (editors: t, new_title: string): t => + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.update_exercise_title(exercise, new_title)) + }; + +let add_buggy_impl = (~settings: CoreSettings.t, editors: t, ~editing_title) => { + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises( + n, + specs, + Exercise.add_buggy_impl(~settings, exercise, ~editing_title), + ) + }; +}; + +let delete_buggy_impl = (editors: t, index: int) => { + switch (editors) { + | Scratch(_) + | Documentation(_) => editors + | Exercises(n, specs, exercise) => + Exercises(n, specs, Exercise.delete_buggy_impl(exercise, index)) + }; +}; + let set_editing_prompt = (editors: t, editing: bool): t => switch (editors) { | Scratch(_) diff --git a/src/haz3lweb/Export.re b/src/haz3lweb/Export.re index d77ce204d6..b020625687 100644 --- a/src/haz3lweb/Export.re +++ b/src/haz3lweb/Export.re @@ -55,6 +55,7 @@ let import_all = (data, ~specs) => { let settings = Store.Settings.import(all.settings); Store.ExplainThisModel.import(all.explainThisModel); let instructor_mode = settings.instructor_mode; + let editing_title = settings.editing_title; let editing_prompt = settings.editing_prompt; let editing_test_val_rep = settings.editing_test_val_rep; let editing_mut_test_rep = settings.editing_mut_test_rep; @@ -66,6 +67,7 @@ let import_all = (data, ~specs) => { all.exercise, ~specs, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, diff --git a/src/haz3lweb/Init.ml b/src/haz3lweb/Init.ml index effa6e12ad..8118a19f71 100644 --- a/src/haz3lweb/Init.ml +++ b/src/haz3lweb/Init.ml @@ -26,6 +26,7 @@ let startup : PersistentData.t = async_evaluation = false; context_inspector = false; instructor_mode = true; + editing_title = false; editing_prompt = false; editing_test_val_rep = false; editing_mut_test_rep = false; diff --git a/src/haz3lweb/Keyboard.re b/src/haz3lweb/Keyboard.re index f0501a66a4..7e4e655f26 100644 --- a/src/haz3lweb/Keyboard.re +++ b/src/haz3lweb/Keyboard.re @@ -227,7 +227,7 @@ let shortcuts = (sys: Key.sys): list(shortcut) => ] @ (if (ExerciseSettings.show_instructor) {instructor_shortcuts} else {[]}); -let handle_key_event = (k: Key.t): option(Update.t) => { +let handle_key_event = (k: Key.t): option(UpdateAction.t) => { let now = (a: Action.t): option(UpdateAction.t) => Some(PerformAction(a)); switch (k) { diff --git a/src/haz3lweb/Log.re b/src/haz3lweb/Log.re index ef8b70dfd5..f751efe36a 100644 --- a/src/haz3lweb/Log.re +++ b/src/haz3lweb/Log.re @@ -12,7 +12,8 @@ let is_action_logged: UpdateAction.t => bool = | FinishImportAll(_) | FinishImportScratchpad(_) | Benchmark(_) - | DebugConsole(_) => false + | DebugConsole(_) + | Startup => false | Reset | TAB | Set(_) @@ -24,6 +25,9 @@ let is_action_logged: UpdateAction.t => bool = | Undo | Redo | UpdateResult(_) + | UpdateTitle(_) + | AddBuggyImplementation + | DeleteBuggyImplementation(_) | UpdatePrompt(_) | UpdateTestValRep(_) | UpdateMutTestRep(_) diff --git a/src/haz3lweb/Main.re b/src/haz3lweb/Main.re index 0b90e99d7f..af8058b770 100644 --- a/src/haz3lweb/Main.re +++ b/src/haz3lweb/Main.re @@ -1,29 +1,12 @@ open Util; open Js_of_ocaml; -open Incr_dom; open Haz3lweb; +open Bonsai.Let_syntax; let scroll_to_caret = ref(true); let edit_action_applied = ref(true); let last_edit_action = ref(JsUtil.timestamp()); -let observe_font_specimen = (id, update) => - ResizeObserver.observe( - ~node=JsUtil.get_elem_by_id(id), - ~f= - (entries, _) => { - let specimen = Js.to_array(entries)[0]; - let rect = specimen##.contentRect; - update( - Haz3lweb.FontMetrics.{ - row_height: rect##.bottom -. rect##.top, - col_width: rect##.right -. rect##.left, - }, - ); - }, - (), - ); - let restart_caret_animation = () => // necessary to trigger reflow // @@ -36,7 +19,7 @@ let restart_caret_animation = () => | _ => () }; -let apply = (model, action, state, ~schedule_action): Model.t => { +let apply = (model, action, ~schedule_action): Model.t => { restart_caret_animation(); if (UpdateAction.is_edit(action)) { last_edit_action := JsUtil.timestamp(); @@ -48,7 +31,7 @@ let apply = (model, action, state, ~schedule_action): Model.t => { last_edit_action := JsUtil.timestamp(); switch ( try({ - let new_model = Update.apply(model, action, state, ~schedule_action); + let new_model = Update.apply(model, action, ~schedule_action); Log.update(action); new_model; }) { @@ -62,9 +45,7 @@ let apply = (model, action, state, ~schedule_action): Model.t => { ) { | Ok(model) => model | Error(FailedToPerform(err)) => - // TODO(andrew): reinstate this history functionality print_endline(Update.Failure.show(FailedToPerform(err))); - //{...model, history: ActionHistory.failure(err, model.history)}; model; | Error(err) => print_endline(Update.Failure.show(err)); @@ -72,74 +53,63 @@ let apply = (model, action, state, ~schedule_action): Model.t => { }; }; -module App = { - module Model = Model; - module Action = Update; - module State = State; - - let on_startup = (~schedule_action, m: Model.t) => { - let _ = - observe_font_specimen("font-specimen", fm => - schedule_action(Haz3lweb.Update.SetMeta(FontMetrics(fm))) - ); - - NinjaKeys.initialize(NinjaKeys.options(schedule_action)); - JsUtil.focus_clipboard_shim(); - - /* initialize state. */ - let state = State.init(); - - /* Initial evaluation on a worker */ - Update.schedule_evaluation(~schedule_action, m); +let app = + Bonsai.state_machine0( + (module Model), + (module Update), + ~apply_action= + (~inject, ~schedule_event) => + apply(~schedule_action=x => schedule_event(inject(x))), + ~default_model=Model.load(Model.blank), + ); - Os.is_mac := - Dom_html.window##.navigator##.platform##toUpperCase##indexOf( - Js.string("MAC"), - ) - >= 0; - Async_kernel.Deferred.return(state); +/* This subcomponent is used to run an effect once when the app starts up, + After the first draw */ +let on_startup = effect => { + let%sub startup_completed = Bonsai.toggle'(~default_model=false); + let%sub after_display = { + switch%sub (startup_completed) { + | {state: false, set_state, _} => + let%arr effect = effect + and set_state = set_state; + Bonsai.Effect.Many([set_state(true), effect]); + | {state: true, _} => Bonsai.Computation.return(Ui_effect.Ignore) + }; }; + Bonsai.Edge.after_display(after_display); +}; - let create = - ( - model: Incr.t(Haz3lweb.Model.t), - ~old_model as _: Incr.t(Haz3lweb.Model.t), - ~inject, - ) => { - open Incr.Let_syntax; - let%map model = model; - /* Note: mapping over the old_model here may - trigger an additional redraw */ - Component.create( - ~apply_action=apply(model), - model, - Haz3lweb.Page.view(~inject, model), - ~on_display=(_, ~schedule_action) => { - if (edit_action_applied^ - && JsUtil.timestamp() - -. last_edit_action^ > 1000.0) { - /* If an edit action has been applied, but no other edit action - has been applied for 1 second, save the model. */ - edit_action_applied := false; - print_endline("Saving..."); - schedule_action(Update.Save); - }; - if (scroll_to_caret.contents && !model.settings.instructor_mode) { - scroll_to_caret := false; - JsUtil.scroll_cursor_into_view_if_needed(); - }; - }, +let view = { + let%sub app = app; + let%sub () = { + on_startup( + Bonsai.Value.map(~f=((_model, inject)) => inject(Startup), app), ); }; + let%sub after_display = { + let%arr (_model, inject) = app; + if (scroll_to_caret.contents) { + scroll_to_caret := false; + JsUtil.scroll_cursor_into_view_if_needed(); + }; + if (edit_action_applied^ + && JsUtil.timestamp() + -. last_edit_action^ > 1000.0) { + /* If an edit action has been applied, but no other edit action + has been applied for 1 second, save the model. */ + edit_action_applied := false; + print_endline("Saving..."); + inject(Update.Save); + } else { + Ui_effect.Ignore; + }; + }; + let%sub () = Bonsai.Edge.after_display(after_display); + let%arr (model, inject) = app; + Haz3lweb.Page.view(~inject, model); }; switch (JsUtil.Fragment.get_current()) { | Some("debug") => DebugMode.go() -| _ => - Incr_dom.Start_app.start( - (module App), - ~debug=false, - ~bind_to_element_with_id="container", - ~initial_model=Model.load(Model.blank), - ) +| _ => Bonsai_web.Start.start(view, ~bind_to_element_with_id="container") }; diff --git a/src/haz3lweb/Model.re b/src/haz3lweb/Model.re index 5d4f6d36a7..413247bec0 100644 --- a/src/haz3lweb/Model.re +++ b/src/haz3lweb/Model.re @@ -33,6 +33,7 @@ let ui_state_init = { mousedown: false, }; +[@deriving sexp] type t = { editors: Editors.t, settings: Settings.t, @@ -41,7 +42,7 @@ type t = { ui_state, }; -let cutoff = (===); +let equal = (===); let mk = (editors, results) => { editors, @@ -58,6 +59,7 @@ let load_editors = ~settings, ~mode: Settings.mode, ~instructor_mode: bool, + ~editing_title: bool, ~editing_prompt: bool, ~editing_test_val_rep: bool, ~editing_mut_test_rep: bool, @@ -78,6 +80,7 @@ let load_editors = ~settings, ~specs=ExerciseSettings.exercises, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, @@ -106,6 +109,7 @@ let load = (init_model: t): t => { ~settings=settings.core, ~mode=settings.mode, ~instructor_mode=settings.instructor_mode, + ~editing_title=settings.editing_title, ~editing_prompt=settings.editing_prompt, ~editing_test_val_rep=settings.editing_test_val_rep, ~editing_mut_test_rep=settings.editing_mut_test_rep, diff --git a/src/haz3lweb/Settings.re b/src/haz3lweb/Settings.re index bded832645..6c7af34aba 100644 --- a/src/haz3lweb/Settings.re +++ b/src/haz3lweb/Settings.re @@ -22,6 +22,7 @@ type t = { async_evaluation: bool, context_inspector: bool, instructor_mode: bool, + editing_title: bool, editing_prompt: bool, editing_test_val_rep: bool, editing_mut_test_rep: bool, diff --git a/src/haz3lweb/Store.re b/src/haz3lweb/Store.re index b88cc5589b..91e2825277 100644 --- a/src/haz3lweb/Store.re +++ b/src/haz3lweb/Store.re @@ -284,6 +284,7 @@ module Exercise = { ~settings: CoreSettings.t, spec, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, @@ -300,6 +301,7 @@ module Exercise = { data, ~spec, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, @@ -342,6 +344,7 @@ module Exercise = { ~settings: CoreSettings.t, ~specs, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, @@ -359,10 +362,11 @@ module Exercise = { | Some(data) => let exercise = try( - Exercise.deserialize_exercise( + deserialize_exercise( data, ~spec, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, @@ -382,7 +386,7 @@ module Exercise = { (n, specs, exercise); } | None => - // initialize exercise from spec + // invalid current exercise key saved, load the first exercise let first_spec = List.nth(specs, 0); ( 0, @@ -390,6 +394,7 @@ module Exercise = { load_exercise( first_spec, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, @@ -401,7 +406,7 @@ module Exercise = { } | None => failwith("parse error") } - | None => init(~settings, ~instructor_mode) + | None => init(~instructor_mode, ~settings) }; }; @@ -410,6 +415,7 @@ module Exercise = { ~specs, ~instructor_mode: bool, ~settings: CoreSettings.t, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, @@ -432,12 +438,13 @@ module Exercise = { load_exercise( spec, ~instructor_mode, - ~settings, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, ~editing_impl_grd_rep, ~editing_module_name, + ~settings, ) |> Exercise.persistent_state_of_state(~instructor_mode); (key, exercise); @@ -450,12 +457,13 @@ module Exercise = { prep_exercise_export( ~specs, ~instructor_mode, - ~settings, + ~editing_title=false, ~editing_prompt=false, ~editing_test_val_rep=false, ~editing_mut_test_rep=false, ~editing_impl_grd_rep=false, ~editing_module_name=false, + ~settings, ) |> sexp_of_exercise_export |> Sexplib.Sexp.to_string; @@ -470,12 +478,13 @@ module Exercise = { data, ~specs, ~instructor_mode: bool, + ~editing_title: bool, + ~editing_prompt: bool, + ~editing_test_val_rep: bool, + ~editing_mut_test_rep: bool, + ~editing_impl_grd_rep: bool, + ~editing_module_name: bool, ~settings: CoreSettings.t, - ~editing_prompt, - ~editing_test_val_rep, - ~editing_mut_test_rep, - ~editing_impl_grd_rep, - ~editing_module_name, ) => { let exercise_export = data |> deserialize_exercise_export; save_exercise_id(exercise_export.cur_exercise); @@ -491,6 +500,7 @@ module Exercise = { persistent_state, ~spec, ~instructor_mode, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, diff --git a/src/haz3lweb/Update.re b/src/haz3lweb/Update.re index 884204230c..aeb0831244 100644 --- a/src/haz3lweb/Update.re +++ b/src/haz3lweb/Update.re @@ -1,8 +1,26 @@ open Util; +open Js_of_ocaml; open Haz3lcore; include UpdateAction; // to prevent circularity +let observe_font_specimen = (id, update) => + ResizeObserver.observe( + ~node=JsUtil.get_elem_by_id(id), + ~f= + (entries, _) => { + let specimen = Js.to_array(entries)[0]; + let rect = specimen##.contentRect; + update( + FontMetrics.{ + row_height: rect##.bottom -. rect##.top, + col_width: rect##.right -. rect##.left, + }, + ); + }, + (), + ); + let update_settings = (a: settings_action, {settings, _} as model: Model.t): Model.t => switch (a) { @@ -175,6 +193,7 @@ let update_settings = settings: { ...settings, instructor_mode: !settings.instructor_mode, + editing_title: false, editing_prompt: false, editing_test_val_rep: false, editing_mut_test_rep: false, @@ -182,6 +201,16 @@ let update_settings = editing_module_name: false, }, }; + | EditingTitle => + let editing = !settings.editing_title; + { + ...model, + editors: Editors.set_editing_title(model.editors, editing), + settings: { + ...settings, + editing_title: editing, + }, + }; | EditingPrompt => let editing = !settings.editing_prompt; { @@ -270,6 +299,25 @@ let schedule_evaluation = (~schedule_action, model: Model.t): unit => }; }; +let on_startup = + (~schedule_action: UpdateAction.t => unit, m: Model.t): Model.t => { + let _ = + observe_font_specimen("font-specimen", fm => + schedule_action(UpdateAction.SetMeta(FontMetrics(fm))) + ); + NinjaKeys.initialize(NinjaKeys.options(schedule_action)); + JsUtil.focus_clipboard_shim(); + /* initialize state. */ + /* Initial evaluation on a worker */ + schedule_evaluation(~schedule_action, m); + Os.is_mac := + Dom_html.window##.navigator##.platform##toUpperCase##indexOf( + Js.string("MAC"), + ) + >= 0; + m; +}; + let update_cached_data = (~schedule_action, update, m: Model.t): Model.t => { let update_dynamics = reevaluate_post_update(update); /* If we switch editors, or change settings which require statics @@ -305,12 +353,13 @@ let switch_scratch_slide = ~settings, editors: Editors.t, ~instructor_mode, - idx: int, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, ~editing_impl_grd_rep, ~editing_module_name, + idx: int, ) : option(Editors.t) => switch (editors) { @@ -325,12 +374,13 @@ let switch_scratch_slide = Store.Exercise.load_exercise( spec, ~instructor_mode, - ~settings, + ~editing_title, ~editing_prompt, ~editing_test_val_rep, ~editing_mut_test_rep, ~editing_impl_grd_rep, ~editing_module_name, + ~settings, ); Some(Exercises(idx, specs, exercise)); }; @@ -435,9 +485,7 @@ let ui_state_update = }; }; -let apply = - (model: Model.t, update: t, _state: State.t, ~schedule_action) - : Result.t(Model.t) => { +let apply = (model: Model.t, update: t, ~schedule_action): Result.t(Model.t) => { let perform_action = (model: Model.t, a: Action.t): Result.t(Model.t) => { switch ( Editors.perform_action(~settings=model.settings.core, model.editors, a) @@ -448,6 +496,7 @@ let apply = }; let m: Result.t(Model.t) = switch (update) { + | Startup => Ok(on_startup(~schedule_action, model)) | Reset => Ok(Model.reset(model)) | Set(Evaluation(_) as s_action) => Ok(update_settings(s_action, model)) | Set(s_action) => @@ -526,9 +575,10 @@ let apply = Model.save_and_return({...model, editors}); | SwitchScratchSlide(n) => let instructor_mode = model.settings.instructor_mode; - let editors = Editors.set_editing_prompt(model.editors, false); + let editors = Editors.set_editing_title(model.editors, false); let settings = { ...model.settings, + editing_title: false, editing_prompt: false, editing_test_val_rep: false, editing_mut_test_rep: false, @@ -537,9 +587,10 @@ let apply = }; switch ( switch_scratch_slide( - editors, ~settings=model.settings.core, + editors, ~instructor_mode, + ~editing_title=false, ~editing_prompt=false, ~editing_test_val_rep=false, ~editing_mut_test_rep=false, @@ -627,6 +678,25 @@ let apply = let results = ModelResults.union((_, _a, b) => Some(b), model.results, results); Ok({...model, results}); + | UpdateTitle(new_title) => + Model.save_and_return({ + ...model, + editors: Editors.update_exercise_title(model.editors, new_title), + }) + | AddBuggyImplementation => + Model.save_and_return({ + ...model, + editors: + Editors.add_buggy_impl( + ~settings=model.settings.core, + model.editors, + ~editing_title=model.settings.editing_title, + ), + }) + | DeleteBuggyImplementation(index) => + let editors = Editors.delete_buggy_impl(model.editors, index); + print_endline(Editors.show(editors)); + Model.save_and_return({...model, editors}); | UpdatePrompt(new_prompt) => Model.save_and_return({ ...model, diff --git a/src/haz3lweb/UpdateAction.re b/src/haz3lweb/UpdateAction.re index 86dc33bc12..b60452aa32 100644 --- a/src/haz3lweb/UpdateAction.re +++ b/src/haz3lweb/UpdateAction.re @@ -24,6 +24,7 @@ type settings_action = | Benchmark | ContextInspector | InstructorMode + | EditingTitle | EditingPrompt | EditingTestValRep | EditingMutTestRep @@ -50,6 +51,12 @@ type benchmark_action = | Start | Finish; +// To-do: Use this to update either title or model +[@deriving (show({with_path: false}), sexp, yojson)] +type edit_action = + | Title + | Model; + [@deriving (show({with_path: false}), sexp, yojson)] type export_action = | ExportScratchSlide @@ -62,6 +69,7 @@ type export_action = [@deriving (show({with_path: false}), sexp, yojson)] type t = /* meta */ + | Startup | Reset | Set(settings_action) | SetMeta(set_meta) @@ -88,6 +96,9 @@ type t = | ToggleStepper(ModelResults.Key.t) | StepperAction(ModelResults.Key.t, stepper_action) | UpdateResult(ModelResults.t) + | UpdateTitle(string) + | AddBuggyImplementation + | DeleteBuggyImplementation(int) | UpdatePrompt(string) | UpdateTestValRep(int, int, int) | UpdateMutTestRep(int) @@ -126,6 +137,7 @@ let is_edit: t => bool = | Benchmark | ContextInspector | InstructorMode + | EditingTitle | EditingPrompt | EditingTestValRep | EditingMutTestRep @@ -149,6 +161,9 @@ let is_edit: t => bool = | FinishImportAll(_) | FinishImportScratchpad(_) | ResetCurrentEditor + | UpdateTitle(_) + | AddBuggyImplementation + | DeleteBuggyImplementation(_) | UpdatePrompt(_) | UpdateTestValRep(_) | UpdateMutTestRep(_) @@ -164,7 +179,8 @@ let is_edit: t => bool = | DebugConsole(_) | InitImportAll(_) | InitImportScratchpad(_) - | Benchmark(_) => false; + | Benchmark(_) + | Startup => false; let reevaluate_post_update: t => bool = fun @@ -189,6 +205,7 @@ let reevaluate_post_update: t => bool = | Assist | Dynamics | InstructorMode + | EditingTitle | EditingPrompt | EditingTestValRep | EditingMutTestRep @@ -203,6 +220,9 @@ let reevaluate_post_update: t => bool = | ShowBackpackTargets(_) | FontMetrics(_) => false } + | AddBuggyImplementation + | DeleteBuggyImplementation(_) + | UpdateTitle(_) => false | Save | InitImportAll(_) | InitImportScratchpad(_) @@ -227,7 +247,8 @@ let reevaluate_post_update: t => bool = | SwitchDocumentationSlide(_) | Reset | Undo - | Redo => true; + | Redo + | Startup => true; let should_scroll_to_caret = fun @@ -244,6 +265,7 @@ let should_scroll_to_caret = | Benchmark | ContextInspector | InstructorMode + | EditingTitle | EditingPrompt | EditingTestValRep | EditingMutTestRep @@ -260,6 +282,9 @@ let should_scroll_to_caret = } | UpdateResult(_) | ToggleStepper(_) + | UpdateTitle(_) + | AddBuggyImplementation + | DeleteBuggyImplementation(_) | UpdatePrompt(_) | UpdateTestValRep(_) | UpdateMutTestRep(_) @@ -275,7 +300,8 @@ let should_scroll_to_caret = | Reset | Undo | Redo - | TAB => true + | TAB + | Startup => true | PerformAction(a) => switch (a) { | Move(_) diff --git a/src/haz3lweb/dune b/src/haz3lweb/dune index 8d25155dc5..d3e42ec636 100644 --- a/src/haz3lweb/dune +++ b/src/haz3lweb/dune @@ -9,7 +9,8 @@ (name workerServer) (modules WorkerServer) (libraries - incr_dom + bonsai + bonsai.web virtual_dom.input_widgets util ppx_yojson_conv.expander @@ -38,7 +39,8 @@ ezjs_idb workerServer str - incr_dom + bonsai + bonsai.web virtual_dom.input_widgets util ppx_yojson_conv.expander @@ -66,7 +68,8 @@ js_of_ocaml-ppx ppx_let ppx_sexp_conv - ppx_deriving.show))) + ppx_deriving.show + bonsai.ppx_bonsai))) (executable (name worker) diff --git a/src/haz3lweb/exercises/Ex_OddlyRecursive.ml b/src/haz3lweb/exercises/Ex_OddlyRecursive.ml index 9a5310d51b..7648e34980 100644 --- a/src/haz3lweb/exercises/Ex_OddlyRecursive.ml +++ b/src/haz3lweb/exercises/Ex_OddlyRecursive.ml @@ -6,7 +6,6 @@ let exercise : Exercise.spec = { id = Option.get (Id.of_string "3335e34d-d211-4332-91e2-815e9e183885"); title = "Oddly Recursive"; - version = 1; module_name = "Ex_OddlyRecursive"; prompt; point_distribution = diff --git a/src/haz3lweb/exercises/Ex_RecursiveFibonacci.ml b/src/haz3lweb/exercises/Ex_RecursiveFibonacci.ml index e17f303efc..381db4f816 100644 --- a/src/haz3lweb/exercises/Ex_RecursiveFibonacci.ml +++ b/src/haz3lweb/exercises/Ex_RecursiveFibonacci.ml @@ -6,7 +6,6 @@ let exercise : Exercise.spec = { id = Option.get (Id.of_string "12f5e34d-d211-4332-91e2-815e9e183885"); title = "Recursive Fibonacci"; - version = 1; module_name = "Ex_RecursiveFibonacci"; prompt; point_distribution = diff --git a/src/haz3lweb/view/Cell.re b/src/haz3lweb/view/Cell.re index 13a64a7f3b..c10d3faa0e 100644 --- a/src/haz3lweb/view/Cell.re +++ b/src/haz3lweb/view/Cell.re @@ -354,6 +354,25 @@ let title_cell = title => { ]); }; +let wrong_impl_caption = (~inject, sub: string, n: int) => { + div( + ~attrs=[Attr.class_("wrong-impl-cell-caption")], + [ + caption("", ~rest=sub), + div( + ~attrs=[Attr.class_("instructor-edit-icon")], + [ + Widgets.button( + Icons.delete, + _ => inject(UpdateAction.DeleteBuggyImplementation(n)), + ~tooltip="Delete Buggy Implementation", + ), + ], + ), + ], + ); +}; + /* An editor view that is not selectable or editable, * and does not show error holes or test results. * Used in Docs to display the header example */ diff --git a/src/haz3lweb/view/DebugMode.re b/src/haz3lweb/view/DebugMode.re index 543e7cc757..39cba26eb8 100644 --- a/src/haz3lweb/view/DebugMode.re +++ b/src/haz3lweb/view/DebugMode.re @@ -48,35 +48,8 @@ let view = { ); }; -module App = { - module Model = { - type t = unit; - let cutoff = (_, _) => false; - }; - module Action = { - type t = unit; - let sexp_of_t = _ => Sexplib.Sexp.unit; - }; - module State = { - type t = unit; - }; - let on_startup = (~schedule_action as _, _) => - Async_kernel.Deferred.return(); - let create = (_, ~old_model as _, ~inject as _) => - Incr_dom.Incr.return() - |> Incr_dom.Incr.map(~f=_ => - Incr_dom.Component.create( - ~apply_action=(_, _, ~schedule_action as _) => (), - (), - view, - ) - ); -}; - let go = () => - Incr_dom.Start_app.start( - (module App), - ~debug=false, + Bonsai_web.Start.start( + Bonsai.Computation.return(view), ~bind_to_element_with_id="container", - ~initial_model=(), ); diff --git a/src/haz3lweb/view/ExerciseMode.re b/src/haz3lweb/view/ExerciseMode.re index 31571e1725..2cfc8c4f35 100644 --- a/src/haz3lweb/view/ExerciseMode.re +++ b/src/haz3lweb/view/ExerciseMode.re @@ -57,14 +57,81 @@ let view = ~mousedown_updates=[SwitchEditor(this_pos)], ~settings, ~highlights, - ~caption=Cell.caption(caption, ~rest=?subcaption), + ~caption= + switch (this_pos) { + | HiddenBugs(n) => Cell.wrong_impl_caption(~inject, caption, n) + | _ => Cell.caption(caption, ~rest=?subcaption) + }, ~target_id=Exercise.show_pos(this_pos), ~test_results=ModelResult.test_results(di.result), ~footer?, editor, ); }; - let title_view = Cell.title_cell(eds.title); + + let update_title = _ => { + let new_title = + Obj.magic( + Js_of_ocaml.Js.some(JsUtil.get_elem_by_id("title-input-box")), + )##.value; + let update_events = [ + inject(Set(EditingTitle)), + inject(UpdateTitle(new_title)), + ]; + Virtual_dom.Vdom.Effect.Many(update_events); + }; + + let title_view = { + Cell.simple_cell_view([ + div( + ~attrs=[Attr.class_("title-cell")], + [ + settings.instructor_mode + ? settings.editing_title + ? div( + ~attrs=[Attr.class_("title-edit")], + [ + input( + ~attrs=[ + Attr.class_("title-text"), + Attr.id("title-input-box"), + Attr.value(eds.title), + ], + (), + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [Widgets.button(Icons.confirm, update_title)], + ), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.cancel, _ => + inject(Set(EditingTitle)) + ), + ], + ), + ], + ) + : div( + ~attrs=[Attr.class_("title-edit")], + [ + text(eds.title), + div( + ~attrs=[Attr.class_("edit-icon")], + [ + Widgets.button(Icons.pencil, _ => + inject(Set(EditingTitle)) + ), + ], + ), + ], + ) + : div(~attrs=[Attr.class_("title-text")], [text(eds.title)]), + ], + ), + ]); + }; let update_module_name = _ => { let new_module_name = @@ -283,18 +350,40 @@ let view = let wrong_impl_views = List.mapi( (i, (Exercise.{impl, _}, di)) => { - InstructorOnly( - () => - editor_view( - HiddenBugs(i), - ~caption="Wrong Implementation " ++ string_of_int(i + 1), - ~editor=impl, - ~di, - ), + editor_view( + HiddenBugs(i), + ~caption="Mutant " ++ string_of_int(i + 1), + ~editor=impl, + ~di, ) }, List.combine(eds.hidden_bugs, hidden_bugs), ); + + let add_wrong_impl_view = + Cell.simple_cell_view([ + Cell.simple_cell_item([ + div( + ~attrs=[Attr.class_("wrong-impl-cell-caption")], + [ + div( + ~attrs=[ + Attr.class_("instructor-edit-icon"), + Attr.id("add-icon"), + ], + [ + Widgets.button( + Icons.add, + _ => inject(UpdateAction.AddBuggyImplementation), + ~tooltip="Add Buggy Implementation", + ), + ], + ), + ], + ), + ]), + ]); + let mutation_testing_view = Always( Grading.MutationTestingReport.view( @@ -365,6 +454,19 @@ let view = ~settings, ), ); + + let wrong_impl_views = + InstructorOnly( + () => + Cell.simple_cell_view([ + Cell.simple_cell_item( + [Cell.caption("Mutation Tests")] + @ wrong_impl_views + @ [add_wrong_impl_view], + ), + ]), + ); + [score_view, title_view, module_name_view, prompt_view] @ render_cells( settings, @@ -373,9 +475,7 @@ let view = correct_impl_view, correct_impl_ctx_view, your_tests_view, - ] - @ wrong_impl_views - @ [ + wrong_impl_views, mutation_testing_view, your_impl_view, syntax_grading_view, diff --git a/src/haz3lweb/view/Icons.re b/src/haz3lweb/view/Icons.re index d31ec0c28e..06bba15d53 100644 --- a/src/haz3lweb/view/Icons.re +++ b/src/haz3lweb/view/Icons.re @@ -255,3 +255,24 @@ let command_palette_sparkle = "m554.76 426.6c6.5195-23.285 24.715-41.48 48-48-23.297-6.5-41.5-24.707-48-48-6.5 23.293-24.707 41.5-48 48 23.281 6.5195 41.477 24.715 48 48z", ], ); + +let add = + simple_icon( + ~view="0 0 24 24", + [ + "M12.75 9C12.75 8.58579 12.4142 8.25 12 8.25C11.5858 8.25 11.25 8.58579 11.25 9L11.25 11.25H9C8.58579 11.25 8.25 11.5858 8.25 12C8.25 12.4142 8.58579 12.75 9 12.75H11.25V15C11.25 15.4142 11.5858 15.75 12 15.75C12.4142 15.75 12.75 15.4142 12.75 15L12.75 12.75H15C15.4142 12.75 15.75 12.4142 15.75 12C15.75 11.5858 15.4142 11.25 15 11.25H12.75V9Z", + "M12 1.25C6.06294 1.25 1.25 6.06294 1.25 12C1.25 17.9371 6.06294 22.75 12 22.75C17.9371 22.75 22.75 17.9371 22.75 12C22.75 6.06294 17.9371 1.25 12 1.25ZM2.75 12C2.75 6.89137 6.89137 2.75 12 2.75C17.1086 2.75 21.25 6.89137 21.25 12C21.25 17.1086 17.1086 21.25 12 21.25C6.89137 21.25 2.75 17.1086 2.75 12Z", + ], + ); + +let delete = + simple_icon( + ~view="0 0 24 24", + [ + "M12 2.75C11.0215 2.75 10.1871 3.37503 9.87787 4.24993C9.73983 4.64047 9.31134 4.84517 8.9208 4.70713C8.53026 4.56909 8.32557 4.1406 8.46361 3.75007C8.97804 2.29459 10.3661 1.25 12 1.25C13.634 1.25 15.022 2.29459 15.5365 3.75007C15.6745 4.1406 15.4698 4.56909 15.0793 4.70713C14.6887 4.84517 14.2602 4.64047 14.1222 4.24993C13.813 3.37503 12.9785 2.75 12 2.75Z", + "M2.75 6C2.75 5.58579 3.08579 5.25 3.5 5.25H20.5001C20.9143 5.25 21.2501 5.58579 21.2501 6C21.2501 6.41421 20.9143 6.75 20.5001 6.75H3.5C3.08579 6.75 2.75 6.41421 2.75 6Z", + "M5.91508 8.45011C5.88753 8.03681 5.53015 7.72411 5.11686 7.75166C4.70356 7.77921 4.39085 8.13659 4.41841 8.54989L4.88186 15.5016C4.96735 16.7844 5.03641 17.8205 5.19838 18.6336C5.36678 19.4789 5.6532 20.185 6.2448 20.7384C6.83639 21.2919 7.55994 21.5307 8.41459 21.6425C9.23663 21.75 10.2751 21.75 11.5607 21.75H12.4395C13.7251 21.75 14.7635 21.75 15.5856 21.6425C16.4402 21.5307 17.1638 21.2919 17.7554 20.7384C18.347 20.185 18.6334 19.4789 18.8018 18.6336C18.9637 17.8205 19.0328 16.7844 19.1183 15.5016L19.5818 8.54989C19.6093 8.13659 19.2966 7.77921 18.8833 7.75166C18.47 7.72411 18.1126 8.03681 18.0851 8.45011L17.6251 15.3492C17.5353 16.6971 17.4712 17.6349 17.3307 18.3405C17.1943 19.025 17.004 19.3873 16.7306 19.6431C16.4572 19.8988 16.083 20.0647 15.391 20.1552C14.6776 20.2485 13.7376 20.25 12.3868 20.25H11.6134C10.2626 20.25 9.32255 20.2485 8.60915 20.1552C7.91715 20.0647 7.54299 19.8988 7.26957 19.6431C6.99616 19.3873 6.80583 19.025 6.66948 18.3405C6.52891 17.6349 6.46488 16.6971 6.37503 15.3492L5.91508 8.45011Z", + "M9.42546 10.2537C9.83762 10.2125 10.2051 10.5132 10.2464 10.9254L10.7464 15.9254C10.7876 16.3375 10.4869 16.7051 10.0747 16.7463C9.66256 16.7875 9.29502 16.4868 9.25381 16.0746L8.75381 11.0746C8.71259 10.6625 9.0133 10.2949 9.42546 10.2537Z", + "M15.2464 11.0746C15.2876 10.6625 14.9869 10.2949 14.5747 10.2537C14.1626 10.2125 13.795 10.5132 13.7538 10.9254L13.2538 15.9254C13.2126 16.3375 13.5133 16.7051 13.9255 16.7463C14.3376 16.7875 14.7051 16.4868 14.7464 16.0746L15.2464 11.0746Z", + ], + ); diff --git a/src/haz3lweb/view/Page.re b/src/haz3lweb/view/Page.re index 1d84c25ea8..e1fe99986e 100644 --- a/src/haz3lweb/view/Page.re +++ b/src/haz3lweb/view/Page.re @@ -26,6 +26,7 @@ let key_handler = | Some(action) => let settings = get_settings(model); settings.editing_prompt + || settings.editing_title || settings.editing_test_val_rep || settings.editing_mut_test_rep || settings.editing_impl_grd_rep @@ -71,11 +72,15 @@ let handlers = inject(PerformAction(Paste(pasted_text))); }), ]; +<<<<<<< HEAD model.settings.editing_prompt || model.settings.editing_test_val_rep || model.settings.editing_mut_test_rep || model.settings.editing_impl_grd_rep || model.settings.editing_module_name +======= + model.settings.editing_title +>>>>>>> title-editor ? attrs : attrs @ [Attr.on_keypress(_ => Effect.Prevent_default)]; }; @@ -109,7 +114,9 @@ let main_view = ~inject: UpdateAction.t => Ui_effect.t(unit), {settings, editors, explainThisModel, results, ui_state, _}: Model.t, ) => { + print_endline("here, at main view, getting editor"); let editor = Editors.get_editor(editors); + print_endline("got editor!"); let cursor_info = Indicated.ci_of(editor.state.zipper, editor.state.meta.statics.info_map); let highlights = diff --git a/src/haz3lweb/www/style/cell.css b/src/haz3lweb/www/style/cell.css index e10c38a126..a3fd22369e 100644 --- a/src/haz3lweb/www/style/cell.css +++ b/src/haz3lweb/www/style/cell.css @@ -62,10 +62,50 @@ color: var(--BR4); } +.title-edit .edit-icon { + margin-left: 0.5em; + cursor: pointer; + fill: #7a6219; +} + +.title-cell .title-edit { + font-size: 1.5rem; + font-weight: bold; + color: var(--light-text-color); + flex-grow: 1; + display: flex; + align-items: center; +} + +.title-edit .edit-icon:hover { + animation: wobble 0.6s ease 0s 1 normal forwards; +} + .cell-prompt { padding: 1em; } +.wrong-impl-cell-caption { + flex-grow: 1; + display: flex; + align-items: center; +} + +.instructor-edit-icon { + margin-top: 0.175em; + margin-left: 1em; + cursor: pointer; + fill: #7a6219; +} + +#add-icon { + margin-left: 0em; +} + +.instructor-edit-icon:hover { + animation: wobble 0.6s ease 0s 1 normal forwards; +} + /* DOCUMENTATION SLIDES */ .slide-img { diff --git a/src/haz3lweb/www/style/exercise-mode.css b/src/haz3lweb/www/style/exercise-mode.css index 5eb8e2d74c..b69c7037f0 100644 --- a/src/haz3lweb/www/style/exercise-mode.css +++ b/src/haz3lweb/www/style/exercise-mode.css @@ -234,4 +234,4 @@ #main.Exercises .context-entry { max-width: fit-content; /* Correct implementation type sigs */ -} +} \ No newline at end of file diff --git a/src/util/dune b/src/util/dune index 889c95bd2c..a3494db579 100644 --- a/src/util/dune +++ b/src/util/dune @@ -1,6 +1,6 @@ (library (name util) - (libraries re base ptmap incr_dom virtual_dom yojson) + (libraries re base ptmap bonsai bonsai.web virtual_dom yojson) (js_of_ocaml) (preprocess (pps @@ -12,8 +12,10 @@ (env (dev + (flags :standard -warn-error -A) (js_of_ocaml (flags :standard --debuginfo --noinline --dynlink --linkall --sourcemap))) (release + (flags :standard -warn-error +A-58) (js_of_ocaml (flags :standard)))) diff --git a/test/Test_Elaboration.re b/test/Test_Elaboration.re index 1c5a7c7271..2516f25227 100644 --- a/test/Test_Elaboration.re +++ b/test/Test_Elaboration.re @@ -74,20 +74,18 @@ let consistent_if = () => dhexp_of_uexp(u6), ); -let u7: Exp.t = - Ap( - Forward, - Fun( - Var("x") |> Pat.fresh, - BinOp(Int(Plus), Int(4) |> Exp.fresh, Int(5) |> Exp.fresh) - |> Exp.fresh, - None, - None, - ) - |> Exp.fresh, - Var("y") |> Exp.fresh, +// x => 4 + 5 +let f = + Fun( + Var("x") |> Pat.fresh, + BinOp(Int(Plus), Int(4) |> Exp.fresh, Int(5) |> Exp.fresh) |> Exp.fresh, + None, + None, ) |> Exp.fresh; +let unapplied_function = () => alco_check("A function", f, dhexp_of_uexp(f)); + +let u7: Exp.t = Ap(Forward, f, Var("y") |> Exp.fresh) |> Exp.fresh; let ap_fun = () => alco_check("Application of a function", u7, dhexp_of_uexp(u7)); @@ -179,6 +177,33 @@ let let_fun = () => dhexp_of_uexp(u9), ); +let deferral = () => + alco_check( + "string_sub(\"hello\", 1, _)", + dhexp_of_uexp( + DeferredAp( + Var("string_sub") |> Exp.fresh, + [ + String("hello") |> Exp.fresh, + Int(1) |> Exp.fresh, + Deferral(InAp) |> Exp.fresh, + ], + ) + |> Exp.fresh, + ), + dhexp_of_uexp( + DeferredAp( + Var("string_sub") |> Exp.fresh, + [ + String("hello") |> Exp.fresh, + Int(1) |> Exp.fresh, + Deferral(InAp) |> Exp.fresh, + ], + ) + |> Exp.fresh, + ), + ); + let elaboration_tests = [ test_case("Single integer", `Quick, single_integer), test_case("Empty hole", `Quick, empty_hole), @@ -186,7 +211,13 @@ let elaboration_tests = [ test_case("Let expression", `Quick, let_exp), test_case("Inconsistent binary operation", `Quick, bin_op), test_case("Consistent if statement", `Quick, consistent_if), + test_case("An unapplied function", `Quick, unapplied_function), test_case("Application of function on free variable", `Quick, ap_fun), test_case("Inconsistent case statement", `Quick, inconsistent_case), test_case("Let expression for a function", `Quick, let_fun), + test_case( + "Function application with a deferred argument", + `Quick, + deferral, + ), ]; diff --git a/test/Test_Evaluator.re b/test/Test_Evaluator.re new file mode 100644 index 0000000000..fc159425b2 --- /dev/null +++ b/test/Test_Evaluator.re @@ -0,0 +1,44 @@ +open Alcotest; +open Haz3lcore; +let dhexp_typ = testable(Fmt.using(Exp.show, Fmt.string), DHExp.fast_equal); + +let ids = List.init(12, _ => Id.mk()); +let id_at = x => x |> List.nth(ids); +let statics = Statics.mk(CoreSettings.on, Builtins.ctx_init); + +// Get the type from the statics +let type_of = f => { + let s = statics(f); + switch (Id.Map.find(IdTagged.rep_id(f), s)) { + | InfoExp({ty, _}) => Some(ty) + | _ => None + }; +}; + +let int_evaluation = + Evaluator.evaluate(Builtins.env_init, {d: Exp.Int(8) |> Exp.fresh}); + +let evaluation_test = (msg, expected, unevaluated) => + check( + dhexp_typ, + msg, + expected, + Evaluator.Result.unbox( + snd(Evaluator.evaluate(Builtins.env_init, {d: unevaluated})), + ), + ); + +let test_int = () => + evaluation_test("8", Int(8) |> Exp.fresh, Int(8) |> Exp.fresh); + +let test_sum = () => + evaluation_test( + "4 + 5", + Int(9) |> Exp.fresh, + BinOp(Int(Plus), Int(4) |> Exp.fresh, Int(5) |> Exp.fresh) |> Exp.fresh, + ); + +let tests = [ + test_case("Integer literal", `Quick, test_int), + test_case("Integer sum", `Quick, test_sum), +]; diff --git a/test/Test_Statics.re b/test/Test_Statics.re new file mode 100644 index 0000000000..71fdafc8ba --- /dev/null +++ b/test/Test_Statics.re @@ -0,0 +1,126 @@ +open Alcotest; +open Haz3lcore; + +let testable_typ = testable(Fmt.using(Typ.show, Fmt.string), Typ.fast_equal); +module FreshId = { + let arrow = (a, b) => Arrow(a, b) |> Typ.fresh; + let unknown = a => Unknown(a) |> Typ.fresh; + let int = Typ.fresh(Int); + let float = Typ.fresh(Float); + let prod = a => Prod(a) |> Typ.fresh; + let string = Typ.fresh(String); +}; +let ids = List.init(12, _ => Id.mk()); +let id_at = x => x |> List.nth(ids); +let statics = Statics.mk(CoreSettings.on, Builtins.ctx_init); +let alco_check = Alcotest.option(testable_typ) |> Alcotest.check; + +// Get the type from the statics +let type_of = f => { + let s = statics(f); + switch (Id.Map.find(IdTagged.rep_id(f), s)) { + | InfoExp({ty, _}) => Some(ty) + | _ => None + }; +}; + +let unapplied_function = () => + alco_check( + "Unknown param", + Some(FreshId.(arrow(unknown(Internal), int))), + type_of( + Fun( + Var("x") |> Pat.fresh, + BinOp(Int(Plus), Int(4) |> Exp.fresh, Int(5) |> Exp.fresh) + |> Exp.fresh, + None, + None, + ) + |> Exp.fresh, + ), + ); + +let tests = + FreshId.[ + test_case("Function with unknown param", `Quick, () => + alco_check( + "x => 4 + 5", + Some(arrow(unknown(Internal), int)), + type_of( + Fun( + Var("x") |> Pat.fresh, + BinOp(Int(Plus), Int(4) |> Exp.fresh, Int(5) |> Exp.fresh) + |> Exp.fresh, + None, + None, + ) + |> Exp.fresh, + ), + ) + ), + test_case("Function with known param", `Quick, () => + alco_check( + "x : Int => 4 + 5", + Some(arrow(int, int)), + type_of( + Fun( + Cast(Var("x") |> Pat.fresh, int, unknown(Internal)) |> Pat.fresh, + BinOp(Int(Plus), Int(4) |> Exp.fresh, Int(5) |> Exp.fresh) + |> Exp.fresh, + None, + None, + ) + |> Exp.fresh, + ), + ) + ), + test_case("bifunction", `Quick, () => + alco_check( + "x : Int, y: Int => x + y", + Some(arrow(prod([int, int]), int)), + type_of( + Fun( + Tuple([ + Cast(Var("x") |> Pat.fresh, int, unknown(Internal)) + |> Pat.fresh, + Cast(Var("y") |> Pat.fresh, int, unknown(Internal)) + |> Pat.fresh, + ]) + |> Pat.fresh, + BinOp(Int(Plus), Var("x") |> Exp.fresh, Var("y") |> Exp.fresh) + |> Exp.fresh, + None, + None, + ) + |> Exp.fresh, + ), + ) + ), + test_case("function application", `Quick, () => + alco_check( + "float_of_int(1)", + Some(float), + type_of( + Ap(Forward, Var("float_of_int") |> Exp.fresh, Int(1) |> Exp.fresh) + |> Exp.fresh, + ), + ) + ), + test_case("function deferral", `Quick, () => + alco_check( + "string_sub(\"hello\", 1, _)", + Some(arrow(int, string)), + type_of( + DeferredAp( + Var("string_sub") |> Exp.fresh, + [ + String("hello") |> Exp.fresh, + Int(1) |> Exp.fresh, + Deferral(InAp) |> Exp.fresh, + ], + ) + |> Exp.fresh, + ), + ) + ), + ]; diff --git a/test/haz3ltest.re b/test/haz3ltest.re index e405fba7b8..3e13ae44b7 100644 --- a/test/haz3ltest.re +++ b/test/haz3ltest.re @@ -4,6 +4,10 @@ let (suite, _) = run_and_report( ~and_exit=false, "Dynamics", - [("Elaboration", Test_Elaboration.elaboration_tests)], + [ + ("Elaboration", Test_Elaboration.elaboration_tests), + ("Statics", Test_Statics.tests), + ("Evaluator", Test_Evaluator.tests), + ], ); Junit.to_file(Junit.make([suite]), "junit_tests.xml");