(* * pa_optcomp.ml * ------------- * Copyright : (c) 2008, Jeremie Dimino * Licence : BSD3 * * This file is a part of optcomp. *) open Camlp4.Sig open Camlp4.PreCast external filter : 'a Gram.not_filtered -> 'a = "%identity" external not_filtered : 'a -> 'a Gram.not_filtered = "%identity" (* Subset of supported caml types *) type typ = | Tvar of string | Tbool | Tint | Tchar | Tstring | Ttuple of typ list (* Subset of supported caml values *) type value = | Bool of bool | Int of int | Char of char | String of string | Tuple of value list type ident = string (* An identifier. It is either a lower or a upper identifier. *) module Env = Map.Make(struct type t = ident let compare = compare end) type env = value Env.t type directive = | Dir_let of ident * Ast.expr | Dir_default of ident * Ast.expr | Dir_if of Ast.expr | Dir_else | Dir_elif of Ast.expr | Dir_endif | Dir_include of Ast.expr | Dir_error of Ast.expr | Dir_warning of Ast.expr | Dir_directory of Ast.expr (* This one is not part of optcomp but this is one of the directives handled by camlp4 we probably want to use. *) | Dir_default_quotation of Ast.expr (* Quotations are evaluated by the token filters, but are expansed after. Evaluated quotations are kept in this table, which quotation id to to values: *) let quotations : (int, value) Hashtbl.t = Hashtbl.create 42 let next_quotation_id = let r = ref 0 in fun _ -> incr r; !r (* +-------------+ | Environment | +-------------+ *) let env = ref Env.empty let define id value = env := Env.add id value !env let _ = define "ocaml_version" (Scanf.sscanf Sys.ocaml_version "%d.%d" (fun major minor -> Tuple [Int major; Int minor])) let dirs = ref [] let add_include_dir dir = dirs := dir :: !dirs (* +--------------+ | Dependencies | +--------------+ *) module String_set = Set.Make(String) (* All depencies of the file being parsed *) let dependencies = ref String_set.empty (* Where to write dependencies *) let dependency_filename = ref None (* The file being parsed. This is set when the first (token, location) pair is fetched. *) let source_filename = ref None let write_depencies () = match !dependency_filename, !source_filename with | None, _ | _, None -> () | Some dependency_filename, Some source_filename -> let oc = open_out dependency_filename in if not (String_set.is_empty !dependencies) then begin output_string oc "# automatically generated by optcomp\n"; output_string oc source_filename; output_string oc ": "; output_string oc (String.concat " " (String_set.elements !dependencies)); output_char oc '\n' end; close_out oc (* +----------------------------------------+ | Value to expression/pattern conversion | +----------------------------------------+ *) let rec expr_of_value _loc = function | Bool true -> <:expr< true >> | Bool false -> <:expr< false >> | Int x -> <:expr< $int:string_of_int x$ >> | Char x -> <:expr< $chr:Char.escaped x$ >> | String x -> <:expr< $str:String.escaped x$ >> | Tuple [] -> <:expr< () >> | Tuple [x] -> expr_of_value _loc x | Tuple l -> <:expr< $tup:Ast.exCom_of_list (List.map (expr_of_value _loc) l)$ >> let rec patt_of_value _loc = function | Bool true -> <:patt< true >> | Bool false -> <:patt< false >> | Int x -> <:patt< $int:string_of_int x$ >> | Char x -> <:patt< $chr:Char.escaped x$ >> | String x -> <:patt< $str:String.escaped x$ >> | Tuple [] -> <:patt< () >> | Tuple [x] -> patt_of_value _loc x | Tuple l -> <:patt< $tup:Ast.paCom_of_list (List.map (patt_of_value _loc) l)$ >> (* +-----------------------+ | Expression evaluation | +-----------------------+ *) let rec type_of_value = function | Bool _ -> Tbool | Int _ -> Tint | Char _ -> Tchar | String _ -> Tstring | Tuple l -> Ttuple (List.map type_of_value l) let rec string_of_type = function | Tvar v -> "'" ^ v | Tbool -> "bool" | Tint -> "int" | Tchar -> "char" | Tstring -> "string" | Ttuple l -> "(" ^ String.concat " * " (List.map string_of_type l) ^ ")" let invalid_type loc expected real = Loc.raise loc (Failure (Printf.sprintf "this expression has type %s but is used with type %s" (string_of_type real) (string_of_type expected))) let type_of_patt patt = let rec aux (a, n) = function | <:patt< $tup:x$ >> -> let l, x = List.fold_left (fun (l, x) patt -> let t, x = aux x patt in (t :: l, x)) ([], (a, n)) (Ast.list_of_patt x []) in (Ttuple(List.rev l), x) | _ -> (Tvar(Printf.sprintf "%c%s" (char_of_int (Char.code 'a' + a)) (if n = 0 then "" else string_of_int n)), if a = 25 then (0, n + 1) else (a + 1, n)) in fst (aux (0, 0) patt) let rec eval env = function (* Literals *) | <:expr< true >> -> Bool true | <:expr< false >> -> Bool false | <:expr< $int:x$ >> -> Int(int_of_string x) | <:expr< $chr:x$ >> -> Char(Camlp4.Struct.Token.Eval.char x) | <:expr< $str:x$ >> -> String(Camlp4.Struct.Token.Eval.string ~strict:() x) (* Tuples *) | <:expr< $tup:x$ >> -> Tuple(List.map (eval env) (Ast.list_of_expr x [])) (* Variables *) | <:expr@loc< $lid:x$ >> | <:expr@loc< $uid:x$ >> -> begin try Env.find x env with Not_found -> Loc.raise loc (Failure (Printf.sprintf "unbound value %s" x)) end (* Value comparing *) | <:expr< $x$ = $y$ >> -> let x, y = eval_same env x y in Bool(x = y) | <:expr< $x$ < $y$ >> -> let x, y = eval_same env x y in Bool(x < y) | <:expr< $x$ > $y$ >> -> let x, y = eval_same env x y in Bool(x > y) | <:expr< $x$ <= $y$ >> -> let x, y = eval_same env x y in Bool(x <= y) | <:expr< $x$ >= $y$ >> -> let x, y = eval_same env x y in Bool(x >= y) | <:expr< $x$ <> $y$ >> -> let x, y = eval_same env x y in Bool(x <> y) (* min and max *) | <:expr< min $x$ $y$ >> -> let x, y = eval_same env x y in min x y | <:expr< max $x$ $y$ >> -> let x, y = eval_same env x y in max x y (* Arithmetic *) | <:expr< $x$ + $y$ >> -> Int(eval_int env x + eval_int env y) | <:expr< $x$ - $y$ >> -> Int(eval_int env x - eval_int env y) | <:expr< $x$ * $y$ >> -> Int(eval_int env x * eval_int env y) | <:expr< $x$ / $y$ >> -> Int(eval_int env x / eval_int env y) | <:expr< $x$ mod $y$ >> -> Int(eval_int env x mod eval_int env y) (* Boolean operations *) | <:expr< not $x$ >> -> Bool(not (eval_bool env x)) | <:expr< $x$ or $y$ >> -> Bool(eval_bool env x or eval_bool env y) | <:expr< $x$ || $y$ >> -> Bool(eval_bool env x || eval_bool env y) | <:expr< $x$ && $y$ >> -> Bool(eval_bool env x && eval_bool env y) (* String operations *) | <:expr< $x$ ^ $y$ >> -> String(eval_string env x ^ eval_string env y) (* Pair operations *) | <:expr< fst $x$ >> -> fst (eval_pair env x) | <:expr< snd $x$ >> -> snd (eval_pair env x) (* Let-binding *) | <:expr< let $p$ = $x$ in $y$ >> -> let vx = eval env x in let env = try bind env p vx with Exit -> invalid_type (Ast.loc_of_expr x) (type_of_patt p) (type_of_value vx) in eval env y | e -> Loc.raise (Ast.loc_of_expr e) (Stream.Error "expression not supported") and bind env patt value = match patt with | <:patt< $lid:id$ >> -> Env.add id value env | <:patt< $tup:patts$ >> -> let patts = Ast.list_of_patt patts [] in begin match value with | Tuple values when List.length values = List.length patts -> List.fold_left2 bind env patts values | _ -> raise Exit end | _ -> Loc.raise (Ast.loc_of_patt patt) (Stream.Error "pattern not supported") and eval_same env ex ey = let vx = eval env ex and vy = eval env ey in let tx = type_of_value vx and ty = type_of_value vy in if tx = ty then (vx, vy) else invalid_type (Ast.loc_of_expr ey) tx ty and eval_int env e = match eval env e with | Int x -> x | v -> invalid_type (Ast.loc_of_expr e) Tint (type_of_value v) and eval_bool env e = match eval env e with | Bool x -> x | v -> invalid_type (Ast.loc_of_expr e) Tbool (type_of_value v) and eval_string env e = match eval env e with | String x -> x | v -> invalid_type (Ast.loc_of_expr e) Tstring (type_of_value v) and eval_pair env e = match eval env e with | Tuple [x; y] -> (x, y) | v -> invalid_type (Ast.loc_of_expr e) (Ttuple [Tvar "a"; Tvar "b"]) (type_of_value v) (* +-----------------------+ | Parsing of directives | +-----------------------+ *) let rec skip_space stream = match Stream.peek stream with | Some((BLANKS _ | COMMENT _), _) -> Stream.junk stream; skip_space stream | _ -> () let parse_equal stream = skip_space stream; match Stream.next stream with | KEYWORD "=", _ -> () | _, loc -> Loc.raise loc (Stream.Error "'=' expected") let rec parse_eol stream = let tok, loc = Stream.next stream in match tok with | BLANKS _ | COMMENT _ -> parse_eol stream | NEWLINE | EOI -> () | _ -> Loc.raise loc (Stream.Error "end of line expected") (* Return wether a keyword can be interpreted as an identifier *) let keyword_is_id str = let rec aux i = if i = String.length str then true else match str.[i] with | 'A' .. 'Z' | 'a' .. 'z' | '0' .. '9' -> aux (i + 1) | _ -> false in aux 0 let parse_ident stream = skip_space stream; let tok, loc = Stream.next stream in begin match tok with | LIDENT id | UIDENT id -> id | KEYWORD kwd when keyword_is_id kwd -> kwd | _ -> Loc.raise loc (Stream.Error "identifier expected") end let parse_expr stream = (* Lists of opened brackets *) let opened_brackets = ref [] in (* Return the next token of [stream] until all opened parentheses have been closed and a newline is reached *) let rec next_token _ = Some(match Stream.next stream, !opened_brackets with | (NEWLINE, loc), [] -> EOI, loc | (KEYWORD("(" | "[" | "{" as b), _) as x, l -> opened_brackets := b :: l; x | (KEYWORD ")", loc) as x, "(" :: l -> opened_brackets := l; x | (KEYWORD "]", loc) as x, "[" :: l -> opened_brackets := l; x | (KEYWORD "}", loc) as x, "{" :: l -> opened_brackets := l; x | x, _ -> x) in Gram.parse_tokens_before_filter Syntax.expr_eoi (not_filtered (Stream.from next_token)) let parse_directive stream = match Stream.peek stream with | Some(KEYWORD "#", loc) -> Stream.junk stream; (* Move the location to the beginning of the line *) let (file_name, start_line, start_bol, start_off, stop_line, stop_bol, stop_off, ghost) = Loc.to_tuple loc in let loc = Loc.of_tuple (file_name, start_line, start_bol, start_bol, start_line, start_bol, start_bol, ghost) in begin match parse_ident stream with | "let" -> let id = parse_ident stream in parse_equal stream; let expr = parse_expr stream in Some(Dir_let(id, expr), loc) | "let_default" -> let id = parse_ident stream in parse_equal stream; let expr = parse_expr stream in Some(Dir_default(id, expr), loc) (* For compatibility *) | "define" -> let id = parse_ident stream in let expr = parse_expr stream in Some(Dir_let(id, expr), loc) (* For compatibility *) | "default" -> let id = parse_ident stream in let expr = parse_expr stream in Some(Dir_default(id, expr), loc) | "if" -> Some(Dir_if(parse_expr stream), loc) | "else" -> parse_eol stream; Some(Dir_else, loc) | "elif" -> Some(Dir_elif(parse_expr stream), loc) | "endif" -> parse_eol stream; Some(Dir_endif, loc) | "include" -> Some(Dir_include(parse_expr stream), loc) | "directory" -> Some(Dir_directory(parse_expr stream), loc) | "error" -> Some(Dir_error(parse_expr stream), loc) | "warning" -> Some(Dir_warning(parse_expr stream), loc) | "default_quotation" -> Some(Dir_default_quotation(parse_expr stream), loc) | dir -> Loc.raise loc (Stream.Error (Printf.sprintf "bad directive ``%s''" dir)) end | _ -> None let parse_command_line_define str = match Gram.parse_string Syntax.expr (Loc.mk "") str with | <:expr< $lid:id$ = $e$ >> | <:expr< $uid:id$ = $e$ >> -> define id (eval !env e) | _ -> invalid_arg str (* +----------------+ | BLock skipping | +----------------+ *) let rec skip_line stream = match Stream.next stream with | NEWLINE, _ -> () | EOI, loc -> Loc.raise loc (Stream.Error "#endif missing") | _ -> skip_line stream let rec next_directive stream = match parse_directive stream with | Some dir -> dir | None -> skip_line stream; next_directive stream let rec next_endif stream = let dir, loc = next_directive stream in match dir with | Dir_if _ -> skip_if stream; next_endif stream | Dir_else | Dir_elif _ | Dir_endif -> dir | _ -> next_endif stream and skip_if stream = let dir, loc = next_directive stream in match dir with | Dir_if _ -> skip_if stream; skip_if stream | Dir_else -> skip_else stream | Dir_elif _ -> skip_if stream | Dir_endif -> () | _ -> skip_if stream and skip_else stream = let dir, loc = next_directive stream in match dir with | Dir_if _ -> skip_if stream; skip_else stream | Dir_else -> Loc.raise loc (Stream.Error "#else without #if") | Dir_elif _ -> Loc.raise loc (Stream.Error "#elif without #if") | Dir_endif -> () | _ -> skip_else stream (* +-----------------+ | Token filtering | +-----------------+ *) type context = Ctx_if | Ctx_else (* State of the token filter *) type state = { stream : (Gram.Token.t * Loc.t) Stream.t; (* Input stream *) mutable bol : bool; (* Wether we are at the beginning of a line *) mutable stack : context list; (* Nested contexts *) on_eoi : Gram.Token.t * Loc.t -> Gram.Token.t * Loc.t; (* Eoi handler, it is used to restore the previous sate on #include directives *) } (* Read and return one token *) let really_read state = let tok, loc = Stream.next state.stream in state.bol <- tok = NEWLINE; match tok with | QUOTATION ({ q_name = "optcomp" } as quot) -> let id = next_quotation_id () in Hashtbl.add quotations id (eval !env (Gram.parse_string Syntax.expr_eoi (Loc.move `start quot.q_shift loc) quot.q_contents)); (* Replace the quotation by its id *) (QUOTATION { quot with q_contents = string_of_int id }, loc) | EOI -> (* If end of input is reached, we call the eoi handler. It may continue if we were parsing an included file *) if state.stack <> [] then Loc.raise loc (Stream.Error "#endif missing"); state.on_eoi (tok, loc) | _ -> (tok, loc) (* Return the next token from a stream, interpreting directives. *) let rec next_token state_ref = let state = !state_ref in if state.bol then match parse_directive state.stream, state.stack with | Some(Dir_if e, _), _ -> let rec aux e = if eval_bool !env e then begin state.stack <- Ctx_if :: state.stack; next_token state_ref end else match next_endif state.stream with | Dir_else -> state.stack <- Ctx_else :: state.stack; next_token state_ref | Dir_elif e -> aux e | Dir_endif -> next_token state_ref | _ -> assert false in aux e | Some(Dir_else, loc), ([] | Ctx_else :: _) -> Loc.raise loc (Stream.Error "#else without #if") | Some(Dir_elif _, loc), ([] | Ctx_else :: _) -> Loc.raise loc (Stream.Error "#elif without #if") | Some(Dir_endif, loc), [] -> Loc.raise loc (Stream.Error "#endif without #if") | Some(Dir_else, loc), Ctx_if :: l -> skip_else state.stream; state.stack <- l; next_token state_ref | Some(Dir_elif _, loc), Ctx_if :: l -> skip_if state.stream; state.stack <- l; next_token state_ref | Some(Dir_endif, loc), _ :: l -> state.stack <- l; next_token state_ref | Some(Dir_let(id, e), _), _ -> define id (eval !env e); next_token state_ref | Some(Dir_default(id, e), _), _ -> if not (Env.mem id !env) then define id (eval !env e); next_token state_ref | Some(Dir_include e, _), _ -> let fname = eval_string !env e in (* Try to looks up in all include directories *) let fname = try List.find (fun dir -> Sys.file_exists (Filename.concat dir fname)) !dirs with (* Just try in the current directory *) Not_found -> fname in dependencies := String_set.add fname !dependencies; let ic = open_in fname in let nested_state = { stream = Gram.Token.Filter.filter (Gram.get_filter ()) (filter (Gram.lex (Loc.mk fname) (Stream.of_channel ic))); bol = true; stack = []; on_eoi = (fun _ -> (* Restore previous state and close channel on eoi *) state_ref := state; close_in ic; next_token state_ref) } in (* Replace current state with the new one *) state_ref := nested_state; next_token state_ref | Some(Dir_directory e, loc), _ -> let dir = eval_string !env e in add_include_dir dir; next_token state_ref | Some(Dir_error e, loc), _ -> Loc.raise loc (Failure (eval_string !env e)) | Some(Dir_warning e, loc), _ -> Syntax.print_warning loc (eval_string !env e); next_token state_ref | Some(Dir_default_quotation e, loc), _ -> Syntax.Quotation.default := eval_string !env e; next_token state_ref | None, _ -> really_read state else really_read state let stream_filter filter stream = (* Set the source filename *) begin match !source_filename with | Some _ -> () | None -> match Stream.peek stream with | None -> () | Some(tok, loc) -> source_filename := Some(Loc.file_name loc) end; let state_ref = ref { stream = stream; bol = true; stack = []; on_eoi = (fun x -> x) } in filter (Stream.from (fun _ -> Some(next_token state_ref))) (* +----------------------+ | Quotations expansion | +----------------------+ *) let expand f loc _ contents = try f loc (Hashtbl.find quotations (int_of_string contents)) with exn -> Loc.raise loc (Failure "fatal error in optcomp!") (* +--------------+ | Registration | +--------------+ *) let _ = Camlp4.Options.add "-let" (Arg.String parse_command_line_define) " Binding for a #let directive."; Camlp4.Options.add "-depend" (Arg.String (fun filename -> dependency_filename := Some filename)) " Write dependencies to ."; Pervasives.at_exit write_depencies; Syntax.Quotation.add "optcomp" Syntax.Quotation.DynAst.expr_tag (expand expr_of_value); Syntax.Quotation.add "optcomp" Syntax.Quotation.DynAst.patt_tag (expand patt_of_value); Gram.Token.Filter.define_filter (Gram.get_filter ()) stream_filter