summaryrefslogtreecommitdiff
path: root/src/GF/Infra/ReadFiles.hs
blob: 414df59bbd8487d665b0863a5805b55b0a23b690 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
----------------------------------------------------------------------
-- |
-- Module      : ReadFiles
-- Maintainer  : AR
-- Stability   : (stable)
-- Portability : (portable)
--
-- > CVS $Date: 2005/03/15 17:18:52 $ 
-- > CVS $Author: aarne $
-- > CVS $Revision: 1.21 $
--
-- Decide what files to read as function of dependencies and time stamps.
--
-- make analysis for GF grammar modules. AR 11\/6\/2003--24\/2\/2004
--
-- to find all files that have to be read, put them in dependency order, and
-- decide which files need recompilation. Name @file.gf@ is returned for them,
-- and @file.gfc@ or @file.gfr@ otherwise.
-----------------------------------------------------------------------------

module ReadFiles (-- * Heading 1
		  getAllFiles,fixNewlines,ModName,getOptionsFromFile,
		  -- * Heading 2
		  gfcFile,gfFile,gfrFile,isGFC,resModName,isOldFile
		 ) where

import Arch (selectLater, modifiedFiles, ModTime, getModTime,laterModTime)

import Option
import Operations
import UseIO

import System
import Char
import Monad
import List
import Directory

type ModName = String
type ModEnv  = [(ModName,ModTime)]

getAllFiles :: Options -> [InitPath] -> ModEnv -> FileName -> IOE [FullPath]
getAllFiles opts ps env file = do

  -- read module headers from all files recursively
  ds0  <- getImports ps file
  let ds = [((snd m,map fst ms),p) | ((m,ms),p) <- ds0]
  if oElem beVerbose opts 
    then ioeIO $ putStrLn $ "all modules:" +++ show (map (fst . fst) ds)
    else return ()
    -- get a topological sorting of files: returns file names --- deletes paths
  ds1 <- ioeErr $ either 
           return 
           (\ms -> Bad $ "circular modules" +++ 
                     unwords (map show (head ms))) $ topoTest $ map fst ds

  -- associate each file name with its path --- more optimal: save paths in ds1
  let paths = [(f,p) | ((f,_),p) <- ds]
  let pds1 = [(p,f) | f <- ds1, Just p <- [lookup f paths]]
  if oElem fromSource opts 
    then return [gfFile (prefixPathName p f) | (p,f) <- pds1]
    else do


    ds2 <- ioeIO $ mapM (selectFormat env) pds1

    let ds4 = needCompile opts (map fst ds0) ds2
    return ds4

-- to decide whether to read gf or gfc, or if in env; returns full file path

data CompStatus =
   CSComp -- compile: read gf
 | CSRead -- read gfc
 | CSEnv  -- gfc is in env
 | CSEnvR -- also gfr is in env
 | CSDont -- don't read at all
 | CSRes  -- read gfr
  deriving (Eq,Show)

selectFormat :: ModEnv -> (InitPath,ModName) -> IO (ModName,(InitPath,CompStatus))
selectFormat env (p,f) = do
  let pf = prefixPathName p f
  let mtenv = lookup f env   -- Nothing if f is not in env
  let rtenv = lookup (resModName f) env
  mtgfc <- getModTime $ gfcFile pf
  mtgf  <- getModTime $ gfFile pf
  let stat = case (rtenv,mtenv,mtgfc,mtgf) of
        (Just tenv,_,_,Just tgf) | laterModTime tenv tgf -> CSEnvR
        (_,Just tenv,_,Just tgf) | laterModTime tenv tgf -> CSEnv
        (_,_,Just tgfc,Just tgf) | laterModTime tgfc tgf -> CSRead
        (_,_,_,        Nothing) -> CSRead -- source does not exist
        _ -> CSComp
  return $ (f, (p,stat))


needCompile :: Options -> 
               [ModuleHeader] -> [(ModName,(InitPath,CompStatus))] -> [FullPath]
needCompile opts headers sfiles0 = paths $ res $ mark $ iter changed where

  deps = [(snd m,map fst ms) | (m,ms) <- headers]
  typ m = maybe MTyOther id $ lookup m [(m,t) | ((t,m),_) <- headers]
  uses m = [(n,u) | ((_,n),ms) <- headers, (k,u) <- ms, k==m]
  stat0 m = maybe CSComp snd $ lookup m sfiles0

  allDeps = [(m,iterFix add ms) | (m,ms) <- deps] where
    add os = [m | o <- os, Just n <- [lookup o deps],m <- n]

  -- only treat reused, interface, or instantiation if needed
  sfiles = map relevant sfiles0
  relevant fp@(f,(p,st)) = 
    let us = uses f in
    if not (all noComp us) then
      fp else 
    if (elem (typ f) [] ---- MTyIncomplete, MTyIncResource]
        || 
        (not (null us) && all isAux us)) then
      (f,(p,CSDont)) else
      fp

  isAux = flip elem [MUReuse,MUInstance,MUComplete] . snd
  noComp = flip elem [CSRead,CSEnv,CSEnvR] . stat0 . fst

  -- start with the changed files themselves; returns [ModName]
  changed = [f | (f,(_,CSComp)) <- sfiles] 

  -- add other files that depend on some changed file; returns [ModName]
  iter np = let new = [f | (f,fs) <- deps, 
                           not (elem f np), any (flip elem np) fs]
            in  if null new then np else (iter (new ++ np))

  -- for each module in the full list, compile if depends on what needs compile
  -- returns [FullPath]
  mark cs = [(f,(path,st)) | 
                (f,(path,st0)) <- sfiles, 
                let st = if (elem f cs) then CSComp else st0]


  -- if a compilable file depends on a resource, read gfr instead of gfc/env
  -- but don't read gfr if already in env (by CSEnvR)
  -- Also read res if the option "retain" is present
  -- Also, if a "with" file has to be compiled, read its mother file from source

  res cs = map mkRes cs where
    mkRes x@(f,(path,st)) | elem st [CSRead,CSEnv] = case typ f of
      t | (not (null [m | (m,(_,CSComp)) <- cs,
                                   Just ms <- [lookup m allDeps], elem f ms])
                    || oElem retainOpers opts)
        -> if elem t [MTyResource,MTyIncResource] 
              then (f,(path,CSRes)) else
              if t == MTyIncomplete
              then (f,(path,CSComp)) else
              x
      _ -> x
    mkRes x = x



  -- construct list of paths to read
  paths cs = [mkName f p st | (f,(p,st)) <- cs, elem st [CSComp, CSRead,CSRes]]

  mkName f p st = mk $ prefixPathName p f where
    mk = case st of
      CSComp -> gfFile
      CSRead -> gfcFile
      CSRes  -> gfrFile

isGFC :: FilePath -> Bool
isGFC = (== "gfc") . fileSuffix

gfcFile :: FilePath -> FilePath
gfcFile = suffixFile "gfc"

gfrFile :: FilePath -> FilePath
gfrFile = suffixFile "gfr"

gfFile :: FilePath -> FilePath
gfFile  = suffixFile "gf"

resModName :: ModName -> ModName
resModName = ('#':)

-- to get imports without parsing the whole files

getImports :: [InitPath] -> FileName -> IOE [(ModuleHeader,InitPath)]
getImports ps = get [] where
  get ds file0 = do
    let name = justModuleName file0 ---- fileBody file0
    (p,s) <- tryRead name
    let ((typ,mname),imps) = importsOfFile s
    let namebody = justFileName name
    ioeErr $ testErr  (mname == namebody) $ 
             "module name" +++ mname +++ "differs from file name" +++ namebody
    case imps of
      _ | elem name (map (snd . fst . fst) ds) -> return ds  --- file already read
      [] -> return $ (((typ,name),[]),p):ds
      _ -> do
        let files = map (gfFile . fst) imps 
        foldM get ((((typ,name),imps),p):ds) files
  tryRead name = do
    file <- do
      let file_gf = gfFile name
      b <- doesFileExistPath ps file_gf                   -- try gf file first
      if b then return file_gf else return (gfcFile name) -- gfc next

    readFileIfPath ps $ file



-- internal module dep information

data ModUse =
   MUReuse
 | MUInstance
 | MUComplete
 | MUOther
  deriving (Eq,Show)

data ModTyp =
   MTyResource
 | MTyIncomplete
 | MTyIncResource -- interface, incomplete resource
 | MTyOther
  deriving (Eq,Show)

type ModuleHeader = ((ModTyp,ModName),[(ModName,ModUse)])

importsOfFile :: String -> ModuleHeader
importsOfFile = 
  getModuleHeader .          -- analyse into mod header
  filter (not . spec) .      -- ignore keywords and special symbols
  unqual .                   -- take away qualifiers
  unrestr .                  -- take away union restrictions
  takeWhile (not . term) .   -- read until curly or semic
  lexs .                     -- analyse into lexical tokens
  unComm                     -- ignore comments before the headed line
 where
    term = flip elem ["{",";"]
    spec = flip elem ["of", "open","in",":", "->","=", "(", ")",",","**","union"]
    unqual ws = case ws of
      "(":q:ws' -> unqual ws'
      w:ws' -> w:unqual ws'
      _ -> ws
    unrestr ws = case ws of
      "[":ws' -> unrestr $ tail $ dropWhile (/="]") ws'
      w:ws' -> w:unrestr ws'
      _ -> ws

getModuleHeader :: [String] -> ModuleHeader -- with, reuse
getModuleHeader ws = case ws of
  "incomplete":ws2 -> let ((ty,name),us) = getModuleHeader ws2 in
    case ty of
      MTyResource -> ((MTyIncResource,name),us)
      _ -> ((MTyIncomplete,name),us)
  "interface":ws2 -> let ((_,name),us) = getModuleHeader ("resource":ws2) in
    ((MTyIncResource,name),us)
 
  "resource":name:ws2 -> case ws2 of
    "reuse":m:_ -> ((MTyResource,name),[(m,MUReuse)])
    m:"with":ms -> ((MTyResource,name),(m,MUOther):[(n,MUComplete) | n <- ms])
    ms -> ((MTyResource,name),[(n,MUOther) | n <- ms])

  "instance":name:m:ws2 -> case ws2 of
    "reuse":n:_ -> ((MTyResource,name),(m,MUInstance):[(n,MUReuse)])
    n:"with":ms -> 
      ((MTyResource,name),(m,MUInstance):(n,MUComplete):[(n,MUOther) | n <- ms])
    ms -> ((MTyResource,name),(m,MUInstance):[(n,MUOther) | n <- ms])

  "concrete":name:a:ws2 -> case span (/= "with") ws2 of

    (es,_:ms) -> ((MTyOther,name),
                  [(m,MUOther)    | m <- es] ++
                  [(n,MUComplete) | n <- ms])
    --- m:"with":ms -> ((MTyOther,name),(m,MUOther):[(n,MUComplete) | n <- ms])
    (ms,[]) -> ((MTyOther,name),[(n,MUOther) | n <- a:ms])

  _:name:ws2 -> case ws2 of
    "reuse":m:_ -> ((MTyOther,name),[(m,MUReuse)])
    ---- m:n:"with":ms -> 
    ----  ((MTyOther,name),(m,MUInstance):(n,MUOther):[(n,MUComplete) | n <- ms])
    m:"with":ms -> ((MTyOther,name),(m,MUOther):[(n,MUComplete) | n <- ms])
    ms -> ((MTyOther,name),[(n,MUOther) | n <- ms])

unComm s = case s of
      '-':'-':cs -> unComm $ dropWhile (/='\n') cs
      '{':'-':cs -> dpComm cs
      c:cs -> c : unComm cs
      _ -> s
    
dpComm s = case s of
      '-':'}':cs -> unComm cs
      c:cs -> dpComm cs
      _ -> s
    
lexs s = x:xs where 
      (x,y) = head $ lex s
      xs = if null y then [] else lexs y

-- | options can be passed to the compiler by comments in @--#@, in the main file
getOptionsFromFile ::  FilePath -> IO Options
getOptionsFromFile file = do
  s <- readFileIf file
  let ls = filter (isPrefixOf "--#") $ lines s
  return $ fst $ getOptions "-" $ map (unwords . words . drop 3) ls

-- | check if old GF file
isOldFile :: FilePath -> IO Bool
isOldFile f = do
  s <- readFileIf f
  let s' = unComm s
  return $ not (null s') && old (head (words s'))
 where
   old = flip elem $ words 
     "cat category data def flags fun include lin lincat lindef lintype oper param pattern printname rule"



-- | old GF tolerated newlines in quotes. No more supported!
fixNewlines :: String -> String
fixNewlines s = case s of
     '"':cs -> '"':mk cs
     c  :cs -> c:fixNewlines cs
     _ -> s
 where
   mk s = case s of
     '\\':'"':cs -> '\\':'"': mk cs
     '"'     :cs -> '"' :fixNewlines cs
     '\n'    :cs -> '\\':'n': mk cs
     c       :cs -> c : mk cs
     _ -> s