diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java index 8712cbf..2b73da7 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java @@ -3,20 +3,21 @@ package com.annimon.ownlang.util; import com.annimon.ownlang.Console; import com.annimon.ownlang.stages.Stage; import com.annimon.ownlang.stages.StagesData; +import com.annimon.ownlang.util.input.SourceLoaderStage; public class ErrorsLocationFormatterStage implements Stage, String> { @Override public String perform(StagesData stagesData, Iterable input) { final var sb = new StringBuilder(); - final String source = stagesData.get(SourceLoaderStage.TAG_SOURCE); + final String source = stagesData.getOrDefault(SourceLoaderStage.TAG_SOURCE, ""); final var lines = source.split("\r?\n"); for (SourceLocatedError error : input) { sb.append(Console.newline()); sb.append(error); sb.append(Console.newline()); final Range range = error.getRange(); - if (range != null) { + if (range != null && lines.length > 0) { printPosition(sb, range.normalize(), lines); } } diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSource.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSource.java new file mode 100644 index 0000000..c54d1a2 --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSource.java @@ -0,0 +1,15 @@ +package com.annimon.ownlang.util.input; + +import java.io.IOException; + +public interface InputSource { + String getPath(); + + String load() throws IOException; + + default String getBasePath() { + int i = getPath().lastIndexOf("/"); + if (i == -1) return ""; + return getPath().substring(0, i + 1); + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceFile.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceFile.java new file mode 100644 index 0000000..8a9a786 --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceFile.java @@ -0,0 +1,30 @@ +package com.annimon.ownlang.util.input; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; + +public record InputSourceFile(String path) implements InputSource { + + @Override + public String getPath() { + return path; + } + + @Override + public String load() throws IOException { + if (Files.isReadable(Path.of(path))) { + try (InputStream is = new FileInputStream(path)) { + return SourceLoaderStage.readStream(is); + } + } + throw new IOException(path + " not found"); + } + + @Override + public String toString() { + return "File " + path; + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceProgram.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceProgram.java new file mode 100644 index 0000000..392bfb0 --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceProgram.java @@ -0,0 +1,19 @@ +package com.annimon.ownlang.util.input; + +public record InputSourceProgram(String program) implements InputSource { + + @Override + public String getPath() { + return "."; + } + + @Override + public String load() { + return program; + } + + @Override + public String toString() { + return "Program"; + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceResource.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceResource.java new file mode 100644 index 0000000..bece07e --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/InputSourceResource.java @@ -0,0 +1,27 @@ +package com.annimon.ownlang.util.input; + +import java.io.IOException; +import java.io.InputStream; + +public record InputSourceResource(String path) implements InputSource { + + @Override + public String getPath() { + return path; + } + + @Override + public String load() throws IOException { + try (InputStream is = getClass().getResourceAsStream(path)) { + if (is != null) { + return SourceLoaderStage.readStream(is); + } + } + throw new IOException(path + " not found"); + } + + @Override + public String toString() { + return "Resource " + path; + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLoaderStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/SourceLoaderStage.java similarity index 61% rename from ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLoaderStage.java rename to ownlang-core/src/main/java/com/annimon/ownlang/util/input/SourceLoaderStage.java index f117c2e..f25682d 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLoaderStage.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/SourceLoaderStage.java @@ -1,37 +1,25 @@ -package com.annimon.ownlang.util; +package com.annimon.ownlang.util.input; import com.annimon.ownlang.exceptions.OwnLangRuntimeException; import com.annimon.ownlang.stages.Stage; import com.annimon.ownlang.stages.StagesData; import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -public class SourceLoaderStage implements Stage { +public class SourceLoaderStage implements Stage { public static final String TAG_SOURCE = "source"; @Override - public String perform(StagesData stagesData, String name) { + public String perform(StagesData stagesData, InputSource inputSource) { try { - String result = readSource(name); + String result = inputSource.load(); stagesData.put(TAG_SOURCE, result); return result; } catch (IOException e) { - throw new OwnLangRuntimeException("Unable to read input " + name, e); - } - } - - private String readSource(String name) throws IOException { - try (InputStream is = getClass().getResourceAsStream("/" + name)) { - if (is != null) { - return readStream(is); - } - } - try (InputStream is = new FileInputStream(name)) { - return readStream(is); + throw new OwnLangRuntimeException("Unable to read input " + inputSource, e); } } diff --git a/ownlang-desktop/build.gradle b/ownlang-desktop/build.gradle index d1e7740..89176bb 100644 --- a/ownlang-desktop/build.gradle +++ b/ownlang-desktop/build.gradle @@ -56,4 +56,3 @@ tasks.register('runOptimizationDumper', JavaExec) { classpath = sourceSets.main.runtimeClasspath args '../program.own' } -// diff --git a/ownlang-desktop/src/main/java/com/annimon/ownlang/Main.java b/ownlang-desktop/src/main/java/com/annimon/ownlang/Main.java index 4ba9469..c5452bb 100644 --- a/ownlang-desktop/src/main/java/com/annimon/ownlang/Main.java +++ b/ownlang-desktop/src/main/java/com/annimon/ownlang/Main.java @@ -3,13 +3,13 @@ package com.annimon.ownlang; import com.annimon.ownlang.exceptions.OwnLangParserException; import com.annimon.ownlang.exceptions.StoppedException; import com.annimon.ownlang.parser.Beautifier; -import com.annimon.ownlang.parser.SourceLoader; import com.annimon.ownlang.parser.Token; import com.annimon.ownlang.parser.ast.Statement; import com.annimon.ownlang.parser.error.ParseErrorsFormatterStage; import com.annimon.ownlang.parser.linters.LinterStage; import com.annimon.ownlang.parser.optimization.OptimizationStage; import com.annimon.ownlang.stages.*; +import com.annimon.ownlang.util.input.SourceLoaderStage; import com.annimon.ownlang.utils.Repl; import com.annimon.ownlang.utils.Sandbox; import com.annimon.ownlang.utils.TimeMeasurement; @@ -23,17 +23,12 @@ import java.util.concurrent.TimeUnit; public final class Main { public static void main(String[] args) throws IOException { - if (args.length == 0) { - try { - runDefault(); - } catch (IOException ioe) { - printUsage(); - } + final RunOptions options = new RunOptions(); + if (args.length == 0 && (options.detectDefaultProgramPath() == null)) { + printUsage(); return; } - final RunOptions options = new RunOptions(); - String input = null; for (int i = 0; i < args.length; i++) { switch (args[i]) { case "-a": @@ -82,73 +77,69 @@ public final class Main { case "-f": case "--file": - if (i + 1 < args.length) { - input = SourceLoader.readSource(args[i + 1]); + if (i + 1 < args.length) { + options.programPath = args[i + 1]; createOwnLangArgs(args, i + 2); i++; } break; case "--sandbox": - createOwnLangArgs(args, i + 1); - final String[] ownlangArgs = Shared.getOwnlangArgs(); - String[] newArgs = new String[ownlangArgs.length]; - System.arraycopy(ownlangArgs, 0, newArgs, 0, ownlangArgs.length); - Sandbox.main(newArgs); + Sandbox.main(createOwnLangArgs(args, i + 1)); return; default: - if (input == null) { - input = args[i]; + if (options.programSource == null) { + options.programSource = args[i]; createOwnLangArgs(args, i + 1); } break; } } - if (input == null) { - throw new IllegalArgumentException("Empty input"); - } if (options.beautifyMode) { + String input = new SourceLoaderStage() + .perform(new StagesDataMap(), options.toInputSource()); System.out.println(Beautifier.beautify(input)); return; } - run(input, options); - } - - private static void runDefault() throws IOException { - final RunOptions options = new RunOptions(); - run(SourceLoader.readSource("program.own"), options); + run(options); } private static void printUsage() { - System.out.println("OwnLang version " + Version.VERSION + "\n\n" + - "Usage: ownlang [options]\n" + - " options:\n" + - " -f, --file [input] Run program file. Required.\n" + - " -r, --repl Enter to a REPL mode\n" + - " -l, --lint Find bugs in code\n" + - " -o N, --optimize N Perform optimization with N passes\n" + - " -b, --beautify Beautify source code\n" + - " -a, --showast Show AST of program\n" + - " -t, --showtokens Show lexical tokens\n" + - " -m, --showtime Show elapsed time of parsing and execution"); + System.out.println("OwnLang version %s\n\n".formatted(Version.VERSION) + """ + Usage: ownlang [options] + options: + -f, --file [input] Run program file. Required. + -r, --repl Enter to a REPL mode + -l, --lint Find bugs in code + -o N, --optimize N Perform optimization with N (0...9) passes + -b, --beautify Beautify source code + -a, --showast Show AST of program + -t, --showtokens Show lexical tokens + -m, --showtime Show elapsed time of parsing and execution + """); } - private static void createOwnLangArgs(String[] javaArgs, int index) { - if (index >= javaArgs.length) return; - final String[] ownlangArgs = new String[javaArgs.length - index]; - System.arraycopy(javaArgs, index, ownlangArgs, 0, ownlangArgs.length); + private static String[] createOwnLangArgs(String[] javaArgs, int index) { + final String[] ownlangArgs; + if (index >= javaArgs.length) { + ownlangArgs = new String[0]; + } else { + ownlangArgs = new String[javaArgs.length - index]; + System.arraycopy(javaArgs, index, ownlangArgs, 0, ownlangArgs.length); + } Shared.setOwnlangArgs(ownlangArgs); + return ownlangArgs; } - private static void run(String input, RunOptions options) { + private static void run(RunOptions options) { final var measurement = new TimeMeasurement(); final var scopedStages = new ScopedStageFactory(measurement::start, measurement::stop); final var stagesData = new StagesDataMap(); - stagesData.put(SourceLoaderStage.TAG_SOURCE, input); try { - scopedStages.create("Lexer", new LexerStage()) + scopedStages.create("Source loader", new SourceLoaderStage()) + .then(scopedStages.create("Lexer", new LexerStage())) .then(scopedStages.create("Parser", new ParserStage())) .thenConditional(options.optimizationLevel > 0, scopedStages.create("Optimization", @@ -157,7 +148,7 @@ public final class Main { scopedStages.create("Linter", new LinterStage())) .then(scopedStages.create("Function adding", new FunctionAddingStage())) .then(scopedStages.create("Execution", new ExecutionStage())) - .perform(stagesData, input); + .perform(stagesData, options.toInputSource()); } catch (OwnLangParserException ex) { final var error = new ParseErrorsFormatterStage() .perform(stagesData, ex.getParseErrors()); @@ -185,20 +176,4 @@ public final class Main { } } } - - private static class RunOptions { - boolean showTokens, showAst, showMeasurements; - boolean lintMode; - boolean beautifyMode; - int optimizationLevel; - - RunOptions() { - showTokens = false; - showAst = false; - showMeasurements = false; - lintMode = false; - beautifyMode = false; - optimizationLevel = 0; - } - } } diff --git a/ownlang-desktop/src/main/java/com/annimon/ownlang/RunOptions.java b/ownlang-desktop/src/main/java/com/annimon/ownlang/RunOptions.java new file mode 100644 index 0000000..10ed0c3 --- /dev/null +++ b/ownlang-desktop/src/main/java/com/annimon/ownlang/RunOptions.java @@ -0,0 +1,55 @@ +package com.annimon.ownlang; + +import com.annimon.ownlang.util.input.InputSource; +import com.annimon.ownlang.util.input.InputSourceFile; +import com.annimon.ownlang.util.input.InputSourceProgram; +import com.annimon.ownlang.util.input.InputSourceResource; +import java.nio.file.Files; +import java.nio.file.Path; + +public class RunOptions { + private static final String RESOURCE_PREFIX = "resource:"; + private static final String DEFAULT_PROGRAM = "program.own"; + + // input + String programPath; + String programSource; + // modes + boolean lintMode; + boolean beautifyMode; + int optimizationLevel; + // flags + boolean showTokens; + boolean showAst; + boolean showMeasurements; + + String detectDefaultProgramPath() { + if (getClass().getResource("/" + DEFAULT_PROGRAM) != null) { + return RESOURCE_PREFIX + "/" + DEFAULT_PROGRAM; + } + if (Files.isReadable(Path.of(DEFAULT_PROGRAM))) { + return DEFAULT_PROGRAM; + } + return null; + } + + InputSource toInputSource() { + if (programSource != null) { + return new InputSourceProgram(programSource); + } + if (programPath == null) { + // No arguments. Default to program.own + programPath = detectDefaultProgramPath(); + if (programPath == null) { + throw new IllegalArgumentException("Empty input"); + } + } + + if (programPath.startsWith(RESOURCE_PREFIX)) { + String path = programPath.substring(RESOURCE_PREFIX.length()); + return new InputSourceResource(path); + } else { + return new InputSourceFile(programPath); + } + } +} diff --git a/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java b/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java index 63ae75e..4bf86c4 100644 --- a/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java +++ b/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java @@ -12,7 +12,9 @@ import com.annimon.ownlang.parser.error.ParseErrorsFormatterStage; import com.annimon.ownlang.parser.optimization.OptimizationStage; import com.annimon.ownlang.parser.visitors.AbstractVisitor; import com.annimon.ownlang.stages.*; -import com.annimon.ownlang.util.SourceLoaderStage; +import com.annimon.ownlang.util.input.InputSource; +import com.annimon.ownlang.util.input.InputSourceFile; +import com.annimon.ownlang.util.input.SourceLoaderStage; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -24,11 +26,12 @@ import static org.junit.jupiter.api.Assertions.*; public class ProgramsTest { private static final String RES_DIR = "src/test/resources"; - private static Stage testPipeline; + private static Stage testPipeline; - public static Stream data() { + public static Stream data() { return scanDirectory(RES_DIR) - .map(File::getPath); + .map(File::getPath) + .map(InputSourceFile::new); } @BeforeAll @@ -85,16 +88,16 @@ public class ProgramsTest { @ParameterizedTest @MethodSource("data") - public void testProgram(String programPath) { + public void testProgram(InputSource inputSource) { final StagesDataMap stagesData = new StagesDataMap(); try { - testPipeline.perform(stagesData, programPath); + testPipeline.perform(stagesData, inputSource); } catch (OwnLangParserException ex) { final var error = new ParseErrorsFormatterStage() .perform(stagesData, ex.getParseErrors()); - fail(programPath + "\n" + error, ex); + fail(inputSource + "\n" + error, ex); } catch (Exception oae) { - fail(programPath, oae); + fail(inputSource.toString(), oae); Console.handleException(stagesData, Thread.currentThread(), oae); } }