1structure ReadHMF :> ReadHMF =
2struct
3
4open Holmake_types
5
6datatype cond_position = GrabbingText | NoTrueCondYet | SkippingElses
7val empty_condstate = [] : cond_position list
8
9infix |>
10fun x |> f = f x
11
12fun readline lnum strm = let
13  fun recurse (lnum, acc) latest =
14      case latest of
15        NONE => if null acc then NONE
16              else SOME (lnum + 1, String.concat (List.rev acc))
17      | SOME "\n" => SOME (lnum + 1, String.concat (List.rev acc))
18      | SOME s => let
19          val s0 = if String.sub(s, size s - 2) = #"\r" then
20                     String.extract(s, 0, SOME (size s - 2))
21                   else String.extract(s, 0, SOME (size s - 1))
22        in
23          if String.sub(s0, size s0 - 1) = #"\\" then
24            recurse (lnum + 1,
25                     " " :: String.extract(s0, 0, SOME (size s0 - 1)) :: acc)
26                    (TextIO.inputLine strm)
27          else
28            SOME (lnum + 1, String.concat (List.rev (s0 :: acc)))
29        end
30in
31  recurse (lnum, []) (TextIO.inputLine strm)
32end
33
34datatype buf = B of { lnum : int,
35                      strm : TextIO.instream,
36                      name : string,
37                      curr : (int * string) option }
38
39fun init_buf fname = let
40  val fname = OS.Path.mkAbsolute {path=fname, relativeTo=OS.FileSys.getDir()}
41  val istrm = TextIO.openIn fname
42in
43  B { lnum = 1, strm = istrm, curr = readline 1 istrm, name = fname }
44end
45
46fun close_buf (B r) = TextIO.closeIn (#strm r)
47
48fun currentline (B r) = Option.map #2 (#curr r)
49
50fun advance (b as B r) =
51  case #curr r of
52    NONE => b
53  | SOME (n,s) => B { lnum = n, strm = #strm r, name = #name r,
54                      curr = readline n (#strm r) }
55
56fun error (B r) s =
57    raise Fail (#name r ^":"^Int.toString (#lnum r)^": "^s)
58
59fun strip_leading_wspace s = let
60  open Substring
61  val ss = full s
62in
63  string (dropl (fn c => c = #" " orelse c = #"\r") ss)
64end
65
66fun drop_twspace s = let
67  open Substring
68  val ss = full s
69in
70  string (dropr Char.isSpace ss)
71end
72
73
74fun first_special s = let
75  fun recurse i = if i = size s then NONE
76                  else if String.sub(s,i) = #"=" then SOME #"="
77                  else if String.sub(s,i) = #":" then SOME #":"
78                  else recurse (i + 1)
79in
80  recurse 0
81end
82
83fun strip_trailing_comment s = let
84  fun recurse i =
85      if i >= size s then s
86      else if String.sub(s,i) = #"\\" then recurse (i + 2)
87      else if String.sub(s,i) = #"#" then String.substring(s,0,i)
88      else recurse (i + 1)
89in
90  recurse 0
91end
92
93val ss = Substring.full
94
95fun read_delimited_string b dchar s = let
96  (* assume s begins with dchar *)
97  val s' = String.extract(s,1,NONE)
98  open Substring
99  val (result, rest) = position (str dchar) (ss s')
100  val _ = size rest <> 0 orelse error b ("No matching "^str dchar)
101in
102  (string result, string rest
103                         |> (fn s => String.extract(s, 1, NONE))
104                         |> strip_leading_wspace)
105end
106
107fun read_quoted_string b s = let
108  val c = String.sub(s, 0)
109in
110  case c of
111    #"'" => read_delimited_string b c s
112  | #"\"" => read_delimited_string b c s
113  | _ => error b ("Bad argument delimiter: "^str c)
114end
115
116fun split_at_rightmost_rparen ss = let
117  open Substring
118  fun recurse i =
119      if i < 0 then (ss, full "")
120      else if sub(ss,i) = #")" then (slice(ss,0,SOME i), slice(ss,i,NONE))
121      else recurse (i - 1)
122in
123  recurse (size ss - 1)
124end
125
126fun evaluate_cond b env s =
127    if String.isPrefix "ifdef" s orelse String.isPrefix "ifndef" s then let
128        val (sense, sz, nm) =
129            if String.sub(s,2) = #"n" then (false, 6, "ifndef")
130            else (true, 5, "ifdef")
131        val s' = strip_leading_wspace (String.extract(s, sz, NONE))
132        val q = extract_normal_quotation (Substring.full s')
133        val s2 = perform_substitution env q
134      in
135        case String.tokens Char.isSpace s2 of
136          [s] => (case lookup env s of
137                    [LIT ""] => SOME (not sense)
138                  | [] => SOME (not sense)
139                  | _ => SOME sense)
140        | _ => error b ("ReadHMF: "^nm^" not followed by a variable name.")
141      end
142    else if String.isPrefix "ifeq" s orelse String.isPrefix "ifneq" s then let
143        val (sense, sz, nm) =
144            if String.sub(s,2) = #"n" then (false, 5, "ifneq")
145            else (true, 4, "ifeq")
146        val s = String.extract(s,sz,NONE) |> strip_leading_wspace |> drop_twspace
147        val (arg1, arg2) =
148            case String.sub(s,0) of
149              #"(" => let
150                open Substring
151                val (arg1s, blob2s) = position "," (full (String.extract(s,1,NONE)))
152                val _ = size blob2s <> 0 orelse
153                        error b (nm ^ " with parens requires args separated by \
154                                        \commas")
155                val (arg2s, parenblob) =
156                    split_at_rightmost_rparen (slice(blob2s,1,NONE))
157                val _ = size parenblob <> 0 orelse
158                        error b ("No right-paren in "^nm^" line")
159              in
160                (arg1s |> string |> drop_twspace |> strip_leading_wspace,
161                 arg2s |> string |> drop_twspace |> strip_leading_wspace)
162              end
163            | _ => let
164                val (arg1, s) = read_quoted_string b s
165                val (arg2, s) = read_quoted_string b s
166                val _ = size (drop_twspace s) = 0 orelse
167                        error b ("Extraneous junk after complete "^nm^" directive")
168              in
169                (arg1, arg2)
170              end
171        val (q1, q2) = (extract_normal_quotation (ss arg1),
172                        extract_normal_quotation (ss arg2))
173        val (s1, s2) = (perform_substitution env q1, perform_substitution env q2)
174      in
175        SOME ((s1 = s2) = sense)
176      end
177    else NONE
178
179fun getline env (condstate, b) =
180    case (currentline b, condstate) of
181      (NONE, []) => (b, NONE, condstate)
182    | (NONE, _ :: _) => error b "ReadHMF: unterminated conditional"
183    | (SOME s, SkippingElses :: rest) => let
184        val s = strip_leading_wspace s
185      in
186        if String.isPrefix "endif" s then getline env (rest, advance b)
187        else if String.isPrefix "ifdef" s orelse String.isPrefix "ifndef" s orelse
188                String.isPrefix "ifeq" s orelse String.isPrefix "ifneq" s
189        then
190          getline env (SkippingElses::condstate, advance b)
191        else
192          getline env (condstate, advance b)
193      end
194    | (SOME s, NoTrueCondYet::rest) => let
195        val s = strip_leading_wspace s
196      in
197        if String.isPrefix "endif" s then getline env (rest, advance b)
198        else if String.isPrefix "if" s then
199          getline env (SkippingElses :: condstate, advance b)
200        else if String.isPrefix "else" s then let
201            val s = strip_leading_wspace (String.extract(s, 4, NONE))
202          in
203            if String.isPrefix "if" s then
204              case evaluate_cond b env s of
205                NONE => error b "ReadHMF: bogus string following else"
206              | SOME false => getline env (condstate, advance b)
207              | SOME true => getline env (GrabbingText::rest, advance b)
208            else if s = "" then getline env (GrabbingText::rest, advance b)
209            else error b "ReadHMF: bogus string following else"
210          end
211        else getline env (condstate, advance b)
212      end
213    | (SOME s0, _) => let
214        val s = strip_leading_wspace s0
215      in
216        if String.isPrefix "endif" s then
217          if null condstate then error b "ReadHMF: unpaired endif"
218          else getline env (tl condstate, advance b)
219        else if String.isPrefix "else" s then
220          if null condstate then error b "ReadHMF: unpaired else"
221          else getline env (SkippingElses::tl condstate, advance b)
222        else if String.isPrefix "if" s then
223          case evaluate_cond b env s of
224            NONE => (b, SOME s0, condstate)
225          | SOME false => getline env (NoTrueCondYet::condstate, advance b)
226          | SOME true => getline env (GrabbingText::condstate, advance b)
227        else (b, SOME s0, condstate)
228      end
229
230fun read_commands env (cs,b) head =
231    case getline env (cs, b) of
232      (b, NONE, cs) => ((cs, b), RULE head)
233    | (b, SOME s, cs) => let
234        val s' = strip_leading_wspace s
235      in
236        if s' = "" orelse String.sub(s',0) = #"#" then
237          read_commands env (cs, advance b) head
238        else
239          case String.sub(s',0) of
240            #"\t" => read_commands env (cs, advance b) (head ^ s' ^ "\n")
241          | c => ((cs, b), RULE head)
242      end
243
244
245fun process_line env (condstate, b) = let
246  val (b, line_opt, condstate) = getline env (condstate, b)
247in
248  case line_opt of
249    NONE => ((condstate, b), EOF)
250  | SOME s => let
251      val s' = strip_leading_wspace s
252    in
253      if s' = "" orelse String.sub(s',0) = #"#" then
254        process_line env (condstate, advance b)
255      else let
256          val c1 = String.sub(s',0)
257        in
258          if c1 = #"\t" then error b "TAB starts an unattached command"
259          else
260            case first_special s' of
261                NONE => error b ("Unrecognised character: \""^
262                                 String.toString (str c1) ^ "\"")
263              | SOME #"=" => ((condstate, advance b), DEFN (strip_trailing_comment s))
264              | SOME #":" => read_commands
265                                 env
266                                 (condstate, advance b)
267                                 (strip_trailing_comment s' ^ "\n")
268              | SOME _ => error b "ReadHMF: can't happen"
269        end
270    end
271end
272
273fun readall (acc as (tgt1,env,ruledb,depdb)) csb =
274    case process_line env csb of
275      (csb as (cs, b), EOF) => let
276        val _ = close_buf b
277        fun foldthis (tgt,deps,acc) =
278            case Binarymap.peek(acc,tgt) of
279              NONE => Binarymap.insert(acc,tgt,
280                                       {dependencies = deps, commands = []})
281            | SOME {dependencies, commands} =>
282              Binarymap.insert(acc,tgt, {dependencies = dependencies @ deps,
283                                         commands = commands})
284      in
285        (env,Binarymap.foldl foldthis ruledb depdb,tgt1)
286      end
287    | (csb, x) => let
288        fun warn s = TextIO.output(TextIO.stdErr, s ^ "\n")
289      in
290        case to_token x of
291          HM_defn def => readall (tgt1,env_extend def env, ruledb, depdb) csb
292        | HM_rule rinfo => let
293            val (rdb',depdb',tgts) = extend_ruledb warn env rinfo (ruledb,depdb)
294            val tgt1' =
295                case tgt1 of
296                  NONE => List.find (fn s => s <> ".PHONY") tgts
297                | _ => tgt1
298          in
299            readall (tgt1',env,rdb',depdb') csb
300          end
301      end
302
303fun read fname env =
304    readall (NONE, env, empty_ruledb,
305             Binarymap.mkDict String.compare)
306            (empty_condstate, init_buf fname)
307
308fun readlist e vref =
309  map dequote (tokenize (perform_substitution e [VREF vref]))
310
311fun memoise f =
312    let
313      val stash = ref (Binarymap.mkDict String.compare)
314      fun lookup s =
315          case Binarymap.peek(!stash, s) of
316              NONE =>
317              let
318                val actual = f s
319              in
320                stash := Binarymap.insert(!stash, s, actual);
321                actual
322              end
323            | SOME r => r
324    in
325      lookup
326    end
327
328fun find_includes0 dirname =
329  let
330    fun warn s = TextIO.output(TextIO.stdErr, s ^ "\n")
331    val hm_fname = OS.Path.concat(dirname, "Holmakefile")
332  in
333    if OS.FileSys.access(hm_fname, [OS.FileSys.A_READ]) then
334      let
335        val (e, _, _) = read hm_fname (base_environment())
336        val raw_incs = readlist e "INCLUDES" @ readlist e "PRE_INCLUDES"
337      in
338        map (fn p => OS.Path.mkAbsolute {path = p, relativeTo = dirname})
339            raw_incs
340      end handle e => (warn ("Bogus Holmakefile in " ^ dirname ^
341                             " - ignoring it");
342                       [])
343
344    else []
345  end
346
347val find_includes = memoise find_includes0
348
349infix ++
350val op ++ = OS.Path.concat
351fun canonicalise d1 d2 = OS.Path.mkAbsolute{path = d2, relativeTo = d1}
352fun fromList l = Binaryset.addList (Binaryset.empty String.compare, l)
353
354(* returns updated accumulator and list of new places to visit *)
355fun extend_path_with_includes0 (A as (visited,prem,postm)) dir verbosity =
356    if Binaryset.member(visited, dir) then (A,[])
357    else
358      if OS.FileSys.access (dir ++ "Holmakefile", [OS.FileSys.A_READ]) then
359        let
360          open Holmake_types
361          val _ = if verbosity > 1 then
362                    print ("Visiting " ^ dir ^ " for first time\n")
363                  else ()
364          val extensions =
365              holpathdb.search_for_extensions find_includes [dir]
366          val _ = List.app holpathdb.extend_db extensions
367          val base_env = let
368            fun foldthis ({vname,path}, env) =
369                env_extend (vname, [LIT path]) env
370          in
371            List.foldl foldthis (base_environment()) extensions
372          end
373          val (env, _, _) = read (dir ++ "Holmakefile") base_env
374          fun envlist id =
375              map dequote (tokenize (perform_substitution env [VREF id]))
376          fun diag nm incs =
377              if null incs orelse verbosity < 2 then ()
378              else
379                print (nm ^ " = " ^ String.concatWith ", " incs ^ "\n")
380          val pre_incs = map (canonicalise dir) (envlist "PRE_INCLUDES")
381          val _ = diag "PRE_INCLUDES" pre_incs
382          val post_incs = map (canonicalise dir) (envlist "INCLUDES")
383          val _ = diag "INCLUDES" post_incs
384          fun maybeinsert(m,k,v) =
385              if null v then m else Binarymap.insert(m,k,v)
386        in
387          ((Binaryset.add(visited,dir),
388            maybeinsert(prem,dir,pre_incs),
389            maybeinsert(postm,dir,post_incs)),
390           Binaryset.listItems (fromList (pre_incs @ post_incs)))
391        end handle e => (if verbosity > 0 then
392                           (TextIO.output(TextIO.stdErr,
393                                          "[bogus Holmakefile in " ^ dir ^
394                                          " - ignoring it]\n");
395                            TextIO.flushOut TextIO.stdErr;
396                            (A, [])
397                           )
398                         else (A, []))
399      else (A, [])
400
401fun extend_paths A cfg worklist =
402    case worklist of
403        [] => A
404      | d::ds =>
405        let
406          val (A',new) = extend_path_with_includes0 A d cfg
407        in
408          extend_paths A' cfg (ds @ new)
409        end
410
411
412val empty_strset = Binaryset.empty String.compare
413val empty_strmap = Binarymap.mkDict String.compare
414fun extend_path_with_includes (cfg as {lpref,verbosity=v}) =
415    let
416      val wlist = [OS.FileSys.getDir()]
417      val (_, prem, postm) =
418          extend_paths (empty_strset, empty_strmap, empty_strmap) v wlist
419      fun m s = holpathdb.reverse_lookup {path = s}
420      fun foldthis nm (dirname,incs,acc) = (
421        if v > 0 then
422          print (m dirname ^ "/Holmakefile:" ^ nm ^ " +=\n  " ^
423                 String.concatWith "\n  " (map m incs) ^ "\n")
424        else ();
425        Binaryset.addList(acc,incs)
426      )
427      fun acc_range nm = Binarymap.foldl (foldthis nm) empty_strset
428      val all_preincs = Binaryset.listItems (acc_range "PRE_INCLUDES" prem)
429      val all_incs = Binaryset.listItems (acc_range "INCLUDES" postm)
430    in
431      lpref := all_preincs @ !lpref @ all_incs
432    end
433
434end (* struct *)
435