1(*
2 * Copyright 2014, NICTA
3 *
4 * This software may be distributed and modified according to the terms of
5 * the BSD 2-Clause license. Note that NO WARRANTY is provided.
6 * See "LICENSE_BSD2.txt" for details.
7 *
8 * @TAG(NICTA_BSD)
9 *)
10
11structure Main = struct
12
13open OS.Process
14
15
16(* takes a file name on the command-line, and attempts to parse it *)
17fun die s = (print s; print "\n"; exit failure)
18fun warn s = (TextIO.output(TextIO.stdErr, s^"\n");
19              TextIO.flushOut TextIO.stdErr)
20val execname = CommandLine.name
21
22
23val _ = Feedback.errorThreshold := NONE;
24val _ = Feedback.informf := (fn s => (TextIO.output(TextIO.stdOut, s);
25                                      TextIO.flushOut TextIO.stdOut))
26
27fun quote s = "\"" ^ s ^ "\""
28
29
30val commas = String.concat o separate ", "
31fun writeln s = Feedback.informStr(0,s)
32
33fun print_addressed_vars cse = let
34  open ProgramAnalysis
35  val globs = get_globals cse
36  val pfx1 = "There are "^Int.toString (length globs)^ " globals: "
37  val _ = writeln (String.concat
38                       (separate "\n   " (pfx1 :: map srcname globs)))
39  val addressed = get_addressed cse
40  val addr_vars = map MString.dest (MSymTab.keys addressed)
41  val pfx2 = "There are "^Int.toString (length addr_vars)^
42             " addressed variables: "
43  val _ = writeln (String.concatWith "\n  " (pfx2 :: addr_vars))
44in
45  ()
46end
47
48fun print_embedded_fncalls cse = let
49  open ProgramAnalysis
50  val calls = get_embedded_fncalls cse
51  fun filter call =
52      case call of
53        DirectCall s => let
54        in
55          case get_modifies cse s of
56            NONE => SOME s
57          | SOME varset => if not (Binaryset.isEmpty varset) then SOME s
58                           else NONE
59        end
60      | _ => NONE
61  val call_list = List.mapPartial filter (Binaryset.listItems calls)
62  val pfx = "These " ^ Int.toString (length call_list) ^
63            " functions MUST be shown to be side-effect free (and don't look as if they are): "
64in
65  writeln (String.concat (separate "\n   " (pfx :: call_list)))
66end
67
68val printmv = ProgramAnalysis.mvar_toString
69
70fun print_modifies cse = let
71  open ProgramAnalysis
72  val functions = get_functions cse
73  fun print f = let
74    val fnm = if is_recursivefn cse f then f ^ " (recursive)" else f
75  in
76    writeln ("   " ^ StringCvt.padRight #" " 50 fnm ^ ":  " ^
77             (case get_modifies cse f of
78                NONE => "<is or calls unannotated proto>"
79              | SOME varset =>
80                String.concat
81                    (separate " " (map printmv (Binaryset.listItems varset)))))
82  end
83in
84  writeln "Modifies info:";
85  app print functions
86end
87
88fun print_reads cse = let
89  open ProgramAnalysis
90  val functions = get_functions cse
91  val reads = get_read_globals cse
92  fun print f = let
93    val vars = Binaryset.foldr (fn (mv,acc) => printmv mv :: acc)
94                               []
95                               (valOf (Symtab.lookup reads f))
96               handle Option => ["<is or calls unannotated proto>"]
97  in
98    writeln ("   " ^ StringCvt.padRight #" " 50 f ^ ":  "^
99             String.concat (separate " " vars))
100  end
101in
102  writeln "Function reads:";
103  app print functions
104end
105
106fun calc_protoes cse = let
107  open ProgramAnalysis
108  val functions = get_functions cse
109  val defined_functions = get_defined_functions cse
110  fun foldthis (f, acc) =
111      if isSome (Symtab.lookup defined_functions f) then acc
112      else Binaryset.add(acc, f)
113in
114  List.foldl foldthis (Binaryset.empty String.compare) functions
115end
116
117fun print_protoes cse = let
118in
119  writeln "Protoes (only):";
120  Binaryset.app (fn v => writeln ("   " ^ v)) (calc_protoes cse)
121end
122
123fun print_unmodified_globals cse = let
124in
125  writeln "Unmodifed, unaddressed globals:";
126  writeln ("   " ^
127           (cse |> ProgramAnalysis.calc_untouched_globals
128                |> MSymTab.keys
129                |> map MString.dest
130                |> commas))
131end
132
133val filename = ref ""
134fun produce_dotfile cse = let
135  open ProgramAnalysis
136  val os = TextIO.openOut (!filename ^ ".dot")
137  fun print s = TextIO.output(os, s ^ "\n")
138  val fns = get_functions cse
139  val {callgraph,...} = compute_callgraphs cse
140  fun print_fn f = let
141    val callees = case Symtab.lookup callgraph f of
142                    NONE => Binaryset.empty String.compare
143                  | SOME s => s
144  in
145    Binaryset.app (fn c => print (f ^ " -> " ^ c)) callees
146  end
147in
148  print "digraph {";
149  print "graph[fontsize=8]";
150  print "node[shape=plaintext]";
151  List.app print_fn fns;
152  print "}";
153  TextIO.closeOut os
154end
155
156fun produce_toposort cse = let
157  open ProgramAnalysis
158  fun lift f fnm = case Symtab.lookup f fnm of
159                     NONE => []
160                   | SOME s => Binaryset.listItems s
161  val {callgraph,callers} = compute_callgraphs cse
162  val toposort = Topo_Sort.topo_sort {cmp = String.compare,
163                                      graph = lift callgraph,
164                                      converse = lift callers}
165                                     (get_functions cse)
166  fun printcomp [] = raise Fail "Empty SCC??"
167    | printcomp [x] = writeln ("   "^x)
168    | printcomp list = writeln ("   " ^ commas list)
169in
170  writeln "Topological sort of functions in callgraph";
171  List.app printcomp toposort
172end
173
174
175fun print_uncalledfns cse = let
176  open ProgramAnalysis
177  val {callgraph = graph,...} = compute_callgraphs cse
178  val fns = get_functions cse
179  fun foldthis (fname, acc) =
180      case Symtab.lookup graph fname of
181        NONE => fname :: acc
182      | SOME s => if Binaryset.isEmpty s then fname::acc
183                  else acc
184  val uncalled = List.foldl foldthis [] fns
185in
186  writeln "Uncalled functions";
187  List.app (fn s => writeln ("   "^s)) (List.rev uncalled)
188end
189
190fun print_fnspecs cse = let
191  open ProgramAnalysis Absyn
192  val specdb = function_specs cse
193  val _ = writeln "Function specification information:"
194  fun doit (fname, specs) () = let
195  in
196    writeln fname ;
197    List.app (fn fs => writeln ("   "^fnspec2string fs)) specs
198  end
199in
200  Symtab.fold doit specdb ()
201end
202
203
204datatype pc_problem = Reads of ProgramAnalysis.modify_var
205                    | Writes of ProgramAnalysis.modify_var
206                    | IsProto
207
208fun mapOne f s =
209    case Binaryset.find (fn _ => true) s of
210      NONE => NONE
211    | SOME i => SOME (f i)
212
213fun print_bogus_pures cse = let
214  open ProgramAnalysis Absyn
215  val specdb = function_specs cse
216  open Feedback
217  fun foldthis (fname, fspecs) () = let
218    open Binaryset
219    val idset = all_IDattributes fspecs
220    val pure_problem =
221        case get_modifies cse fname of
222          NONE => SOME IsProto
223        | SOME s => mapOne Writes s
224    val pc_attr = member(idset, "pure") orelse member(idset, "const")
225  in
226    if member(idset, "noreturn") then ()
227    else
228      case pure_problem of
229        NONE => if not pc_attr then
230                  informStr(2, "NOTE: "^fname^
231                               " is pure, but not annotated pure or const")
232                else ()
233      | SOME (Writes mv) =>
234        if pc_attr then
235          informStr(1, "WARNING: "^fname^ " writes "^printmv mv^
236                       " but is annotated pure or const")
237        else ()
238      | SOME IsProto =>
239        if pc_attr then
240          informStr(1, "WARNING: "^fname^" is annotated pure or const, but \
241                                         \is an unannotated prototype")
242        else ()
243      | SOME _ => (* can't happen *) ()
244  end
245in
246  Symtab.fold foldthis specdb ()
247end
248
249fun print_bogus_consts cse = let
250  open ProgramAnalysis Absyn
251  val specdb = function_specs cse
252  val read_globals = get_read_globals cse
253  fun foldthis (fname, fspecs) () = let
254    val reads_prob =
255        case Symtab.lookup read_globals fname of
256          NONE => SOME IsProto
257        | SOME s => mapOne Reads s
258    val prob =
259        case reads_prob of
260          NONE => let
261          in
262            case get_modifies cse fname of
263              NONE => SOME IsProto
264            | SOME s => mapOne Writes s
265          end
266        | x => x
267    val idset = all_IDattributes fspecs
268    open Binaryset Feedback
269    fun prob2str IsProto = "is or calls a proto"
270      | prob2str (Reads mv) = "reads "^printmv mv
271      | prob2str (Writes mv) = "writes "^printmv mv
272  in
273    if member(idset, "noreturn") then ()
274    else
275      case prob of
276        NONE => if not (member(idset, "const")) then
277                          informStr(2, "NOTE: "^fname^
278                                       " is const, but not annotated const")
279                        else ()
280      | SOME p => if member (idset, "const") then
281                    informStr(1, "WARNING: "^fname^" is declared const but "^
282                                 prob2str p)
283                  else ()
284  end
285in
286  Symtab.fold foldthis specdb ()
287end
288
289
290fun print_unannotated_protoes cse = let
291  open ProgramAnalysis
292  val protoes = calc_protoes cse
293  fun foldthis (fnm, acc) =
294      case get_modifies cse fnm of
295        NONE => fnm::acc
296      | SOME _ => acc
297in
298  writeln "Unannotated protoes:";
299  List.app (fn s => writeln ("   "^s))
300           (List.rev (Binaryset.foldl foldthis [] protoes))
301end
302
303fun mmsizes cse = let
304  open Absyn ProgramAnalysis
305  val fns = get_fninfo cse
306  fun foldthis (name,(rettype,_,pvis)) _ = let
307    fun bytesize ty = case ty of Void => 0 | _ => sizeof cse ty
308    val retsize = bytesize rettype
309    val psizes = map (bytesize o get_vi_type) pvis
310  in
311    print (String.concatWith " " (Int.toString retsize :: name ::
312                                  map Int.toString psizes));
313    print "\n"
314  end
315in
316  Symtab.fold foldthis fns ()
317end
318
319
320fun equal x y = (x = y)
321
322fun cmdline_options hdlr args = let
323  fun recurse args =
324      case args of
325        [] => args
326      | h::t => if h = "--" then t
327                else if String.isPrefix "--" h then let
328                    val h' = String.extract (h, 2, NONE)
329                    val flds = String.fields (equal #"=") h'
330                  in
331                    if length flds = 1 then (hdlr (h', NONE); recurse t)
332                    else let
333                        val a = hd flds
334                        val () = hdlr (a, SOME (String.extract(h',size a,NONE)))
335                      in
336                        recurse t
337                      end
338                  end
339                else if String.isPrefix "-" h andalso size h > 1 then let
340                  in
341                    if size h > 2 then
342                      hdlr(String.substring(h,1,1),
343                           SOME (String.extract(h,2,NONE)))
344                    else
345                      hdlr(String.substring(h,1,1), NONE);
346                    recurse t
347                  end
348                else h::t
349in
350  recurse args
351end
352
353fun decl_toString d = let
354  open Absyn
355in
356  case d of
357    VarDecl (_, sw, _, _, _) => "declaration of global variable "^node sw
358  | StructDecl (sw, _) => "declaration of struct "^node sw
359  | TypeDecl tynms => "typedef of "^
360                      String.concatWith ", " (map (node o #2) tynms)
361  | ExtFnDecl {name,...} => "declaration of function "^node name
362  | EnumDecl (sow,_) => "declaration of "^(case node sow of
363                                             NONE => "anonymous"
364                                           | SOME s => s)^
365                        " enum"
366end
367
368fun print_fnslocs cse ast = let
369  open Absyn
370  fun recurse [] = ()
371    | recurse (Decl dw :: t) =
372      (warn ("Ignoring "^decl_toString (node dw)); recurse t)
373    | recurse (FnDefn ((retty,fnm),params,specs,body) :: t) =
374      (print (node fnm^" " ^
375              SourcePos.toString (left fnm) ^ " " ^
376              SourcePos.toString (right body)^"\n");
377       recurse t)
378in
379  recurse ast
380end
381
382fun print_ast cse ast = let
383  open Absyn_Serial
384
385  fun serial_defn (FnDefn ((retty,fnm),params,specs,body))
386    = Nm ("Function", [varspec_serial (retty,fnm),
387        list_serial varspec_serial params,
388        list_serial fnspec_serial specs,
389        list_serial bi_serial (node body)])
390    | serial_defn (Decl dw) = decl_serial (node dw)
391  fun print_lines ss = app print (map (fn s => s ^ "\n") ss)
392in
393  app (print_lines o lines_serial o serial_defn) ast
394end
395
396fun adjusted_complex_fncalls cse ast = let
397  open Absyn_Serial
398
399  fun is_adjusted s = case snode s of
400      Assign(_,e) => (case enode e of EFnCall _ => true | _ => false)
401    | Return(SOME e) => (case enode e of EFnCall _ => true | _ => false)
402    | _ => false
403  fun note_adjusteds s = if is_adjusted s
404    then print ("adjusted fncall at: " ^
405              SourcePos.toString (sleft s) ^ " " ^
406              SourcePos.toString (sright s)^"\n")
407    else app note_adjusteds (sub_stmts s)
408  fun note_bi (BI_Stmt s) = note_adjusteds s
409    | note_bi _ = ()
410  fun note_defn (FnDefn (_,_,_,body))
411    = app note_bi (node body)
412    | note_defn _ = ()
413in app note_defn ast end
414
415fun unhandled_asm cse ast = let
416  open Absyn
417  fun warn_asm asm = K () (ProgramAnalysis.split_asm_stmt asm)
418    handle Fail s => print s
419  fun warn_stmt s = case snode s of
420      (AsmStmt asm) => warn_asm (AsmStmt asm)
421    | _ => app warn_stmt (sub_stmts s)
422  fun warn_bi (BI_Stmt s) = warn_stmt s
423    | warn_bi _ = ()
424  fun warn_defn (FnDefn (_,_,_,body))
425    = app warn_bi (node body)
426    | warn_defn _ = ()
427in app warn_defn ast end
428
429val analyses = ref ([] : (ProgramAnalysis.csenv -> Absyn.ext_decl list -> unit) list)
430val includes = ref ([] : string list)
431val error_lookahead = ref 15
432val verbosity = Feedback.verbosity_level
433
434fun add_analysis f = analyses := f :: !analyses
435fun add_cse_analysis f = analyses := (fn cse => fn ast => f cse) :: !analyses
436
437val cpp = ref (SOME "/usr/bin/cpp")
438val parse_only = ref false
439val underscore_idents = ref false
440val show_help = ref false
441val bad_option = ref false
442val munge_info_fname = ref (NONE : string option)
443
444val options = let
445  open GetOpt
446  fun intref r s =
447    case Int.fromString s of
448        NONE => (show_help := true; bad_option := true)
449      | SOME i => r := i
450  fun cse_analysis nm f =
451    {short = "", long = [nm], help = "Do "^nm^" analysis",
452     desc = NoArg (fn () => add_cse_analysis f)}
453  fun ast_analysis nm f =
454    {short = "", long = [nm], help = "Do "^nm^" analysis",
455     desc = NoArg (fn () => add_analysis f)}
456in
457  [{short = "h?", long = ["help"], help = "Show usage information",
458    desc = NoArg (fn () => show_help := true)},
459   {short = "I", long = [], help = "Add include directory (repeatable)",
460    desc = ReqArg ((fn dir => includes := dir :: !includes), "directory")},
461   {short = "v", long = [], help = "Set parser error verbosity",
462    desc = ReqArg (intref verbosity, "number")},
463   {short = "l", long = [], help = "Set parser lookahead",
464    desc = ReqArg (intref error_lookahead, "number")},
465   cse_analysis "addressed_vars" print_addressed_vars,
466   cse_analysis "bogus_const" print_bogus_consts,
467   cse_analysis "bogus_pure" print_bogus_pures,
468   {short = "", long = ["cpp"], help = "Set cpp path (see also --nocpp)",
469    desc = ReqArg ((fn p => cpp := SOME p), "path")},
470   cse_analysis "embedded_fncalls" print_embedded_fncalls,
471   ast_analysis "fnslocs" print_fnslocs,
472   cse_analysis "fnspecs" print_fnspecs,
473   cse_analysis "mmbytes" mmsizes,
474   cse_analysis "modifies" print_modifies,
475   {short = "", long = ["munge_info_fname"], help = "Path for munge info",
476    desc = ReqArg ((fn p => munge_info_fname := SOME p), "path")},
477   {short = "", long = ["nocpp"], help = "Don't use cpp",
478    desc = NoArg (fn () => cpp := NONE)},
479   {short = "", long = ["nolinedirectives"], help = "Ignore #line directives",
480    desc = NoArg (fn () => SourceFile.observe_line_directives := false)},
481   cse_analysis "protoes"             print_protoes,
482   {short = "", long = ["rawsyntaxonly"], help = "Don't perform any analyses",
483    desc = NoArg (fn () => parse_only := true)},
484   {short = "", long = ["underscore_idents"], help = "Allow identifiers starting with underscores",
485    desc = NoArg (fn () => underscore_idents := true)},
486   cse_analysis "reads" print_reads,
487   cse_analysis "toposort" produce_toposort,
488   cse_analysis "produce dotfile" produce_dotfile,
489   cse_analysis "unannotated_protoes" print_unannotated_protoes,
490   cse_analysis "uncalledfns" print_uncalledfns,
491   cse_analysis "unmodifiedglobs" print_unmodified_globals,
492   ast_analysis "ast" print_ast,
493   ast_analysis "adjusted_complex_fncalls" adjusted_complex_fncalls,
494   ast_analysis "unhandled_asm" unhandled_asm
495  ]
496end
497
498
499
500
501fun docpp (SOME p) {includes, filename} =
502  let
503    val includes_string = String.concat (map (fn s => "-I\""^s^"\" ") includes)
504    open OS.FileSys OS.Process
505    val tmpname = tmpName()
506    val cmdline =
507        p ^ " " ^ includes_string ^ " -CC \"" ^ filename ^ "\" > " ^ tmpname
508  in
509    if isSuccess (system cmdline) then tmpname
510    else raise Feedback.WantToExit ("cpp failed on "^filename)
511  end
512  | docpp NONE {filename, ...} = filename
513
514val usage_msg = GetOpt.usageInfo {
515    header =
516    "Usage: \n  "^execname()^" [options] filename\n\n\
517    \Use -l to adjust error lookahead. (The higher the number, the more the parser\n\
518    \will try to make sense of stuff with parse errors.)\n\n\
519    \For no analyses at all (not even typechecking), use --rawsyntaxonly.\n\n\
520    \All options:\n",
521    options = options
522  }
523
524
525
526fun doit args =
527  let
528    val (_, realargs) =
529        GetOpt.getOpt {argOrder = GetOpt.RequireOrder, options = options,
530                       errFn = die} args
531    val _ = if !show_help then
532              (print usage_msg ; OS.Process.exit OS.Process.success)
533            else if !bad_option then die usage_msg
534            else ()
535  in
536    case realargs of
537      [] => die usage_msg
538    | [fname] =>
539      let
540        val (ast,n) = StrictCParser.parse (docpp (!cpp)) (!error_lookahead)
541                                          (List.rev (!includes)) fname
542      in
543        if !parse_only then ()
544        else
545          let
546            val ((ast', inits), cse) =
547                ProgramAnalysis.process_decls
548                  {anon_vars = false, owners = [],
549                   munge_info_fname = !munge_info_fname,
550                   allow_underscore_idents = !underscore_idents}
551                  (SyntaxTransforms.remove_typedefs ast)
552            val _ = ProgramAnalysis.export_mungedb cse
553            val _ = filename := fname
554            fun do_analyses alist =
555                case alist of
556                  [] => exit (if !Feedback.numErrors = 0 then success else failure)
557                | f::fs => let
558                    val () = f cse ast'
559                  in
560                    do_analyses fs
561                  end
562          in
563            do_analyses (List.rev (!analyses))
564          end
565      end
566    | _ => die usage_msg
567  end
568
569end;
570