this repo has no description

various

+1399
+412
doc/dark_repl.mld
··· 1 + {0 Building a REPL} 2 + 3 + @scrolly Building a REPL in OCaml 4 + {ol 5 + {li 6 + {b The Expression Type} 7 + 8 + A REPL evaluates expressions. We start with a tiny language: 9 + integer literals, addition, let-bindings, and variables. 10 + Four constructors is all we need. 11 + 12 + {[ 13 + type expr = 14 + | Lit of int 15 + | Add of expr * expr 16 + | Let of string * expr * expr 17 + | Var of string 18 + ]} 19 + } 20 + {li 21 + {b Values and Environments} 22 + 23 + Evaluation produces values. For now, just integers. An 24 + environment maps variable names to their values using a 25 + simple association list. 26 + 27 + {[ 28 + type expr = 29 + | Lit of int 30 + | Add of expr * expr 31 + | Let of string * expr * expr 32 + | Var of string 33 + 34 + type value = Int of int 35 + 36 + type env = (string * value) list 37 + 38 + let empty_env : env = [] 39 + 40 + let extend env name v = (name, v) :: env 41 + 42 + let lookup env name = 43 + match List.assoc_opt name env with 44 + | Some v -> v 45 + | None -> failwith ("unbound: " ^ name) 46 + ]} 47 + } 48 + {li 49 + {b The Evaluator} 50 + 51 + Pattern matching makes the evaluator beautifully direct. 52 + Each expression form maps to a straightforward computation. 53 + Let-bindings extend the environment for the body expression. 54 + 55 + {[ 56 + type expr = 57 + | Lit of int 58 + | Add of expr * expr 59 + | Let of string * expr * expr 60 + | Var of string 61 + 62 + type value = Int of int 63 + 64 + type env = (string * value) list 65 + 66 + let empty_env : env = [] 67 + 68 + let extend env name v = (name, v) :: env 69 + 70 + let lookup env name = 71 + match List.assoc_opt name env with 72 + | Some v -> v 73 + | None -> failwith ("unbound: " ^ name) 74 + 75 + let rec eval env = function 76 + | Lit n -> Int n 77 + | Add (a, b) -> 78 + let (Int x) = eval env a in 79 + let (Int y) = eval env b in 80 + Int (x + y) 81 + | Let (name, rhs, body) -> 82 + let v = eval env rhs in 83 + eval (extend env name v) body 84 + | Var name -> lookup env name 85 + ]} 86 + } 87 + {li 88 + {b A Tiny Tokenizer} 89 + 90 + To read user input, we need a tokenizer. It splits a string 91 + into meaningful chunks: numbers, identifiers, operators, and 92 + parentheses. Whitespace is consumed but not produced. 93 + 94 + {[ 95 + type expr = 96 + | Lit of int 97 + | Add of expr * expr 98 + | Let of string * expr * expr 99 + | Var of string 100 + 101 + type value = Int of int 102 + type env = (string * value) list 103 + let empty_env : env = [] 104 + let extend env name v = (name, v) :: env 105 + let lookup env name = 106 + match List.assoc_opt name env with 107 + | Some v -> v 108 + | None -> failwith ("unbound: " ^ name) 109 + 110 + let rec eval env = function 111 + | Lit n -> Int n 112 + | Add (a, b) -> 113 + let (Int x) = eval env a in 114 + let (Int y) = eval env b in 115 + Int (x + y) 116 + | Let (name, rhs, body) -> 117 + let v = eval env rhs in 118 + eval (extend env name v) body 119 + | Var name -> lookup env name 120 + 121 + type token = 122 + | TNum of int 123 + | TIdent of string 124 + | TPlus | TEqual 125 + | TLParen | TRParen 126 + | TLet | TIn 127 + 128 + let is_alpha c = 129 + (c >= 'a' && c <= 'z') 130 + || (c >= 'A' && c <= 'Z') 131 + || c = '_' 132 + 133 + let is_digit c = c >= '0' && c <= '9' 134 + 135 + let tokenize input = 136 + let len = String.length input in 137 + let pos = ref 0 in 138 + let tokens = ref [] in 139 + while !pos < len do 140 + let c = input.[!pos] in 141 + if c = ' ' || c = '\t' || c = '\n' then 142 + incr pos 143 + else if is_digit c then begin 144 + let start = !pos in 145 + while !pos < len && is_digit input.[!pos] do 146 + incr pos done; 147 + let s = String.sub input start (!pos - start) in 148 + tokens := TNum (int_of_string s) :: !tokens 149 + end else if is_alpha c then begin 150 + let start = !pos in 151 + while !pos < len && is_alpha input.[!pos] do 152 + incr pos done; 153 + let s = String.sub input start (!pos - start) in 154 + let tok = match s with 155 + | "let" -> TLet | "in" -> TIn 156 + | _ -> TIdent s in 157 + tokens := tok :: !tokens 158 + end else begin 159 + let tok = match c with 160 + | '+' -> TPlus | '=' -> TEqual 161 + | '(' -> TLParen | ')' -> TRParen 162 + | _ -> failwith "unexpected char" in 163 + tokens := tok :: !tokens; 164 + incr pos 165 + end 166 + done; 167 + List.rev !tokens 168 + ]} 169 + } 170 + {li 171 + {b The Parser} 172 + 173 + A recursive descent parser turns tokens into our expression AST. 174 + It handles operator precedence naturally: addition is parsed as 175 + a left-associative chain of atoms. 176 + 177 + {[ 178 + type expr = 179 + | Lit of int 180 + | Add of expr * expr 181 + | Let of string * expr * expr 182 + | Var of string 183 + 184 + type value = Int of int 185 + type env = (string * value) list 186 + let empty_env : env = [] 187 + let extend env name v = (name, v) :: env 188 + let lookup env name = 189 + match List.assoc_opt name env with 190 + | Some v -> v 191 + | None -> failwith ("unbound: " ^ name) 192 + 193 + let rec eval env = function 194 + | Lit n -> Int n 195 + | Add (a, b) -> 196 + let (Int x) = eval env a in 197 + let (Int y) = eval env b in 198 + Int (x + y) 199 + | Let (name, rhs, body) -> 200 + let v = eval env rhs in 201 + eval (extend env name v) body 202 + | Var name -> lookup env name 203 + 204 + type token = 205 + | TNum of int | TIdent of string 206 + | TPlus | TEqual 207 + | TLParen | TRParen 208 + | TLet | TIn 209 + 210 + let is_alpha c = 211 + (c >= 'a' && c <= 'z') 212 + || (c >= 'A' && c <= 'Z') || c = '_' 213 + let is_digit c = c >= '0' && c <= '9' 214 + 215 + let tokenize input = 216 + let len = String.length input in 217 + let pos = ref 0 in 218 + let tokens = ref [] in 219 + while !pos < len do 220 + let c = input.[!pos] in 221 + if c = ' ' || c = '\t' || c = '\n' then 222 + incr pos 223 + else if is_digit c then begin 224 + let start = !pos in 225 + while !pos < len && is_digit input.[!pos] 226 + do incr pos done; 227 + let s = String.sub input start 228 + (!pos - start) in 229 + tokens := TNum (int_of_string s) :: !tokens 230 + end else if is_alpha c then begin 231 + let start = !pos in 232 + while !pos < len && is_alpha input.[!pos] 233 + do incr pos done; 234 + let s = String.sub input start 235 + (!pos - start) in 236 + let tok = match s with 237 + | "let" -> TLet | "in" -> TIn 238 + | _ -> TIdent s in 239 + tokens := tok :: !tokens 240 + end else begin 241 + let tok = match c with 242 + | '+' -> TPlus | '=' -> TEqual 243 + | '(' -> TLParen | ')' -> TRParen 244 + | _ -> failwith "unexpected char" in 245 + tokens := tok :: !tokens; incr pos 246 + end 247 + done; 248 + List.rev !tokens 249 + 250 + let parse tokens = 251 + let toks = ref tokens in 252 + let next () = 253 + match !toks with 254 + | [] -> failwith "unexpected end" 255 + | t :: rest -> toks := rest; t in 256 + let peek () = 257 + match !toks with [] -> None | t :: _ -> Some t in 258 + let rec parse_expr () = 259 + let left = parse_atom () in 260 + parse_add left 261 + and parse_add left = 262 + match peek () with 263 + | Some TPlus -> 264 + ignore (next ()); 265 + let right = parse_atom () in 266 + parse_add (Add (left, right)) 267 + | _ -> left 268 + and parse_atom () = 269 + match next () with 270 + | TNum n -> Lit n 271 + | TIdent s -> Var s 272 + | TLParen -> 273 + let e = parse_expr () in 274 + ignore (next ()); e 275 + | TLet -> 276 + let (TIdent name) = next () in 277 + ignore (next ()); 278 + let rhs = parse_expr () in 279 + ignore (next ()); 280 + let body = parse_expr () in 281 + Let (name, rhs, body) 282 + | _ -> failwith "unexpected token" in 283 + parse_expr () 284 + ]} 285 + } 286 + {li 287 + {b The Read-Eval-Print Loop} 288 + 289 + Now we connect all the pieces. The REPL reads a line, 290 + tokenizes it, parses the tokens, evaluates the expression, 291 + and prints the result. A persistent environment accumulates 292 + bindings across interactions. 293 + 294 + {[ 295 + type expr = 296 + | Lit of int 297 + | Add of expr * expr 298 + | Let of string * expr * expr 299 + | Var of string 300 + 301 + type value = Int of int 302 + type env = (string * value) list 303 + let empty_env : env = [] 304 + let extend env name v = (name, v) :: env 305 + let lookup env name = 306 + match List.assoc_opt name env with 307 + | Some v -> v 308 + | None -> failwith ("unbound: " ^ name) 309 + 310 + let rec eval env = function 311 + | Lit n -> Int n 312 + | Add (a, b) -> 313 + let (Int x) = eval env a in 314 + let (Int y) = eval env b in 315 + Int (x + y) 316 + | Let (name, rhs, body) -> 317 + let v = eval env rhs in 318 + eval (extend env name v) body 319 + | Var name -> lookup env name 320 + 321 + type token = 322 + | TNum of int | TIdent of string 323 + | TPlus | TEqual 324 + | TLParen | TRParen 325 + | TLet | TIn 326 + 327 + let is_alpha c = 328 + (c >= 'a' && c <= 'z') 329 + || (c >= 'A' && c <= 'Z') || c = '_' 330 + let is_digit c = c >= '0' && c <= '9' 331 + 332 + let tokenize input = 333 + let len = String.length input in 334 + let pos = ref 0 in 335 + let tokens = ref [] in 336 + while !pos < len do 337 + let c = input.[!pos] in 338 + if c = ' ' || c = '\t' || c = '\n' then 339 + incr pos 340 + else if is_digit c then begin 341 + let start = !pos in 342 + while !pos < len && is_digit input.[!pos] 343 + do incr pos done; 344 + tokens := TNum (int_of_string 345 + (String.sub input start 346 + (!pos - start))) :: !tokens 347 + end else if is_alpha c then begin 348 + let start = !pos in 349 + while !pos < len && is_alpha input.[!pos] 350 + do incr pos done; 351 + let s = String.sub input start 352 + (!pos - start) in 353 + tokens := (match s with 354 + | "let" -> TLet | "in" -> TIn 355 + | _ -> TIdent s) :: !tokens 356 + end else begin 357 + tokens := (match c with 358 + | '+' -> TPlus | '=' -> TEqual 359 + | '(' -> TLParen | ')' -> TRParen 360 + | _ -> failwith "unexpected") :: !tokens; 361 + incr pos 362 + end 363 + done; List.rev !tokens 364 + 365 + let parse tokens = 366 + let toks = ref tokens in 367 + let next () = match !toks with 368 + | [] -> failwith "end" 369 + | t :: r -> toks := r; t in 370 + let peek () = match !toks with 371 + | [] -> None | t :: _ -> Some t in 372 + let rec expr () = 373 + let l = atom () in add l 374 + and add left = match peek () with 375 + | Some TPlus -> 376 + ignore (next ()); 377 + add (Add (left, atom ())) 378 + | _ -> left 379 + and atom () = match next () with 380 + | TNum n -> Lit n 381 + | TIdent s -> Var s 382 + | TLParen -> 383 + let e = expr () in 384 + ignore (next ()); e 385 + | TLet -> 386 + let (TIdent name) = next () in 387 + ignore (next ()); 388 + let rhs = expr () in 389 + ignore (next ()); 390 + Let (name, rhs, expr ()) 391 + | _ -> failwith "unexpected" in 392 + expr () 393 + 394 + let print_value = function 395 + | Int n -> Printf.printf "=> %d\n" n 396 + 397 + let repl () = 398 + let env = ref empty_env in 399 + try while true do 400 + print_string "> "; 401 + let line = input_line stdin in 402 + let tokens = tokenize line in 403 + let ast = parse tokens in 404 + let result = eval !env ast in 405 + print_value result 406 + done with End_of_file -> 407 + print_endline "Goodbye." 408 + 409 + let () = repl () 410 + ]} 411 + } 412 + }
+2
doc/dune
··· 1 + (documentation 2 + (package odoc-scrollycode-extension))
+198
doc/index.mld
··· 1 + {0 Scrollycode Demos} 2 + 3 + {%html: 4 + <link rel="preconnect" href="https://fonts.googleapis.com"> 5 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 6 + <link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Source+Code+Pro:ital,wght@0,300..900;1,300..900&family=Source+Serif+4:ital,opsz,wght@0,8..60,300..900;1,8..60,300..900&display=swap" rel="stylesheet"> 7 + <style> 8 + .odoc-nav, .odoc-tocs, .odoc-search { display: none !important; } 9 + .odoc-preamble > h1 { display: none !important; } 10 + .odoc-preamble, .odoc-content { max-width: none !important; padding: 0 !important; margin: 0 !important; } 11 + body.odoc, .odoc { padding: 0 !important; margin: 0 !important; max-width: none !important; } 12 + 13 + .demos-index { 14 + font-family: 'Source Serif 4', Georgia, serif; 15 + background: #f4f0e8; 16 + min-height: 100vh; 17 + } 18 + 19 + .demos-header { 20 + max-width: 680px; 21 + margin: 0 auto; 22 + padding: 6rem 2rem 3rem; 23 + text-align: center; 24 + } 25 + 26 + .demos-header h1 { 27 + font-family: 'Fraunces', serif; 28 + font-size: clamp(2rem, 5vw, 3.2rem); 29 + font-weight: 800; 30 + line-height: 1.15; 31 + letter-spacing: -0.02em; 32 + margin-bottom: 1rem; 33 + color: #2c2416; 34 + } 35 + 36 + .demos-header h1 em { 37 + font-style: italic; 38 + color: #c25832; 39 + } 40 + 41 + .demos-header p { 42 + color: #6b5d4d; 43 + max-width: 50ch; 44 + margin: 0 auto; 45 + font-size: 1.05rem; 46 + line-height: 1.6; 47 + } 48 + 49 + .demos-list { 50 + max-width: 720px; 51 + margin: 0 auto; 52 + padding: 1rem 2rem 6rem; 53 + display: flex; 54 + flex-direction: column; 55 + gap: 1.25rem; 56 + } 57 + 58 + .demo-card { 59 + display: block; 60 + background: #fff; 61 + border: 1px solid rgba(44,36,22,0.08); 62 + border-radius: 10px; 63 + padding: 1.5rem 1.75rem; 64 + text-decoration: none; 65 + color: inherit; 66 + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease; 67 + } 68 + 69 + .demo-card:hover { 70 + border-color: #c25832; 71 + box-shadow: 0 4px 20px rgba(194,88,50,0.1); 72 + transform: translateY(-2px); 73 + } 74 + 75 + .demo-card-header { 76 + display: flex; 77 + align-items: baseline; 78 + gap: 0.75rem; 79 + margin-bottom: 0.5rem; 80 + } 81 + 82 + .demo-card-number { 83 + font-family: 'Source Code Pro', monospace; 84 + font-size: 0.68rem; 85 + font-weight: 600; 86 + letter-spacing: 0.08em; 87 + color: #c25832; 88 + flex-shrink: 0; 89 + } 90 + 91 + .demo-card h2 { 92 + font-family: 'Fraunces', serif; 93 + font-size: 1.2rem; 94 + font-weight: 700; 95 + letter-spacing: -0.01em; 96 + margin: 0; 97 + } 98 + 99 + .demo-card p { 100 + color: #6b5d4d; 101 + font-size: 0.92rem; 102 + line-height: 1.6; 103 + margin: 0.5rem 0; 104 + } 105 + 106 + .demo-card .tags { 107 + display: flex; 108 + flex-wrap: wrap; 109 + gap: 0.4rem; 110 + margin-top: 0.75rem; 111 + } 112 + 113 + .tag { 114 + font-family: 'Source Code Pro', monospace; 115 + font-size: 0.65rem; 116 + letter-spacing: 0.04em; 117 + padding: 0.2em 0.6em; 118 + border-radius: 4px; 119 + background: rgba(194,88,50,0.07); 120 + color: #c25832; 121 + } 122 + 123 + .tag.dark { 124 + background: #1a1a2e; 125 + color: rgba(255,255,255,0.5); 126 + } 127 + 128 + .demos-footer { 129 + text-align: center; 130 + padding: 0 2rem 4rem; 131 + font-family: 'Source Code Pro', monospace; 132 + font-size: 0.72rem; 133 + color: #6b5d4d; 134 + letter-spacing: 0.04em; 135 + } 136 + 137 + .demos-footer a { 138 + color: #c25832; 139 + text-decoration: none; 140 + } 141 + </style> 142 + 143 + <div class="demos-index"> 144 + 145 + <div class="demos-header"> 146 + <h1>Scrollycoding in <em>OCaml</em></h1> 147 + <p>Three odoc extension plugins, each rendering the same scrollycode pattern with a radically different visual theme. Authored as <code>.mld</code> files using <code>@scrolly.&lt;theme&gt;</code> custom tags.</p> 148 + </div> 149 + 150 + <div class="demos-list"> 151 + 152 + <a class="demo-card" href="warm_parser.html"> 153 + <div class="demo-card-header"> 154 + <span class="demo-card-number">01</span> 155 + <h2>The Warm Workshop</h2> 156 + </div> 157 + <p>Building a JSON parser step by step. Warm cream background with a dark navy code panel, Fraunces serif display type, and earthy burnt-sienna accents. Classic scrollycode split layout.</p> 158 + <div class="tags"> 159 + <span class="tag">@scrolly.warm</span> 160 + <span class="tag">Fraunces + Source Serif</span> 161 + <span class="tag">parser tutorial</span> 162 + </div> 163 + </a> 164 + 165 + <a class="demo-card" href="dark_repl.html"> 166 + <div class="demo-card-header"> 167 + <span class="demo-card-number">02</span> 168 + <h2>The Dark Terminal</h2> 169 + </div> 170 + <p>Building a REPL from scratch. Near-black cinematic theme with phosphor-green accents, JetBrains Mono code font, and the code panel as the visual hero. Prose in a narrow left column.</p> 171 + <div class="tags"> 172 + <span class="tag dark">@scrolly.dark</span> 173 + <span class="tag">JetBrains Mono + Outfit</span> 174 + <span class="tag">REPL tutorial</span> 175 + </div> 176 + </a> 177 + 178 + <a class="demo-card" href="notebook_testing.html"> 179 + <div class="demo-card-header"> 180 + <span class="demo-card-number">03</span> 181 + <h2>The Notebook</h2> 182 + </div> 183 + <p>Building a test framework incrementally. Clean editorial aesthetic with a soft white background, blue-violet accents, Newsreader serif headings, and generous whitespace.</p> 184 + <div class="tags"> 185 + <span class="tag">@scrolly.notebook</span> 186 + <span class="tag">Newsreader + DM Sans</span> 187 + <span class="tag">testing tutorial</span> 188 + </div> 189 + </a> 190 + 191 + </div> 192 + 193 + <div class="demos-footer"> 194 + Powered by <a href="https://ocaml.github.io/odoc/">odoc</a> custom tag extensions &middot; Inspired by <a href="https://codehike.org">Code Hike</a> 195 + </div> 196 + 197 + </div> 198 + %}
+403
doc/notebook_testing.mld
··· 1 + {0 Building a Test Framework} 2 + 3 + @scrolly Building a Test Framework in OCaml 4 + {ol 5 + {li 6 + {b A Single Assertion} 7 + 8 + The simplest possible test: check that a condition holds. 9 + If it fails, raise an exception with a message. This is 10 + the foundation everything else builds on. 11 + 12 + {[ 13 + exception Test_failure of string 14 + 15 + let assert_equal ~expected ~actual msg = 16 + if expected <> actual then 17 + raise (Test_failure 18 + (Printf.sprintf "%s: expected %s, got %s" 19 + msg 20 + (string_of_int expected) 21 + (string_of_int actual))) 22 + ]} 23 + } 24 + {li 25 + {b Collecting Tests} 26 + 27 + A test is a named function. We store tests in a mutable list 28 + so they can be registered declaratively with a simple helper. 29 + Each test is just a unit function that might raise. 30 + 31 + {[ 32 + exception Test_failure of string 33 + 34 + let assert_equal ~expected ~actual msg = 35 + if expected <> actual then 36 + raise (Test_failure 37 + (Printf.sprintf "%s: expected %s, got %s" 38 + msg 39 + (string_of_int expected) 40 + (string_of_int actual))) 41 + 42 + type test = { 43 + name : string; 44 + fn : unit -> unit; 45 + } 46 + 47 + let tests : test list ref = ref [] 48 + 49 + let register name fn = 50 + tests := { name; fn } :: !tests 51 + 52 + let () = register "addition" (fun () -> 53 + assert_equal ~expected:4 ~actual:(2 + 2) 54 + "two plus two") 55 + 56 + let () = register "multiplication" (fun () -> 57 + assert_equal ~expected:6 ~actual:(2 * 3) 58 + "two times three") 59 + ]} 60 + } 61 + {li 62 + {b A Test Runner} 63 + 64 + The runner iterates through registered tests, catching 65 + exceptions to report pass or fail. It counts results 66 + and prints a summary at the end. 67 + 68 + {[ 69 + exception Test_failure of string 70 + 71 + let assert_equal ~expected ~actual msg = 72 + if expected <> actual then 73 + raise (Test_failure 74 + (Printf.sprintf "%s: expected %s, got %s" 75 + msg 76 + (string_of_int expected) 77 + (string_of_int actual))) 78 + 79 + type test = { 80 + name : string; 81 + fn : unit -> unit; 82 + } 83 + 84 + let tests : test list ref = ref [] 85 + 86 + let register name fn = 87 + tests := { name; fn } :: !tests 88 + 89 + type result = 90 + | Pass 91 + | Fail of string 92 + 93 + let run_one test = 94 + try test.fn (); Pass 95 + with 96 + | Test_failure msg -> Fail msg 97 + | exn -> Fail (Printexc.to_string exn) 98 + 99 + let run_all () = 100 + let results = 101 + List.rev !tests 102 + |> List.map (fun t -> (t.name, run_one t)) 103 + in 104 + let passed = 105 + List.length 106 + (List.filter 107 + (fun (_, r) -> r = Pass) results) 108 + in 109 + let total = List.length results in 110 + List.iter (fun (name, result) -> 111 + match result with 112 + | Pass -> 113 + Printf.printf " PASS %s\n" name 114 + | Fail msg -> 115 + Printf.printf " FAIL %s: %s\n" name msg 116 + ) results; 117 + Printf.printf "\n%d/%d tests passed\n" 118 + passed total; 119 + if passed < total then exit 1 120 + ]} 121 + } 122 + {li 123 + {b Better Assertions} 124 + 125 + Real frameworks need more than integer equality. We add 126 + string comparison, boolean checks, and a generic raises 127 + assertion that checks an exception is thrown. 128 + 129 + {[ 130 + exception Test_failure of string 131 + 132 + let assert_equal ~expected ~actual msg = 133 + if expected <> actual then 134 + raise (Test_failure 135 + (Printf.sprintf "%s: expected %s, got %s" 136 + msg 137 + (string_of_int expected) 138 + (string_of_int actual))) 139 + 140 + let assert_string_equal ~expected ~actual msg = 141 + if expected <> actual then 142 + raise (Test_failure 143 + (Printf.sprintf 144 + "%s: expected %S, got %S" 145 + msg expected actual)) 146 + 147 + let assert_true condition msg = 148 + if not condition then 149 + raise (Test_failure msg) 150 + 151 + let assert_raises fn msg = 152 + try fn (); 153 + raise (Test_failure 154 + (msg ^ ": expected exception")) 155 + with 156 + | Test_failure _ as e -> raise e 157 + | _ -> () 158 + 159 + type test = { 160 + name : string; 161 + fn : unit -> unit; 162 + } 163 + 164 + let tests : test list ref = ref [] 165 + 166 + let register name fn = 167 + tests := { name; fn } :: !tests 168 + 169 + type result = Pass | Fail of string 170 + 171 + let run_one test = 172 + try test.fn (); Pass 173 + with 174 + | Test_failure msg -> Fail msg 175 + | exn -> Fail (Printexc.to_string exn) 176 + 177 + let run_all () = 178 + let results = 179 + List.rev !tests 180 + |> List.map (fun t -> (t.name, run_one t)) 181 + in 182 + let passed = List.length 183 + (List.filter 184 + (fun (_, r) -> r = Pass) results) in 185 + let total = List.length results in 186 + List.iter (fun (name, result) -> 187 + match result with 188 + | Pass -> 189 + Printf.printf " PASS %s\n" name 190 + | Fail msg -> 191 + Printf.printf " FAIL %s: %s\n" 192 + name msg 193 + ) results; 194 + Printf.printf "\n%d/%d tests passed\n" 195 + passed total; 196 + if passed < total then exit 1 197 + ]} 198 + } 199 + {li 200 + {b Test Suites} 201 + 202 + As projects grow, tests need organization. We add a suite 203 + concept that groups related tests under a name. Suites 204 + can be nested and run independently. 205 + 206 + {[ 207 + exception Test_failure of string 208 + 209 + let assert_equal ~expected ~actual msg = 210 + if expected <> actual then 211 + raise (Test_failure 212 + (Printf.sprintf "%s: expected %s, got %s" 213 + msg 214 + (string_of_int expected) 215 + (string_of_int actual))) 216 + 217 + let assert_string_equal ~expected ~actual msg = 218 + if expected <> actual then 219 + raise (Test_failure 220 + (Printf.sprintf "%s: expected %S, got %S" 221 + msg expected actual)) 222 + 223 + let assert_true condition msg = 224 + if not condition then 225 + raise (Test_failure msg) 226 + 227 + let assert_raises fn msg = 228 + try fn (); 229 + raise (Test_failure 230 + (msg ^ ": expected exception")) 231 + with Test_failure _ as e -> raise e | _ -> () 232 + 233 + type test = { name : string; fn : unit -> unit } 234 + type result = Pass | Fail of string 235 + 236 + type suite = { 237 + suite_name : string; 238 + mutable suite_tests : test list; 239 + } 240 + 241 + let suites : suite list ref = ref [] 242 + 243 + let create_suite name = 244 + let s = { suite_name = name; 245 + suite_tests = [] } in 246 + suites := s :: !suites; s 247 + 248 + let add_test suite name fn = 249 + suite.suite_tests <- 250 + { name; fn } :: suite.suite_tests 251 + 252 + let run_one test = 253 + try test.fn (); Pass 254 + with 255 + | Test_failure msg -> Fail msg 256 + | exn -> Fail (Printexc.to_string exn) 257 + 258 + let run_suite suite = 259 + Printf.printf "Suite: %s\n" suite.suite_name; 260 + let results = 261 + List.rev suite.suite_tests 262 + |> List.map (fun t -> 263 + (t.name, run_one t)) in 264 + let passed = List.length 265 + (List.filter 266 + (fun (_, r) -> r = Pass) results) in 267 + let total = List.length results in 268 + List.iter (fun (name, result) -> 269 + match result with 270 + | Pass -> 271 + Printf.printf " PASS %s\n" name 272 + | Fail msg -> 273 + Printf.printf " FAIL %s: %s\n" 274 + name msg 275 + ) results; 276 + Printf.printf " %d/%d passed\n\n" 277 + passed total; 278 + passed = total 279 + 280 + let run_all_suites () = 281 + let all_ok = List.for_all run_suite 282 + (List.rev !suites) in 283 + if not all_ok then exit 1 284 + ]} 285 + } 286 + {li 287 + {b Expect Tests} 288 + 289 + The crown jewel: expect tests capture actual output and 290 + compare it to an expected snapshot. On first run, they 291 + record the output. On later runs, they detect regressions. 292 + This is how tools like ppx_expect and Cram tests work. 293 + 294 + {[ 295 + exception Test_failure of string 296 + 297 + let assert_equal ~expected ~actual msg = 298 + if expected <> actual then 299 + raise (Test_failure 300 + (Printf.sprintf "%s: expected %s, got %s" 301 + msg 302 + (string_of_int expected) 303 + (string_of_int actual))) 304 + 305 + let assert_string_equal ~expected ~actual msg = 306 + if expected <> actual then 307 + raise (Test_failure 308 + (Printf.sprintf "%s: expected %S, got %S" 309 + msg expected actual)) 310 + 311 + let assert_true condition msg = 312 + if not condition then 313 + raise (Test_failure msg) 314 + 315 + let assert_raises fn msg = 316 + try fn (); 317 + raise (Test_failure 318 + (msg ^ ": expected exception")) 319 + with Test_failure _ as e -> raise e | _ -> () 320 + 321 + type test = { name : string; fn : unit -> unit } 322 + type result = Pass | Fail of string 323 + 324 + type suite = { 325 + suite_name : string; 326 + mutable suite_tests : test list; 327 + } 328 + 329 + let suites : suite list ref = ref [] 330 + 331 + let create_suite name = 332 + let s = { suite_name = name; 333 + suite_tests = [] } in 334 + suites := s :: !suites; s 335 + 336 + let add_test suite name fn = 337 + suite.suite_tests <- 338 + { name; fn } :: suite.suite_tests 339 + 340 + let run_one test = 341 + try test.fn (); Pass 342 + with 343 + | Test_failure msg -> Fail msg 344 + | exn -> Fail (Printexc.to_string exn) 345 + 346 + (* Expect test infrastructure *) 347 + let expect_dir = "_expect" 348 + 349 + let expect_test suite name fn = 350 + add_test suite name (fun () -> 351 + let buf = Buffer.create 256 in 352 + fn (Buffer.add_string buf); 353 + let actual = Buffer.contents buf in 354 + let path = Printf.sprintf "%s/%s/%s.expected" 355 + expect_dir suite.suite_name name in 356 + if Sys.file_exists path then begin 357 + let ic = open_in path in 358 + let expected = really_input_string ic 359 + (in_channel_length ic) in 360 + close_in ic; 361 + assert_string_equal 362 + ~expected ~actual 363 + (name ^ " snapshot") 364 + end else begin 365 + let dir = Filename.dirname path in 366 + ignore (Sys.command 367 + ("mkdir -p " ^ dir)); 368 + let oc = open_out path in 369 + output_string oc actual; 370 + close_out oc; 371 + Printf.printf 372 + " NEW %s (snapshot saved)\n" name 373 + end) 374 + 375 + let run_suite suite = 376 + Printf.printf "Suite: %s\n" suite.suite_name; 377 + let results = 378 + List.rev suite.suite_tests 379 + |> List.map (fun t -> 380 + (t.name, run_one t)) in 381 + let passed = List.length 382 + (List.filter 383 + (fun (_, r) -> r = Pass) results) in 384 + let total = List.length results in 385 + List.iter (fun (name, result) -> 386 + match result with 387 + | Pass -> 388 + Printf.printf " PASS %s\n" name 389 + | Fail msg -> 390 + Printf.printf " FAIL %s: %s\n" 391 + name msg 392 + ) results; 393 + Printf.printf " %d/%d passed\n\n" 394 + passed total; 395 + passed = total 396 + 397 + let run_all_suites () = 398 + let all_ok = List.for_all run_suite 399 + (List.rev !suites) in 400 + if not all_ok then exit 1 401 + ]} 402 + } 403 + }
+384
doc/warm_parser.mld
··· 1 + {0 Building a JSON Parser} 2 + 3 + @scrolly Building a JSON Parser in OCaml 4 + {ol 5 + {li 6 + {b Defining the Value Type} 7 + 8 + Every parser starts with a type. JSON has six kinds of values: 9 + null, booleans, numbers, strings, arrays, and objects. 10 + We encode this directly as an OCaml variant. 11 + 12 + {[ 13 + (* >type json = 14 + (* > | Null 15 + (* > | Bool of bool 16 + (* > | Number of float 17 + (* > | String of string 18 + (* > | Array of json list 19 + (* > | Object of (string * json) list 20 + ]} 21 + } 22 + {li 23 + {b A Simple Scanner} 24 + 25 + Before parsing structure, we need to skip whitespace and 26 + peek at the next meaningful character. Our scanner works 27 + on a string with a mutable position index. 28 + 29 + {[ 30 + type json = 31 + | Null 32 + | Bool of bool 33 + | Number of float 34 + | String of string 35 + | Array of json list 36 + | Object of (string * json) list 37 + 38 + (* >type scanner = { 39 + (* > input : string; 40 + (* > mutable pos : int; 41 + (* >} 42 + (* > 43 + (* >let peek s = 44 + (* > while s.pos < String.length s.input 45 + (* > && s.input.[s.pos] = ' ' do 46 + (* > s.pos <- s.pos + 1 47 + (* > done; 48 + (* > if s.pos < String.length s.input 49 + (* > then Some s.input.[s.pos] 50 + (* > else None 51 + (* > 52 + (* >let advance s = s.pos <- s.pos + 1 53 + ]} 54 + } 55 + {li 56 + {b Parsing Strings} 57 + 58 + JSON strings are delimited by double quotes. We scan character 59 + by character, collecting into a buffer. This handles the simple 60 + case without escape sequences. 61 + 62 + {[ 63 + type json = 64 + | Null 65 + | Bool of bool 66 + | Number of float 67 + | String of string 68 + | Array of json list 69 + | Object of (string * json) list 70 + 71 + type scanner = { 72 + input : string; 73 + mutable pos : int; 74 + } 75 + 76 + let peek s = 77 + while s.pos < String.length s.input 78 + && s.input.[s.pos] = ' ' do 79 + s.pos <- s.pos + 1 80 + done; 81 + if s.pos < String.length s.input 82 + then Some s.input.[s.pos] 83 + else None 84 + 85 + let advance s = s.pos <- s.pos + 1 86 + 87 + (* >let parse_string s = 88 + (* > advance s; 89 + (* > let buf = Buffer.create 64 in 90 + (* > while s.pos < String.length s.input 91 + (* > && s.input.[s.pos] <> '"' do 92 + (* > Buffer.add_char buf s.input.[s.pos]; 93 + (* > advance s 94 + (* > done; 95 + (* > advance s; 96 + (* > Buffer.contents buf 97 + ]} 98 + } 99 + {li 100 + {b Parsing Numbers} 101 + 102 + Numbers in JSON can be integers or floats. We collect consecutive 103 + digit and dot characters, then use float_of_string to parse them. 104 + A production parser would handle exponents too. 105 + 106 + {[ 107 + type json = 108 + | Null 109 + | Bool of bool 110 + | Number of float 111 + | String of string 112 + | Array of json list 113 + | Object of (string * json) list 114 + 115 + type scanner = { 116 + input : string; 117 + mutable pos : int; 118 + } 119 + 120 + let peek s = 121 + while s.pos < String.length s.input 122 + && s.input.[s.pos] = ' ' do 123 + s.pos <- s.pos + 1 124 + done; 125 + if s.pos < String.length s.input 126 + then Some s.input.[s.pos] 127 + else None 128 + 129 + let advance s = s.pos <- s.pos + 1 130 + 131 + let parse_string s = 132 + advance s; 133 + let buf = Buffer.create 64 in 134 + while s.pos < String.length s.input 135 + && s.input.[s.pos] <> '"' do 136 + Buffer.add_char buf s.input.[s.pos]; 137 + advance s 138 + done; 139 + advance s; 140 + Buffer.contents buf 141 + 142 + (* >let is_digit c = c >= '0' && c <= '9' 143 + (* > 144 + (* >let parse_number s = 145 + (* > let start = s.pos in 146 + (* > while s.pos < String.length s.input 147 + (* > && (is_digit s.input.[s.pos] 148 + (* > || s.input.[s.pos] = '.' 149 + (* > || s.input.[s.pos] = '-') do 150 + (* > advance s 151 + (* > done; 152 + (* > float_of_string 153 + (* > (String.sub s.input start (s.pos - start)) 154 + ]} 155 + } 156 + {li 157 + {b The Recursive Core} 158 + 159 + Now the magic: parse_value dispatches on the next character 160 + to decide what kind of JSON value to parse. For atoms like 161 + null, true, false we match literal strings. For compound 162 + structures, we recurse. 163 + 164 + {[ 165 + type json = 166 + | Null 167 + | Bool of bool 168 + | Number of float 169 + | String of string 170 + | Array of json list 171 + | Object of (string * json) list 172 + 173 + type scanner = { 174 + input : string; 175 + mutable pos : int; 176 + } 177 + 178 + let peek s = 179 + while s.pos < String.length s.input 180 + && s.input.[s.pos] = ' ' do 181 + s.pos <- s.pos + 1 182 + done; 183 + if s.pos < String.length s.input 184 + then Some s.input.[s.pos] 185 + else None 186 + 187 + let advance s = s.pos <- s.pos + 1 188 + 189 + let parse_string s = 190 + advance s; 191 + let buf = Buffer.create 64 in 192 + while s.pos < String.length s.input 193 + && s.input.[s.pos] <> '"' do 194 + Buffer.add_char buf s.input.[s.pos]; 195 + advance s 196 + done; 197 + advance s; 198 + Buffer.contents buf 199 + 200 + let is_digit c = c >= '0' && c <= '9' 201 + 202 + let parse_number s = 203 + let start = s.pos in 204 + while s.pos < String.length s.input 205 + && (is_digit s.input.[s.pos] 206 + || s.input.[s.pos] = '.' 207 + || s.input.[s.pos] = '-') do 208 + advance s 209 + done; 210 + float_of_string 211 + (String.sub s.input start (s.pos - start)) 212 + 213 + (* >let expect s c = 214 + (* > match peek s with 215 + (* > | Some c' when c' = c -> advance s 216 + (* > | _ -> failwith "unexpected character" 217 + (* > 218 + (* >let rec parse_value s = 219 + (* > match peek s with 220 + (* > | Some '"' -> String (parse_string s) 221 + (* > | Some c when is_digit c || c = '-' -> 222 + (* > Number (parse_number s) 223 + (* > | Some 't' -> 224 + (* > s.pos <- s.pos + 4; Bool true 225 + (* > | Some 'f' -> 226 + (* > s.pos <- s.pos + 5; Bool false 227 + (* > | Some 'n' -> 228 + (* > s.pos <- s.pos + 4; Null 229 + (* > | Some '[' -> parse_array s 230 + (* > | Some '{' -> parse_object s 231 + (* > | _ -> failwith "unexpected token" 232 + (* > 233 + (* >and parse_array s = 234 + (* > advance s; 235 + (* > let items = ref [] in 236 + (* > (match peek s with 237 + (* > | Some ']' -> advance s 238 + (* > | _ -> 239 + (* > items := [parse_value s]; 240 + (* > while peek s = Some ',' do 241 + (* > advance s; 242 + (* > items := parse_value s :: !items 243 + (* > done; 244 + (* > expect s ']'); 245 + (* > Array (List.rev !items) 246 + (* > 247 + (* >and parse_object s = 248 + (* > advance s; 249 + (* > let pairs = ref [] in 250 + (* > (match peek s with 251 + (* > | Some '}' -> advance s 252 + (* > | _ -> 253 + (* > let key = parse_string s in 254 + (* > expect s ':'; 255 + (* > let value = parse_value s in 256 + (* > pairs := [(key, value)]; 257 + (* > while peek s = Some ',' do 258 + (* > advance s; 259 + (* > let k = parse_string s in 260 + (* > expect s ':'; 261 + (* > let v = parse_value s in 262 + (* > pairs := (k, v) :: !pairs 263 + (* > done; 264 + (* > expect s '}'); 265 + (* > Object (List.rev !pairs) 266 + ]} 267 + } 268 + {li 269 + {b The Public API} 270 + 271 + Finally we wrap the scanner in a clean top-level function. 272 + Pass a string in, get a JSON value out. The entire parser 273 + is about 80 lines of OCaml — no dependencies, no magic. 274 + 275 + {[ 276 + type json = 277 + | Null 278 + | Bool of bool 279 + | Number of float 280 + | String of string 281 + | Array of json list 282 + | Object of (string * json) list 283 + 284 + type scanner = { 285 + input : string; 286 + mutable pos : int; 287 + } 288 + 289 + let peek s = 290 + while s.pos < String.length s.input 291 + && s.input.[s.pos] = ' ' do 292 + s.pos <- s.pos + 1 293 + done; 294 + if s.pos < String.length s.input 295 + then Some s.input.[s.pos] 296 + else None 297 + 298 + let advance s = s.pos <- s.pos + 1 299 + 300 + let parse_string s = 301 + advance s; 302 + let buf = Buffer.create 64 in 303 + while s.pos < String.length s.input 304 + && s.input.[s.pos] <> '"' do 305 + Buffer.add_char buf s.input.[s.pos]; 306 + advance s 307 + done; 308 + advance s; 309 + Buffer.contents buf 310 + 311 + let is_digit c = c >= '0' && c <= '9' 312 + 313 + let parse_number s = 314 + let start = s.pos in 315 + while s.pos < String.length s.input 316 + && (is_digit s.input.[s.pos] 317 + || s.input.[s.pos] = '.' 318 + || s.input.[s.pos] = '-') do 319 + advance s 320 + done; 321 + float_of_string 322 + (String.sub s.input start (s.pos - start)) 323 + 324 + let expect s c = 325 + match peek s with 326 + | Some c' when c' = c -> advance s 327 + | _ -> failwith "unexpected character" 328 + 329 + let rec parse_value s = 330 + match peek s with 331 + | Some '"' -> String (parse_string s) 332 + | Some c when is_digit c || c = '-' -> 333 + Number (parse_number s) 334 + | Some 't' -> 335 + s.pos <- s.pos + 4; Bool true 336 + | Some 'f' -> 337 + s.pos <- s.pos + 5; Bool false 338 + | Some 'n' -> 339 + s.pos <- s.pos + 4; Null 340 + | Some '[' -> parse_array s 341 + | Some '{' -> parse_object s 342 + | _ -> failwith "unexpected token" 343 + 344 + and parse_array s = 345 + advance s; 346 + let items = ref [] in 347 + (match peek s with 348 + | Some ']' -> advance s 349 + | _ -> 350 + items := [parse_value s]; 351 + while peek s = Some ',' do 352 + advance s; 353 + items := parse_value s :: !items 354 + done; 355 + expect s ']'); 356 + Array (List.rev !items) 357 + 358 + and parse_object s = 359 + advance s; 360 + let pairs = ref [] in 361 + (match peek s with 362 + | Some '}' -> advance s 363 + | _ -> 364 + let key = parse_string s in 365 + expect s ':'; 366 + let value = parse_value s in 367 + pairs := [(key, value)]; 368 + while peek s = Some ',' do 369 + advance s; 370 + let k = parse_string s in 371 + expect s ':'; 372 + let v = parse_value s in 373 + pairs := (k, v) :: !pairs 374 + done; 375 + expect s '}'); 376 + Object (List.rev !pairs) 377 + 378 + (* >let parse input = 379 + (* > let s = { input; pos = 0 } in 380 + (* > let v = parse_value s in 381 + (* > v 382 + ]} 383 + } 384 + }