Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions demo-repository/exercises/demo3/demo/descr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# id: `demo3/demo`

This example demonstrates the ability to put exercises in sub-directories.

## Questions

Define four functions matching the usual arithmetic operations:

```
plus : int -> int -> int
times : int -> int -> int
minus : int -> int -> int
divide : int -> int -> int
```

## Bonus question

The hidden prelude (so-called `prepare.ml`) contains a predicate

`mystere_int : int -> bool`

so could you find for which `n` we get `mystere_int n = true` ?
2 changes: 2 additions & 0 deletions demo-repository/exercises/demo3/demo/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"learnocaml_version":"1","kind":"exercise","stars":0,
"title":"Demo exercise (in a sub-directory)"}
2 changes: 2 additions & 0 deletions demo-repository/exercises/demo3/demo/prelude.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
(* Some code is loaded in the toplevel before your code. *)
let greetings = "Hello world!"
1 change: 1 addition & 0 deletions demo-repository/exercises/demo3/demo/prepare.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let mystere_int n = n = 12345
4 changes: 4 additions & 0 deletions demo-repository/exercises/demo3/demo/solution.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
let plus = (+)
let times = ( * )
let minus = ( - )
let divide = ( / )
6 changes: 6 additions & 0 deletions demo-repository/exercises/demo3/demo/template.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

let plus x y = x + y ;;

let minus x y = y - x ;;

let times x y = x *
26 changes: 26 additions & 0 deletions demo-repository/exercises/demo3/demo/test.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
open Test_lib
open Report

let () =
set_result @@
ast_sanity_check code_ast @@ fun () ->
[ Section
([ Text "Function:" ; Code "plus" ],
test_function_2_against_solution
[%ty : int -> int -> int ] "plus"
[ (1, 1) ; (2, 2) ; (10, -10) ]) ;
Section
([ Text "Function:" ; Code "minus" ],
test_function_2_against_solution
[%ty : int -> int -> int ] "minus"
[ (1, 1) ; (4, -2) ; (0, 10) ]) ;
Section
([ Text "Function:" ; Code "times" ],
test_function_2_against_solution
[%ty : int -> int -> int ] "times"
[ (1, 3) ; (2, 4) ; (3, 0) ]) ;
Section
([ Text "Function:" ; Code "divide" ],
test_function_2_against_solution
[%ty : int -> int -> int ] "divide"
[ (12, 4) ; (12, 5) ; (3, 0) ]) ]
2 changes: 1 addition & 1 deletion demo-repository/exercises/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"groups":
{ "demo":
{ "title": "Demo exercise pack",
"exercises": [ "demo", "demo2" ] } } }
"exercises": [ "demo", "demo2", "demo3/demo" ] } } }
63 changes: 63 additions & 0 deletions src/grader/cmo_builder.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
(* This file is part of Learn-OCaml.
*
* Copyright (C) 2021-2022 OCaml Software Foundation.
*
* Learn-OCaml is distributed under the terms of the MIT license. See the
* included LICENSE file for details. *)

open Lwt

let re_given = "^given_[0-9A-Za-z_]+$"

let check_given str =
Str.string_match (Str.regexp re_given) str 0

let re_shortid_ascii = "^[0-9A-Za-z_-]+$"

let check_shortid_ascii id =
Str.string_match (Str.regexp re_shortid_ascii) id 0

let given_of_shortid id =
if not (check_shortid_ascii id) then
Format.printf "Warning: Short id '%s' does not match '%s'."
id re_shortid_ascii;
Str.global_replace (Str.regexp "_") "_0" id
|> Str.global_replace (Str.regexp "-") "_1"
|> fun s -> "given_" ^ s

let shortid_of_given str =
if not (check_given str) then
failwith
(Format.sprintf "Incorrect given prefix: '%s' does not match '%s'."
str re_given);
Str.replace_first (Str.regexp "^given_") "" str
|> Str.global_replace (Str.regexp "_1") "-"
|> Str.global_replace (Str.regexp "_0") "_"

let compile_given exo json_path =
(* let subdir = "gen.learn-ocaml" in *)
let dir, base = Filename.dirname json_path, Filename.basename json_path in
let given =
Str.replace_first (Str.regexp "\\.json$") "" base
|> given_of_shortid in
let output_prefix = Filename.concat dir given in
let output_ml = Filename.concat dir (given ^ ".ml") in
let prelude = Learnocaml_exercise.(decipher File.prelude exo) in
let prepare = Learnocaml_exercise.(decipher File.prepare exo) in
let both = prelude ^ " ;;\n" ^ prepare in
let write filename str =
Lwt_io.with_file ~mode:Lwt_io.Output filename
(fun oc -> Lwt_io.write oc str) in
let compile source_file output_prefix =
let orig = !Clflags.binary_annotations in
Clflags.binary_annotations := true;
Compile.implementation ~start_from:Clflags.Compiler_pass.Parsing
~source_file ~output_prefix;
Clflags.binary_annotations := orig in
(* minor detail: display the basename "given_exo.ml" instead of subdir/exo *)
Format.printf "%-24s (build .cmo)@." given;
Lwt_utils.mkdir_p dir >>= fun () ->
write output_ml both >>= fun () ->
Lwt.return (compile output_ml output_prefix) >>= fun () ->
(* then overwrite to remove prepare.ml *)
write output_ml prelude
27 changes: 27 additions & 0 deletions src/grader/cmo_builder.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
(* This file is part of Learn-OCaml.
*
* Copyright (C) 2021-2022 OCaml Software Foundation.
*
* Learn-OCaml is distributed under the terms of the MIT license. See the
* included LICENSE file for details. *)

(** Return [true] if the string matches [^[0-9A-Za-z_-]+$] *)
val check_shortid_ascii: string -> bool

(** Return [true] if the string matches [^given_[0-9A-Za-z_]+$] *)
val check_given: string -> bool

(** Escape "-" and "_" & Prepend "given_".
Emit a warning if [check_shortid_ascii] doesn't hold on the input string. *)
val given_of_shortid: string -> string

(** Remove "given_" prefix & Unescape "-" and "_".
Raise a [Failure] if [check_given] doesn't hold on the input string. *)
val shortid_of_given: string -> string

(** Take an exercise and a json filename "./www/exercises/part/exo.json",
compile [prelude ^ " ;;\n" ^ prepare]
to "./www/exercises/part/given_exo.{cmi,cmo,cmt}",
and write prelude (even if that last step is somewhat redundant)
to "./www/exercises/part/given_exo.ml" *)
val compile_given : Learnocaml_exercise.t -> string -> unit Lwt.t
10 changes: 10 additions & 0 deletions src/grader/dune
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,15 @@
(preprocess (per_module ((pps ppx_ocplib_i18n learnocaml_ppx_metaquot) Grading)))
)

(library
(name cmo_builder)
(wrapped false)
(modes byte)
(libraries compiler-libs.bytecomp
lwt_utils
learnocaml_data)
(modules Cmo_builder)
)

(library
(name grading_cli)
Expand All @@ -185,6 +194,7 @@
ocplib-ocamlres
ezjsonm
lwt_utils
cmo_builder
learnocaml_report
learnocaml_data)
(modules Grading_cli Grader_cli)
Expand Down
11 changes: 11 additions & 0 deletions src/grader/grader_cli.ml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* Learn-OCaml is distributed under the terms of the MIT license. See the
* included LICENSE file for details. *)

let build_cmo = ref false
let build_grade = ref true
let display_std_outputs = ref false
let dump_outputs = ref None
let dump_reports = ref None
Expand Down Expand Up @@ -48,6 +50,15 @@ let read_student_file exercise_dir path =
Lwt_io.with_file ~mode:Lwt_io.Input fn Lwt_io.read

let grade ?(print_result=false) ?dirname meta exercise output_json =
(if !build_cmo then
match output_json with
| Some json_path ->
(* Note: fails if the prelude/prepare don't compile *)
Cmo_builder.compile_given exercise json_path
| None ->
Lwt_io.eprintf "[Warning] json_path = None; please report.@."
else Lwt.return_unit) >>= fun () ->
if not !build_grade then Lwt.return (Ok ()) else
Lwt.catch
(fun () ->
let code_to_grade = match !grade_student with
Expand Down
21 changes: 21 additions & 0 deletions src/grader/grader_cli.mli
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@

(** {2 Configuration options} *)

(** Should should generate exo.{cmi,cmo,cmt} for [prelude ^ " ;;\n" ^ prepare].

This experimental option can be set from command-line
(explicit mode: learn-ocaml build [--enable-cmo-build|--disable-cmo-build])
and thereby regenerate [*.cm*] files unconditionally.
Otherwise (implicit mode), it is on by default for changed exercises only.

This feature is useful for learn-ocaml.el but optional (it can be disabled)
yet it may become mandatory (not an option anymore) if/when the js_of_ocaml
frontend also fetches [*.cmo] binaries for [{solution,test}.ml].

Note: the initial value of this [bool ref] (in [*.ml]) is a dummy value. *)
val build_cmo: bool ref

(** Grade the exercise (to be set to [false] if the exercise did not change).

Use case: if we run [learn-ocaml build --enable-cmo-build] (explicit mode)
on an unchanged exercise, we will have [!build_cmo && not !build_grade], so
the [*.cmo] will be built, but the grading will be skipped. *)
val build_grade: bool ref

(** Should stdout / stderr of the grader be echoed *)
val display_std_outputs: bool ref

Expand Down
25 changes: 18 additions & 7 deletions src/main/learnocaml_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ module Args = struct
Some false, info ["disable-"^opt] ~doc:("Disable "^doc);
]

(* See also the comments at the beginning of file [grader/grader_cli.mli] *)
let build_cmo = enable "cmo-build"
"the build of *.{cmi,cmo,cmt} for each repository exercise \
(experimental option; build changed exercises by default; \
'$(b,--enable-cmo-build)' will also build un-changed exercises)"

let try_ocaml = enable "tryocaml"
"the 'TryOCaml' tab (enabled by default if the repository contains a \
$(i,tutorials) directory)"
Expand All @@ -187,10 +193,12 @@ module Args = struct

let jobs =
value & opt int 1 & info ["jobs";"j"] ~docv:"INT" ~doc:
"Number of building jobs to run in parallel"
"Number of building jobs to run in parallel \
(only accept '$(b,--jobs=1)' for now, cf. issue #414)."

type t = {
contents_dir: string;
build_cmo: bool option;
try_ocaml: bool option;
lessons: bool option;
exercises: bool option;
Expand All @@ -201,10 +209,10 @@ module Args = struct

let builder_conf =
let apply
contents_dir try_ocaml lessons exercises playground toplevel base_url
= { contents_dir; try_ocaml; lessons; exercises; playground; toplevel; base_url }
contents_dir build_cmo try_ocaml lessons exercises playground toplevel base_url
= { contents_dir; build_cmo; try_ocaml; lessons; exercises; playground; toplevel; base_url }
in
Term.(const apply $contents_dir $try_ocaml $lessons $exercises $playground $toplevel $base_url)
Term.(const apply $contents_dir $build_cmo $try_ocaml $lessons $exercises $playground $toplevel $base_url)

let repo_conf =
let apply repo_dir exercises_filtered jobs =
Expand All @@ -216,8 +224,10 @@ module Args = struct
repo_dir/"tutorials";
Learnocaml_process_playground_repository.playground_dir :=
repo_dir/"playground";
Learnocaml_process_exercise_repository.n_processes := jobs;
()
Learnocaml_process_exercise_repository.n_processes := 1;
if jobs >= 2
then failwith "only accept --jobs=1 for now (cf. issue #414)"
else ()
in
Term.(const apply $repo_dir $exercises_filtered $jobs)

Expand Down Expand Up @@ -361,7 +371,8 @@ let main o =
(fun _ -> Learnocaml_process_playground_repository.main (o.app_dir))
>>= fun playground_ret ->
if_enabled o.builder.Builder.exercises (o.repo_dir/"exercises")
(fun _ -> Learnocaml_process_exercise_repository.main (o.app_dir))
(fun _ ->
Learnocaml_process_exercise_repository.main o.builder.Builder.build_cmo o.app_dir)
>>= fun exercises_ret ->
Lwt_io.with_file ~mode:Lwt_io.Output (o.app_dir/"js"/"learnocaml-config.js")
(fun oc ->
Expand Down
2 changes: 2 additions & 0 deletions src/repo/dune
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
lwt_utils
omd
markup
compiler-libs
cmo_builder
grading_cli
learnocaml_repository
learnocaml_store
Expand Down
34 changes: 22 additions & 12 deletions src/repo/learnocaml_process_exercise_repository.ml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ let spawn_grader
| Unix.WEXITED 0 -> Lwt.return (Ok ())
| _ -> Lwt.return (Error (-1))

let main dest_dir =
let main build_cmo dest_dir =
let exercises_index =
match !exercises_index with
| Some exercises_index -> exercises_index
Expand Down Expand Up @@ -217,18 +217,21 @@ let main dest_dir =
changed, dump_outputs, dump_reports) :: acc)
all_exercises [] in
begin
let listmap, grade =
let listmap, compile_grade =
if !n_processes = 1 then
Lwt_list.map_s,
fun dump_outputs dump_reports ?print_result ?dirname
meta exercise json_path ->
Grader_cli.dump_outputs := dump_outputs;
Grader_cli.dump_reports := dump_reports;
Grader_cli.grade ?print_result ?dirname meta exercise json_path
>|= fun r -> print_grader_error exercise r; r
fun (build_cmo, build_grade) dump_outputs dump_reports ?print_result ?dirname
meta exercise json_path ->
Grader_cli.build_cmo := build_cmo;
Grader_cli.build_grade := build_grade;
Grader_cli.dump_outputs := dump_outputs;
Grader_cli.dump_reports := dump_reports;
Grader_cli.grade ?print_result ?dirname meta exercise json_path
>|= fun r -> print_grader_error exercise r; r
else
let () = failwith "only accept --jobs=1 for now (cf. issue #414)" in
Lwt_list.map_p,
spawn_grader
fun (_todo: bool * bool) -> spawn_grader
in
listmap (fun (id, ex_dir, exercise, json_path, changed, dump_outputs,dump_reports) ->
let dst_ex_dir = String.concat Filename.dir_sep [dest_dir; "static"; id] in
Expand All @@ -240,15 +243,22 @@ let main dest_dir =
Lwt_utils.copy_tree d dst
else Lwt.return_unit)
(Lwt_unix.files_of_directory ex_dir) >>= fun () ->
if not changed then begin
let build_cmo =
match build_cmo with
| None -> changed (* by default *)
| Some explicit -> explicit in
if not changed && not build_cmo then begin
Format.printf "%-24s (no changes)@." id ;
Lwt.return true
end else begin
grade dump_outputs dump_reports
compile_grade (build_cmo, changed) dump_outputs dump_reports
~dirname:(!exercises_dir / id) (Index.find index id) exercise (Some json_path)
>>= function
| Ok () ->
Format.printf "%-24s [OK]@." id ;
if not changed then
Format.printf "%-24s (no changes)@." id
else
Format.printf "%-24s [OK]@." id ;
Lwt.return true
| Error _ ->
Format.printf "%-24s [FAILED]@." id ;
Expand Down
4 changes: 2 additions & 2 deletions src/repo/learnocaml_process_exercise_repository.mli
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ val n_processes: int ref

(** Main *)

(** [dest_dir] -> success *)
val main: string -> bool Lwt.t
(** [build_cmo] -> [dest_dir] -> success *)
val main: bool option -> string -> bool Lwt.t
2 changes: 1 addition & 1 deletion tests/runtests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ run_server () {
red "PROBLEM, server is not running.\\n"

red "LS:"
ls -Rl "$dir"
ls -Rl "$srcdir/$dir"
echo ""

red "LOGS:"
Expand Down