1     object ShaderLines =
2         val list = List<String>.new
3     
4         def add (s : String) =
5             list.add s
6     
7     type FileDefinitions = struct
8         path : String
9         definitions : List<String>
10    
11        def compare (other : FileDefinitions) =
12            let path_result = path.compare other.path
13            if path_result <> Ordering/Equal
14            then path_result
15            else definitions.compare other.definitions
16    
17        is Compare
18    
19    let matches (line : String) (definitions : List<String>) =
20        let s = line.trim
21        assert s.starts_with "#if " || s.starts_with "#elif "
22    
23        let index = if s.starts_with "#if" then 4 else 6
24        let blocks = s.drop index |> split " || "
25    
26        let f (expr : String) =
27            let is_neg = expr[0] == '!'
28            let name = if is_neg then expr.drop 1 else expr
29            let contains = definitions.any { _ == name }
30            contains <> is_neg
31    
32        for block in blocks do
33            if block.split " && " |> all { f _ } then
34                return true
35    
36        false
37    
38    let process (source_directory : String
39                 text : String
40                 definitions : mut List<String>
41                 paths : mut List<String>) : List<String> =
42        let lines = text.lines
43        let result = List<String>.new
44        let mut add = true
45        let mut level = 0
46        let mut exclude_level = 0
47        let level_invoked = List<bool>.new
48    
49        for line in lines do
50            if not add then
51                if line.contains "#if" then
52                    level += 1
53                else if line.contains "#else if" then
54                    if exclude_level == level && not level_invoked.last then
55                        if matches line definitions then
56                            add = true
57                            let index = level_invoked.size - 1
58                            level_invoked[index] = true
59    
60                else if line.contains "#else" || line.contains "#endif" then
61                    if exclude_level == level
62                       && (not level_invoked.last || line.contains "#endif")
63                    then
64                        add = true
65    
66                    if line.contains "#endif" then
67                        level -= 1
68    
69            else if line.contains "#if" then
70                level += 1
71                if not matches line definitions then
72                    add = false
73                    exclude_level = level
74                    level_invoked.add false
75                else
76                    level_invoked.add true
77    
78            else if line.contains "#else if" then
79                add = false
80                exclude_level = level
81    
82            else if line.contains "#else" then
83                add = false
84                exclude_level = level
85    
86            else if line.contains "#endif" then
87                level -= 1
88    
89            else if line.contains "#include" then
90                let s = line.trim
91                if s.ends_with '"' then
92                    let path = s.drop 10 |> drop_last 1
93                    let full_path = "$source_directory/$path"
94                    let include_text = fs/read_text full_path
95                    let include_lines = process source_directory include_text
96                                                definitions paths
97                    result.add_all include_lines
98                    paths.add full_path
99                else
100                   val name = s.drop 9
101                   assert name == "object"
102                   result.add_all ShaderLines.list
103           else
104               result.add line
105               if line.starts_with "#define" && line.trim.count { _ == ' ' } == 1 then
106                   let name = line.drop 8
107                   definitions.add name
108   
109       result
110   
111   let get_out_path (directory : String
112                     path : String
113                     suffix : String) =
114       let file_name = if Path.is_absolute path
115                       then Path.get_file_name path
116                       else path
117   
118       let dot_index = file_name.last_index_of '.'
119       if suffix.is_empty
120       then "$directory/$file_name"
121       else "$directory/${file_name.take dot_index}_$suffix${file_name.drop dot_index}"
122   
123   let process_dict (forced_paths : List<String>
124                     source_directory : String
125                     definitions : Map<String, FileDefinitions>
126                     dependencies : Map<String, List<String>>
127                     out_dependencies : mut Map<String, List<String>>) =
128       for out_path, (path, list) in definitions do
129           let modified = if fs/exists out_path
130                          then fs/modified_time out_path
131                          else Time.new
132   
133           let source_modified = fs/modified_time path
134           if modified >= source_modified
135              && dependencies.contains path
136              && dependencies[path].all { s -> modified >= fs/modified_time s
137                                               && not forced_paths.contains s }
138           then
139               continue
140   
141           let text = fs/read_text path
142           let mut_list = list.to_mut_list
143           let paths = List<String>.new
144           let result = process source_directory text mut_list paths
145           let sb = StringBuilder.new
146           for item in result do
147               sb.append item
148               sb.append '\n'
149   
150           fs/write_file out_path sb.as_string
151           out_dependencies.add path paths
152   
153   def get_default_definitions (directory : String) (source_directory : String) =
154       let result = Map<String, FileDefinitions>.new
155       let files = fs/list_files source_directory
156       let shader_files = files.filter { x -> x.ends_with ".comp"
157                                              || x.ends_with ".vert"
158                                              || x.ends_with ".frag" }
159       for file in shader_files do
160           let in_path = "$source_directory/$file"
161           let out_path = get_out_path directory file ""
162           result.add out_path (FileDefinitions in_path List.new)
163   
164       result
165   
166   def add_definitions (definitions : mut Map<String, FileDefinitions>
167                        directory : String
168                        source_directory : String
169                        items : Slice<(String, Slice<FileDefinitions>)>) =
170       for path, slice in items do
171           let full_path = if Path.is_absolute path
172                           then path
173                           else "$source_directory/$path"
174   
175           for suffix, list in slice do
176               let out_path = get_out_path directory path suffix
177               definitions.add out_path (FileDefinitions full_path list)
178   
179   let save_dependencies (directory : String
180                          dependencies : Map<String, List<String>>) =
181       let sb = StringBuilder.new
182       let list = dependencies.to_mut_list
183       list.sort
184   
185       for from_path, to_paths in list do
186           sb.append from_path
187              append '\n'
188   
189           for path in to_paths do
190               sb.append path
191                  append '\n'
192   
193           sb.append '\n'
194   
195       sb.pop |> ignore
196       let path = "$directory/dependencies.txt"
197       fs/write_file path sb.as_string
198   
199   let save_definitions (directory : String
200                         definitions : Map<String, FileDefinitions>) =
201       let sb = StringBuilder.new
202       let list = definitions.to_mut_list
203       list.sort
204   
205       for data_path, (src_path, items) in list do
206           sb.append data_path
207              append ", "
208              append src_path
209   
210           if items.size > 0 then
211               sb.append ','
212               for item in items do
213                   sb.append ' '
214                      append item
215   
216           sb.append '\n'
217   
218       let path = "$directory/definitions.txt"
219       fs/write_file path sb.as_string
220   
221   let load_definitions (directory : String) =
222       let result = Map<String, FileDefinitions>.new
223       let path = "$directory/definitions.txt"
224       if not fs/exists path then
225           return result
226   
227       let text = fs/read_text path
228       let lines = text.lines
229       for line in lines do
230           let parts = line.split ", "
231           let data_path = parts[0].trim
232           let src_path = parts[1].trim
233           let list = if parts.size > 2
234                      then parts[2].split ' ' |> map { _.trim }
235                      else List<String>.new
236   
237           result.add data_path (FileDefinitions src_path list)
238   
239       result
240   
241   let load_dependencies (directory : String) =
242       let path = "$directory/dependencies.txt"
243       let result = Map<String, List<String>>.new
244       if not fs/exists path then
245           return result
246   
247       let text = fs/read_text path
248       let lines = text.lines
249       let mut maybe_path : Option<String> = None
250       let mut paths = List<String>.new
251   
252       for line in lines do
253           if line.trim.is_empty then
254               result.add maybe_path.unwrap paths
255               paths = List<String>.new
256               maybe_path = None
257   
258           else if maybe_path.is_none then
259               maybe_path = Some line
260           else
261               paths.add line
262   
263       if maybe_path ? Some p then
264           result.add p paths
265   
266       result
267   
268   let get_forced_paths (directory : String) (source_directory : String) =
269       let text = ShaderLines.list.join_to_string separator = "\n"
270       let path = "$directory/lines.txt"
271       let exists = fs/exists path
272       let loaded_text = if exists
273                         then fs/read_text path
274                         else ""
275   
276       let paths = List<String>.new
277       if text <> loaded_text || not exists then
278           val s = "$source_directory/default.glsl"
279           paths.add s
280           fs/write_file path text
281   
282       paths
283   
284   module shader
285   
286   def preprocess (directory : String) (source_directory : String) =
287       if not fs/exists directory then
288           fs/create_path directory
289   
290       let loaded_definitions = load_definitions directory
291       let definitions = get_default_definitions directory source_directory
292       add_definitions definitions directory source_directory Slice@zero
293       let forced_paths = get_forced_paths directory source_directory
294   
295       for path, item in definitions do
296           if loaded_definitions.contains path
297              && (loaded_definitions[path].definitions.size <> item.definitions.size
298                  || loaded_definitions[path].definitions.any
299                         { not item.definitions.contains _ })
300           then
301               fs/remove_file path
302   
303       let dependencies = load_dependencies directory
304       let out_dependencies = Map<String, List<String>>.new
305   
306       process_dict forced_paths source_directory definitions
307                    dependencies out_dependencies
308   
309       if out_dependencies.size <> 0 then
310           for path, _ in dependencies do
311               if not out_dependencies.contains path then
312                   out_dependencies.add path dependencies[path]
313   
314           save_dependencies directory out_dependencies
315           save_definitions directory definitions
316