├── .gitignore ├── README.md ├── bin ├── alpha.ml ├── anf.ml ├── close.ml ├── dune ├── fresh.ml ├── hoist.ml ├── jit.ml ├── lang.ml ├── lower.ml ├── main.ml ├── norm.ml └── util.ml └── dune-project /.gitignore: -------------------------------------------------------------------------------- 1 | *.annot 2 | *.cmo 3 | *.cma 4 | *.cmi 5 | *.a 6 | *.o 7 | *.cmx 8 | *.cmxs 9 | *.cmxa 10 | 11 | # ocamlbuild working directory 12 | _build/ 13 | 14 | # ocamlbuild targets 15 | *.byte 16 | *.native 17 | 18 | # oasis generated files 19 | setup.data 20 | setup.log 21 | 22 | # Merlin configuring file for Vim and Emacs 23 | .merlin 24 | 25 | # Dune generated files 26 | *.install 27 | 28 | # Local OPAM switch 29 | _opam/ 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lamb 2 | 3 | An implementation of Colin James' "Compiling Lambda Calculus". 4 | 5 | Source: [compiling-lambda-calculus](https://compiler.club/compiling-lambda-calculus/) 6 | -------------------------------------------------------------------------------- /bin/alpha.ml: -------------------------------------------------------------------------------- 1 | (* 2 | Alpha conversion 3 | 4 | the process of making each variable's name unique 5 | so that e.g. 6 | \x . \x . x + x 7 | becomes 8 | \x0 . \x1 . x1 + x1 9 | *) 10 | 11 | open Lang 12 | module SMap = Map.Make(String) 13 | 14 | let convert = 15 | let rec f env = function 16 | | Var x -> begin 17 | (* if this variable has already been renamed 18 | we just use that. otherwise its an error 19 | because its a 'use of unbound var x'. *) 20 | match SMap.find_opt x env with 21 | | Some x' -> Var x' 22 | | _ -> failwith "Scope checking failed!" 23 | end 24 | | Lam (x, e) -> 25 | (* introduce the lambda's parameter as a fresh 26 | variable and include it in the body's env. *) 27 | let x' = Fresh.fresh x in 28 | Lam (x', f (SMap.add x x' env) e) 29 | 30 | (* the rest of this is just mapping to converted *) 31 | | Int _ as t -> t 32 | | App (e1, e2) -> App (f env e1, f env e2) 33 | | Bop (op, e1, e2) -> Bop (op, f env e1, f env e2) 34 | | If (c, t, e) -> If (f env c, f env t, f env e) 35 | in f SMap.empty -------------------------------------------------------------------------------- /bin/anf.ml: -------------------------------------------------------------------------------- 1 | open Util 2 | 3 | type atom = 4 | | Int of int 5 | | Var of var 6 | | Glob of var 7 | 8 | and expr = 9 | | Return of atom 10 | (* return atom *) 11 | | Break of atom 12 | (* break atom *) 13 | | Fun of var * var list * expr * body 14 | (* let var(var, list) = expr in body *) 15 | | App of var * var * atom list * body 16 | (* let var = var(atom, list) in body *) 17 | | Bop of var * bop * atom * atom * body 18 | (* let var = atom bop atom in body*) 19 | | If of var * atom * expr * expr * body 20 | (* let var = if atom then expr else expr in body *) 21 | | Tuple of var * atom list * body 22 | (* let var = (atom; list) in body *) 23 | | Get of var * var * int * body 24 | (* let var = var[int] in body *) 25 | 26 | and body = expr 27 | and var = string 28 | and bop = Lang.bop 29 | 30 | let show_bop = Lang.show_bop 31 | 32 | let print = 33 | let pr i = print_string (String.make i '\t'); print_endline in 34 | let f = Printf.sprintf in 35 | 36 | let show_atom = function 37 | | Int i -> string_of_int i 38 | | Var v -> v 39 | | Glob v -> "@" ^ v 40 | in 41 | 42 | let rec pe i = function 43 | | Return a -> pr i ("return " ^ show_atom a) 44 | | Break a -> pr i ("break " ^ show_atom a) 45 | 46 | | Fun (n, vs, e, body) -> 47 | let vs' = join_mapped id vs in 48 | pr i (f "let %s(%s) =" n vs'); 49 | pe (i + 1) e; 50 | pe_body i body; 51 | 52 | | App (d, c, vs, body) -> 53 | let vs' = join_mapped show_atom vs in 54 | pr i (f "let %s = %s(%s)" d c vs'); 55 | pe_body i body 56 | 57 | | Bop (d, op, a1, a2, body) -> 58 | let (op', a1', a2') = show_bop op, show_atom a1, show_atom a2 in 59 | pr i (f "let %s = %s %s %s"d a1' op' a2'); 60 | pe_body i body; 61 | 62 | | If (d, c, t, e, body) -> 63 | pr i (f "let %s = if %s then" d (show_atom c)); 64 | pe (i + 1) t; 65 | pr i ("else"); 66 | pe (i + 1) e; 67 | pe_body i body 68 | 69 | | Tuple (d, es, body) -> 70 | let es = join_mapped show_atom es in 71 | pr i (f "let %s = (%s)" d es); 72 | pe_body i body 73 | 74 | | Get (d, v, n, body) -> 75 | pr i (f "let %s = %s[%d]" d v n); 76 | pe_body i body 77 | 78 | and pe_body i = pr i "in"; pe i 79 | 80 | in pe -------------------------------------------------------------------------------- /bin/close.ml: -------------------------------------------------------------------------------- 1 | open Anf 2 | open Fresh 3 | 4 | module VS = Set.Make(String) 5 | 6 | let free_vars e = 7 | let open VS in 8 | let in_atom = function Var v -> singleton v | _ -> empty in 9 | let (@) = union in 10 | 11 | let rec go = function 12 | | Return a | Break a -> in_atom a 13 | 14 | | Fun (f, vs, v, b) -> 15 | let in_value = diff (go v) (of_list vs) in 16 | in_value @ remove f (go b) 17 | 18 | (* | Join (d, p, v, b) -> 19 | let in_value = match p with 20 | | Some p -> remove p (go v) 21 | | None -> go v 22 | in 23 | in_value @ remove d (go b) 24 | *) 25 | (* | Jump (_, Some a) -> in_atom a *) 26 | 27 | | App (d, v, vs, b) -> 28 | let vs' = List.map in_atom vs in 29 | let in_value = add v (of_list (List.map VS.choose vs')) in 30 | in_value @ remove d (go b) 31 | 32 | | Bop (d, _, e1, e2, b) -> 33 | let in_value = (in_atom e1) @ (in_atom e2) in 34 | in_value @ remove d (go b) 35 | 36 | | If (d, c, t, e, b) -> 37 | in_atom c @ go t @ go e @ remove d (go b) 38 | 39 | | Tuple (d, es, b) -> 40 | let es' = List.map in_atom es in 41 | let in_values = of_list (List.map VS.choose es') in 42 | in_values @ remove d (go b) 43 | 44 | | Get (d, t, _, b) -> 45 | singleton t @ remove d (go b) 46 | 47 | in go e 48 | 49 | let convert = 50 | let rec go = function 51 | | Return _ | Break _ as r -> r 52 | | Bop (d, op, e1, e2, b) -> Bop (d, op, e1, e2, go b) 53 | | If (d, c, t, e, b) -> If (d, c, go t, go e, go b) 54 | | Tuple (d, es, b) -> Tuple (d, es, go b) 55 | | Get (d, v, i, b) -> Get (d, v, i, go b) 56 | 57 | | Fun (f, args, v, b) -> 58 | (* let f(args) = v in e *) 59 | let env = fresh "env" in 60 | 61 | (* find all free values in v and remove 62 | the arguments from f bc they are bound *) 63 | let free = VS.(elements (diff (free_vars v) (of_list args))) in 64 | 65 | (* helper function to create "let x = env[i] in e" *) 66 | let get (e, i) x = (Get (x, env, i, e), i + 1) in 67 | 68 | (* greates the `get`s from `env` *) 69 | let v' = fst (List.fold_left get (go v, 1) free) in 70 | let e' = 71 | let vs = List.map (fun v -> Var v) free in 72 | (* create the actual closure `(func; args...)` *) 73 | Tuple (f, Glob f :: vs, go b) 74 | in 75 | (* return then new function with the new value and body *) 76 | Fun (f, env :: args, v', e') 77 | 78 | | App (d, f, vs, b) -> 79 | (* transform "let d = f(vs) in e" 80 | to "let ptr = f[0] in let d = ptr(f, vs) in e" *) 81 | let ptr = fresh f in 82 | Get (ptr, f, 0, App (d, ptr, Var f :: vs, go b)) 83 | 84 | in go -------------------------------------------------------------------------------- /bin/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (public_name Lamb) 3 | (name main) 4 | (flags -g) 5 | (modes byte exe) 6 | (libraries 7 | llvm 8 | llvm.executionengine 9 | llvm.analysis 10 | llvm.scalar_opts 11 | llvm.passmgr_builder 12 | ctypes 13 | ) 14 | ) 15 | -------------------------------------------------------------------------------- /bin/fresh.ml: -------------------------------------------------------------------------------- 1 | let fresh = 2 | let c = ref (-1) in 3 | fun p -> 4 | incr c; 5 | p ^ string_of_int !c -------------------------------------------------------------------------------- /bin/hoist.ml: -------------------------------------------------------------------------------- 1 | open Anf 2 | open Fresh 3 | 4 | (** let var(var, list) = 5 | join: ... 6 | join: ... 7 | list: ... *) 8 | type func = var * var list * expr 9 | 10 | type 'a dlist = 'a list -> 'a list 11 | let cons x = fun xs -> x :: xs 12 | let nil = fun xs -> [] @ xs 13 | let (@-) xs ys tl = xs (ys tl) 14 | 15 | let hoist e : func list = 16 | (* collect al functions and the 17 | final expression (the entry block). 18 | for example: 19 | let f1 = ... 20 | let f2 = ... 21 | join j3 = ... 22 | in expr 23 | becomes: 24 | let f2 = ... 25 | let j3 = ... 26 | let f1 = expr *) 27 | 28 | let rec go : expr -> func dlist * expr = function 29 | | Return _ | Break _ | App _ | Bop _ | Tuple _ | Get _ 30 | as e -> (nil, e) 31 | 32 | | Fun (f, xs, v, b) -> 33 | (* gather functions in value and body *) 34 | let (v_fs, v') = go v in 35 | let (b_fs, b') = go b in 36 | (* create an entry join containing the func's body *) 37 | let fn = (f, xs, v') in 38 | (* return all functions and joins *) 39 | (v_fs @- b_fs @- cons fn, b') 40 | 41 | | If (d, c, t, e, b) -> 42 | let (t_fs, t') = go t in 43 | let (e_fs, e') = go e in 44 | let (b_fs, b') = go b in 45 | let expr = If (d, c, t', e', b') in 46 | (e_fs @- t_fs @- b_fs, expr) 47 | in 48 | let (funcs, expr) = go e in 49 | (* pack everything up and return *) 50 | let main : func = ("main", [], expr) in 51 | funcs [] @ [main] -------------------------------------------------------------------------------- /bin/jit.ml: -------------------------------------------------------------------------------- 1 | module LlEE = Llvm_executionengine 2 | 3 | let lines str = List.length (String.split_on_char '\n' str) 4 | 5 | let exec mdl = 6 | let lines_before = lines (Llvm.string_of_llmodule mdl) in 7 | 8 | let pm = Llvm.PassManager.create () in 9 | Llvm_scalar_opts.( 10 | add_instruction_combination pm; 11 | add_reassociation pm; 12 | add_basic_alias_analysis pm; 13 | add_dead_store_elimination pm; 14 | add_cfg_simplification pm; 15 | add_gvn pm; 16 | ); 17 | 18 | let pmbld = Llvm_passmgr_builder.create () in 19 | Llvm_passmgr_builder.set_opt_level 3 pmbld; 20 | Llvm_passmgr_builder.set_size_level 3 pmbld; 21 | Llvm_passmgr_builder.populate_module_pass_manager pm pmbld; 22 | 23 | ignore (Llvm.PassManager.run_module mdl pm); 24 | Llvm_analysis.assert_valid_module mdl; 25 | 26 | let lines_after = lines (Llvm.string_of_llmodule mdl) in 27 | Printf.printf "\toptimized from %d to %d lines (removing %d)\n" 28 | lines_before lines_after (lines_before - lines_after); 29 | 30 | assert (LlEE.initialize ()); 31 | let ee = LlEE.create mdl in 32 | let ty = Foreign.funptr Ctypes.(void @-> returning int64_t) in 33 | let fptr = LlEE.get_function_address "main" ty ee in 34 | fptr () 35 | -------------------------------------------------------------------------------- /bin/lang.ml: -------------------------------------------------------------------------------- 1 | type expr = 2 | | Int of int 3 | | Var of var 4 | | Lam of var * expr 5 | | App of expr * expr 6 | | Bop of bop * expr * expr 7 | | If of expr * expr * expr 8 | 9 | and var = string 10 | and bop = Add | Sub | Mul | Div 11 | 12 | let show_bop = function 13 | Add -> "+" | Sub -> "-" | Mul -> "*" | Div -> "/" 14 | 15 | let print e = 16 | let pr = print_string in 17 | let f = Printf.sprintf in 18 | let opn simple = (if simple then pr "(") in 19 | let cls simple = (if simple then pr ")") in 20 | 21 | let rec pe simple = function 22 | | Int l -> pr (string_of_int l) 23 | | Var v -> pr v 24 | | Lam (v, b) -> 25 | opn simple; 26 | pr (f "\\%s . " v); 27 | pe false b; 28 | cls simple 29 | | App (e1, e2) -> 30 | pe true e1; pr " "; pe false e2 31 | | Bop (op, e1, e2) -> 32 | pe true e1; 33 | pr (f " %s " (show_bop op)); 34 | pe false e2 35 | | If (c, t, e) -> 36 | pr "if "; pe false c; 37 | pr " then "; pe false t; 38 | pr " else "; pe false e 39 | in 40 | print_char '\t'; 41 | pe false e; 42 | print_newline () -------------------------------------------------------------------------------- /bin/lower.ml: -------------------------------------------------------------------------------- 1 | open Anf 2 | 3 | module SMap = Map.Make(String) 4 | 5 | let ctx = Llvm.create_context () 6 | let bld = Llvm.builder ctx 7 | let mdl = Llvm.create_module ctx "module" 8 | let i64 = Llvm.i64_type ctx 9 | let malloc = 10 | let fty = Llvm.function_type (Llvm.pointer_type i64) [|i64|] in 11 | Llvm.declare_function "malloc" fty mdl 12 | let func_ty arity = 13 | let args = Array.make arity i64 in 14 | Llvm.function_type i64 args 15 | let tuple_ty len = 16 | let els = Array.make len i64 in 17 | Llvm.struct_type ctx els 18 | 19 | let get_const i = Llvm.const_int i64 i 20 | let get_var env v = SMap.find v env 21 | let get_func_ptr g = Option.get (Llvm.lookup_function g mdl) 22 | 23 | 24 | let lower_atom env = function 25 | | Int i ->get_const i 26 | | Var v -> get_var env v 27 | | Glob g -> Llvm.build_ptrtoint (get_func_ptr g) i64 "casted" bld 28 | 29 | let lower_bop = 30 | let open Lang in function 31 | | Add -> Llvm.build_add 32 | | Sub -> Llvm.build_sub 33 | | Mul -> Llvm.build_mul 34 | | Div -> Llvm.build_sdiv 35 | 36 | let rec lower_expr env = function 37 | | Return a -> Llvm.build_ret (lower_atom env a) bld 38 | 39 | | Break a -> 40 | let bb = SMap.find "\b" env in 41 | let bb' = Llvm.block_of_value bb in 42 | let _ = Llvm.build_br bb' bld in 43 | lower_atom env a 44 | 45 | | Tuple (d, es, b) -> 46 | (* malloc the tuple *) 47 | let size = get_const (List.length es * 8) in 48 | let tup = Llvm.build_call malloc [|size|] "tup" bld in 49 | (* let tup = Llvm.build_malloc (tuple_ty (List.length es)) "tup" bld in *) 50 | 51 | (* set each of the tuple's items *) 52 | let set i v = 53 | let gep = Llvm.build_gep tup [|get_const i|] "gep" bld in 54 | ignore (Llvm.build_store v gep bld); 55 | in 56 | List.iteri (fun i e -> set i (lower_atom env e)) es; 57 | 58 | (* convert the tuple to an i64 so it can be used *) 59 | let tup' = Llvm.build_ptrtoint tup i64 d bld in 60 | lower_expr (SMap.add d tup' env) b 61 | 62 | | App (d, f, es, b) -> 63 | let es' = List.map (lower_atom env) es in 64 | let ptr = get_var env f in 65 | let fty = Llvm.pointer_type (func_ty (List.length es)) in 66 | let func = Llvm.build_inttoptr ptr fty "func" bld in 67 | 68 | let res = Llvm.build_call func (Array.of_list es') "res" bld in 69 | lower_expr (SMap.add d res env) b 70 | 71 | | Bop (d, op, x, y, b) -> 72 | let build_op = lower_bop op in 73 | let x' = lower_atom env x in 74 | let y' = lower_atom env y in 75 | let r = build_op x' y' "bopr" bld in 76 | lower_expr (SMap.add d r env) b 77 | 78 | | If (d, c, t, e, b) -> 79 | let currf = Llvm.block_parent (Llvm.insertion_block bld) in 80 | let tbb = Llvm.append_block ctx "then" currf in 81 | let ebb = Llvm.append_block ctx "else" currf in 82 | let pbb = Llvm.append_block ctx "phi" currf in 83 | 84 | let c' = Llvm.build_is_not_null (lower_atom env c) "cond" bld in 85 | let _ = Llvm.build_cond_br c' tbb ebb bld in 86 | let env' = SMap.add "\b" (Llvm.value_of_block pbb) env in 87 | 88 | Llvm.position_at_end tbb bld; 89 | let t' = lower_expr env' t in 90 | let tbb' = Llvm.insertion_block bld in 91 | 92 | Llvm.position_at_end ebb bld; 93 | let e' = lower_expr env' e in 94 | let ebb' = Llvm.insertion_block bld in 95 | 96 | Llvm.move_block_after ebb' pbb; 97 | Llvm.position_at_end pbb bld; 98 | let p = Llvm.build_phi [t', tbb'; e', ebb'] "phi" bld in 99 | lower_expr (SMap.add d p env) b 100 | 101 | | Get (d, t, i, b) -> 102 | let int_to_ptr v n = Llvm.build_inttoptr v (Llvm.pointer_type i64) n bld in 103 | let tup = int_to_ptr (get_var env t) "tup" in 104 | 105 | let gep = Llvm.build_gep tup [|get_const i|] "ptr" bld in 106 | 107 | let v = Llvm.build_load gep d bld in 108 | lower_expr (SMap.add d v env) b 109 | 110 | | _ -> Llvm.build_unreachable bld 111 | (* failwith "Invalid expression survived hoisting phase!" *) 112 | 113 | let lower_fn (f, xs, e) = 114 | (* create function *) 115 | let ty = func_ty (List.length xs) in 116 | let fn = Llvm.define_function f ty mdl in 117 | 118 | (* gather args *) 119 | let params = Array.to_list (Llvm.params fn) in 120 | let rec bind env = function 121 | | x :: xs, v :: vs -> 122 | Llvm.set_value_name x v; 123 | bind (SMap.add x v env) (xs, vs) 124 | | _ -> env 125 | in let env = bind SMap.empty (xs, params) in 126 | 127 | (* build blocks *) 128 | let entry = Llvm.entry_block fn in 129 | Llvm.position_at_end entry bld; 130 | ignore (lower_expr env e) 131 | 132 | let lower fns = 133 | List.iter lower_fn fns; 134 | mdl -------------------------------------------------------------------------------- /bin/main.ml: -------------------------------------------------------------------------------- 1 | open Lang 2 | 3 | let print_fns = 4 | let go (f, xs, e) = 5 | Printf.printf "\tlet %s(%s) =\n" f Util.(join_mapped id xs); 6 | Anf.print 2 e 7 | 8 | in function 9 | | first :: rest -> 10 | go first; 11 | List.iter (fun x -> print_newline (); go x) rest 12 | | [] -> () 13 | 14 | let print_module mdl = 15 | let str = Llvm.string_of_llmodule mdl in 16 | let lines = String.split_on_char '\n' str in 17 | List.iter (fun s -> print_endline ("\t" ^ s)) lines 18 | 19 | let () = 20 | (* let e = Lam ("x", Lam ("y", Bop (Add, Var "x", Var "y"))) in *) 21 | let e = App (App (Lam ("x", Lam ("y", If (Var "x", Var "y", Int 0))), Int 111), Int 222) in 22 | (* let e = If (Int 1, If (Int 2, Int 12, Int 10), Int 0) in *) 23 | (* let e = App (Lam ("x", Var "x"), Int 2) in *) 24 | 25 | print_endline "expression:"; 26 | print e; 27 | 28 | print_endline "\nalpha-conversion:"; 29 | let e = Alpha.convert e in 30 | print e; 31 | 32 | print_endline "\na-normalization:"; 33 | let e = Norm.normalize e in 34 | Anf.print 1 e; 35 | 36 | 37 | print_endline "\nclosure-conversion:"; 38 | let e = Close.convert e in 39 | Anf.print 1 e; 40 | 41 | print_endline "\nhoisting:"; 42 | let fs = Hoist.hoist e in 43 | print_fns fs; 44 | 45 | print_endline "\nlowering:"; 46 | let ir = Lower.lower fs in 47 | print_module ir; 48 | 49 | print_endline "executing:"; 50 | let result = Jit.exec ir in 51 | Printf.printf "\tresult: %s\n" (Int64.to_string result) 52 | -------------------------------------------------------------------------------- /bin/norm.ml: -------------------------------------------------------------------------------- 1 | open Lang 2 | open Anf 3 | open Fresh 4 | 5 | type le = Lang.expr 6 | type ae = Anf.expr 7 | type aa = Anf.atom 8 | 9 | let normalize e = 10 | let mk_ret v = Return v in 11 | let mk_brk v = Break v in 12 | let ( let* ) = (@@) in 13 | 14 | (* [cont] is a function that uses the produced `Anf.expr`. 15 | for example: when normalizing a lambda we pass `mk_ret` 16 | so that the `Anf.expr` that the normalization of the 17 | lambda's body produces is put in `Return`. *) 18 | let rec go (e : le) (cont : aa -> ae) : ae = match e with 19 | | Int i -> cont (Int i) 20 | | Var v -> cont (Var v) 21 | 22 | | Lam (v, e) -> 23 | (* we create a function with fresh name `f` and 24 | normalize its body (passing `mk_ret` of course) 25 | and produce "let f(v) = e' in " *) 26 | let f = fresh "f" in 27 | let e' = go e mk_ret in 28 | Fun (f, [v], e', cont (Var f)) 29 | 30 | | App (f, v) -> 31 | (* we check that `f` is a named value and apply it. 32 | The return value is stored in fresh variable `r` 33 | producing "let r = f(v) in " *) 34 | let* f' = go f in 35 | let* v' = go v in 36 | begin match f' with 37 | | Var f' -> 38 | let r = fresh "funr" in 39 | App (r, f', [v'], cont (Var r)) 40 | | _ -> failwith "Can only apply named values" 41 | end 42 | 43 | | Bop (op, e1, e2) -> 44 | let* e1' = go e1 in 45 | let* e2' = go e2 in 46 | let r = fresh "bopr" in 47 | Bop (r, op, e1', e2', cont (Var r)) 48 | 49 | | If (c, t, e) -> 50 | let* c' = go c in 51 | let r = fresh "phi" in 52 | If (r, c', go t mk_brk, go e mk_brk, cont (Var r)) 53 | 54 | in go e mk_ret -------------------------------------------------------------------------------- /bin/util.ml: -------------------------------------------------------------------------------- 1 | let id x = x 2 | 3 | let rec join_mapped f = function 4 | | [] -> "" 5 | | [x] -> f x 6 | | x::xs -> f x ^ ", " ^ join_mapped f xs -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.4) 2 | 3 | (name Lamb) 4 | 5 | (generate_opam_files false) 6 | 7 | (source 8 | (github username/reponame)) 9 | 10 | (authors "Author Name") 11 | 12 | (maintainers "Maintainer Name") 13 | 14 | (license LICENSE) 15 | 16 | (documentation https://url/to/documentation) 17 | 18 | (package 19 | (name Lamb) 20 | (synopsis "A short synopsis") 21 | (description "A longer description") 22 | (depends ocaml dune) 23 | (tags 24 | (topics "to describe" your project))) 25 | 26 | ; See the complete stanza docs at https://dune.readthedocs.io/en/stable/dune-files.html#dune-project 27 | 28 | --------------------------------------------------------------------------------