diff --git a/lib/resolve.ml b/lib/resolve.ml new file mode 100644 index 0000000..aaa8852 --- /dev/null +++ b/lib/resolve.ml @@ -0,0 +1,217 @@ +open Ast +open Types + +let target_fields = + [ "srcs"; "include"; "use"; "cflags"; "cxxflags"; "ldflags"; "define"; "pkg"; "install"; + "soname"; "args" ] + +let rule_fields = [ "inputs"; "outputs"; "command"; "description" ] +let install_fields = [ "files"; "to" ] + +let fields_of (b : block) = + List.filter_map (function FField f -> Some f | FIf _ -> None) b.items + +let check_fields (b : block) known = + List.iter + (fun (f : field) -> + if not (List.mem f.key known) then + Diag.error ~span:f.kspan ~hint:(Suggest.hint f.key known) "%s %s has no field %S" b.kind + b.bname f.key) + (fields_of b) + +let get b key = List.concat_map (fun (f : field) -> if f.key = key then f.values else []) (fields_of b) +let texts vs = List.map (fun (v : value) -> v.text) vs +let get1 b key = match texts (get b key) with [] -> None | l -> Some (String.concat " " l) + +let subst table s = + let n = String.length s in + let b = Buffer.create n in + let i = ref 0 in + while !i < n do + if s.[!i] = '$' && !i + 1 < n && s.[!i + 1] = '{' then + match String.index_from_opt s (!i + 2) '}' with + | None -> (Buffer.add_char b s.[!i]; incr i) + | Some j -> + let name = String.sub s (!i + 2) (j - !i - 2) in + (match List.assoc_opt name table with + | Some v -> Buffer.add_string b v + | None -> Buffer.add_string b (String.sub s !i (j - !i + 1))); + i := j + 1 + else (Buffer.add_char b s.[!i]; incr i) + done; + Buffer.contents b + +let rules_of blocks = + List.concat_map + (fun ((b : block), base) -> + if b.kind <> "rule" then [] + else begin + check_fields b rule_fields; + let inputs = texts (get b "inputs") in + let outputs = texts (get b "outputs") in + let command = texts (get b "command") in + let descr = Option.value (get1 b "description") ~default:("gen " ^ b.bname) in + if outputs = [] then Diag.error ~span:b.nspan "rule %s declares no outputs" b.bname; + if command = [] then Diag.error ~span:b.nspan "rule %s declares no command" b.bname; + let files = + List.concat_map (fun p -> Glob.expand (Eval.under base p)) inputs |> List.sort_uniq compare + in + if files = [] then + Diag.error ~span:b.nspan ~hint:"the inputs pattern matched nothing" + "rule %s has no inputs" b.bname; + List.map + (fun f -> + let stem = Filename.remove_extension (Filename.basename f) in + let table0 = + [ ("in", f); ("stem", stem); ("dir", Filename.dirname f); + ("base", Filename.basename f); ("ext", Filename.extension f); ("name", b.bname) ] + in + let routs = List.map (fun o -> Eval.under base (subst table0 o)) outputs in + let table = + table0 @ ("out", List.hd routs) + :: List.mapi (fun i o -> (Printf.sprintf "out%d" i, o)) routs + in + { + rname = b.bname; + rin = [ f ]; + routs; + rcmd = List.map (subst table) command; + rdesc = subst table descr; + rspan = b.nspan; + }) + files + end) + blocks + +let target_of pkgs ((b : block), base) = + check_fields b target_fields; + let kind = + match b.kind with + | "lib" -> Lib + | "shared" -> Shared + | "bin" -> Bin + | _ -> Test + in + let pkg_names = texts (get b "pkg") in + let pkg_cflags = List.concat_map (fun p -> + match Hashtbl.find_opt pkgs p with + | Some (c, _) -> c + | None -> Diag.error ~span:b.nspan + ~hint:(Printf.sprintf "add 'pkg %s' at the top level first" p) + "%s uses pkg %S, which was never checked" b.bname p) pkg_names + in + let pkg_libs = List.concat_map (fun p -> + match Hashtbl.find_opt pkgs p with Some (_, l) -> l | None -> []) pkg_names + in + { + name = b.bname; + kind; + dir = base; + srcs = []; + gen_srcs = []; + includes = List.map (Eval.under base) (texts (get b "include")); + uses = texts (get b "use"); + cflags = texts (get b "cflags") @ pkg_cflags; + cxxflags = texts (get b "cxxflags") @ pkg_cflags; + ldflags = texts (get b "ldflags") @ pkg_libs; + defines = texts (get b "define"); + pkgs = pkg_names; + install = get1 b "install"; + soname = get1 b "soname"; + args = texts (get b "args"); + span = b.nspan; + } + +let sources_of ~extra ((b : block), base) (t : target) = + let pats = texts (get b "srcs") in + if pats = [] then + Diag.error ~span:b.nspan ~hint:"add a line like: srcs src/*.c" "%s %s declares no srcs" + b.kind b.bname; + let files = + List.concat_map (fun p -> Glob.expand_with ~extra (Eval.under base p)) pats + |> List.sort_uniq compare + in + if files = [] then + Diag.error ~span:b.nspan + ~hint:(Printf.sprintf "no file matches %s" (String.concat " " pats)) + "%s %s matched no sources" b.kind b.bname; + let gen, real = List.partition (fun f -> List.mem f extra) files in + { t with srcs = real; gen_srcs = gen } + +let installs_of blocks = + List.concat_map + (fun ((b : block), base) -> + if b.kind <> "install" then [] + else begin + check_fields b install_fields; + let dest = match get1 b "to" with + | Some d -> d + | None -> Diag.error ~span:b.nspan ~hint:"add: to include" "install %s has no destination" b.bname + in + let files = + List.concat_map (fun p -> Glob.expand (Eval.under base p)) (texts (get b "files")) + in + if files = [] then Diag.error ~span:b.nspan "install %s matched no files" b.bname; + List.map (fun f -> { from = f; dest }) files + end) + blocks + +let project (env : Eval.env) = + let blocks = env.blocks in + let rules = rules_of blocks in + let generated = List.concat_map (fun r -> r.routs) rules in + let tblocks = + List.filter (fun ((b : block), _) -> List.mem b.kind [ "lib"; "shared"; "bin"; "test" ]) blocks + in + let targets = + List.map (fun bb -> sources_of ~extra:generated bb (target_of env.pkgs bb)) tblocks + in + List.iter + (fun (b : block) -> + if not (List.mem b.kind [ "lib"; "shared"; "bin"; "test"; "rule"; "install"; "toolchain" ]) then + Diag.error ~span:b.kspan "unknown block %S" b.kind) + (List.map fst blocks); + let seen = Hashtbl.create 8 in + List.iter + (fun t -> + match Hashtbl.find_opt seen t.name with + | Some (prev : target) -> + Diag.error ~span:t.span ~notes:[ (prev.span, "first declared here") ] + "target %S is declared twice" t.name + | None -> Hashtbl.add seen t.name t) + targets; + let p = + { + pname = env.pname; + tc = env.tc; + targets; + rules; + defines = env.defines; + config_header = env.config_header; + installs = installs_of blocks; + prefix = (match Hashtbl.find_opt env.vars "prefix" with Some [ p ] -> p | _ -> "/usr/local"); + platform = (match Hashtbl.find_opt env.vars "platform" with Some [ p ] -> p | _ -> "linux"); + } + in + List.iter + (fun t -> + List.iter + (fun u -> + match find p u with + | None -> + Diag.error ~span:t.span + ~hint:(Suggest.hint u (List.map (fun x -> x.name) targets)) + "%s uses %S, which is not a target" t.name u + | Some x when x.kind = Bin || x.kind = Test -> + Diag.error ~span:t.span "%s uses %S, which is a %s, not a library" t.name u + (kind_name x.kind) + | Some _ -> ()) + t.uses) + targets; + let rec cyc seen t = + if List.mem t.name seen then + Diag.error ~span:t.span "dependency cycle: %s" (String.concat " -> " (List.rev (t.name :: seen))); + List.iter (fun u -> match find p u with Some x -> cyc (t.name :: seen) x | None -> ()) t.uses + in + List.iter (cyc []) targets; + p