Reorganize compiler for more flexibility.
[jagi.git] / src / jagi / fs / Compiler.java
1 package jagi.fs;
2
3 import jagi.*;
4 import java.util.*;
5 import java.util.regex.*;
6 import java.nio.file.*;
7 import java.nio.file.attribute.*;
8 import java.io.*;
9
10 public class Compiler {
11     private final Map<Path, File> files = new HashMap<>();
12
13     public static class ClassOutput {
14         public final String name;
15         public final ByteArrayOutputStream buf = new ByteArrayOutputStream();
16
17         public ClassOutput(String name) {
18             this.name = name;
19         }
20     }
21
22     public static class CompilationException extends RuntimeException {
23         public final Path file;
24         private final List<? extends Object> messages;
25
26         public CompilationException(Path file, List<? extends Object> messages) {
27             this.file = file;
28             this.messages = messages;
29         }
30
31         public String getMessage() {
32             return(file + ": compilation failed");
33         }
34
35         public String messages() {
36             StringBuilder buf = new StringBuilder();
37             for(Object msg : messages)
38                 buf.append(msg.toString() + "\n");
39             return(buf.toString());
40         }
41
42         public void printStackTrace(PrintStream out) {
43             out.print(messages());
44             super.printStackTrace(out);
45         }
46     }
47
48     public static class Compilation implements AutoCloseable {
49         private final Path dir, srcdir, outdir;
50         private final List<Path> infiles = new ArrayList<>();
51         private List<String> output = null;
52
53         public Compilation() throws IOException {
54             dir = Files.createTempDirectory("javac");
55             srcdir = dir.resolve("src");
56             outdir = dir.resolve("out");
57             Files.createDirectory(srcdir);
58             Files.createDirectory(outdir);
59         }
60
61         public void add(Path p) {
62             infiles.add(p);
63         }
64
65         public boolean compile() throws IOException {
66             List<String> args = new ArrayList<>();
67             args.add("javac");
68             args.add("-d");
69             args.add(outdir.toString());
70             for(Path p : infiles)
71                 args.add(p.toString());
72             ProcessBuilder cmd = new ProcessBuilder(args);
73             cmd.redirectErrorStream(true);
74             cmd.redirectOutput(ProcessBuilder.Redirect.PIPE);
75             Process proc = cmd.start();
76             List<String> output = new ArrayList<>();
77             BufferedReader fp = new BufferedReader(new InputStreamReader(proc.getInputStream(), Utils.UTF8));
78             while(true) {
79                 String ln = fp.readLine();
80                 if(ln == null)
81                     break;
82                 output.add(ln);
83             }
84             int status;
85             try {
86                 status = proc.waitFor();
87             } catch(InterruptedException e) {
88                 Thread.currentThread().interrupt();
89                 throw(new IOException("compilation interrupted"));
90             }
91             this.output = output;
92             return(status == 0);
93         }
94
95         public List<String> output() {
96             if(output == null)
97                 throw(new IllegalStateException());
98             return(output);
99         }
100
101         public Collection<ClassOutput> classes() throws IOException {
102             Collection<ClassOutput> ret = new ArrayList<>();
103             for(Path p : (Iterable<Path>)Files.walk(outdir)::iterator) {
104                 Path rel = outdir.relativize(p);
105                 String fn = rel.getName(rel.getNameCount() - 1).toString();
106                 if(!Files.isRegularFile(p) || !fn.endsWith(".class"))
107                     continue;
108                 StringBuilder clnm = new StringBuilder();
109                 for(int i = 0; i < rel.getNameCount() - 1; i++) {
110                     clnm.append(rel.getName(i));
111                     clnm.append('.');
112                 }
113                 clnm.append(fn.substring(0, fn.length() - 6));
114                 ClassOutput cls = new ClassOutput(clnm.toString());
115                 Files.copy(p, cls.buf);
116                 ret.add(cls);
117             }
118             return(ret);
119         }
120
121         private static void remove(Path p) throws IOException {
122             if(Files.isDirectory(p)) {
123                 for(Path ent : (Iterable<Path>)Files.list(p)::iterator)
124                     remove(ent);
125             }
126             Files.delete(p);
127         }
128
129         public void close() throws IOException {
130             remove(dir);
131         }
132     }
133
134     public static class BufferedClassLoader extends ClassLoader {
135         public final Map<String, byte[]> contents;
136
137         public BufferedClassLoader(Collection<ClassOutput> contents) {
138             this.contents = new HashMap<>();
139             for(ClassOutput clc : contents)
140                 this.contents.put(clc.name, clc.buf.toByteArray());
141         }
142
143         public Class<?> findClass(String name) throws ClassNotFoundException {
144             byte[] c = contents.get(name);
145             if(c == null)
146                 throw(new ClassNotFoundException(name));
147             return(defineClass(name, c, 0, c.length));
148         }
149     }
150
151     public static class Module {
152         public final Path file;
153         public final ClassLoader code;
154
155         public Module(Path file) throws IOException {
156             this.file = file;
157             try(Compilation c = new Compilation()) {
158                 split(c);
159                 if(!c.compile())
160                     throw(new CompilationException(file, c.output()));
161                 code = new BufferedClassLoader(c.classes());
162             }
163         }
164
165         private static final Pattern classpat = Pattern.compile("^((public|abstract)\\s+)*(class|interface)\\s+(\\S+)");
166         public void split(Compilation c) throws IOException {
167             StringBuilder head = new StringBuilder();
168             BufferedWriter cur = null;
169             try(BufferedReader fp = Files.newBufferedReader(file)) {
170                 for(String ln = fp.readLine(); ln != null; ln = fp.readLine()) {
171                     Matcher m = classpat.matcher(ln);
172                     if(m.find()) {
173                         String clnm = m.group(4);
174                         Path sp = c.srcdir.resolve(clnm + ".java");
175                         c.add(sp);
176                         if(cur != null)
177                             cur.close();
178                         cur = Files.newBufferedWriter(sp);
179                         cur.append(head);
180                     }
181                     if(cur != null) {
182                         cur.append(ln); cur.append('\n');
183                     } else {
184                         head.append(ln); head.append('\n');
185                     }
186                 }
187             } finally {
188                 if(cur != null)
189                     cur.close();
190             }
191         }
192     }
193
194     public static class File {
195         public final Path name;
196         private FileTime mtime = null;
197         private Module mod = null;
198
199         private File(Path name) {
200             this.name = name;
201         }
202
203         public void update() throws IOException {
204             synchronized(this) {
205                 FileTime mtime = Files.getLastModifiedTime(name);
206                 if((this.mtime == null) || (this.mtime.compareTo(mtime) < 0)) {
207                     mod = new Module(name);
208                     this.mtime = mtime;
209                 }
210             }
211         }
212
213         public Module mod() {
214             if(mod == null)
215                 throw(new RuntimeException("file has not yet been updated"));
216             return(mod);
217         }
218     }
219
220     public File file(Path name) {
221         synchronized(files) {
222             File ret = files.get(name);
223             if(ret == null)
224                 files.put(name, ret = new File(name));
225             return(ret);
226         }
227     }
228
229     private static Compiler global = null;
230     public static Compiler get() {
231         if(global == null) {
232             synchronized(Compiler.class) {
233                 if(global == null)
234                     global = new Compiler();
235             }
236         }
237         return(global);
238     }
239 }