diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e5f95d..c05594a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 3.0 Early Access Preview 4 (... 2020) + +Implemented: +- Support for `repeat` loops; +- TigerJython specific libraries (only as part of the release, no sources); + ### 3.0 Early Access Preview 3 (25 May 2020) @@ -9,7 +15,7 @@ Implemented: - Syntax checker honours Python version and other preferences; - Customise appearance with font, font-size, and themes; -Bugfixes: +Bug fixes: - Line numbers on JRE 8 were not displayed correctly; diff --git a/build.sbt b/build.sbt index dc075d3..3ee5498 100644 --- a/build.sbt +++ b/build.sbt @@ -54,7 +54,7 @@ val buildDate = "%d %s %d".format( val buildTag = "-SNAPSHOT" -val buildVersion = "ea+03" +val buildVersion = "ea+04" // This is needed to run/test the project without having to restart SBT afterwards fork in run := true @@ -96,7 +96,7 @@ libraryDependencies += "org.openjfx" % "javafx-graphics" % fxVersion classifier // Other dependencies libraryDependencies += "org.fxmisc.richtext" % "richtextfx" % "0.10.5" -libraryDependencies += "org.python" % "jython" % "2.7.2" +libraryDependencies += "org.python" % "jython-standalone" % "2.7.2" // When building a common JAR, some files are typically shared and we need to discard // any superfluous files diff --git a/src/main/scala/tigerjython/core/JythonExecutor.scala b/src/main/scala/tigerjython/core/JythonExecutor.scala index 4cc8b14..6463a95 100644 --- a/src/main/scala/tigerjython/core/JythonExecutor.scala +++ b/src/main/scala/tigerjython/core/JythonExecutor.scala @@ -28,7 +28,9 @@ object JythonExecutor { def run(filename: String): Unit = { Options.importSite = false Options.Qnew = true - PythonInterpreter.initialize(System.getProperties, null, null) + val postProperties = new java.util.Properties() + postProperties.setProperty("python.cachedir.skip", "false") + PythonInterpreter.initialize(System.getProperties, postProperties, null) jython.JythonBuiltins.initialize() interpreter = new PythonInterpreter() interpreter.execfile(filename) diff --git a/src/main/scala/tigerjython/errorhandling/StaticErrorChecker.scala b/src/main/scala/tigerjython/errorhandling/StaticErrorChecker.scala index 4873fbc..1f0fecc 100644 --- a/src/main/scala/tigerjython/errorhandling/StaticErrorChecker.scala +++ b/src/main/scala/tigerjython/errorhandling/StaticErrorChecker.scala @@ -39,7 +39,8 @@ object StaticErrorChecker { else new SyntaxChecker(programCode, filename) syntaxChecker.strictCode = Preferences.syntaxCheckIsStrict.get - syntaxChecker.rejectDeadCode = Preferences.syntaxCheckRejectDeadCode.get + // syntaxChecker.rejectDeadCode = Preferences.syntaxCheckRejectDeadCode.get + syntaxChecker.rejectDeadCode = Preferences.syntaxCheckIsStrict.get syntaxChecker.repeatStatement = Preferences.repeatLoop.get syntaxChecker.check() match { case Some((pos, msg)) => diff --git a/src/main/scala/tigerjython/execute/PythonCodeTranslator.scala b/src/main/scala/tigerjython/execute/PythonCodeTranslator.scala new file mode 100644 index 0000000..f94a2e9 --- /dev/null +++ b/src/main/scala/tigerjython/execute/PythonCodeTranslator.scala @@ -0,0 +1,80 @@ +package tigerjython.execute + +import tigerjython.core.Preferences +import tigerpython.parser.errors.ErrorHandler +import tigerpython.parser.lexer.{Lexer, TokenType} +import tigerpython.parser.parsing.ParserState + +/** + * In order to support some syntax enhancements such as `repeat` loops, we need to replace some tokens in the source + * code by others to create a fully compatible script. This object is the interface for such code transformations. + * + * @author Tobias Kohn + */ +object PythonCodeTranslator { + + private val REPEAT_STRING = Map[Int, String]( + 2 -> "for _tj_repeat_counter_ in xrange(long(%s))", + 3 -> "for _tj_repeat_counter in range(int(%s))" + ) + + /** + * Takes a Python program as input and transforms it so as to build a fully compatible script. + * + * If the code is transformed, the new program is returned. If the code requires no changes, the function returns + * `None`. + * + * @param code The source code as a `String` value. + * @return Either `None` or a new `String` representing the transformed code. + */ + def translate(code: String): Option[String] = { + val version = PythonInstallations.getSelectedVersionNumber + if (Preferences.repeatLoop.get) + _translate(code, version) + else + None + } + + protected def _translate(code: String, pythonVersion: Int): Option[String] = { + val parserState = ParserState(code, pythonVersion, ErrorHandler.SilentErrorHandler) + parserState.repeatStatement = Preferences.repeatLoop.get + val lexer = new Lexer(code, parserState, 0) + val result = new StringBuilder() + var position: Int = 0 + for (token <- lexer) + token.tokenType match { + case TokenType.REPEAT => + result ++= code.substring(position, token.pos) + val n = lexer.head + if (n != null && n.tokenType == TokenType.COLON) { + result ++= "while True" + position = token.endPos + } else + if (n != null) { + position = n.pos + var bracketLevel: Int = 0 + while (lexer.hasNext && !(bracketLevel == 0 && lexer.head.tokenType == TokenType.COLON)) + lexer.next.tokenType match { + case TokenType.LEFT_BRACE | TokenType.LEFT_BRACKET | TokenType.LEFT_PARENS => + bracketLevel += 1 + case TokenType.RIGHT_BRACE | TokenType.RIGHT_BRACKET | TokenType.RIGHT_PARENS => + bracketLevel -= 1 + case _ => + } + val hd = lexer.head + if (hd != null) { + val argument = code.substring(position, hd.pos) + result ++= REPEAT_STRING(pythonVersion).format(argument) + position = hd.pos + } + } else + position = token.endPos + case _ => + } + if (position > 0) { + result ++= code.substring(position) + Some(result.toString) + } else + None + } +} diff --git a/src/main/scala/tigerjython/jython/Builtins.java b/src/main/scala/tigerjython/jython/Builtins.java index 704e904..dff4b81 100644 --- a/src/main/scala/tigerjython/jython/Builtins.java +++ b/src/main/scala/tigerjython/jython/Builtins.java @@ -51,4 +51,12 @@ public static PyObject msgDlg(PyObject message) { JOptionPane.showMessageDialog(null, message); return Py.None; } + + public static java.awt.Color makeColor(String value) { + javafx.scene.paint.Color color = javafx.scene.paint.Color.valueOf(value); + int r = (int)Math.round(color.getRed() * 255); + int g = (int)Math.round(color.getGreen() * 255); + int b = (int)Math.round(color.getBlue() * 255); + return new java.awt.Color(r, g, b); + } } diff --git a/src/main/scala/tigerjython/jython/JythonBuiltins.scala b/src/main/scala/tigerjython/jython/JythonBuiltins.scala index 56387ac..ef86a00 100644 --- a/src/main/scala/tigerjython/jython/JythonBuiltins.scala +++ b/src/main/scala/tigerjython/jython/JythonBuiltins.scala @@ -22,7 +22,7 @@ object JythonBuiltins { * This is the list of all names of methods to be redefined. */ val builtinNames: Array[String] = Array( - "input", "msgDlg", "raw_input" + "input", "msgDlg", "raw_input", "makeColor" ) /** diff --git a/src/main/scala/tigerjython/ui/editor/EditorTab.scala b/src/main/scala/tigerjython/ui/editor/EditorTab.scala index c98a36d..2b42601 100644 --- a/src/main/scala/tigerjython/ui/editor/EditorTab.scala +++ b/src/main/scala/tigerjython/ui/editor/EditorTab.scala @@ -20,7 +20,7 @@ import javafx.stage.Popup import org.fxmisc.flowless.VirtualizedScrollPane import org.fxmisc.richtext.CodeArea import tigerjython.errorhandling._ -import tigerjython.execute.PythonExecutor +import tigerjython.execute.{PythonCodeTranslator, PythonExecutor} import tigerjython.plugins.EventManager import tigerjython.ui.{TabFrame, TigerJythonApplication, ZoomMixin} @@ -177,6 +177,8 @@ abstract class EditorTab extends TabFrame { def getFile: java.io.File = file + def getDefaultFileSuffix: String = ".py" + def getSelectedText: String = editor.getSelectedText @@ -185,19 +187,15 @@ abstract class EditorTab extends TabFrame { def hasExecutableFile: Boolean = if (file != null) { - save() _execFile = file + saveExecutable() true - } else - if (!isEmpty) { + } else if (!isEmpty) { if (_execFile == null) { - _execFile = java.io.File.createTempFile(caption.getValue, ".py") + _execFile = java.io.File.createTempFile(caption.getValue + " (", ")" + getDefaultFileSuffix) _execFile.deleteOnExit() } - val writer = new FileWriter(_execFile) - val printer = new PrintWriter(writer) - printer.print(editor.getText()) - printer.close() + saveExecutable() true } else false @@ -233,33 +231,33 @@ abstract class EditorTab extends TabFrame { } def run(): Unit = { - if (hasExecutableFile) { - outputPane.clear() - errorPane.clear() - infoPane.getSelectionModel.select(0) - EventManager.fireOnRun() + if (isRunning) + stop() + outputPane.clear() + errorPane.clear() + infoPane.getSelectionModel.select(0) + EventManager.fireOnRun() - // Check syntax - onFX(() => { - StaticErrorChecker.checkSyntax(caption.get(), editor.getText()) match { - case Some((line, offs, msg)) => - displayError(line, offs, msg) - case None => - Platform.runLater(() => _run()) - } - }) - } else - new Alert(Alert.AlertType.ERROR, "Please save the file first!", ButtonType.OK).showAndWait() + // Check syntax + onFX(() => { + StaticErrorChecker.checkSyntax(caption.get(), editor.getText()) match { + case Some((line, offs, msg)) => + displayError(line, offs, msg) + case None => + Platform.runLater(() => _run()) + } + }) } - protected def _run(): Unit = { - // Execute the code - val executor = PythonExecutor(this) - if (executor != null) - executor.run() - else - new Alert(Alert.AlertType.ERROR, "Please choose an appropriate interpreter", ButtonType.OK).showAndWait() - } + protected def _run(): Unit = + if (hasExecutableFile) { + // Execute the code + val executor = PythonExecutor(this) + if (executor != null) + executor.run() + else + new Alert(Alert.AlertType.ERROR, "Please choose an appropriate interpreter", ButtonType.OK).showAndWait() + } def save(): Unit = if (file != null) synchronized { @@ -269,6 +267,23 @@ abstract class EditorTab extends TabFrame { printer.close() } + protected def saveExecutable(): Unit = { + val editorText = editor.getText() + val text = + PythonCodeTranslator.translate(editorText) match { + case Some(text) => + text + case None => + editorText + } + synchronized { + val writer = new FileWriter(_execFile) + val printer = new PrintWriter(writer) + printer.print(text) + printer.close() + } + } + def setFile(file: java.io.File): Unit = { this.file = file if (file != null) { @@ -299,5 +314,6 @@ abstract class EditorTab extends TabFrame { handleError(errorText) }) EventManager.fireOnStopped() + save() } } diff --git a/src/main/scala/tigerjython/ui/preferences/PythonPreferencesPane.scala b/src/main/scala/tigerjython/ui/preferences/PythonPreferencesPane.scala index f41664c..1f8e714 100644 --- a/src/main/scala/tigerjython/ui/preferences/PythonPreferencesPane.scala +++ b/src/main/scala/tigerjython/ui/preferences/PythonPreferencesPane.scala @@ -77,13 +77,23 @@ class PythonPreferencesPane extends PreferencePane { checkErrors.selectedProperty().bindBidirectional(Preferences.checkSyntax) strictErrorChecking.setSelected(Preferences.syntaxCheckIsStrict.get) strictErrorChecking.selectedProperty().bindBidirectional(Preferences.syntaxCheckIsStrict) + strictErrorChecking.disableProperty().bind(checkErrors.selectedProperty().not()) Seq(checkErrors, strictErrorChecking) } + protected def createExtOptions(): Seq[Node] = { + val repeatLoop = new CheckBox("Repeat loop") + UIString("prefs.jython.repeatloop") += repeatLoop.textProperty() + repeatLoop.setSelected(Preferences.repeatLoop.get) + repeatLoop.selectedProperty().bindBidirectional(Preferences.repeatLoop) + Seq(repeatLoop) + } + override lazy val node: Node = { val result = new VBox() result.getChildren.addAll(createInstallationChooser(): _*) result.getChildren.addAll(createErrorCheckOptions(): _*) + result.getChildren.addAll(createExtOptions(): _*) new StackPane(result) } } diff --git a/src/main/scala/tigerjython/utils/OSProcess.scala b/src/main/scala/tigerjython/utils/OSProcess.scala index c50bbce..68cacb6 100644 --- a/src/main/scala/tigerjython/utils/OSProcess.scala +++ b/src/main/scala/tigerjython/utils/OSProcess.scala @@ -99,8 +99,8 @@ class OSProcess(val cmd: String) { def abort(): Unit = if (process != null) synchronized { process.destroy() - _execTime = System.currentTimeMillis() - _execTime - _completed(-1) + //_execTime = System.currentTimeMillis() - _execTime + //_completed(-1) } private def _completed(result: Int): Unit = {