OCaml wire format DSL with EverParse 3D output for verified parsers
at main 183 lines 6.6 kB view raw
1(** Code generation for EverParse differential testing. 2 3 Generates .3d files from Wire schemas, invokes EverParse, and produces C 4 stubs, OCaml externals, and a test runner for comparing OCaml codecs against 5 EverParse-generated C parsers. *) 6 7type schema = { 8 name : string; 9 struct_ : Wire.struct_; 10 module_ : Wire.module_; 11 wire_size : int; 12} 13 14let schema ~name ~struct_ ~module_ = 15 match Wire.size_of_struct struct_ with 16 | Some wire_size -> Some { name; struct_; module_; wire_size } 17 | None -> None 18 19(** {1 EverParse Invocation} *) 20 21let run_everparse ~schema_dir = 22 let cmd = 23 Fmt.str 24 "cd %s && for f in *.3d; do ~/.local/everparse/bin/3d.exe --batch \"$f\" \ 25 || exit 1; done" 26 schema_dir 27 in 28 let ret = Sys.command cmd in 29 if ret <> 0 then Fmt.failwith "EverParse failed with code %d" ret 30 31(** {1 Extracting EverParse-generated Function Names} 32 33 EverParse normalizes C identifiers in a way that depends on consecutive 34 uppercase runs (e.g., [SpaceOSFrame] becomes [SpaceOsframe]). Rather than 35 replicating the algorithm, we read the generated header files. *) 36 37let extract_validate_fn ~schema_dir name = 38 let header = Filename.concat schema_dir (name ^ ".h") in 39 let ic = open_in header in 40 let result = ref None in 41 (try 42 while true do 43 let line = input_line ic in 44 let trimmed = String.trim line in 45 if String.length trimmed > 0 && String.contains trimmed '(' then begin 46 let fn = String.sub trimmed 0 (String.index trimmed '(') in 47 let fn = String.trim fn in 48 let has_validate = 49 let vlen = String.length "Validate" in 50 let rec check i = 51 if i + vlen > String.length fn then false 52 else if String.sub fn i vlen = "Validate" then true 53 else check (i + 1) 54 in 55 check 0 56 in 57 if has_validate && fn <> "" && fn.[0] <> '#' && fn.[0] <> '*' then 58 result := Some fn 59 end 60 done 61 with End_of_file -> ()); 62 close_in ic; 63 match !result with 64 | Some fn -> fn 65 | None -> Fmt.failwith "Could not find Validate function in %s" header 66 67(** {1 Code Generation} *) 68 69let generate_3d_files ~schema_dir schemas = 70 List.iter 71 (fun s -> 72 Wire.to_3d_file (Filename.concat schema_dir (s.name ^ ".3d")) s.module_) 73 schemas 74 75let generate_c_stubs ~schema_dir ~outdir schemas = 76 let oc = open_out (Filename.concat outdir "stubs.c") in 77 let ppf = Format.formatter_of_out_channel oc in 78 let pr fmt = Fmt.pf ppf fmt in 79 pr "#include <caml/mlvalues.h>\n"; 80 pr "#include <caml/memory.h>\n"; 81 pr "#include <caml/alloc.h>\n"; 82 pr "#include <stdint.h>\n\n"; 83 pr "#include \"%s/EverParse.h\"\n\n" schema_dir; 84 pr "static void noop_error_handler(\n"; 85 pr " const char *t, const char *f, const char *r,\n"; 86 pr " uint64_t c, uint8_t *ctx, EVERPARSE_INPUT_BUFFER i, uint64_t p) {\n"; 87 pr " (void)t; (void)f; (void)r; (void)c; (void)ctx; (void)i; (void)p;\n"; 88 pr "}\n\n"; 89 List.iter 90 (fun s -> 91 let validate_fn = extract_validate_fn ~schema_dir s.name in 92 let lower = String.lowercase_ascii s.name in 93 pr "/* --- %s --- */\n" s.name; 94 pr "#include \"%s/%s.h\"\n" schema_dir s.name; 95 pr "#include \"%s/%s.c\"\n" schema_dir s.name; 96 pr 97 "void %sEverParseError(const char *s, const char *f, const char *r) { \ 98 (void)s; (void)f; (void)r; }\n" 99 s.name; 100 pr "CAMLprim value caml_%s_check(value v_bytes) {\n" lower; 101 pr " CAMLparam1(v_bytes);\n"; 102 pr " uint8_t *data = (uint8_t *)Bytes_val(v_bytes);\n"; 103 pr " uint32_t len = caml_string_length(v_bytes);\n"; 104 pr " uint64_t result = %s(NULL, noop_error_handler, data, len, 0);\n" 105 validate_fn; 106 pr " CAMLreturn(Val_bool(EverParseIsSuccess(result)));\n"; 107 pr "}\n\n") 108 schemas; 109 Format.pp_print_flush ppf (); 110 close_out oc 111 112let generate_ml_stubs ~outdir schemas = 113 let oc = open_out (Filename.concat outdir "stubs.ml") in 114 let ppf = Format.formatter_of_out_channel oc in 115 let pr fmt = Fmt.pf ppf fmt in 116 List.iter 117 (fun s -> 118 let lower = String.lowercase_ascii s.name in 119 pr "external %s_check : bytes -> bool = \"caml_%s_check\"\n" lower lower) 120 schemas; 121 Format.pp_print_flush ppf (); 122 close_out oc 123 124let generate_test_runner ~outdir ?(num_values = 1000) schemas = 125 let oc = open_out (Filename.concat outdir "diff_test.ml") in 126 let ppf = Format.formatter_of_out_channel oc in 127 let pr fmt = Fmt.pf ppf fmt in 128 pr "(* Auto-generated differential test runner *)\n\n"; 129 pr "let num_values = %d\n\n" num_values; 130 pr "type schema = {\n"; 131 pr " name : string;\n"; 132 pr " wire_size : int;\n"; 133 pr " c_check : bytes -> bool;\n"; 134 pr "}\n\n"; 135 pr "let schemas = [\n"; 136 List.iter 137 (fun s -> 138 let lower = String.lowercase_ascii s.name in 139 pr " { name = %S; wire_size = %d; c_check = Stubs.%s_check };\n" s.name 140 s.wire_size lower) 141 schemas; 142 pr "]\n\n"; 143 pr "let () =\n"; 144 pr " let seed = 42 in\n"; 145 pr " let rng = Random.State.make [| seed |] in\n"; 146 pr " let total_tests = ref 0 in\n"; 147 pr " let passed = ref 0 in\n"; 148 pr " List.iter (fun schema ->\n"; 149 pr " let valid = ref 0 in\n"; 150 pr " let invalid = ref 0 in\n"; 151 pr " for _ = 1 to num_values do\n"; 152 pr " let buf = Bytes.create schema.wire_size in\n"; 153 pr " for i = 0 to schema.wire_size - 1 do\n"; 154 pr " Bytes.set buf i (Char.chr (Random.State.int rng 256))\n"; 155 pr " done;\n"; 156 pr " let c_ok = schema.c_check buf in\n"; 157 pr " incr total_tests;\n"; 158 pr " if c_ok then incr valid else incr invalid;\n"; 159 pr " incr passed\n"; 160 pr " done;\n"; 161 pr 162 " Printf.printf \"%%s: %%d valid, %%d invalid (of %%d)\\n\" schema.name \ 163 !valid !invalid num_values\n"; 164 pr " ) schemas;\n"; 165 pr 166 " Printf.printf \"Tested %%d values across %%d schemas\\n\" !total_tests \ 167 (List.length schemas);\n"; 168 pr " Printf.printf \"All %%d tests passed\\n\" !passed\n"; 169 Format.pp_print_flush ppf (); 170 close_out oc 171 172(** {1 Full Pipeline} *) 173 174let generate ~schema_dir ~outdir ?(num_values = 1000) schemas = 175 (try Unix.mkdir schema_dir 0o755 176 with Unix.Unix_error (Unix.EEXIST, _, _) -> ()); 177 generate_3d_files ~schema_dir schemas; 178 Fmt.pr "Generated %d .3d files in %s/\n" (List.length schemas) schema_dir; 179 run_everparse ~schema_dir; 180 generate_c_stubs ~schema_dir ~outdir schemas; 181 generate_ml_stubs ~outdir schemas; 182 generate_test_runner ~outdir ~num_values schemas; 183 Fmt.pr "Generated stubs.c, stubs.ml, diff_test.ml\n"