Skip to content

Commit

Permalink
Move ocaml kind field to include blocks, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gpetiot committed Feb 12, 2024
1 parent 2d46880 commit c6e6906
Show file tree
Hide file tree
Showing 10 changed files with 115 additions and 48 deletions.
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,6 @@ The possible labels are:
- `env=ENV` -- see the "Named execution environments" section
- `set-VAR=VALUE` -- set an environment variable
- `unset-VAR` -- unset an environment variable
- `exec` -- execute this block. Only allowed when including an OCaml file.

#### Shell Scripts

Expand Down Expand Up @@ -252,8 +251,6 @@ These can then be included in the document:
```ocaml
```

Use the `exec` flag to execute as an OCaml block.

Non-OCaml files can also be read and included in a block:

<!-- $MDX file=any_file.txt -->
Expand Down
83 changes: 48 additions & 35 deletions lib/block.ml
Original file line number Diff line number Diff line change
Expand Up @@ -30,29 +30,36 @@ let locate_errors ~loc r =
(fun l -> List.map (fun (`Msg m) -> `Msg (locate_error_msg ~loc m)) l)
r

type ocaml_kind = Impl | Intf
module OCaml_kind = struct
type t = Impl | Intf

let infer_from_file file =
match Filename.(remove_extension (basename file), extension file) with
| _, (".ml" | ".mlt" | ".eliom") -> Some Impl
| _, (".mli" | ".eliomi") -> Some Intf
| _ -> None
end

module Header = struct
type t = Shell of [ `Sh | `Bash ] | OCaml of ocaml_kind | Other of string
type t = Shell of [ `Sh | `Bash ] | OCaml | Other of string

let pp ppf = function
| Shell `Sh -> Fmt.string ppf "sh"
| Shell `Bash -> Fmt.string ppf "bash"
| OCaml _ -> Fmt.string ppf "ocaml"
| OCaml -> Fmt.string ppf "ocaml"
| Other s -> Fmt.string ppf s

let of_string = function
| "" -> None
| "sh" -> Some (Shell `Sh)
| "bash" -> Some (Shell `Bash)
| "ocaml" -> Some (OCaml Impl)
| "ocaml" -> Some OCaml
| s -> Some (Other s)

let infer_from_file file =
match Filename.(remove_extension (basename file), extension file) with
| ("dune" | "dune-project"), _ -> Some (Other "scheme")
| _, (".ml" | ".mlt" | ".eliom") -> Some (OCaml Impl)
| _, (".mli" | ".eliomi") -> Some (OCaml Intf)
| _, (".ml" | ".mli" | ".mlt" | ".eliom" | ".eliomi") -> Some OCaml
| _, ".sh" -> Some (Shell `Sh)
| _ -> None
end
Expand Down Expand Up @@ -85,14 +92,14 @@ type ocaml_value = {
non_det : Label.non_det option;
errors : Output.t list;
header : Header.t option;
kind : ocaml_kind;
}

type toplevel_value = { env : Ocaml_env.t; non_det : Label.non_det option }

type include_ocaml_file = {
part_included : string option;
ocaml_value : ocaml_value option;
kind : OCaml_kind.t;
}

type include_other_file = { header : Header.t option }
Expand Down Expand Up @@ -140,8 +147,8 @@ let header t =
| Raw { header; _ } -> header
| OCaml { header; _ } -> header
| Cram { language; _ } -> Some (Header.Shell language)
| Toplevel _ -> Some (Header.OCaml Impl)
| Include { file_kind = Fk_ocaml _; _ } -> Some (Header.OCaml Impl)
| Toplevel _ -> Some Header.OCaml
| Include { file_kind = Fk_ocaml _; _ } -> Some Header.OCaml
| Include { file_kind = Fk_other b; _ } -> b.header

let dump_value ppf = function
Expand Down Expand Up @@ -206,7 +213,7 @@ let pp_error ?syntax ?delim ppf outputs =
outputs err_delim
| _ -> ()

let has_output t =
let has_errors t =
match get_ocaml_value t with
| Some { errors = _ :: _; _ } -> true
| _ -> false
Expand All @@ -221,7 +228,7 @@ let pp_value ?syntax ppf t =

let pp_footer ?syntax ppf t =
let delim =
if has_output t then (
if has_errors t then (
pp_value ?syntax ppf t;
None)
else t.delim
Expand Down Expand Up @@ -392,20 +399,20 @@ let get_block_config l =
file_inc = get_label (function File x -> Some x | _ -> None) l;
}

let mk_ocaml_value env non_det errors header ~kind =
{ env = Ocaml_env.mk env; non_det; errors; header; kind }
let mk_ocaml_value env non_det errors header =
{ env = Ocaml_env.mk env; non_det; errors; header }

let mk_ocaml ~loc ~config ~header ~contents ~errors ~kind =
let mk_ocaml ~loc ~config ~header ~contents ~errors =
let kind = "OCaml" in
match config with
| { file_inc = None; part = None; env; non_det; _ } -> (
(* TODO: why does this call guess_ocaml_kind when infer_block already did? *)
match guess_ocaml_kind contents with
| `Code -> Ok (OCaml (mk_ocaml_value env non_det errors header ~kind))
| `Code -> Ok (OCaml (mk_ocaml_value env non_det errors header))
| `Toplevel ->
loc_error ~loc "toplevel syntax is not allowed in OCaml blocks.")
| { file_inc = Some _; _ } ->
label_not_allowed ~loc ~label:"file" ~kind:"OCaml"
| { part = Some _; _ } -> label_not_allowed ~loc ~label:"part" ~kind:"OCaml"
| { file_inc = Some _; _ } -> label_not_allowed ~loc ~label:"file" ~kind
| { part = Some _; _ } -> label_not_allowed ~loc ~label:"part" ~kind

let mk_cram ~loc ?language ~config ~header ~errors () =
let kind = "shell" in
Expand Down Expand Up @@ -437,31 +444,40 @@ let mk_toplevel ~loc ~config ~contents ~errors =
| { part = Some _; _ } -> label_not_allowed ~loc ~label:"part" ~kind

let mk_include ~loc ~config ~header ~errors =
let kind = "include" in
match config with
| { file_inc = Some file_included; part; non_det; env; _ } -> (
let* () = check_no_errors ~loc errors in
match header with
| Some (Header.OCaml k) ->
let kind =
match header with
| Some Header.OCaml -> `OCaml
| None -> (
match OCaml_kind.infer_from_file file_included with
| Some _ -> `OCaml
| None -> `Other)
| _ -> `Other
in
match kind with
| `OCaml ->
let kind =
match Header.infer_from_file file_included with
| Some (Header.OCaml k') -> k'
| _ -> k
Util.Option.value ~default:OCaml_kind.Impl
(OCaml_kind.infer_from_file file_included)
in
let part_included = part in
let ocaml_value =
match kind with
| Impl -> Some (mk_ocaml_value env non_det errors header ~kind)
| Impl -> Some (mk_ocaml_value env non_det errors header)
| Intf -> None
in
let file_kind = Fk_ocaml { part_included = part; ocaml_value } in
let file_kind = Fk_ocaml { part_included; ocaml_value; kind } in
Ok (Include { file_included; file_kind })
| _ -> (
| `Other -> (
match part with
| None ->
let file_kind = Fk_other { header } in
Ok (Include { file_included; file_kind })
| Some _ ->
label_not_allowed ~loc ~label:"part" ~kind:"non-OCaml include"))
| { file_inc = None; _ } -> label_required ~loc ~label:"file" ~kind:"include"
| { file_inc = None; _ } -> label_required ~loc ~label:"file" ~kind

let infer_block ~loc ~config ~header ~contents ~errors =
match config with
Expand All @@ -470,9 +486,9 @@ let infer_block ~loc ~config ~header ~contents ~errors =
match header with
| Some (Header.Shell language) ->
mk_cram ~loc ~language ~config ~header ~errors ()
| Some (Header.OCaml kind) -> (
| Some Header.OCaml -> (
match guess_ocaml_kind contents with
| `Code -> mk_ocaml ~loc ~config ~header ~contents ~errors ~kind
| `Code -> mk_ocaml ~loc ~config ~header ~contents ~errors
| `Toplevel -> mk_toplevel ~loc ~config ~contents ~errors)
| _ ->
let* () =
Expand All @@ -488,7 +504,7 @@ let mk ~loc ~section ~labels ~legacy_labels ~header ~delim ~contents ~errors =
let config = get_block_config labels in
let* value =
match block_kind with
| Some OCaml -> mk_ocaml ~loc ~config ~header ~contents ~errors ~kind:Impl
| Some OCaml -> mk_ocaml ~loc ~config ~header ~contents ~errors
| Some Cram -> mk_cram ~loc ~config ~header ~errors ()
| Some Toplevel -> mk_toplevel ~loc ~config ~contents ~errors
| Some Include -> mk_include ~loc ~config ~header ~errors
Expand Down Expand Up @@ -554,7 +570,4 @@ let is_active ?section:s t =
| None -> Re.execp (Re.Perl.compile_pat p) "")
| None -> true
in
let executable =
match t.value with OCaml { kind = Intf; _ } -> false | _ -> true
in
active && t.version_enabled && t.os_type_enabled && (not t.skip) && executable
active && t.version_enabled && t.os_type_enabled && not t.skip
8 changes: 5 additions & 3 deletions lib/block.mli
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

(** Code blocks headers. *)

type ocaml_kind = Impl | Intf
module OCaml_kind : sig
type t = Impl | Intf
end

module Header : sig
type t = Shell of [ `Sh | `Bash ] | OCaml of ocaml_kind | Other of string
type t = Shell of [ `Sh | `Bash ] | OCaml | Other of string

val pp : Format.formatter -> t -> unit
val of_string : string -> t option
Expand All @@ -37,7 +39,6 @@ type ocaml_value = {
errors : Output.t list;
(** [header] defines whether a header was specified for the block. *)
header : Header.t option;
kind : ocaml_kind;
}

type toplevel_value = {
Expand All @@ -51,6 +52,7 @@ type include_ocaml_file = {
(** [part_included] is the part of the file to synchronize with.
If lines is not specified synchronize the whole file. *)
ocaml_value : ocaml_value option;
kind : OCaml_kind.t;
}

type include_other_file = { header : Header.t option }
Expand Down
2 changes: 1 addition & 1 deletion lib/mli_parser.ml
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ let make_block code_block file_contents =
Ok (header, language_label :: labels)
| None ->
(* If not specified, blocks are run as ocaml blocks *)
Ok (Some (OCaml Impl), [])
Ok (Some OCaml, [])
in
match handle_header code_block.Code_block.metadata with
| Error _ as e -> e
Expand Down
6 changes: 4 additions & 2 deletions lib/test/mdx_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,10 @@ let run_exn ~non_deterministic ~silent_eval ~record_backtrace ~syntax ~silent
match Block.value t with
| Raw _ -> print_block ()
| Include
{ file_included; file_kind = Fk_ocaml { part_included; ocaml_value } }
->
{
file_included;
file_kind = Fk_ocaml { part_included; ocaml_value; _ };
} ->
assert (syntax <> Some Cram);
let new_block =
update_file_or_block ?root file file_included t part_included
Expand Down
12 changes: 12 additions & 0 deletions test/bin/mdx-test/expect/dune.inc
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@
(alias runtest)
(action (diff errors/test-case.md errors.actual)))

(rule
(target exec-include.actual)
(deps (package mdx) (source_tree exec-include))
(action
(with-stdout-to %{target}
(chdir exec-include
(run ocaml-mdx test --output - test-case.md)))))

(rule
(alias runtest)
(action (diff exec-include/test-case.md exec-include.actual)))

(rule
(target exit.actual)
(deps (package mdx) (source_tree exit))
Expand Down
7 changes: 7 additions & 0 deletions test/bin/mdx-test/expect/exec-include/code.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
(* $MDX part-begin=OK *)
let f x = x + 1
(* $MDX part-end *)

(* $MDX part-begin=KO *)
let k = x = 1
(* $MDX part-end *)
1 change: 1 addition & 0 deletions test/bin/mdx-test/expect/exec-include/code.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
val f : int -> int
34 changes: 34 additions & 0 deletions test/bin/mdx-test/expect/exec-include/test-case.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.mli files are included but not executed:
<!-- $MDX file=code.mli -->
```ocaml
val f : int -> int
```

Use `skip` to not include the .mli files:
<!-- $MDX file=code.mli,skip -->
```ocaml
```

.ml files are included and executed:
<!-- $MDX file=code.ml,part=OK -->
```ocaml
let f x = x + 1
```

<!-- $MDX file=code.ml,part=KO -->
```ocaml
let k = x = 1
```
```mdx-error
Line 1, characters 9-10:
Error: Unbound value x
```

Use `skip` to not include the .ml files:
<!-- $MDX file=code.ml,part=OK,skip -->
```ocaml
```

<!-- $MDX file=code.ml,part=KO,skip -->
```ocaml
```
7 changes: 3 additions & 4 deletions test/lib/test_block.ml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ let test_infer_from_file =
make_test ~file:"dune" ~expected:(Some (Other "scheme"));
make_test ~file:"dune-project" ~expected:(Some (Other "scheme"));
make_test ~file:"foo.sh" ~expected:(Some (Shell `Sh));
make_test ~file:"foo/foo/foo.ml" ~expected:(Some (OCaml Impl));
make_test ~file:"foo/foo/foo.mli" ~expected:(Some (OCaml Intf));
make_test ~file:"foo/foo/foo.ml" ~expected:(Some OCaml);
]

let test_mk =
Expand All @@ -40,10 +39,10 @@ let test_mk =
in
[
make_test ~name:"invalid ocaml" ~labels:[ Block_kind OCaml ]
~header:(Some (OCaml Impl)) ~contents:[ "# let x = 2;;" ]
~header:(Some OCaml) ~contents:[ "# let x = 2;;" ]
~expected:(Error (`Msg "toplevel syntax is not allowed in OCaml blocks."));
make_test ~name:"invalid toplevel" ~labels:[ Block_kind Toplevel ]
~header:(Some (OCaml Impl)) ~contents:[ "let x = 2;;" ]
~header:(Some OCaml) ~contents:[ "let x = 2;;" ]
~expected:(Error (`Msg "invalid toplevel syntax in toplevel blocks."));
]

Expand Down

0 comments on commit c6e6906

Please sign in to comment.