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 definitions paths
96                    result.add_all include_lines
97                    paths.add full_path
98                else
99                    val name = s.drop 9
100                   assert name == "object"
101                   result.add_all ShaderLines.list
102           else
103               result.add line
104               if line.starts_with "#define" && line.trim.count { _ == ' ' } == 1 then
105                   let name = line.drop 8
106                   definitions.add name
107   
108       result
109   
110   let get_out_path (directory : String
111                     path : String
112                     suffix : String) =
113       let file_name = if Path.is_absolute path
114                       then Path.get_file_name path
115                       else path
116   
117       let dot_index = file_name.last_index_of '.'
118       if suffix.is_empty
119       then "$directory/$file_name"
120       else "$directory/${file_name.take dot_index}_$suffix${file_name.drop dot_index}"
121   
122   let process_dict (forced_paths : List<String>
123                     source_directory : String
124                     definitions : Map<String, FileDefinitions>
125                     dependencies : Map<String, List<String>>
126                     out_dependencies : mut Map<String, List<String>>) =
127       for out_path, (path, list) in definitions do
128           let modified = if fs/exists out_path
129                          then fs/modified_time out_path
130                          else Time.new
131   
132           let source_modified = fs/modified_time path
133           if modified >= source_modified
134              && dependencies.contains path
135              && dependencies[path].all { s -> modified >= fs/modified_time s
136                                               && not forced_paths.contains s }
137           then
138               continue
139   
140           let text = fs/read_text path
141           let mut_list = list.to_mut_list
142           let paths = List<String>.new
143           let result = process source_directory text mut_list paths
144           let sb = StringBuilder.new
145           for item in result do
146               sb.append item
147               sb.append '\n'
148   
149           fs/write_file out_path sb.as_string
150           out_dependencies.add path paths
151   
152   def get_default_definitions (directory : String) (source_directory : String) =
153       let result = Map<String, FileDefinitions>.new
154       let files = fs/list_files source_directory
155       let shader_files = files.filter { x -> x.ends_with ".comp"
156                                              || x.ends_with ".vert"
157                                              || x.ends_with ".frag" }
158       for file in shader_files do
159           let in_path = "$source_directory/$file"
160           let out_path = get_out_path directory file ""
161           result.add out_path (FileDefinitions in_path List.new)
162   
163       result
164   
165   def add_definitions (definitions : mut Map<String, FileDefinitions>
166                        directory : String
167                        source_directory : String
168                        items : Slice<(String, Slice<FileDefinitions>)>) =
169       for path, slice in items do
170           let full_path = if Path.is_absolute path
171                           then path
172                           else "$source_directory/$path"
173   
174           for suffix, list in slice do
175               let out_path = get_out_path directory path suffix
176               definitions.add out_path (FileDefinitions full_path list)
177   
178   let save_dependencies (directory : String
179                          dependencies : Map<String, List<String>>) =
180       let sb = StringBuilder.new
181       let list = dependencies.to_mut_list
182       list.sort
183   
184       for from_path, to_paths in list do
185           sb.append from_path
186              append '\n'
187   
188           for path in to_paths do
189               sb.append path
190                  append '\n'
191   
192           sb.append '\n'
193   
194       sb.pop |> ignore
195       let path = "$directory/dependencies.txt"
196       fs/write_file path sb.as_string
197   
198   let save_definitions (directory : String
199                         definitions : Map<String, FileDefinitions>) =
200       let sb = StringBuilder.new
201       let list = definitions.to_mut_list
202       list.sort
203   
204       for data_path, (src_path, items) in list do
205           sb.append data_path
206              append ", "
207              append src_path
208   
209           if items.size > 0 then
210               sb.append ','
211               for item in items do
212                   sb.append ' '
213                      append item
214   
215           sb.append '\n'
216   
217       let path = "$directory/definitions.txt"
218       fs/write_file path sb.as_string
219   
220   let load_definitions (directory : String) =
221       let result = Map<String, FileDefinitions>.new
222       let path = "$directory/definitions.txt"
223       if not fs/exists path then
224           return result
225   
226       let text = fs/read_text path
227       let lines = text.lines
228       for line in lines do
229           let parts = line.split ", "
230           let data_path = parts[0].trim
231           let src_path = parts[1].trim
232           let list = if parts.size > 2
233                      then parts[2].split ' ' |> map { _.trim }
234                      else List<String>.new
235   
236           result.add data_path (FileDefinitions src_path list)
237   
238       result
239   
240   let load_dependencies (directory : String) =
241       let path = "$directory/dependencies.txt"
242       let result = Map<String, List<String>>.new
243       if not fs/exists path then
244           return result
245   
246       let text = fs/read_text path
247       let lines = text.lines
248       let mut maybe_path : Option<String> = None
249       let mut paths = List<String>.new
250   
251       for line in lines do
252           if line.trim.is_empty then
253               result.add maybe_path.unwrap paths
254               paths = List<String>.new
255               maybe_path = None
256   
257           else if maybe_path.is_none then
258               maybe_path = Some line
259           else
260               paths.add line
261   
262       if maybe_path ? Some p then
263           result.add p paths
264   
265       result
266   
267   let get_forced_paths (directory : String) (source_directory : String) =
268       let text = ShaderLines.list.join_to_string separator = "\n"
269       let path = "$directory/lines.txt"
270       let exists = fs/exists path
271       let loaded_text = if exists
272                         then fs/read_text path
273                         else ""
274   
275       let paths = List<String>.new
276       if text <> loaded_text || not exists then
277           val s = "$source_directory/default.glsl"
278           paths.add s
279           fs/write_file path text
280   
281       paths
282   
283   module shader
284   
285   def preprocess (directory : String) (source_directory : String) =
286       if not fs/exists directory then
287           fs/create_path directory
288   
289       let loaded_definitions = load_definitions directory
290       let definitions = get_default_definitions directory source_directory
291       add_definitions definitions directory source_directory Slice@zero
292       let forced_paths = get_forced_paths directory source_directory
293   
294       for path, item in definitions do
295           if loaded_definitions.contains path
296              && (loaded_definitions[path].definitions.size <> item.definitions.size
297                  || loaded_definitions[path].definitions.any
298                         { not item.definitions.contains _ })
299           then
300               fs/remove_file path
301   
302       let dependencies = load_dependencies directory
303       let out_dependencies = Map<String, List<String>>.new
304   
305       process_dict forced_paths source_directory definitions dependencies out_dependencies
306   
307       if out_dependencies.size <> 0 then
308           for path, _ in dependencies do
309               if not out_dependencies.contains path then
310                   out_dependencies.add path dependencies[path]
311   
312           save_dependencies directory out_dependencies
313           save_definitions directory definitions
314