open Ast type p = { toks : Lexer.t array; mutable pos : int } let stmt_keywords = [ "project"; "set"; "append"; "option"; "include"; "subdir"; "if"; "check"; "pkg"; "config_header"; "message" ] let block_kinds = [ "toolchain"; "lib"; "shared"; "bin"; "test"; "rule"; "install"; "run" ] let check_kinds = [ "header"; "func"; "symbol"; "sizeof"; "compiles" ] let peek p = p.toks.(p.pos) let kind p = (peek p).Lexer.kind let span p = (peek p).Lexer.span let advance p = if p.pos < Array.length p.toks - 1 then p.pos <- p.pos + 1 let bump p = let t = peek p in advance p; t let rec skip_nl p = match kind p with Lexer.Newline -> advance p; skip_nl p | _ -> () let expected p what = Diag.error ~span:(span p) "expected %s, found %s" what (Lexer.describe (kind p)) let word_value p = match kind p with | Lexer.Word w -> let s = span p in advance p; { text = w; span = s; quoted = false } | Lexer.Str w -> let s = span p in advance p; { text = w; span = s; quoted = true } | _ -> expected p "a value" let is_value p = match kind p with Lexer.Word _ | Lexer.Str _ -> true | _ -> false let name_value p what = match kind p with | Lexer.Word w -> let s = span p in advance p; { text = w; span = s; quoted = false } | _ -> expected p what let eat_lbrace p = skip_nl p; match kind p with Lexer.Lbrace -> advance p | _ -> expected p "'{'" let end_of_line p = match kind p with | Lexer.Newline | Lexer.Eof -> () | Lexer.Rbrace -> () | _ -> expected p "end of line" let values_until_eol p = let acc = ref [] in while is_value p do acc := word_value p :: !acc done; end_of_line p; List.rev !acc let at_word p w = match kind p with Lexer.Word x -> x = w | _ -> false (* conditions: or > and > cmp > not > primary *) let rec expr p = or_expr p and or_expr p = let l = ref (and_expr p) in while at_word p "or" do advance p; l := Or (!l, and_expr p) done; !l and and_expr p = let l = ref (cmp_expr p) in while at_word p "and" do advance p; l := And (!l, cmp_expr p) done; !l and cmp_expr p = let l = unary p in match kind p with | Lexer.Word (("==" | "!=") as op) -> ( advance p; let r = word_value p in match l with | Atom a -> Cmp (a, op, r) | _ -> Diag.error ~span:r.span "%s compares values, not conditions" op) | _ -> l and unary p = if at_word p "not" then (let s = span p in advance p; Not (unary p, s)) else primary p and primary p = match kind p with | Lexer.Lparen -> advance p; let e = expr p in (match kind p with Lexer.Rparen -> advance p | _ -> expected p "')'"); e | Lexer.Word _ | Lexer.Str _ -> Atom (word_value p) | _ -> expected p "a condition" let rec fitems p = let acc = ref [] in let stop = ref false in while not !stop do skip_nl p; match kind p with | Lexer.Rbrace -> advance p; stop := true | Lexer.Eof -> Diag.error ~span:(span p) "unclosed block" | Lexer.Word "if" -> let a, b = fif p fitems in acc := FIf (a, b) :: !acc | Lexer.Word key -> let kspan = span p in advance p; let values = values_until_eol p in if values = [] then Diag.error ~span:kspan ~hint:"write a value after the field name" "field %S has no value" key; acc := FField { key; kspan; values } :: !acc | _ -> expected p "a field name or '}'" done; List.rev !acc and fif : 'a. p -> (p -> 'a list) -> (expr * 'a list) list * 'a list = fun p body -> advance p; let cond = expr p in eat_lbrace p; let taken = body p in let arms = ref [ (cond, taken) ] in let fallback = ref [] in let stop = ref false in while not !stop do let save = p.pos in skip_nl p; if at_word p "else" then begin advance p; if at_word p "if" then begin advance p; let c = expr p in eat_lbrace p; arms := (c, body p) :: !arms end else begin eat_lbrace p; fallback := body p; stop := true end end else (p.pos <- save; stop := true) done; (List.rev !arms, !fallback) let check_stmt p kspan = let what = name_value p "a check kind" in let c = match what.text with | "header" -> Header (word_value p) | "sizeof" -> Sizeof (word_value p) | "func" -> let f = word_value p in if at_word p "in" then (advance p; Func (f, Some (word_value p))) else Func (f, None) | "symbol" -> let s = word_value p in if not (at_word p "in") then Diag.error ~span:(span p) ~hint:"write: check symbol NAME in HEADER" "expected 'in'"; advance p; Symbol (s, word_value p) | "compiles" -> let n = word_value p in let body = word_value p in if not body.quoted then Diag.error ~span:body.span "the program for 'check compiles' must be a quoted string"; Compiles (n, body) | other -> Diag.error ~span:what.span ~hint:(Suggest.hint other check_kinds) "unknown check %S" other in end_of_line p; Check (c, kspan) let rec stmt p = let t = peek p in match t.Lexer.kind with | Lexer.Word "project" -> advance p; let name = name_value p "the project name" in end_of_line p; Project name | Lexer.Word "set" -> advance p; let n = name_value p "a variable name" in Set (n, values_until_eol p) | Lexer.Word "append" -> advance p; let n = name_value p "a variable name" in Append (n, values_until_eol p) | Lexer.Word "option" -> advance p; let n = name_value p "an option name" in let ty = name_value p "the option type (bool, string or path)" in if not (List.mem ty.text [ "bool"; "string"; "path" ]) then Diag.error ~span:ty.span ~hint:(Suggest.hint ty.text [ "bool"; "string"; "path" ]) "unknown option type %S" ty.text; Option (n, ty, values_until_eol p) | Lexer.Word "include" -> advance p; let v = word_value p in end_of_line p; Include v | Lexer.Word "subdir" -> advance p; let v = word_value p in end_of_line p; Subdir v | Lexer.Word "config_header" -> advance p; let v = word_value p in end_of_line p; ConfigHeader v | Lexer.Word "message" -> advance p; let lvl = name_value p "a message level (info, warn or error)" in if not (List.mem lvl.text [ "info"; "warn"; "error" ]) then Diag.error ~span:lvl.span ~hint:(Suggest.hint lvl.text [ "info"; "warn"; "error" ]) "unknown message level %S" lvl.text; Message (lvl, values_until_eol p) | Lexer.Word "check" -> advance p; check_stmt p t.Lexer.span | Lexer.Word "pkg" -> advance p; let n = word_value p in let c = if is_value p then Some (word_value p) else None in end_of_line p; Pkg (n, c, t.Lexer.span) | Lexer.Word "if" -> let arms, els = fif p stmts_body in If (arms, els) | Lexer.Word k when List.mem k block_kinds -> advance p; let bname, nspan = if k = "toolchain" then ("", t.Lexer.span) else let v = name_value p (Printf.sprintf "a name for this %s" k) in (v.text, v.span) in eat_lbrace p; Blk { kind = k; kspan = t.Lexer.span; bname; nspan; items = fitems p } | Lexer.Word other -> Diag.error ~span:t.Lexer.span ~hint:(Suggest.hint other (stmt_keywords @ block_kinds)) "unknown declaration %S" other | k -> Diag.error ~span:t.Lexer.span "expected a declaration, found %s" (Lexer.describe k) and stmts_body p = let acc = ref [] in let stop = ref false in while not !stop do skip_nl p; match kind p with | Lexer.Rbrace -> advance p; stop := true | Lexer.Eof -> Diag.error ~span:(span p) "unclosed block" | _ -> acc := stmt p :: !acc done; List.rev !acc let program p = let acc = ref [] in let stop = ref false in while not !stop do skip_nl p; match kind p with Lexer.Eof -> stop := true | _ -> acc := stmt p :: !acc done; List.rev !acc let string ~file src = Diag.register file src; let toks = Array.of_list (Lexer.tokens ~file src) in program { toks; pos = 0 } let file path = match Fs.read path with | exception Sys_error m -> Diag.error "%s" m | src -> string ~file:path src