• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wurstscript / WurstScript / 265

29 Sep 2025 09:00AM UTC coverage: 62.244% (+0.02%) from 62.222%
265

Pull #1096

circleci

Frotty
restore determinism, fix tests
Pull Request #1096: Perf improvements

17480 of 28083 relevant lines covered (62.24%)

0.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

55.12
de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/WurstCompilerJassImpl.java
1
package de.peeeq.wurstio;
2

3
import com.google.common.base.Charsets;
4
import com.google.common.base.Preconditions;
5
import com.google.common.collect.ImmutableList;
6
import com.google.common.collect.Lists;
7
import com.google.common.collect.Maps;
8
import com.google.common.collect.Sets;
9
import com.google.common.io.Files;
10
import config.WurstProjectConfigData;
11
import de.peeeq.wurstio.languageserver.requests.RequestFailedException;
12
import de.peeeq.wurstio.map.importer.ImportFile;
13
import de.peeeq.wurstio.mpq.MpqEditor;
14
import de.peeeq.wurstio.utils.FileReading;
15
import de.peeeq.wurstio.utils.FileUtils;
16
import de.peeeq.wurstscript.*;
17
import de.peeeq.wurstscript.ast.Element;
18
import de.peeeq.wurstscript.ast.*;
19
import de.peeeq.wurstscript.attributes.CompilationUnitInfo;
20
import de.peeeq.wurstscript.attributes.CompileError;
21
import de.peeeq.wurstscript.attributes.ErrorHandler;
22
import de.peeeq.wurstscript.gui.WurstGui;
23
import de.peeeq.wurstscript.jassAst.JassProg;
24
import de.peeeq.wurstscript.jassIm.*;
25
import de.peeeq.wurstscript.jassprinter.JassPrinter;
26
import de.peeeq.wurstscript.luaAst.LuaCompilationUnit;
27
import de.peeeq.wurstscript.parser.WPos;
28
import de.peeeq.wurstscript.translation.imoptimizer.ImOptimizer;
29
import de.peeeq.wurstscript.translation.imtojass.ImAttrType;
30
import de.peeeq.wurstscript.translation.imtojass.ImToJassTranslator;
31
import de.peeeq.wurstscript.translation.imtranslation.*;
32
import de.peeeq.wurstscript.translation.lua.translation.LuaTranslator;
33
import de.peeeq.wurstscript.types.TypesHelper;
34
import de.peeeq.wurstscript.utils.LineOffsets;
35
import de.peeeq.wurstscript.utils.NotNullList;
36
import de.peeeq.wurstscript.utils.TempDir;
37
import de.peeeq.wurstscript.utils.Utils;
38
import org.eclipse.jdt.annotation.Nullable;
39
import org.eclipse.lsp4j.MessageType;
40
import org.jetbrains.annotations.NotNull;
41

42
import java.io.*;
43
import java.lang.ref.WeakReference;
44
import java.nio.charset.StandardCharsets;
45
import java.util.*;
46
import java.util.Map.Entry;
47
import java.util.function.Function;
48

49
import static de.peeeq.wurstio.CompiletimeFunctionRunner.FunctionFlagToRun.CompiletimeFunctions;
50
import static de.peeeq.wurstscript.WurstOperator.PLUS;
51
import static de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum.IS_EXTERN;
52
import static de.peeeq.wurstscript.translation.imtranslation.FunctionFlagEnum.IS_NATIVE;
53

54
public class WurstCompilerJassImpl implements WurstCompiler {
55

56
    private final List<File> files = Lists.newArrayList();
1✔
57
    private final Map<String, Reader> otherInputs = Maps.newLinkedHashMap();
1✔
58
    private @Nullable JassProg prog;
59
    private final WurstGui gui;
60
    private boolean hasCommonJ;
61
    private RunArgs runArgs;
62
    private Optional<File> mapFile = Optional.empty();
1✔
63
    private @Nullable File projectFolder;
64
    private final ErrorHandler errorHandler;
65
    private @Nullable Map<String, File> libCache = null;
1✔
66
    private @Nullable ImProg imProg;
67
    private final List<File> parsedFiles = Lists.newArrayList();
1✔
68
    private final WurstParser parser;
69
    private final WurstChecker checker;
70
    private @Nullable ImTranslator imTranslator;
71
    private final List<File> dependencies = Lists.newArrayList();
1✔
72
    private final @Nullable MpqEditor mapFileMpq;
73
    private final TimeTaker timeTaker;
74

75
    public WurstCompilerJassImpl(@Nullable File projectFolder, WurstGui gui, @Nullable MpqEditor mapFileMpq, RunArgs runArgs) {
76
        this(new TimeTaker.Default(), projectFolder, gui, mapFileMpq, runArgs);
1✔
77
    }
1✔
78

79
    public WurstCompilerJassImpl(TimeTaker timeTaker, @Nullable File projectFolder, WurstGui gui, @Nullable MpqEditor mapFileMpq, RunArgs runArgs) {
1✔
80
        this.timeTaker = timeTaker;
1✔
81
        this.projectFolder = projectFolder;
1✔
82
        this.gui = gui;
1✔
83
        this.runArgs = runArgs;
1✔
84
        this.errorHandler = new ErrorHandler(gui);
1✔
85
        this.parser = new WurstParser(errorHandler, gui);
1✔
86
        this.checker = new WurstChecker(gui, errorHandler);
1✔
87
        this.mapFileMpq = mapFileMpq;
1✔
88
    }
1✔
89

90
    @Override
91
    public void loadFiles(String... filenames) {
92
        gui.sendProgress("Loading Files");
×
93
        for (String filename : filenames) {
×
94
            File file = new File(filename);
×
95
            if (!file.exists()) {
×
96
                throw new Error("File " + filename + " does not exist.");
×
97
            }
98
            files.add(file);
×
99
        }
100
    }
×
101

102
    @Override
103
    public void loadFiles(File... files) {
104
        gui.sendProgress("Loading Files");
1✔
105
        for (File file : files) {
1✔
106
            loadFile(file);
1✔
107
        }
108
    }
1✔
109

110
    @Override
111
    public void runCompiletime(WurstProjectConfigData projectConfigData, boolean isProd, boolean cache) {
112
        if (runArgs.runCompiletimeFunctions()) {
1✔
113
            // compile & inject object-editor data
114
            // TODO run optimizations later?
115
            gui.sendProgress("Running compiletime functions");
1✔
116
            CompiletimeFunctionRunner ctr = new CompiletimeFunctionRunner(imTranslator, getImProg(), getMapFile(), getMapfileMpqEditor(), gui,
1✔
117
                CompiletimeFunctions, projectConfigData, isProd, cache);
118
            ctr.setInjectObjects(runArgs.isInjectObjects());
1✔
119
            ctr.setOutputStream(new PrintStream(System.err));
1✔
120
            ctr.run();
1✔
121
        }
122

123
        if (gui.getErrorCount() > 0) {
1✔
124
            CompileError compileError = gui
×
125
                .getErrorList().get(0);
×
126
            throw new RequestFailedException(MessageType.Error, "Could not compile project (error in running compiletime functions/expressions): ", compileError);
×
127
        }
128

129

130
        if (runArgs.isInjectObjects()) {
1✔
131
            Preconditions.checkNotNull(mapFileMpq);
×
132
            Preconditions.checkNotNull(projectFolder);
×
133
            // add the imports
134
            ImportFile.importFilesFromImports(projectFolder, mapFileMpq);
×
135
        }
136
    }
1✔
137

138
    private void loadFile(File file) throws Error {
139
        Preconditions.checkNotNull(file);
1✔
140
        if (!file.exists()) {
1✔
141
            throw new Error("File " + file + " does not exist.");
×
142
        }
143
        this.files.add(file);
1✔
144
    }
1✔
145

146
    public void loadWurstFilesInDir(File dir) {
147
        for (File f : dir.listFiles()) {
×
148
            if (f.isDirectory()) {
×
149
                loadWurstFilesInDir(f);
×
150
            } else if (Utils.isWurstFile(f)) {
×
151
                loadFile(f);
×
152
            } else if (f.getName().equals("wurst.dependencies")) {
×
153
                dependencies.addAll(checkDependencyFile(f, gui));
×
154
            } else if ((!mapFile.isPresent() || runArgs.isNoExtractMapScript()) && f.getName().equals("war3map.j")) {
×
155
                loadFile(f);
×
156
            }
157
        }
158
    }
×
159

160
    public static ImmutableList<File> checkDependencyFile(File depFile, WurstGui gui) {
161
        List<String> lines;
162
        try {
163
            lines = Files.readLines(depFile, StandardCharsets.UTF_8);
×
164
        } catch (IOException e) {
×
165
            e.printStackTrace();
×
166
            throw new Error(e);
×
167
        }
×
168
        LineOffsets offsets = new LineOffsets();
×
169
        int lineNr = 0;
×
170
        int offset = 0;
×
171
        for (String line : lines) {
×
172
            offsets.set(lineNr, offset);
×
173
            lineNr++;
×
174
            offset += line.length() + 1;
×
175
        }
×
176
        offsets.set(lineNr, offset);
×
177
        lineNr = 0;
×
178
        ImmutableList.Builder<File> dependencies = ImmutableList.builder();
×
179
        for (String line : lines) {
×
180
            int lineOffset = offsets.get(lineNr);
×
181
            WPos pos = new WPos(depFile.getAbsolutePath(), offsets, lineOffset + 1, lineOffset + line.length() + 1);
×
182
            File folder = new File(line);
×
183
            if (!folder.exists()) {
×
184
                gui.sendError(new CompileError(pos, "Folder " + line + " not found."));
×
185
            } else if (!folder.isDirectory()) {
×
186
                gui.sendError(new CompileError(pos, line + " is not a folder."));
×
187
            } else {
188
                dependencies.add(folder);
×
189
            }
190
            lineNr++;
×
191
        }
×
192
        return dependencies.build();
×
193
    }
194

195
    @Override
196
    public @Nullable WurstModel parseFiles() {
197

198
        // search mapFile
199
        for (File file : files) {
1✔
200
            if (file.getName().endsWith(".w3x") || file.getName().endsWith(".w3m")) {
1✔
201
                mapFile = Optional.ofNullable(file);
×
202
            } else if (file.isDirectory()) {
1✔
203
                if (projectFolder != null && !file.getParent().equals(projectFolder.getAbsolutePath())) {
×
204
                    throw new RuntimeException("Cannot set projectFolder to " + file + " because it is already set to non parent " + projectFolder);
×
205
                }
206
                projectFolder = file;
×
207
            }
208
        }
1✔
209

210

211
        // import wurst folder if it exists
212
        Optional<File> l_mapFile = mapFile;
1✔
213
        if (l_mapFile.isPresent()) {
1✔
214
            if (projectFolder == null) {
×
215
                projectFolder = l_mapFile.get().getParentFile();
×
216
            }
217
            File relativeWurstDir = new File(projectFolder, "wurst");
×
218
            if (relativeWurstDir.exists()) {
×
219
                WLogger.info("Importing wurst files from " + relativeWurstDir);
×
220
                loadWurstFilesInDir(relativeWurstDir);
×
221
            } else {
222
                WLogger.info("No wurst folder found in " + relativeWurstDir);
×
223
            }
224
            File dependencyFile = new File(projectFolder, "wurst.dependencies");
×
225
            if (dependencyFile.exists()) {
×
226
                dependencies.addAll(checkDependencyFile(dependencyFile, gui));
×
227
            }
228
            addDependenciesFromFolder(projectFolder, dependencies);
×
229
        }
230

231
        // add directories:
232
        List<File> dirs = Lists.newArrayList();
1✔
233
        for (File file : files) {
1✔
234
            if (file.isDirectory()) {
1✔
235
                dirs.add(file);
×
236
            }
237
        }
1✔
238
        for (File dir : dirs) {
1✔
239
            loadWurstFilesInDir(dir);
×
240
        }
×
241

242
        gui.sendProgress("Parsing Files");
1✔
243
        // parse all the files:
244
        List<CompilationUnit> compilationUnits = new NotNullList<>();
1✔
245

246
        for (File file : files) {
1✔
247
            if (file.isDirectory()) {
1✔
248
                // ignore dirs
249
            } else if (file.getName().endsWith(".w3x") || file.getName().endsWith(".w3m")) {
1✔
250
                CompilationUnit r = processMap(file);
×
251
                if (r != null) {
×
252
                    compilationUnits.add(r);
×
253
                }
254
            } else {
×
255
                if (file.getName().endsWith("common.j")) {
1✔
256
                    hasCommonJ = true;
1✔
257
                }
258
                compilationUnits.add(parseFile(file));
1✔
259
            }
260
        }
1✔
261
        for (Entry<String, Reader> in : otherInputs.entrySet()) {
1✔
262
            compilationUnits.add(parse(in.getKey(), in.getValue()));
1✔
263
        }
1✔
264

265
        try {
266
            addImportedLibs(compilationUnits);
1✔
267
        } catch (CompileError e) {
×
268
            gui.sendError(e);
×
269
            return null;
×
270
        }
1✔
271

272
        if (errorHandler.getErrorCount() > 0)
1✔
273
            return null;
1✔
274

275
        // merge the compilationUnits:
276
        WurstModel merged = mergeCompilationUnits(compilationUnits);
1✔
277
        StringBuilder sb = new StringBuilder();
1✔
278
        for (CompilationUnit cu : merged) {
1✔
279
            sb.append(cu.getCuInfo().getFile()).append(", ");
1✔
280
        }
1✔
281
        WLogger.info("Compiling compilation units: " + sb);
1✔
282

283
        return merged;
1✔
284
    }
285

286
    /**
287
     * Adds dependencies from the _build/dependencies folder
288
     */
289
    public static void addDependenciesFromFolder(File projectFolder, Collection<File> dependencies) {
290
        File dependencyFolder = new File(new File(projectFolder, "_build"), "dependencies");
×
291
        File[] depProjects = dependencyFolder.listFiles();
×
292
        if (depProjects != null) {
×
293
            for (File depFile : depProjects) {
×
294
                if (depFile.isDirectory()
×
295
                    && dependencies.stream().noneMatch(f -> FileUtils.sameFile(f, depFile))) {
×
296
                    dependencies.add(depFile);
×
297
                }
298
            }
299
        }
300
    }
×
301

302
    private void addImportedLibs(List<CompilationUnit> compilationUnits) {
303
        addImportedLibs(compilationUnits, file -> {
1✔
304
            CompilationUnit lib = parseFile(file);
1✔
305
            lib.getCuInfo().setFile(file.getAbsolutePath());
1✔
306
            compilationUnits.add(lib);
1✔
307
            return lib;
1✔
308
        });
309
    }
1✔
310

311
    /**
312
     * this method scans for unsatisfied imports and tries to find them in the lib-path
313
     */
314
    public void addImportedLibs(List<CompilationUnit> compilationUnits, Function<File, CompilationUnit> addCompilationUnit) {
315
        Set<String> packages = Sets.newLinkedHashSet();
1✔
316
        Set<WImport> imports = new LinkedHashSet<>();
1✔
317
        for (CompilationUnit c : compilationUnits) {
1✔
318
            c.getCuInfo().setCuErrorHandler(errorHandler);
1✔
319
            for (WPackage p : c.getPackages()) {
1✔
320
                packages.add(p.getName());
1✔
321
                imports.addAll(p.getImports());
1✔
322
            }
1✔
323
        }
1✔
324

325
        for (WImport imp : imports) {
1✔
326
            resolveImport(addCompilationUnit, packages, imp);
1✔
327
        }
1✔
328

329
    }
1✔
330

331
    private void resolveImport(Function<File, CompilationUnit> addCompilationUnit, Set<String> packages, WImport imp) throws CompileError {
332
        //                WLogger.info("resolving import: " + imp.getPackagename());
333
        if (!packages.contains(imp.getPackagename())) {
1✔
334
            if (getLibs().containsKey(imp.getPackagename())) {
1✔
335
                CompilationUnit lib = loadLibPackage(addCompilationUnit, imp.getPackagename());
1✔
336
                boolean foundPackage = false;
1✔
337
                for (WPackage p : lib.getPackages()) {
1✔
338
                    packages.add(p.getName());
1✔
339
                    if (p.getName().equals(imp.getPackagename())) {
1✔
340
                        foundPackage = true;
1✔
341
                    }
342
                    for (WImport i : p.getImports()) {
1✔
343
                        resolveImport(addCompilationUnit, packages, i);
1✔
344
                    }
1✔
345
                }
1✔
346
                if (!foundPackage) {
1✔
347
                    imp.addError("The import " + imp.getPackagename() + " could not be found in file " + lib.getCuInfo().getFile());
×
348
                }
349
            } else {
1✔
350
                if (imp.getPackagename().equals("Wurst")) {
1✔
351
                    imp.addError("The standard library could not be imported.");
×
352
                }
353
                if (imp.getPackagename().equals("NoWurst")) {
1✔
354
                    // ignore this package
355
                } else {
356
                    imp.addError("The import '" + imp.getPackagename() + "' could not be resolved.\n" + "Available packages: "
1✔
357
                        + Utils.join(getLibs().keySet(), ", "));
1✔
358
                }
359
            }
360
        } else {
361
            //                        WLogger.info("already imported: " + imp.getPackagename());
362
        }
363
    }
1✔
364

365
    private CompilationUnit loadLibPackage(Function<File, CompilationUnit> addCompilationUnit, String imp) {
366
        File file = getLibs().get(imp);
1✔
367
        if (file == null) {
1✔
368
            gui.sendError(new CompileError(new WPos("", null, 0, 0), "Could not find lib-package " + imp + ". Are you missing your wurst.dependencies file?"));
×
369
            return Ast.CompilationUnit(new CompilationUnitInfo(errorHandler), Ast.JassToplevelDeclarations(), Ast.WPackages());
×
370
        } else {
371
            return addCompilationUnit.apply(file);
1✔
372
        }
373
    }
374

375
    public Map<String, File> getLibs() {
376
        Map<String, File> lc = libCache;
1✔
377
        if (lc == null) {
1✔
378
            lc = Maps.newLinkedHashMap();
1✔
379
            libCache = lc;
1✔
380
            for (File libDir : runArgs.getAdditionalLibDirs()) {
1✔
381
                addLibDir(libDir);
1✔
382
            }
1✔
383
            for (File libDir : dependencies) {
1✔
384
                addLibDir(libDir);
×
385
            }
×
386
        }
387
        return lc;
1✔
388
    }
389

390
    private void addLibDir(File libDir) throws Error {
391
        if (!libDir.exists() || !libDir.isDirectory()) {
1✔
392
            throw new Error("Library folder " + libDir + " does not exist.");
×
393
        }
394
        for (File f : libDir.listFiles()) {
1✔
395
            if (f.isDirectory()) {
1✔
396
                // recursively scan directory
397
                addLibDir(f);
1✔
398
            }
399
            if (Utils.isWurstFile(f)) {
1✔
400
                String libName = Utils.getLibName(f);
1✔
401
                getLibs().put(libName, f);
1✔
402
            }
403
        }
404
    }
1✔
405

406
    public void checkProg(WurstModel model) {
407
        checkProg(model, model);
1✔
408
    }
1✔
409

410
    public void checkProg(WurstModel model, Collection<CompilationUnit> toCheck) {
411
        for (CompilationUnit cu : toCheck) {
1✔
412
            Preconditions.checkNotNull(cu);
1✔
413
            if (!model.contains(cu)) {
1✔
414
                // model has changed since then, no need to do 'toCheck'
415
                throw new ModelChangedException();
×
416
            }
417
        }
1✔
418

419
        checker.checkProg(model, toCheck);
1✔
420
    }
1✔
421

422
    public JassProg transformProgToJass() {
423
        ImTranslator imTranslator2 = getImTranslator();
1✔
424
        ImProg imProg2 = getImProg();
1✔
425
        imTranslator2.assertProperties();
1✔
426
        checkNoCompiletimeExpr(imProg2);
1✔
427
        int stage = 2;
1✔
428
        // eliminate
429
        beginPhase(2, "Eliminate generics");
1✔
430
        new EliminateGenerics(imTranslator2, imProg2).transform();
1✔
431
        printDebugImProg("./test-output/im " + stage++ + "_genericsEliminated.im");
1✔
432
        timeTaker.endPhase();
1✔
433
        // eliminate classes
434
        beginPhase(2, "translate classes");
1✔
435

436
        new EliminateClasses(imTranslator2, imProg2, !runArgs.isUncheckedDispatch()).eliminateClasses();
1✔
437
        imTranslator2.assertProperties();
1✔
438
        printDebugImProg("./test-output/im " + stage++ + "_classesEliminated.im");
1✔
439
        timeTaker.endPhase();
1✔
440

441
        new VarargEliminator(imProg2).run();
1✔
442
        printDebugImProg("./test-output/im " + stage++ + "_varargEliminated.im");
1✔
443
        imTranslator2.assertProperties();
1✔
444

445
        timeTaker.endPhase();
1✔
446

447
        if (runArgs.isNoDebugMessages()) {
1✔
448
            beginPhase(3, "remove debug messages");
×
449
            DebugMessageRemover.removeDebugMessages(imProg2);
×
450
            timeTaker.endPhase();
×
451
        } else {
452
            // debug: add stacktraces
453
            if (runArgs.isIncludeStacktraces()) {
1✔
454
                beginPhase(4, "add stack traces");
1✔
455
                new StackTraceInjector2(imProg2, imTranslator2).transform(timeTaker);
1✔
456
                timeTaker.endPhase();
1✔
457
            }
458
        }
459
        imTranslator2.assertProperties();
1✔
460

461
        ImOptimizer optimizer = new ImOptimizer(timeTaker, imTranslator2);
1✔
462

463
        // inliner
464
        if (runArgs.isInline()) {
1✔
465
            beginPhase(5, "inlining");
1✔
466
            optimizer.doInlining();
1✔
467
            imTranslator2.assertProperties();
1✔
468

469
            printDebugImProg("./test-output/im " + stage++ + "_afterinline.im");
1✔
470
            timeTaker.endPhase();
1✔
471
        }
472

473
        // eliminate tuples
474
        beginPhase(6, "eliminate tuples");
1✔
475
        timeTaker.beginPhase("flatten");
1✔
476
        getImProg().flatten(imTranslator2);
1✔
477
        timeTaker.endPhase();
1✔
478
        timeTaker.beginPhase("kill tuples");
1✔
479
        EliminateTuples.eliminateTuplesProg(getImProg(), imTranslator2);
1✔
480
        timeTaker.endPhase();
1✔
481

482
        getImTranslator().assertProperties(AssertProperty.NOTUPLES);
1✔
483

484
        printDebugImProg("./test-output/im " + stage++ + "_withouttuples.im");
1✔
485

486
        timeTaker.endPhase();
1✔
487

488
        beginPhase(7, "eliminate multi arrays");
1✔
489
        new MultiArrayEliminator(imProg2, imTranslator2, runArgs.isIncludeStacktraces() && !runArgs.isNoDebugMessages()).run();
1✔
490
        printDebugImProg("./test-output/im " + stage++ + "_withoutmultiarrays.im");
1✔
491
        imTranslator2.assertProperties();
1✔
492
        timeTaker.endPhase();
1✔
493

494
        beginPhase(8, "remove func refs");
1✔
495
        new FuncRefRemover(imProg2, imTranslator2).run();
1✔
496
        timeTaker.endPhase();
1✔
497

498
        // remove cycles:
499
        beginPhase(9, "remove cyclic functions");
1✔
500
        new CyclicFunctionRemover(imTranslator2, imProg2, timeTaker).work();
1✔
501

502
        printDebugImProg("./test-output/im " + stage++ + "_nocyc.im");
1✔
503
        timeTaker.endPhase();
1✔
504

505
        // flatten
506
        beginPhase(10, "flatten");
1✔
507
        getImProg().flatten(imTranslator2);
1✔
508
        getImTranslator().assertProperties(AssertProperty.NOTUPLES, AssertProperty.FLAT);
1✔
509

510
        printDebugImProg("./test-output/im " + stage++ + "_flat.im");
1✔
511
        timeTaker.endPhase();
1✔
512

513
        if (runArgs.isLocalOptimizations()) {
1✔
514
            beginPhase(11, "local optimizations");
1✔
515
            optimizer.localOptimizations();
1✔
516
            timeTaker.endPhase();
1✔
517
        }
518

519
        printDebugImProg("./test-output/im " + stage++ + "_afterlocalopts.im");
1✔
520

521
        if (runArgs.isNullsetting()) {
1✔
522
            beginPhase(12, "null setting");
×
523
            optimizer.doNullsetting();
×
524
            printDebugImProg("./test-output/im " + stage++ + "_afternullsetting.im");
×
525
            timeTaker.endPhase();
×
526
        }
527

528
        beginPhase(13, "flatten");
1✔
529
        optimizer.removeGarbage();
1✔
530
        imProg.flatten(imTranslator);
1✔
531

532
        // Re-run to avoid #883
533
        optimizer.removeGarbage();
1✔
534
        imProg.flatten(imTranslator);
1✔
535

536
        printDebugImProg("./test-output/im " + stage++ + "_afterremoveGarbage1.im");
1✔
537
        timeTaker.endPhase();
1✔
538

539
        if (runArgs.isHotStartmap() || runArgs.isHotReload()) {
1✔
540
            addJassHotCodeReloadCode();
×
541
        }
542
        if (runArgs.isOptimize()) {
1✔
543
            beginPhase(13, "froptimize");
×
544
            optimizer.optimize();
×
545

546
            optimizer.removeGarbage();
×
547
            imProg.flatten(imTranslator);
×
548
            printDebugImProg("./test-output/im " + stage++ + "_afteroptimize.im");
×
549
        }
550

551

552
        // translate flattened intermediate lang to jass:
553

554
        beginPhase(14, "translate to jass");
1✔
555
        getImTranslator().calculateCallRelationsAndUsedVariables();
1✔
556
        ImToJassTranslator translator =
1✔
557
            new ImToJassTranslator(getImProg(), getImTranslator().getCalledFunctions(), getImTranslator().getMainFunc(), getImTranslator().getConfFunc());
1✔
558
        prog = translator.translate();
1✔
559
        if (errorHandler.getErrorCount() > 0) {
1✔
560
            prog = null;
×
561
        }
562
        timeTaker.endPhase();
1✔
563
        return prog;
1✔
564
    }
565

566
    private void addJassHotCodeReloadCode() {
567
        Preconditions.checkNotNull(imTranslator);
×
568
        Preconditions.checkNotNull(imProg);
×
569
        ImFunction mainFunc = imTranslator.getMainFunc();
×
570
        Preconditions.checkNotNull(mainFunc);
×
571
        Element trace = imProg.getTrace();
×
572

573
        List<ImStmt> stmts = new ArrayList<>();
×
574

575
        // add call to JHCR_Init_init in main
576
        stmts.add(callExtern(trace, CallType.EXECUTE, "JHCR_Init_init"));
×
577

578
        ImFunction statusFunction = JassIm.ImFunction(trace, "JHCR_API_GetLastStatus", JassIm.ImTypeVars(),
×
579
            JassIm.ImVars(), JassIm.ImSimpleType("integer"), JassIm.ImVars(), JassIm.ImStmts(JassIm.ImReturn(trace, JassIm.ImIntVal(0))),
×
580
                List.of());
×
581

582
        imProg.getFunctions().add(statusFunction);
×
583

584
        ImFunctionCall jhcrStatusCall = JassIm.ImFunctionCall(trace, statusFunction, JassIm.ImTypeArguments(), JassIm.ImExprs(), false, CallType.NORMAL);
×
585
        ImFunction I2S = findNative("I2S", trace.attrErrorPos());
×
586
        ImFunctionCall statusCall = JassIm.ImFunctionCall(trace, I2S, JassIm.ImTypeArguments(), JassIm.ImExprs(jhcrStatusCall), false, CallType.NORMAL);
×
587

588

589
        // add reload trigger for pressing escape
590
        ImStmts reloadBody = JassIm.ImStmts(
×
591
            callExtern(trace, CallType.EXECUTE, "JHCR_Init_parse"),
×
592
            callExtern(trace, CallType.NORMAL, "BJDebugMsg", JassIm.ImOperatorCall(PLUS, JassIm.ImExprs(JassIm.ImStringVal("Code reloaded, status: "), statusCall)))
×
593
            );
594
        ImFunction jhcr_reload = JassIm.ImFunction(trace, "jhcr_reload_on_escape", JassIm.ImTypeVars(), JassIm.ImVars(), JassIm.ImVoid(), JassIm.ImVars(), reloadBody, Collections.emptyList());
×
595

596

597
        ImVar trig = JassIm.ImVar(trace, TypesHelper.imTrigger(), "trig", false);
×
598
        mainFunc.getLocals().add(trig);
×
599
        // TriggerRegisterPlayerEventEndCinematic(trig, Player(0))
600
        stmts.add(JassIm.ImSet(trace, JassIm.ImVarAccess(trig), callExtern(trace, CallType.NORMAL, "CreateTrigger")));
×
601
        stmts.add(callExtern(trace, CallType.NORMAL, "TriggerRegisterPlayerEventEndCinematic", JassIm.ImVarAccess(trig),
×
602
            callExtern(trace, CallType.NORMAL, "Player", JassIm.ImIntVal(0))));
×
603
        stmts.add(callExtern(trace, CallType.NORMAL, "TriggerAddAction", JassIm.ImVarAccess(trig),
×
604
            JassIm.ImFuncRef(trace, jhcr_reload)));
×
605

606
        mainFunc.getBody().addAll(0, stmts);
×
607
    }
×
608

609
    @NotNull
610
    private ImFunction findNative(String funcName, WPos trace) {
611
        return imProg.getFunctions()
×
612
            .stream()
×
613
            .filter(ImFunction::isNative)
×
614
            .filter(func -> func.getName().equals(funcName))
×
615
            .findFirst()
×
616
            .orElseGet(() -> {
×
617
                throw new CompileError(trace, "Could not find native " + funcName);
×
618
            });
619
    }
620

621
    @NotNull
622
    private ImFunction findFunction(String funcName, WPos trace) {
623
        return imProg.getFunctions()
×
624
            .stream()
×
625
            .filter(func -> func.getName().equals(funcName))
×
626
            .findFirst()
×
627
            .orElseGet(() -> {
×
628
                throw new CompileError(trace, "Could not find native " + funcName);
×
629
            });
630
    }
631

632
    @NotNull
633
    private ImFunctionCall callExtern(Element trace, CallType callType, String functionName, ImExpr... arguments) {
634
        ImFunction jhcrinit = JassIm.ImFunction(trace, functionName, JassIm.ImTypeVars(), JassIm.ImVars(), JassIm.ImVoid(), JassIm.ImVars(), JassIm.ImStmts(), Collections.singletonList(IS_EXTERN));
×
635
        return JassIm.ImFunctionCall(trace, jhcrinit, JassIm.ImTypeArguments(), JassIm.ImExprs(arguments), true, callType);
×
636
    }
637

638
    public void checkNoCompiletimeExpr(ImProg prog) {
639
        prog.accept(new ImProg.DefaultVisitor() {
1✔
640
            @Override
641
            public void visit(ImCompiletimeExpr e) {
642
                super.visit(e);
×
643
                throw new CompileError(e.attrTrace().attrSource(), "Compiletime expressions require compilation with '-runcompiletimefunctions' option.");
×
644
            }
645
        });
646
    }
1✔
647

648
    public ImTranslator getImTranslator() {
649
        final ImTranslator t = imTranslator;
1✔
650
        if (t != null) {
1✔
651
            return t;
1✔
652
        } else {
653
            throw new Error("translator not initialized");
×
654
        }
655
    }
656

657
    public @Nullable ImProg translateProgToIm(WurstModel root) {
658
        beginPhase(1, "to intermediate lang");
1✔
659
        // translate wurst to intermediate lang:
660
        imTranslator = new ImTranslator(root, errorHandler.isUnitTestMode(), runArgs);
1✔
661
        imProg = getImTranslator().translateProg();
1✔
662
        int stage = 1;
1✔
663
        printDebugImProg("./test-output/im " + stage++ + ".im");
1✔
664
        timeTaker.endPhase();
1✔
665
        return imProg;
1✔
666
    }
667

668
    private void beginPhase(int phase, String description) {
669
        errorHandler.setProgress("Translating wurst. Phase " + phase + ": " + description, 0.6 + 0.01 * phase);
1✔
670
        timeTaker.beginPhase(description);
1✔
671
    }
1✔
672

673
    private void printDebugImProg(String debugFile) {
674
        if (!errorHandler.isUnitTestMode()) {
1✔
675
            // output only in unit test mode
676
            return;
×
677
        }
678
        try {
679
            // TODO remove test output
680
            File file = new File(debugFile);
1✔
681
            file.getParentFile().mkdirs();
1✔
682
            try (Writer w = Files.newWriter(file, Charsets.UTF_8)) {
1✔
683
                getImProg().print(w, 0);
1✔
684
            }
685
        } catch (IOException e) {
×
686
            ErrorReporting.instance.handleSevere(e, getCompleteSourcecode());
×
687
        }
1✔
688
    }
1✔
689

690
    private WurstModel mergeCompilationUnits(List<CompilationUnit> compilationUnits) {
691
        gui.sendProgress("Merging Files");
1✔
692
        WurstModel result = Ast.WurstModel();
1✔
693
        for (CompilationUnit compilationUnit : compilationUnits) {
1✔
694
            // remove from old parent
695
            compilationUnit.setParent(null);
1✔
696
            result.add(compilationUnit);
1✔
697
        }
1✔
698
        return result;
1✔
699
    }
700

701
    private CompilationUnit processMap(File file) {
702
        gui.sendProgress("Processing Map " + file.getName());
×
703
        if (!mapFile.isPresent() || !file.equals(mapFile.get())) {
×
704
            // TODO check if file != mapFile is possible, would be strange
705
            // so this should definitely be done differently
706
            throw new Error("file: " + file + " is not the mapfile: " + mapFile);
×
707
        }
708

709
        MpqEditor mapMpq = mapFileMpq;
×
710
        if (mapMpq == null) {
×
711
            throw new RuntimeException("map mpq is null");
×
712
        }
713

714
        if (runArgs.isNoExtractMapScript()) {
×
715
            return null;
×
716
        }
717

718
        // extract mapscript:
719
        try {
720
            byte[] tempBytes = mapMpq.extractFile("war3map.j");
×
721
            File tempFile = File.createTempFile("war3map", ".j", TempDir.get()); // TODO work directly with bytes without temp file
×
722
            tempFile.deleteOnExit();
×
723
            Files.write(tempBytes, tempFile);
×
724

725
            if (isWurstGenerated(tempFile)) {
×
726
                // the war3map.j file was generated by wurst
727
                // this should not be the case, as we will get duplicate function errors in this case
728
                throw new AbortCompilationException(
×
729
                    "Map was not saved correctly. Please try saving the map again.\n\n" + "This usually happens if you change the name of the map or \n"
730
                        + "if you have used the test-map-button without saving the map first.");
731
            }
732

733
            // move file to wurst directory
734
            File wurstFolder = new File(file.getParentFile(), "wurst");
×
735
            wurstFolder.mkdirs();
×
736
            if (!wurstFolder.isDirectory()) {
×
737
                throw new AbortCompilationException("Could not create Wurst folder at " + wurstFolder + ".");
×
738
            }
739
            File wurstwar3map = new File(wurstFolder, "war3map.j");
×
740
            wurstwar3map.delete();
×
741
            if (tempFile.renameTo(wurstwar3map)) {
×
742
                return parseFile(wurstwar3map);
×
743
            } else {
744
                throw new Error("Could not move war3map.j from " + tempFile + " to " + wurstwar3map);
×
745
            }
746
        } catch (RuntimeException e) {
×
747
            throw e;
×
748
        } catch (Exception e) {
×
749
            throw new Error(e);
×
750
        }
751

752
    }
753

754
    private boolean isWurstGenerated(File tempFile) {
755
        try (FileReader fr = new FileReader(tempFile); BufferedReader in = new BufferedReader(fr)) {
×
756
            String firstLine = in.readLine();
×
757
            WLogger.info("firstLine = '" + firstLine + "'");
×
758
            return firstLine.equals(JassPrinter.WURST_COMMENT);
×
759
        } catch (IOException e) {
×
760
            WLogger.severe(e);
×
761
        }
762
        return false;
×
763
    }
764

765
    // a cache for compilation units, only used for unit tests to avoid parsing standard library too many times
766
    private static final Map<File, WeakReference<CompilationUnit>> fileCompilationUnitCache = new HashMap<>();
1✔
767

768
    private CompilationUnit parseFile(File file) {
769
        if (errorHandler.isUnitTestMode()) {
1✔
770
            // in unit test mode, we use a cache
771
            WeakReference<CompilationUnit> wr = fileCompilationUnitCache.get(file);
1✔
772
            CompilationUnit res = wr == null ? null : wr.get();
1✔
773
            if (res == null) {
1✔
774
                res = parseFile2(file);
1✔
775
                fileCompilationUnitCache.put(file, new WeakReference<>(res));
1✔
776
            } else {
777
                res = res.copy();
1✔
778
            }
779
            return res;
1✔
780
        } else {
781
            return parseFile2(file);
×
782
        }
783
    }
784

785
    private CompilationUnit parseFile2(File file) {
786
        if (file.isDirectory()) {
1✔
787
            throw new Error("Is a directory: " + file);
×
788
        }
789
        parsedFiles.add(file);
1✔
790

791
        gui.sendProgress("Parsing File " + file.getName());
1✔
792
        String source = file.getAbsolutePath();
1✔
793
        try (Reader reader = FileReading.getFileReader(file)) {
1✔
794
            // scanning
795
            return parse(source, reader);
1✔
796

797
        } catch (CompileError e) {
×
798
            gui.sendError(e);
×
799
            return emptyCompilationUnit();
×
800
        } catch (FileNotFoundException e) {
×
801
            gui.sendError(new CompileError(new WPos(source, LineOffsets.dummy, 0, 0), "File not found."));
×
802
            return emptyCompilationUnit();
×
803
        } catch (IOException e) {
×
804
            gui.sendError(new CompileError(new WPos(source, LineOffsets.dummy, 0, 0), "Could not read file."));
×
805
            return emptyCompilationUnit();
×
806
        }
807
    }
808

809
    public CompilationUnit parse(String fileName, Reader reader) {
810
        if (fileName.endsWith(".j")) {
1✔
811
            return parser.parseJass(reader, fileName, hasCommonJ);
1✔
812
        }
813
        if (fileName.endsWith(".jurst")) {
1✔
814
            return parser.parseJurst(reader, fileName, hasCommonJ);
1✔
815
        }
816
        if (runArgs.isPrettyPrint()) {
1✔
817
            parser.setRemoveSugar(false);
×
818
        }
819
        return parser.parse(reader, fileName, hasCommonJ);
1✔
820
    }
821

822
    private CompilationUnit emptyCompilationUnit() {
823
        return parser.emptyCompilationUnit();
×
824
    }
825

826
    public @Nullable JassProg getProg() {
827
        return prog;
×
828
    }
829

830
    public void loadReader(String name, Reader input) {
831
        otherInputs.put(name, input);
1✔
832
    }
1✔
833

834
    public void setHasCommonJ(boolean hasCommonJ) {
835
        this.hasCommonJ = hasCommonJ;
1✔
836
    }
1✔
837

838
    public ImProg getImProg() {
839
        final ImProg imProg2 = imProg;
1✔
840
        if (imProg2 != null) {
1✔
841
            return imProg2;
1✔
842
        } else {
843
            throw new Error("imProg is null");
×
844
        }
845
    }
846

847
    public Optional<File> getMapFile() {
848
        return mapFile;
1✔
849
    }
850

851
    public ErrorHandler getErrorHandler() {
852
        return errorHandler;
1✔
853
    }
854

855
    public String getCompleteSourcecode() {
856

857
        StringBuilder sb = new StringBuilder();
×
858
        try {
859
            for (File f : parsedFiles) {
×
860
                sb.append(" //######################################################\n");
×
861
                sb.append(" // File ").append(f.getAbsolutePath()).append("\n");
×
862
                sb.append(" //######################################################\n");
×
863
                sb.append(Files.toString(f, Charsets.UTF_8));
×
864
            }
×
865

866
            for (Entry<String, Reader> entry : otherInputs.entrySet()) {
×
867
                sb.append(" //######################################################\n");
×
868
                sb.append(" // Input ").append(entry.getKey()).append("\n");
×
869
                sb.append(" //######################################################\n");
×
870
                try (Reader reader = entry.getValue()) {
×
871
                    char[] buffer = new char[1024];
×
872
                    while (true) {
873
                        int len = reader.read(buffer);
×
874
                        if (len < 0) {
×
875
                            break;
×
876
                        }
877
                        sb.append(buffer, 0, len);
×
878
                    }
×
879
                }
880
            }
×
881
        } catch (Throwable t) {
×
882
            sb.append(Utils.printExceptionWithStackTrace(t));
×
883
            WLogger.severe(t);
×
884
        }
×
885
        return sb.toString();
×
886
    }
887

888
    public void setRunArgs(RunArgs runArgs) {
889
        this.runArgs = runArgs;
1✔
890
    }
1✔
891

892
    public void setMapFile(Optional<File> mapFile) {
893
        this.mapFile = mapFile;
×
894
    }
×
895

896
    public @Nullable MpqEditor getMapfileMpqEditor() {
897
        return mapFileMpq;
1✔
898
    }
899

900
    public LuaCompilationUnit transformProgToLua() {
901

902
        ImAttrType.setWurstClassType(null);
1✔
903
        int stage;
904
        if (runArgs.isNoDebugMessages()) {
1✔
905
            beginPhase(3, "remove debug messages");
×
906
            DebugMessageRemover.removeDebugMessages(imProg);
×
907
            timeTaker.endPhase();
×
908
        } else {
909
            // debug: add stacktraces
910
            if (runArgs.isIncludeStacktraces()) {
1✔
911
                beginPhase(4, "add stack traces");
×
912
                new StackTraceInjector2(imProg, imTranslator).transform(timeTaker);
×
913
                timeTaker.endPhase();
×
914
            }
915
        }
916
        ImTranslator imTranslator2 = getImTranslator();
1✔
917
        ImOptimizer optimizer = new ImOptimizer(timeTaker, imTranslator2);
1✔
918
        // inliner
919
        stage = 5;
1✔
920
        if (runArgs.isInline()) {
1✔
921
            beginPhase(5, "inlining");
×
922
            optimizer.doInlining();
×
923
            imTranslator2.assertProperties();
×
924

925
            printDebugImProg("./test-output/lua/im " + stage++ + "_afterinline.im");
×
926
            timeTaker.endPhase();
×
927
        }
928

929
        // eliminate local types
930
        beginPhase(6, "eliminate local type");
1✔
931
        getImProg().flatten(imTranslator2);
1✔
932
        EliminateLocalTypes.eliminateLocalTypesProg(getImProg(), imTranslator2);
1✔
933

934
        optimizer.removeGarbage();
1✔
935
        imProg.flatten(imTranslator);
1✔
936
        timeTaker.endPhase();
1✔
937
        stage = 10;
1✔
938
        if (runArgs.isLocalOptimizations()) {
1✔
939
            beginPhase(10, "local optimizations");
×
940
            optimizer.localOptimizations();
×
941
            timeTaker.endPhase();
×
942
        }
943

944
        printDebugImProg("./test-output/lua/im " + stage++ + "_afterlocalopts.im");
1✔
945

946
        optimizer.removeGarbage();
1✔
947
        imProg.flatten(imTranslator);
1✔
948

949
        // Re-run to avoid #883
950
        optimizer.removeGarbage();
1✔
951
        imProg.flatten(imTranslator);
1✔
952

953
        printDebugImProg("./test-output/lua/im " + stage++ + "_afterremoveGarbage1.im");
1✔
954

955
        stage = 12;
1✔
956
        if (runArgs.isOptimize()) {
1✔
957
            beginPhase(12, "froptimize");
×
958
            optimizer.optimize();
×
959

960
            optimizer.removeGarbage();
×
961
            imProg.flatten(imTranslator);
×
962
            printDebugImProg("./test-output/lua/im " + stage++ + "_afteroptimize.im");
×
963
            timeTaker.endPhase();
×
964
        }
965
        beginPhase(13, "translate to lua");
1✔
966
        LuaTranslator luaTranslator = new LuaTranslator(imProg, imTranslator);
1✔
967
        LuaCompilationUnit luaCode = luaTranslator.translate();
1✔
968
        ImAttrType.setWurstClassType(TypesHelper.imInt());
1✔
969
        timeTaker.endPhase();
1✔
970
        return luaCode;
1✔
971
    }
972
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc