Skip to content

Commit b4f4f58

Browse files
authored
Merge pull request #446 from panglesd/exec-included-blocks
Allow execution of included OCaml code blocks
2 parents 266baf0 + befa05f commit b4f4f58

File tree

12 files changed

+201
-68
lines changed

12 files changed

+201
-68
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#### Added
44

55
- Handle the error-blocks syntax (#439, @jonludlam, @gpetiot)
6+
- Allow execution of included OCaml code blocks (#446, @panglesd)
67

78
#### Fixed
89

lib/block.ml

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ let locate_errors ~loc r =
3030
(fun l -> List.map (fun (`Msg m) -> `Msg (locate_error_msg ~loc m)) l)
3131
r
3232

33+
module OCaml_kind = struct
34+
type t = Impl | Intf
35+
36+
let infer_from_file file =
37+
match Filename.(remove_extension (basename file), extension file) with
38+
| _, (".ml" | ".mlt" | ".eliom") -> Some Impl
39+
| _, (".mli" | ".eliomi") -> Some Intf
40+
| _ -> None
41+
end
42+
3343
module Header = struct
3444
type t = Shell of [ `Sh | `Bash ] | OCaml | Other of string
3545

@@ -85,7 +95,13 @@ type ocaml_value = {
8595
}
8696

8797
type toplevel_value = { env : Ocaml_env.t; non_det : Label.non_det option }
88-
type include_ocaml_file = { part_included : string option }
98+
99+
type include_ocaml_file = {
100+
part_included : string option;
101+
ocaml_value : ocaml_value option;
102+
kind : OCaml_kind.t;
103+
}
104+
89105
type include_other_file = { header : Header.t option }
90106

91107
type include_file_kind =
@@ -118,6 +134,12 @@ type t = {
118134
value : value;
119135
}
120136

137+
let get_ocaml_value t =
138+
match t.value with
139+
| OCaml ocaml_value -> Some ocaml_value
140+
| Include { file_kind = Fk_ocaml { ocaml_value; _ }; _ } -> ocaml_value
141+
| _ -> None
142+
121143
let dump_section = Fmt.(Dump.pair int string)
122144

123145
let header t =
@@ -191,24 +213,22 @@ let pp_error ?syntax ?delim ppf outputs =
191213
outputs err_delim
192214
| _ -> ()
193215

194-
let has_output t =
195-
match t.value with
196-
| OCaml { errors = []; _ } -> false
197-
| OCaml { errors = _; _ } -> true
216+
let has_errors t =
217+
match get_ocaml_value t with
218+
| Some { errors = _ :: _; _ } -> true
198219
| _ -> false
199220

200221
let pp_value ?syntax ppf t =
201222
let delim = t.delim in
202-
match t.value with
203-
| OCaml { errors = []; _ } -> ()
204-
| OCaml { errors; _ } ->
223+
match get_ocaml_value t with
224+
| Some { errors; _ } ->
205225
let errors = error_padding errors in
206226
pp_error ?syntax ?delim ppf errors
207227
| _ -> ()
208228

209229
let pp_footer ?syntax ppf t =
210230
let delim =
211-
if has_output t then (
231+
if has_errors t then (
212232
pp_value ?syntax ppf t;
213233
None)
214234
else t.delim
@@ -379,13 +399,16 @@ let get_block_config l =
379399
file_inc = get_label (function File x -> Some x | _ -> None) l;
380400
}
381401

402+
let mk_ocaml_value env non_det errors header =
403+
{ env = Ocaml_env.mk env; non_det; errors; header }
404+
382405
let mk_ocaml ~loc ~config ~header ~contents ~errors =
383406
let kind = "OCaml" in
384407
match config with
385408
| { file_inc = None; part = None; env; non_det; _ } -> (
386409
(* TODO: why does this call guess_ocaml_kind when infer_block already did? *)
387410
match guess_ocaml_kind contents with
388-
| `Code -> Ok (OCaml { env = Ocaml_env.mk env; non_det; errors; header })
411+
| `Code -> Ok (OCaml (mk_ocaml_value env non_det errors header))
389412
| `Toplevel ->
390413
loc_error ~loc "toplevel syntax is not allowed in OCaml blocks.")
391414
| { file_inc = Some _; _ } -> label_not_allowed ~loc ~label:"file" ~kind
@@ -423,23 +446,38 @@ let mk_toplevel ~loc ~config ~contents ~errors =
423446
let mk_include ~loc ~config ~header ~errors =
424447
let kind = "include" in
425448
match config with
426-
| { file_inc = Some file_included; part; non_det = None; env = None; _ } -> (
427-
let* () = check_no_errors ~loc errors in
428-
match header with
429-
| Some Header.OCaml ->
430-
let file_kind = Fk_ocaml { part_included = part } in
449+
| { file_inc = Some file_included; part; non_det; env; _ } -> (
450+
let kind =
451+
match header with
452+
| Some Header.OCaml -> `OCaml
453+
| None -> (
454+
match OCaml_kind.infer_from_file file_included with
455+
| Some _ -> `OCaml
456+
| None -> `Other)
457+
| _ -> `Other
458+
in
459+
match kind with
460+
| `OCaml ->
461+
let kind =
462+
Util.Option.value ~default:OCaml_kind.Impl
463+
(OCaml_kind.infer_from_file file_included)
464+
in
465+
let part_included = part in
466+
let ocaml_value =
467+
match kind with
468+
| Impl -> Some (mk_ocaml_value env non_det errors header)
469+
| Intf -> None
470+
in
471+
let file_kind = Fk_ocaml { part_included; ocaml_value; kind } in
431472
Ok (Include { file_included; file_kind })
432-
| _ -> (
473+
| `Other -> (
433474
match part with
434475
| None ->
435476
let file_kind = Fk_other { header } in
436477
Ok (Include { file_included; file_kind })
437478
| Some _ ->
438479
label_not_allowed ~loc ~label:"part" ~kind:"non-OCaml include"))
439480
| { file_inc = None; _ } -> label_required ~loc ~label:"file" ~kind
440-
| { non_det = Some _; _ } ->
441-
label_not_allowed ~loc ~label:"non-deterministic" ~kind
442-
| { env = Some _; _ } -> label_not_allowed ~loc ~label:"env" ~kind
443481

444482
let infer_block ~loc ~config ~header ~contents ~errors =
445483
match config with
@@ -524,12 +562,18 @@ let from_raw raw =
524562
~delim:None
525563

526564
let is_active ?section:s t =
527-
let active =
565+
let active_section =
528566
match s with
529567
| Some p -> (
530568
match t.section with
531569
| Some s -> Re.execp (Re.Perl.compile_pat p) (snd s)
532570
| None -> Re.execp (Re.Perl.compile_pat p) "")
533571
| None -> true
534572
in
535-
active && t.version_enabled && t.os_type_enabled && not t.skip
573+
let can_update_content =
574+
match t.value with
575+
(* include blocks are always updated even if not executed *)
576+
| Include _ -> true
577+
| _ -> not t.skip
578+
in
579+
active_section && t.version_enabled && t.os_type_enabled && can_update_content

lib/block.mli

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
(** Code blocks headers. *)
1818

19+
module OCaml_kind : sig
20+
type t = Impl | Intf
21+
end
22+
1923
module Header : sig
2024
type t = Shell of [ `Sh | `Bash ] | OCaml | Other of string
2125

@@ -47,6 +51,8 @@ type include_ocaml_file = {
4751
part_included : string option;
4852
(** [part_included] is the part of the file to synchronize with.
4953
If lines is not specified synchronize the whole file. *)
54+
ocaml_value : ocaml_value option;
55+
kind : OCaml_kind.t;
5056
}
5157

5258
type include_other_file = { header : Header.t option }

lib/test/mdx_test.ml

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,19 @@ let rec remove_padding ?(front = true) = function
190190
let xs = remove_padding ~front xs in
191191
x :: xs
192192

193-
let update_ocaml ~errors = function
194-
| { Block.value = OCaml v; _ } as b ->
195-
{ b with value = OCaml { v with errors } }
196-
(* [eval_ocaml] only called on OCaml blocks *)
193+
let update_errors ~errors t =
194+
let update_ocaml_value (ov : Block.ocaml_value) = { ov with errors } in
195+
match t.Block.value with
196+
| OCaml v -> { t with value = OCaml (update_ocaml_value v) }
197+
| Include
198+
({ file_kind = Fk_ocaml ({ ocaml_value = Some v; _ } as fk); _ } as i) ->
199+
let ocaml_value = Some (update_ocaml_value v) in
200+
let file_kind = Block.Fk_ocaml { fk with ocaml_value } in
201+
{ t with value = Include { i with file_kind } }
202+
| _ -> assert false
203+
204+
let update_include ~contents = function
205+
| { Block.value = Include _; _ } as b -> { b with contents }
197206
| _ -> assert false
198207

199208
let rec error_padding = function
@@ -206,7 +215,7 @@ let rec error_padding = function
206215
let contains_warnings l =
207216
String.is_prefix ~affix:"Warning" l || String.is_infix ~affix:"\nWarning" l
208217

209-
let eval_ocaml ~(block : Block.t) ?syntax ?root c ppf errors =
218+
let eval_ocaml ~(block : Block.t) ?root c errors =
210219
let cmd = block.contents |> remove_padding in
211220
let error_lines =
212221
match eval_test ?root ~block c cmd with
@@ -229,8 +238,7 @@ let eval_ocaml ~(block : Block.t) ?syntax ?root c ppf errors =
229238
| `Output x -> `Output (ansi_color_strip x))
230239
(Output.merge output errors)
231240
in
232-
let updated_block = update_ocaml ~errors block in
233-
Block.pp ?syntax ppf updated_block
241+
update_errors ~errors block
234242

235243
let lines = function Ok x | Error x -> x
236244

@@ -278,9 +286,12 @@ let read_part file part =
278286
(match part with None -> "" | Some p -> p)
279287
file
280288
| Some lines ->
289+
(* in any [string] element of lines, there might be newlines. *)
281290
let contents = String.concat ~sep:"\n" lines in
282291
String.drop contents ~rev:true ~sat:Char.Ascii.is_white
283292
|> String.drop ~sat:(function '\n' -> true | _ -> false)
293+
|> (fun contents -> "\n" ^ contents ^ "\n")
294+
|> String.cuts ~sep:"\n"
284295

285296
let write_parts ~force_output file parts =
286297
let output_file = file ^ ".corrected" in
@@ -292,18 +303,13 @@ let write_parts ~force_output file parts =
292303
flush oc;
293304
close_out oc
294305

295-
let update_block_content ?syntax ppf t content =
296-
Block.pp_header ?syntax ppf t;
297-
Fmt.string ppf "\n";
298-
Output.pp ppf (`Output content);
299-
Fmt.string ppf "\n";
300-
Block.pp_footer ?syntax ppf t
301-
302-
let update_file_or_block ?syntax ?root ppf md_file ml_file block part =
306+
let update_file_or_block ?root md_file ml_file block part =
303307
let root = root_dir ?root ~block () in
304308
let dir = Filename.dirname md_file in
305309
let ml_file = resolve_root ml_file dir root in
306-
update_block_content ?syntax ppf block (read_part ml_file part)
310+
let contents = read_part ml_file part in
311+
let new_block = update_include ~contents block in
312+
new_block
307313

308314
exception Test_block_failure of Block.t * string
309315

@@ -337,26 +343,44 @@ let run_exn ~non_deterministic ~silent_eval ~record_backtrace ~syntax ~silent
337343
in
338344
let preludes = preludes ~prelude ~prelude_str in
339345

346+
let run_ocaml_value t Block.{ env; non_det; errors; header = _; _ } =
347+
let det () =
348+
Mdx_top.in_env env (fun () -> eval_ocaml ~block:t ?root c errors)
349+
in
350+
with_non_det non_deterministic non_det
351+
~on_skip_execution:(fun () -> t)
352+
~on_keep_old_output:det ~on_evaluation:det
353+
in
354+
340355
let test_block ~ppf ~temp_file t =
341356
let print_block () = Block.pp ?syntax ppf t in
342357
if Block.is_active ?section t then
343358
match Block.value t with
344359
| Raw _ -> print_block ()
345-
| Include { file_included; file_kind = Fk_ocaml { part_included } } ->
360+
| Include
361+
{
362+
file_included;
363+
file_kind = Fk_ocaml { part_included; ocaml_value; _ };
364+
} ->
346365
assert (syntax <> Some Cram);
347-
update_file_or_block ?syntax ?root ppf file file_included t
348-
part_included
349-
| Include { file_included; file_kind = Fk_other _ } ->
350-
let new_content = read_part file_included None in
351-
update_block_content ?syntax ppf t new_content
352-
| OCaml { non_det; env; errors; header = _ } ->
353-
let det () =
354-
assert (syntax <> Some Cram);
355-
Mdx_top.in_env env (fun () ->
356-
eval_ocaml ~block:t ?syntax ?root c ppf errors)
366+
let new_block =
367+
update_file_or_block ?root file file_included t part_included
357368
in
358-
with_non_det non_deterministic non_det ~on_skip_execution:print_block
359-
~on_keep_old_output:det ~on_evaluation:det
369+
let updated_block =
370+
match ocaml_value with
371+
(* including without executing *)
372+
| Some _ when t.skip -> new_block
373+
| Some ocaml_value -> run_ocaml_value new_block ocaml_value
374+
| _ -> new_block
375+
in
376+
Block.pp ?syntax ppf updated_block
377+
| Include { file_included; file_kind = Fk_other _ } ->
378+
let contents = read_part file_included None in
379+
let new_block = update_include ~contents t in
380+
Block.pp ?syntax ppf new_block
381+
| OCaml ov ->
382+
let updated_block = run_ocaml_value t ov in
383+
Block.pp ?syntax ppf updated_block
360384
| Cram { language = _; non_det } ->
361385
let tests = Cram.of_lines t.contents in
362386
with_non_det non_deterministic non_det ~on_skip_execution:print_block

test/bin/mdx-test/expect/dune.inc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,18 @@
179179
(alias runtest)
180180
(action (diff errors/test-case.md errors.actual)))
181181

182+
(rule
183+
(target exec-include.actual)
184+
(deps (package mdx) (source_tree exec-include))
185+
(action
186+
(with-stdout-to %{target}
187+
(chdir exec-include
188+
(run ocaml-mdx test --output - test-case.md)))))
189+
190+
(rule
191+
(alias runtest)
192+
(action (diff exec-include/test-case.md exec-include.actual)))
193+
182194
(rule
183195
(target exit.actual)
184196
(deps (package mdx) (source_tree exit))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
(* $MDX part-begin=OK *)
2+
let f x = x + 1
3+
(* $MDX part-end *)
4+
5+
(* $MDX part-begin=KO *)
6+
let k = x = 1
7+
(* $MDX part-end *)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
val f : int -> int
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
.mli files are included but not executed:
2+
<!-- $MDX file=code.mli -->
3+
```ocaml
4+
val f : int -> int
5+
```
6+
7+
.mli files are still included when `skip` is used:
8+
<!-- $MDX file=code.mli,skip -->
9+
```ocaml
10+
val f : int -> int
11+
```
12+
13+
.ml files are included and executed:
14+
<!-- $MDX file=code.ml,part=OK -->
15+
```ocaml
16+
let f x = x + 1
17+
```
18+
19+
<!-- $MDX file=code.ml,part=KO -->
20+
```ocaml
21+
let k = x = 1
22+
```
23+
```mdx-error
24+
Line 1, characters 9-10:
25+
Error: Unbound value x
26+
```
27+
28+
29+
.ml files are still included but no longer executed when `skip` is used:
30+
<!-- $MDX file=code.ml,part=OK,skip -->
31+
```ocaml
32+
let f x = x + 1
33+
```
34+
35+
<!-- $MDX file=code.ml,part=KO,skip -->
36+
```ocaml
37+
let k = x = 1
38+
```

0 commit comments

Comments
 (0)