From 3fe29f8aad87b70bf428a5aecbe187a7f7ef0dd9 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sat, 1 Oct 2022 09:04:16 -0700 Subject: [PATCH 01/18] Add a comptime interpreter The primary reason for adding this is to resolve import comptime function calls, but it will also allow the user to see the result of arbitrary comptime expressions. --- .../serenityos/jakt/comptime/Interpreter.kt | 251 ++++++++++++++++++ .../org/serenityos/jakt/comptime/Value.kt | 106 ++++++++ .../org/serenityos/jakt/comptime/builtins.kt | 68 +++++ .../serenityos/jakt/comptime/extensions.kt | 20 ++ .../serenityos/jakt/psi/caching/JaktCache.kt | 2 + src/main/resources/META-INF/plugin.xml | 1 + 6 files changed, 448 insertions(+) create mode 100644 src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt create mode 100644 src/main/kotlin/org/serenityos/jakt/comptime/Value.kt create mode 100644 src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt create mode 100644 src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt new file mode 100644 index 00000000..cb0b7adc --- /dev/null +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -0,0 +1,251 @@ +package org.serenityos.jakt.comptime + +import com.intellij.openapi.util.Ref +import org.serenityos.jakt.JaktFile +import org.serenityos.jakt.psi.JaktPsiElement +import org.serenityos.jakt.psi.JaktScope +import org.serenityos.jakt.psi.ancestors +import org.serenityos.jakt.psi.api.* +import org.serenityos.jakt.psi.caching.comptimeCache + +class Interpreter(element: JaktPsiElement) { + var scope: Scope + + init { + val outerScopes = element.ancestors().filter { + it is JaktFile || it is JaktFunction || it is JaktStructDeclaration || it is JaktBlock + }.map { + val scope = Scope(null) + if (it is JaktScope) { + for (decl in it.getDeclarations()) + scope[decl.name ?: continue] = evaluate(decl)!! + } + if (it is JaktFile) + initializeGlobalScope(scope) + scope + }.toList() + + for ((index, scope) in outerScopes.withIndex()) { + if (index == 0) + continue + scope.outer = outerScopes[index - 1] + } + + scope = outerScopes.last() + } + + fun pushScope(scope: Scope) { + this.scope = scope + } + + fun popScope() { + scope = scope.outer!! + } + + // A return value of null indicates the expression is not comptime. An exception being + // thrown indicates the expression is comptime, but it malformed in some way and cannot + // be evaluated. + fun evaluate(element: JaktPsiElement): Value? { + val cache = element.comptimeCache() + cache.resolve>(element)?.let { return it.get() } + + try { + val value = evaluateImpl(element) + cache.set(element, Ref(value)) + return value + } catch (e: Throwable) { + cache.set(element, Ref(null)) + throw e + } + } + + private fun evaluateImpl(element: JaktPsiElement): Value? { + return when (element) { + is JaktBooleanLiteral -> BoolValue(element.trueKeyword != null) + is JaktNumericLiteral -> { + element.binaryLiteral?.let { + return IntegerValue(it.text.toLong(2)) + } + + element.octalLiteral?.let { + return IntegerValue(it.text.toLong(8)) + } + + element.hexLiteral?.let { + return IntegerValue(it.text.toLong(16)) + } + + val decimalText = element.decimalLiteral!!.text + return if ("." in decimalText) { + FloatValue(decimalText.toDouble()) + } else IntegerValue(decimalText.toLong(10)) + } + is JaktLiteral -> { + element.byteCharLiteral?.let { + return ByteCharValue(it.text[2].code.toByte()) + } + + element.charLiteral?.let { + return CharValue(it.text.single()) + } + + return StringValue(element.stringLiteral!!.text.drop(1).dropLast(1)) + } + is JaktPlainQualifierExpression -> { + val qualifier = element.plainQualifier + + val parts = generateSequence(qualifier) { it.plainQualifier } + .map { it.identifier.text } + .toMutableList() + .asReversed() + + var value: Value? = scope[parts[0]] + parts.removeFirst() + + for (part in parts) + value = (value as StructValue)[part] + + value!! + } + is JaktTupleExpression -> TupleValue(element.expressionList.map { evaluate(it)!! }) + is JaktArrayExpression -> { + element.elementsArrayBody?.let { body -> + return ArrayValue(body.expressionList.map { evaluate(it)!! }) + } + + val body = element.sizedArrayBody!! + val value = evaluate(body.expressionList[0])!! + val size = evaluate(body.expressionList[1])!! + check(size is IntegerValue) + ArrayValue((0 until size.value).map { value }) + } + is JaktSetExpression -> SetValue(element.expressionList.map { evaluate(it)!! }.toSet()) + is JaktDictionaryExpression -> DictionaryValue( + element.dictionaryElementList.associate { + evaluate(it.expressionList[0])!! to evaluate(it.expressionList[1])!! + } + ) + is JaktFunction -> { + val parameters = element.parameterList.parameterList.map { param -> + FunctionValue.Parameter( + param.identifier.text, + param.expression?.let { evaluate(it)!! } + ) + } + + UserFunctionValue(parameters, element.block ?: element.expression!!) + } + is JaktCallExpression -> { + val target = evaluate(element.expression)!! + check(target is FunctionValue) + + val args = element.argumentList.argumentList.map { evaluate(it.expression)!! } + check(args.size in target.validParamCount) + + target.call(this, getThisValue(element.expression), args) + } + is JaktVariableDeclarationStatement -> { + if (element.parenOpen != null) + TODO() + + val rhs = evaluate(element.expression)!! + val name = element.variableDeclList[0].name!! + scope[name] = rhs + VoidValue + } + is JaktBlock -> { + pushScope(Scope(scope)) + element.statementList.forEach(::evaluate) + popScope() + VoidValue + } + is JaktIfStatement -> { + val canBeExpr = element.canBeExpr + val condition = evaluate(element.expression)!! + check(condition is BoolValue) + + if (condition.value) { + evaluate(element.block).let { + if (canBeExpr) it else VoidValue + } + } else { + element.ifStatement?.let(::evaluate) + ?: element.elseBlock?.let(::evaluate) + ?: VoidValue + } + } + is JaktAccessExpression -> { + val target = evaluate(element.expression)!! + if (element.dotQuestionMark != null) + TODO() + if (element.decimalLiteral != null) { + (target as TupleValue).values[element.decimalLiteral!!.text.toInt()] + } else { + (target as StructValue)[element.identifier!!.text] + } + } + is JaktReturnStatement -> throw ReturnException(element.expression?.let { evaluate(it)!! }) + is JaktExpressionStatement -> { + evaluate(element.expression) + VoidValue + } + + else -> TODO("${element::class.java} it not handled by the Interpreter") + } + } + + private fun getThisValue(expression: JaktExpression): Value? { + return when (expression) { + is JaktAccessExpression -> evaluate(expression.expression)!! + is JaktFieldAccessExpression -> (scope as FunctionScope).thisBinding!! + is JaktIndexedAccessExpression -> evaluate(expression.expressionList.first())!! + else -> null + } + } + + private fun initializeGlobalScope(scope: Scope) { + scope.initialize("StringBuilder", StringBuilderStruct) + } + + open class Scope(var outer: Scope?) { + protected val bindings = mutableMapOf() + + operator fun contains(name: String) = name in bindings + + operator fun get(name: String): Value? = bindings[name] ?: outer?.get(name) + + operator fun set(name: String, value: Value) { + var scope: Scope? = this + + while (scope != null) { + if (name in scope) { + scope[name] = value + return + } + scope = scope.outer + } + + initialize(name, value) + } + + fun initialize(name: String, value: Value) { + bindings[name] = value + } + } + + class FunctionScope(outer: Scope?, val thisBinding: Value?) : Scope(outer) { + fun argument(name: String) = bindings[name]!! + } + + abstract class FlowException : Error() + + class ReturnException(val value: Value?) : FlowException() + + class YieldException(val value: Value?) : FlowException() + + companion object { + fun evaluate(element: JaktPsiElement): Value? { + return Interpreter(element).evaluate(element) + } + } +} diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt new file mode 100644 index 00000000..30374402 --- /dev/null +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt @@ -0,0 +1,106 @@ +package org.serenityos.jakt.comptime + +import org.serenityos.jakt.psi.JaktPsiElement +import org.serenityos.jakt.psi.api.JaktExpression + +// As with everything else in the plugin, we are very lenient when it comes to types. +// All integers are treated as i64, just to make my life easier. Similarly, all float +// types are treated as f64. If we produce a value for something that doesn't actually +// compile, we'll get an IDE error for it anyways +sealed interface Value + +object VoidValue : Value { + override fun toString() = "void" +} + +data class BoolValue(val value: Boolean) : Value { + override fun toString() = value.toString() +} + +data class IntegerValue(val value: Long) : Value { + override fun toString() = value.toString() +} + +data class FloatValue(val value: Double) : Value { + override fun toString() = value.toString() +} + +data class CharValue(val value: Char) : Value { + override fun toString() = "'$value'" +} + +data class ByteCharValue(val value: Byte) : Value { + override fun toString() = "b'$value'" +} + +data class StringValue(val value: String) : Value { + override fun toString() = "\"$value\"" +} + +data class TupleValue(val values: List) : Value { + override fun toString() = values.joinToString(prefix = "(", postfix = ")") +} + +data class ArrayValue(val values: List) : Value { + override fun toString() = values.joinToString(prefix = "[", postfix = "]") +} + +data class SetValue(val values: Set) : Value { + override fun toString() = values.joinToString(prefix = "{", postfix = "}") +} + +data class DictionaryValue(val elements: Map) : Value { + override fun toString() = elements.entries.joinToString(prefix = "{", postfix = "}") { + "${it.key}: ${it.value}" + } +} + +open class StructValue : Value { + protected val fieldsBacker = mutableMapOf() + + val fields: Map + get() = fieldsBacker + + open operator fun get(name: String) = fields[name] +} + +abstract class FunctionValue(val parameters: List) : Value { + val validParamCount: IntRange + get() = parameters.count { it.default != null }..parameters.size + + abstract fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value + + data class Parameter(val name: String, val default: Value? = null) +} + +class UserFunctionValue( + parameters: List, + val body: JaktPsiElement /* JaktBlock | JaktExpression */, // TODO: Storing PSI is bad, right? +) : FunctionValue(parameters) { + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value { + val newScope = Interpreter.FunctionScope(interpreter.scope, thisValue) + + for ((index, param) in parameters.withIndex()) { + if (index <= arguments.lastIndex) { + newScope.initialize(param.name, arguments[index]) + } else { + check(param.default != null) + newScope.initialize(param.name, param.default) + } + } + + interpreter.pushScope(newScope) + + return try { + interpreter.evaluate(body).let { + if (body is JaktExpression) it!! else VoidValue + } + } catch (e: Interpreter.ReturnException) { + e.value ?: VoidValue + } catch (e: Interpreter.FlowException) { + error("Unexpected FlowException in UserFunction: ${e::class.simpleName}") + } finally { + interpreter.popScope() + } + } +} diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt new file mode 100644 index 00000000..3724cf12 --- /dev/null +++ b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt @@ -0,0 +1,68 @@ +package org.serenityos.jakt.comptime + +class BuiltinFunction( + parameters: List, + private val func: (Value?, List) -> Value, +) : FunctionValue(parameters.toList()) { + constructor(vararg parameters: Parameter, func: (Value?, List) -> Value) : + this(parameters.toList(), func) + + constructor(vararg parameterNames: String, func: (Value?, List) -> Value) : + this(parameterNames.map { Parameter(it) }, func) + + constructor(func: (Value?, List) -> Value) : this(emptyList(), func) + + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value { + return func(thisValue, arguments) + } +} + +object StringBuilderStruct : StructValue() { + private val create = BuiltinFunction { thisValue, arguments -> + require(thisValue == null) + require(arguments.isEmpty()) + StringBuilderInstance() + } + + init { + fieldsBacker["create"] = create + } +} + +class StringBuilderInstance : StructValue() { + val builder = StringBuilder() + + init { + fieldsBacker["append"] = append + fieldsBacker["append_string"] = appendString + fieldsBacker["to_string"] = toString + } + + companion object { + private val append = BuiltinFunction("b") { thisValue, arguments -> + require(thisValue is StringBuilderInstance) + require(arguments.size == 1) + val arg = arguments[0] + require(arg is ByteCharValue) + + thisValue.builder.appendCodePoint(arg.value.toInt()) + VoidValue + } + + private val appendString = BuiltinFunction("s") { thisValue, arguments -> + require(thisValue is StringBuilderInstance) + require(arguments.size == 1) + val arg = arguments[0] + require(arg is StringValue) + + thisValue.builder.append(arg.value) + VoidValue + } + + private val toString = BuiltinFunction { thisValue, arguments -> + require(thisValue is StringBuilderInstance) + require(arguments.isEmpty()) + StringValue(thisValue.builder.toString()) + } + } +} diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt new file mode 100644 index 00000000..2026f9b6 --- /dev/null +++ b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt @@ -0,0 +1,20 @@ +package org.serenityos.jakt.comptime + +import com.intellij.psi.util.childrenOfType +import org.serenityos.jakt.psi.JaktPsiElement +import org.serenityos.jakt.psi.ancestorsOfType +import org.serenityos.jakt.psi.api.JaktBlock +import org.serenityos.jakt.psi.api.JaktCallExpression +import org.serenityos.jakt.psi.api.JaktFunction +import org.serenityos.jakt.psi.api.JaktIfStatement +import org.serenityos.jakt.psi.findChildOfType + +// Utility accessors +val JaktIfStatement.ifStatement: JaktIfStatement? + get() = findChildOfType() + +val JaktIfStatement.elseBlock: JaktBlock? + get() = childrenOfType().getOrNull(1) + +val JaktIfStatement.canBeExpr: Boolean + get() = elseKeyword != null && (ifStatement != null || elseBlock != null) diff --git a/src/main/kotlin/org/serenityos/jakt/psi/caching/JaktCache.kt b/src/main/kotlin/org/serenityos/jakt/psi/caching/JaktCache.kt index 26012763..eff3fce5 100644 --- a/src/main/kotlin/org/serenityos/jakt/psi/caching/JaktCache.kt +++ b/src/main/kotlin/org/serenityos/jakt/psi/caching/JaktCache.kt @@ -55,6 +55,8 @@ abstract class JaktCache(project: Project) : Disposable { class JaktResolveCache(project: Project) : JaktCache(project) class JaktTypeCache(project: Project) : JaktCache(project) +class JaktComptimeCache(project: Project) : JaktCache(project) fun PsiElement.resolveCache() = project.service() fun PsiElement.typeCache() = project.service() +fun PsiElement.comptimeCache() = project.service() diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index f6d0f259..8f8bc8f0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -170,6 +170,7 @@ + Date: Sat, 1 Oct 2022 09:57:09 -0700 Subject: [PATCH 02/18] Grammar: Support importing files based on comptime function calls --- .../jakt/annotations/BasicAnnotator.kt | 10 +++--- .../serenityos/jakt/comptime/Interpreter.kt | 9 +++-- .../serenityos/jakt/comptime/extensions.kt | 14 ++++++-- .../ImportNSDeclarationIntention.kt | 10 +++--- .../jakt/psi/declaration/JaktImportMixin.kt | 33 +++++++++++++++---- src/main/resources/grammar/Jakt.bnf | 3 +- 6 files changed, 57 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/annotations/BasicAnnotator.kt b/src/main/kotlin/org/serenityos/jakt/annotations/BasicAnnotator.kt index 8f41a283..33b75464 100644 --- a/src/main/kotlin/org/serenityos/jakt/annotations/BasicAnnotator.kt +++ b/src/main/kotlin/org/serenityos/jakt/annotations/BasicAnnotator.kt @@ -11,7 +11,7 @@ import com.intellij.refactoring.suggested.startOffset import org.serenityos.jakt.JaktTypes import org.serenityos.jakt.psi.ancestorOfType import org.serenityos.jakt.psi.api.* -import org.serenityos.jakt.psi.findChildrenOfType +import org.serenityos.jakt.psi.findChildOfType import org.serenityos.jakt.psi.reference.JaktPlainQualifierMixin import org.serenityos.jakt.psi.reference.exprAncestor import org.serenityos.jakt.psi.reference.hasNamespace @@ -61,14 +61,14 @@ object BasicAnnotator : JaktAnnotator(), DumbAware { is JaktImportBraceEntry -> element.identifier.highlight(Highlights.IMPORT_ENTRY) is JaktExternImport -> element.cSpecifier?.highlight(Highlights.KEYWORD_DECLARATION) is JaktImport -> { - val idents = element.findChildrenOfType(JaktTypes.IDENTIFIER) - idents.first().highlight(Highlights.IMPORT_MODULE) + element.importTarget.identifier?.highlight(Highlights.IMPORT_MODULE) - if (idents.size > 1) { + + if (element.keywordAs != null) { + element.findChildOfType(JaktTypes.IDENTIFIER)?.highlight(Highlights.IMPORT_ALIAS) // The 'as' keyword will be highlighted as an operator here without // the annotation element.keywordAs!!.highlight(Highlights.KEYWORD_IMPORT) - idents[1].highlight(Highlights.IMPORT_ALIAS) } } is JaktArgument -> { diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index cb0b7adc..dfa9853b 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -17,8 +17,13 @@ class Interpreter(element: JaktPsiElement) { }.map { val scope = Scope(null) if (it is JaktScope) { - for (decl in it.getDeclarations()) - scope[decl.name ?: continue] = evaluate(decl)!! + for (decl in it.getDeclarations()) { + try { + scope[decl.name ?: continue] = evaluate(decl)!! + } catch (e: Throwable) { + // Ignore and attempt to continue execution + } + } } if (it is JaktFile) initializeGlobalScope(scope) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt index 2026f9b6..43c9fce0 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt @@ -1,14 +1,22 @@ package org.serenityos.jakt.comptime +import com.intellij.openapi.util.Ref import com.intellij.psi.util.childrenOfType import org.serenityos.jakt.psi.JaktPsiElement -import org.serenityos.jakt.psi.ancestorsOfType import org.serenityos.jakt.psi.api.JaktBlock -import org.serenityos.jakt.psi.api.JaktCallExpression -import org.serenityos.jakt.psi.api.JaktFunction import org.serenityos.jakt.psi.api.JaktIfStatement +import org.serenityos.jakt.psi.caching.comptimeCache import org.serenityos.jakt.psi.findChildOfType +val JaktPsiElement.comptimeValue: Value? + get() = comptimeCache().resolveWithCaching(this) { + try { + Interpreter.evaluate(this) + } catch (e: Throwable) { + null + }.let(::Ref) + }.get() + // Utility accessors val JaktIfStatement.ifStatement: JaktIfStatement? get() = findChildOfType() diff --git a/src/main/kotlin/org/serenityos/jakt/intentions/ImportNSDeclarationIntention.kt b/src/main/kotlin/org/serenityos/jakt/intentions/ImportNSDeclarationIntention.kt index 7709dabf..bd68aba9 100644 --- a/src/main/kotlin/org/serenityos/jakt/intentions/ImportNSDeclarationIntention.kt +++ b/src/main/kotlin/org/serenityos/jakt/intentions/ImportNSDeclarationIntention.kt @@ -8,8 +8,8 @@ import org.serenityos.jakt.psi.JaktPsiFactory import org.serenityos.jakt.psi.ancestorOfType import org.serenityos.jakt.psi.api.JaktImport import org.serenityos.jakt.psi.api.JaktPlainQualifier -import org.serenityos.jakt.psi.declaration.aliasIdent -import org.serenityos.jakt.psi.declaration.nameIdent +import org.serenityos.jakt.psi.declaration.aliasString +import org.serenityos.jakt.psi.declaration.targetString import org.serenityos.jakt.psi.reference.index class ImportNSDeclarationIntention : JaktIntention("Add import for member") { @@ -27,7 +27,7 @@ class ImportNSDeclarationIntention : JaktIntention() - .find { it.nameIdent.text == resolvedFile.name.substringBefore(".jakt") } + .find { it.targetString == resolvedFile.name.substringBefore(".jakt") } ?: return null description = "Add import for \"${qualifier.text}\"" @@ -51,9 +51,9 @@ class ImportNSDeclarationIntention : JaktIntention, JaktIm override fun getReference() = singleRef { resolveFile() } } -val JaktImport.nameIdent: PsiElement - get() = originalElement.findChildrenOfType(JaktTypes.IDENTIFIER).first() +// Note that JaktImport doesn't have a type, and thus doesn't use the typeCache normally. So we +// can repurpose it to save this string +val JaktImport.targetString: String + get() = typeCache().resolveWithCaching(this) { + importTarget.identifier?.let { return@resolveWithCaching it.text } -val JaktImport.aliasIdent: PsiElement? - get() = originalElement.findChildrenOfType(JaktTypes.IDENTIFIER).getOrNull(1) + when (val comptimeValue = importTarget.callExpression?.comptimeValue) { + is StringValue -> comptimeValue.value + is ArrayValue -> { + val project = jaktProject + val containingFile = containingFile.originalFile.virtualFile + for (value in comptimeValue.values) { + if (value is StringValue && project.resolveImportedFile(containingFile, value.value) != null) + return@resolveWithCaching value.value + } + "__UNKNOWN" + } + else -> "__UNKNOWN" + } + } + +val JaktImport.aliasString: String? + get() = originalElement.findChildrenOfType(JaktTypes.IDENTIFIER).firstOrNull()?.text fun JaktImport.resolveFile(): JaktFile? = - jaktProject.resolveImportedFile(containingFile.originalFile.virtualFile, nameIdent.text) + jaktProject.resolveImportedFile(containingFile.originalFile.virtualFile, targetString) diff --git a/src/main/resources/grammar/Jakt.bnf b/src/main/resources/grammar/Jakt.bnf index fc9679c9..567e875d 100644 --- a/src/main/resources/grammar/Jakt.bnf +++ b/src/main/resources/grammar/Jakt.bnf @@ -102,12 +102,13 @@ upper NamespaceDeclaration ::= NAMESPACE_KEYWORD IDENTIFIER NamespaceBody { NamespaceBody ::= CURLY_OPEN NL TopLevelDefinitionList? NL CURLY_CLOSE // Import Statement -upper Import ::= IMPORT_KEYWORD !EXTERN_KEYWORD IDENTIFIER ImportAs? ImportBraceList? { +upper Import ::= IMPORT_KEYWORD !EXTERN_KEYWORD ImportTarget ImportAs? ImportBraceList? { implements="org.serenityos.jakt.psi.declaration.JaktDeclaration" mixin="org.serenityos.jakt.psi.declaration.JaktImportMixin" stubClass="org.serenityos.jakt.stubs.JaktImportStub" elementTypeFactory="org.serenityos.jakt.stubs.JaktStubFactoryKt.jaktStubFactory" } +ImportTarget ::= IDENTIFIER !PAREN_OPEN | CallExpression private ImportAs ::= KEYWORD_AS IDENTIFIER ImportBraceList ::= CURLY_OPEN NL <>? NL CURLY_CLOSE ImportBraceEntry ::= IDENTIFIER { From fde7e5c48e83cf3b3a25b41ade31eefb174cec5d Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sat, 1 Oct 2022 17:22:40 -0700 Subject: [PATCH 03/18] Interpreter: Support most of the prelude structs/methods --- .../serenityos/jakt/comptime/Interpreter.kt | 47 +- .../org/serenityos/jakt/comptime/Value.kt | 61 +- .../org/serenityos/jakt/comptime/builtins.kt | 640 +++++++++++++++++- .../jakt/project/JaktProjectListener.kt | 69 ++ .../serenityos/jakt/psi/caching/JaktCache.kt | 11 + 5 files changed, 755 insertions(+), 73 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index dfa9853b..09aa0e98 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -108,28 +108,50 @@ class Interpreter(element: JaktPsiElement) { parts.removeFirst() for (part in parts) - value = (value as StructValue)[part] + value = value!![part] value!! } is JaktTupleExpression -> TupleValue(element.expressionList.map { evaluate(it)!! }) is JaktArrayExpression -> { element.elementsArrayBody?.let { body -> - return ArrayValue(body.expressionList.map { evaluate(it)!! }) + return ArrayValue(body.expressionList.map { evaluate(it)!! }.toMutableList()) } val body = element.sizedArrayBody!! val value = evaluate(body.expressionList[0])!! val size = evaluate(body.expressionList[1])!! check(size is IntegerValue) - ArrayValue((0 until size.value).map { value }) + ArrayValue((0 until size.value).map { value }.toMutableList()) } - is JaktSetExpression -> SetValue(element.expressionList.map { evaluate(it)!! }.toSet()) + is JaktSetExpression -> SetValue(element.expressionList.map { evaluate(it)!! }.toMutableSet()) is JaktDictionaryExpression -> DictionaryValue( element.dictionaryElementList.associate { evaluate(it.expressionList[0])!! to evaluate(it.expressionList[1])!! - } + }.toMutableMap() ) + is JaktRangeExpression -> { + val start = evaluate(element.expressionList[0])!! as IntegerValue + val end = evaluate(element.expressionList[1])!! as IntegerValue + RangeValue(start.value, end.value, isInclusive = false) + } + is JaktIndexedAccessExpression -> { + val target = evaluate(element.expressionList[0])!! + val value = evaluate(element.expressionList[1])!! + if (target is ArrayValue && value is RangeValue) { + ArraySlice(target, value.range) + } else target[(value as StringValue).value] + } + is JaktAccessExpression -> { + val target = evaluate(element.expression)!! + if (element.dotQuestionMark != null) + TODO() + if (element.decimalLiteral != null) { + (target as TupleValue).values[element.decimalLiteral!!.text.toInt()] + } else { + target[element.identifier!!.text] + } + } is JaktFunction -> { val parameters = element.parameterList.parameterList.map { param -> FunctionValue.Parameter( @@ -179,16 +201,6 @@ class Interpreter(element: JaktPsiElement) { ?: VoidValue } } - is JaktAccessExpression -> { - val target = evaluate(element.expression)!! - if (element.dotQuestionMark != null) - TODO() - if (element.decimalLiteral != null) { - (target as TupleValue).values[element.decimalLiteral!!.text.toInt()] - } else { - (target as StructValue)[element.identifier!!.text] - } - } is JaktReturnStatement -> throw ReturnException(element.expression?.let { evaluate(it)!! }) is JaktExpressionStatement -> { evaluate(element.expression) @@ -209,7 +221,12 @@ class Interpreter(element: JaktPsiElement) { } private fun initializeGlobalScope(scope: Scope) { + scope.initialize("String", StringStruct) scope.initialize("StringBuilder", StringBuilderStruct) + scope.initialize("Error", ErrorStruct) + scope.initialize("File", FileStruct) + scope.initialize("___jakt_get_target_triple_string", jaktGetTargetTripleStringFunction) + scope.initialize("abort", abortFunction) } open class Scope(var outer: Scope?) { diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt index 30374402..8a0d7a42 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt @@ -7,66 +7,55 @@ import org.serenityos.jakt.psi.api.JaktExpression // All integers are treated as i64, just to make my life easier. Similarly, all float // types are treated as f64. If we produce a value for something that doesn't actually // compile, we'll get an IDE error for it anyways -sealed interface Value +sealed class Value { + private val fields = mutableMapOf() -object VoidValue : Value { + open operator fun contains(name: String) = name in fields + + open operator fun get(name: String) = fields[name] + + open operator fun set(name: String, value: Value) { + fields[name] = value + } +} + +object VoidValue : Value() { override fun toString() = "void" } -data class BoolValue(val value: Boolean) : Value { +data class BoolValue(val value: Boolean) : Value() { override fun toString() = value.toString() } -data class IntegerValue(val value: Long) : Value { +data class IntegerValue(val value: Long) : Value() { override fun toString() = value.toString() } -data class FloatValue(val value: Double) : Value { +data class FloatValue(val value: Double) : Value() { override fun toString() = value.toString() } -data class CharValue(val value: Char) : Value { +data class CharValue(val value: Char) : Value() { override fun toString() = "'$value'" } -data class ByteCharValue(val value: Byte) : Value { +data class ByteCharValue(val value: Byte) : Value() { override fun toString() = "b'$value'" } -data class StringValue(val value: String) : Value { - override fun toString() = "\"$value\"" -} - -data class TupleValue(val values: List) : Value { +data class TupleValue(val values: List) : Value() { override fun toString() = values.joinToString(prefix = "(", postfix = ")") } -data class ArrayValue(val values: List) : Value { - override fun toString() = values.joinToString(prefix = "[", postfix = "]") -} - -data class SetValue(val values: Set) : Value { - override fun toString() = values.joinToString(prefix = "{", postfix = "}") -} +abstract class FunctionValue(private val minParamCount: Int, private val maxParamCount: Int) : Value() { + val validParamCount: IntRange + get() = minParamCount..maxParamCount -data class DictionaryValue(val elements: Map) : Value { - override fun toString() = elements.entries.joinToString(prefix = "{", postfix = "}") { - "${it.key}: ${it.value}" + init { + require(minParamCount <= maxParamCount) } -} - -open class StructValue : Value { - protected val fieldsBacker = mutableMapOf() - - val fields: Map - get() = fieldsBacker - open operator fun get(name: String) = fields[name] -} - -abstract class FunctionValue(val parameters: List) : Value { - val validParamCount: IntRange - get() = parameters.count { it.default != null }..parameters.size + constructor(parameters: List) : this(parameters.count { it.default == null }, parameters.size) abstract fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value @@ -74,7 +63,7 @@ abstract class FunctionValue(val parameters: List) : Value { } class UserFunctionValue( - parameters: List, + private val parameters: List, val body: JaktPsiElement /* JaktBlock | JaktExpression */, // TODO: Storing PSI is bad, right? ) : FunctionValue(parameters) { override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value { diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt index 3724cf12..988c16b3 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt @@ -1,47 +1,352 @@ package org.serenityos.jakt.comptime +import org.serenityos.jakt.project.JaktProjectListener +import java.io.File + class BuiltinFunction( - parameters: List, + parameterCount: Int, private val func: (Value?, List) -> Value, -) : FunctionValue(parameters.toList()) { - constructor(vararg parameters: Parameter, func: (Value?, List) -> Value) : - this(parameters.toList(), func) +) : FunctionValue(parameterCount, parameterCount) { + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value { + return func(thisValue, arguments) + } +} - constructor(vararg parameterNames: String, func: (Value?, List) -> Value) : - this(parameterNames.map { Parameter(it) }, func) +data class OptionalValue(val value: Value?) : Value() { + init { + this["has_value"] = hasValue + this["value"] = getValue + this["value_or"] = getValueOr + } - constructor(func: (Value?, List) -> Value) : this(emptyList(), func) + override fun toString() = "Optional(${value?.toString() ?: ""})" - override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value { - return func(thisValue, arguments) + companion object { + private val hasValue = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is OptionalValue) + BoolValue(thisValue.value != null) + } + + private val getValue = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is OptionalValue) + thisValue.value!! + } + + private val getValueOr = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is OptionalValue) + thisValue.value ?: arguments[0] + } + } +} + +data class ArrayIterator( + val array: ArrayValue, + private var nextIndex: Int = 0, + private val endInclusiveIndex: Int = array.values.lastIndex, +) : Value() { + init { + this["next"] = next + } + + companion object { + private val next = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArrayIterator) + if (thisValue.nextIndex > thisValue.endInclusiveIndex) { + OptionalValue(null) + } else OptionalValue(thisValue.array.values[thisValue.nextIndex]).also { + thisValue.nextIndex += 1 + } + } + } +} + +data class ArrayValue(val values: MutableList) : Value() { + override fun toString() = values.joinToString(prefix = "[", postfix = "]") + + init { + this["is_empty"] = isEmpty + this["size"] = size + this["contains"] = contains + this["iterator"] = iterator + this["push"] = push + this["pop"] = pop + this["first"] = first + this["last"] = last + } + + companion object { + private val isEmpty = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArrayValue) + BoolValue(thisValue.values.isEmpty()) + } + + private val size = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArrayValue) + IntegerValue(thisValue.values.size.toLong()) + } + + private val contains = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is ArrayValue) + BoolValue(arguments[0] in thisValue.values) + } + + private val iterator = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArrayValue) + ArrayIterator(thisValue) + } + + private val push = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is ArrayValue) + thisValue.values.add(arguments[0]) + VoidValue + } + + private val pop = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArrayValue) + if (thisValue.values.isEmpty()) { + OptionalValue(null) + } else OptionalValue(thisValue.values.removeLast()) + } + + private val first = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArrayValue) + OptionalValue(thisValue.values.firstOrNull()) + } + + private val last = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArrayValue) + OptionalValue(thisValue.values.lastOrNull()) + } + } +} + +data class ArraySlice(val array: ArrayValue, val range: IntRange) : Value() { + init { + this["is_empty"] = isEmpty + this["contains"] = contains + this["size"] = size + this["iterator"] = iterator + this["to_array"] = toArray + this["first"] = first + this["last"] = last + } + + companion object { + private val isEmpty = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArraySlice) + BoolValue(thisValue.range.isEmpty() || thisValue.array.values.slice(thisValue.range).isEmpty()) + } + + private val contains = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is ArraySlice) + BoolValue(arguments[0] in thisValue.array.values.slice(thisValue.range)) + } + + private val size = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArraySlice) + + // Note: this is what Jakt does, not totally sure why + if (thisValue.range.last > thisValue.array.values.size) { + IntegerValue(0) + } else IntegerValue((thisValue.range.last - thisValue.range.first).toLong()) + } + + private val iterator = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArraySlice) + ArrayIterator(thisValue.array, thisValue.range.first, thisValue.range.last) + } + + private val toArray = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArraySlice) + ArrayValue(thisValue.array.values.slice(thisValue.range).toMutableList()) + } + + private val first = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArraySlice) + thisValue.array.values[thisValue.range.first] + } + + private val last = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is ArraySlice) + thisValue.array.values[thisValue.range.last] + } } } -object StringBuilderStruct : StructValue() { - private val create = BuiltinFunction { thisValue, arguments -> +object StringStruct : Value() { + private val repeated = BuiltinFunction(2) { thisValue, arguments -> + require(thisValue == null) + val (char, count) = arguments + require(char is CharValue && count is IntegerValue) + StringValue(buildString { + repeat(count.value.toInt()) { append(char) } + }) + } + + private val number = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue == null) + val arg = arguments[0] + require(arg is IntegerValue) + StringValue(arg.value.toString()) + } + + init { + this["number"] = number + this["repeated"] = repeated + } +} + +data class StringValue(val value: String) : Value() { + init { + this["is_empty"] = isEmpty + this["length"] = length + this["hash"] = hash + this["substring"] = substring + this["to_uint"] = toUInt + this["to_int"] = toInt + this["is_whitespace"] = isWhitespace + this["contains"] = contains + this["replace"] = replace + this["byte_at"] = byteAt + this["split"] = split + this["starts_with"] = startsWith + this["ends_with"] = endsWith + } + + override fun toString() = "\"$value\"" + + companion object { + private val whiteSpace = setOf(' ', '\t', '\n', 0xb.toChar() /* \v */, 0xc.toChar() /* \f */, '\r') + + private val isEmpty = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringValue) + BoolValue(thisValue.value.isEmpty()) + } + + private val length = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringValue) + IntegerValue(thisValue.value.length.toLong()) + } + + private val hash = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringValue) + val string = thisValue.value + + // See runtime/Jakt/StringHash.h + if (string.isEmpty()) + return@BuiltinFunction IntegerValue(0L) + + var hash = string.length + + for (ch in string) { + hash += ch.code + hash += hash shl 10 + hash = hash or (hash shr 6) + } + + hash += hash shl 3 + hash = hash or (hash shr 11) + hash += hash shl 15 + + IntegerValue(hash.toLong()) + } + + private val substring = BuiltinFunction(2) { thisValue, arguments -> + require(thisValue is StringValue) + val (start, end) = arguments + require(start is IntegerValue && end is IntegerValue) + StringValue(thisValue.value.substring(start.value.toInt(), end.value.toInt())) + } + + private val toUInt = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringValue) + IntegerValue(thisValue.value.toLong()) + } + + private val toInt = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringValue) + IntegerValue(thisValue.value.toLong()) + } + + private val isWhitespace = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringValue) + BoolValue(thisValue.value.all { it in whiteSpace }) + } + + private val contains = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is StringValue) + val arg = arguments[0] + require(arg is StringValue) + BoolValue(arg.value in thisValue.value) + } + + private val replace = BuiltinFunction(2) { thisValue, arguments -> + require(thisValue is StringValue) + val (replace, with) = arguments + require(replace is StringValue && with is StringValue) + StringValue(thisValue.value.replace(replace.value, with.value)) + } + + private val byteAt = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is StringValue) + val arg = arguments[0] + require(arg is IntegerValue) + IntegerValue(thisValue.value.toByteArray()[arg.value.toInt()].toLong()) + } + + private val split = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is StringValue) + val arg = arguments[0] + require(arg is CharValue) + ArrayValue(thisValue.value.split(arg.value).map(::StringValue).toMutableList()) + } + + private val startsWith = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is StringValue) + val arg = arguments[0] + require(arg is StringValue) + BoolValue(thisValue.value.startsWith(arg.value)) + } + + private val endsWith = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is StringValue) + val arg = arguments[0] + require(arg is StringValue) + BoolValue(thisValue.value.endsWith(arg.value)) + } + } +} + +object StringBuilderStruct : Value() { + private val create = BuiltinFunction(0) { thisValue, arguments -> require(thisValue == null) require(arguments.isEmpty()) StringBuilderInstance() } init { - fieldsBacker["create"] = create + this["create"] = create } } -class StringBuilderInstance : StructValue() { +class StringBuilderInstance : Value() { val builder = StringBuilder() init { - fieldsBacker["append"] = append - fieldsBacker["append_string"] = appendString - fieldsBacker["to_string"] = toString + this["append"] = append + this["append_string"] = appendString + this["append_code_point"] = appendCodePoint + this["to_string"] = toString + this["is_empty"] = isEmpty + this["length"] = length + this["clear"] = clear } + override fun toString() = "StringBuilder(content = \"$builder\")" + companion object { - private val append = BuiltinFunction("b") { thisValue, arguments -> + private val append = BuiltinFunction(1) { thisValue, arguments -> require(thisValue is StringBuilderInstance) - require(arguments.size == 1) val arg = arguments[0] require(arg is ByteCharValue) @@ -49,9 +354,8 @@ class StringBuilderInstance : StructValue() { VoidValue } - private val appendString = BuiltinFunction("s") { thisValue, arguments -> + private val appendString = BuiltinFunction(1) { thisValue, arguments -> require(thisValue is StringBuilderInstance) - require(arguments.size == 1) val arg = arguments[0] require(arg is StringValue) @@ -59,10 +363,302 @@ class StringBuilderInstance : StructValue() { VoidValue } - private val toString = BuiltinFunction { thisValue, arguments -> + private val appendCodePoint = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is StringBuilderInstance) + val arg = arguments[0] + require(arg is IntegerValue) + + thisValue.builder.appendCodePoint(arg.value.toInt()) + VoidValue + } + + private val toString = BuiltinFunction(0) { thisValue, _ -> require(thisValue is StringBuilderInstance) - require(arguments.isEmpty()) StringValue(thisValue.builder.toString()) } + + private val isEmpty = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringBuilderInstance) + BoolValue(thisValue.builder.isEmpty()) + } + + private val length = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringBuilderInstance) + IntegerValue(thisValue.builder.length.toLong()) + } + + private val clear = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is StringBuilderInstance) + thisValue.builder.clear() + VoidValue + } + } +} + +// TODO: no clue if this is works the way it does in Jakt +data class DictionaryIterator(val dictionary: DictionaryValue) : Value() { + private val remainingKeys = dictionary.elements.keys + + init { + this["next"] = next + } + + companion object { + private val next = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is DictionaryIterator) + val nextKey = thisValue.remainingKeys.random() + thisValue.remainingKeys.remove(nextKey) + TupleValue(listOf(nextKey, thisValue.dictionary.elements[nextKey]!!)) + } + } +} + +data class DictionaryValue(val elements: MutableMap) : Value() { + init { + this["is_empty"] = isEmpty + this["get"] = get + this["contains"] = contains + this["set"] = set + this["remove"] = remove + this["clear"] = clear + this["size"] = size + this["keys"] = keys + this["iterator"] = iterator + } + + override fun toString() = elements.entries.joinToString(prefix = "{", postfix = "}") { + "${it.key}: ${it.value}" + } + + companion object { + private val isEmpty = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is DictionaryValue) + BoolValue(thisValue.elements.isEmpty()) + } + + private val get = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is DictionaryValue) + require(arguments.size == 1) + OptionalValue(thisValue.elements[arguments[0]]) + } + + private val contains = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is DictionaryValue) + BoolValue(arguments[0] in thisValue.elements) + } + + private val set = BuiltinFunction(2) { thisValue, arguments -> + require(thisValue is DictionaryValue) + thisValue.elements[arguments[0]] = arguments[1] + VoidValue + } + + private val remove = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is DictionaryValue) + BoolValue(thisValue.elements.remove(arguments[0]) != null) + } + + private val clear = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is DictionaryValue) + thisValue.elements.clear() + VoidValue + } + + private val size = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is DictionaryValue) + IntegerValue(thisValue.elements.size.toLong()) + } + + // TODO: What is the ordering guarantee for this in Jakt? + private val keys = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is DictionaryValue) + ArrayValue(thisValue.elements.keys.toMutableList()) + } + + private val iterator = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is DictionaryValue) + DictionaryIterator(thisValue) + } + } +} + +data class SetIterator(val values: MutableSet) : Value() { + init { + this["next"] = next + } + + companion object { + private val next = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is SetIterator) + val nextValue = thisValue.values.random() + thisValue.values.remove(nextValue) + nextValue + } + } +} + +data class SetValue(val values: MutableSet) : Value() { + init { + this["is_empty"] = isEmpty + this["contains"] = contains + this["add"] = add + this["remove"] = remove + this["clear"] = clear + this["size"] = size + this["iterator"] = iterator + } + + override fun toString() = values.joinToString(prefix = "{", postfix = "}") + + companion object { + private val isEmpty = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is SetValue) + BoolValue(thisValue.values.isEmpty()) + } + + private val contains = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is SetValue) + BoolValue(arguments[0] in thisValue.values) + } + + private val add = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is SetValue) + thisValue.values.add(arguments[0]) + VoidValue + } + + private val remove = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue is SetValue) + BoolValue(thisValue.values.remove(arguments[0])) + } + + private val clear = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is SetValue) + thisValue.values.clear() + VoidValue + } + + private val size = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is SetValue) + IntegerValue(thisValue.values.size.toLong()) + } + + private val iterator = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is SetValue) + SetIterator(thisValue.values.toMutableSet()) + } + } +} + +data class RangeValue(val start: Long, val end: Long, val isInclusive: Boolean) : Value() { + private var current = start + private val forwards = start <= end + + val range: IntRange + get() = if (isInclusive) start.toInt()..end.toInt() else start.toInt() until end.toInt() + + init { + this["next"] = next + this["inclusive"] = inclusive + this["exclusive"] = exclusive + } + + private fun getAndAdvance(): Long { + return current.also { + if (forwards) current++ else current-- + } + } + + private fun isDone(): Boolean { + return when { + forwards && isInclusive -> current > end + forwards -> current >= end + isInclusive -> current < start + else -> current <= start + } + } + + companion object { + private val next = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is RangeValue) + if (thisValue.isDone()) { + OptionalValue(null) + } else OptionalValue(IntegerValue(thisValue.getAndAdvance())) + } + + private val inclusive = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is RangeValue) + thisValue.copy(isInclusive = true) + } + + private val exclusive = BuiltinFunction(0) { thisValue, _ -> + require(thisValue is RangeValue) + thisValue.copy(isInclusive = false) + } } } + +object ErrorStruct : Value() { + private val fromErrno = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue == null) + val arg = arguments[0] + require(arg is IntegerValue) + ErrorInstance(arg.value) + } + + init { + this["from_errno"] = fromErrno + } +} + +class ErrorInstance(private val codeValue: Long) : Value() { + override fun toString() = "Error(code = $codeValue)" +} + +object FileStruct : Value() { + private val exists = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue == null) + val arg = arguments[0] + require(arg is StringValue) + BoolValue(File(arg.value).exists()) + } + + private val openForReading = BuiltinFunction(1) { thisValue, arguments -> + require(thisValue == null) + val arg = arguments[0] + require(arg is StringValue) + FileInstance(File(arg.value)) + } + + init { + this["exists"] = exists + this["open_for_reading"] = openForReading + } +} + +class FileInstance(val file: File) : Value() { + init { + this["read_all"] = readAll + } + + companion object { + private val readAll = BuiltinFunction(0) { thisValue, arguments -> + require(thisValue is FileInstance) + require(arguments.isEmpty()) + StringValue(thisValue.file.readText()) + } + } +} + +// Free functions + +val jaktGetTargetTripleStringFunction = BuiltinFunction(0) { _, _ -> + JaktProjectListener.targetTriple.get() ?: StringValue("unknown-unknown-unknown-unknown") +} + +val abortFunction = BuiltinFunction(0) { _, _ -> error("aborted") } + +// TODO: saturated/truncated functions when we have generic information + +// TODO: Format functions, not trivial since Kotlin does not support the +// Serenity/Python-style format arguments diff --git a/src/main/kotlin/org/serenityos/jakt/project/JaktProjectListener.kt b/src/main/kotlin/org/serenityos/jakt/project/JaktProjectListener.kt index c304a5f5..9b7a8735 100644 --- a/src/main/kotlin/org/serenityos/jakt/project/JaktProjectListener.kt +++ b/src/main/kotlin/org/serenityos/jakt/project/JaktProjectListener.kt @@ -2,10 +2,79 @@ package org.serenityos.jakt.project import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManagerListener +import org.serenityos.jakt.comptime.StringValue +import java.util.concurrent.atomic.AtomicReference +import kotlin.concurrent.thread class JaktProjectListener : ProjectManagerListener { override fun projectOpened(project: Project) { JaktProjectServiceImpl.copyPreludeFile(project) JaktUpdateNotification.showIfNecessary(project) + + tryDetermineSystemTargetTriple() + } + + private fun tryDetermineSystemTargetTriple() = thread { + if (targetTriple.get() != null) + return@thread + + // First, try to get it from an installed compiler + val commands = setOf( + "clang++ -print-target-tuple", + "clang++14 -print-target-tuple", + "gcc -dumpmachine", + "g++ -dumpmachine", + "cc -dumpmachine", + "c++ -dumpmachine", + ) + + for (command in commands) { + try { + val process = Runtime.getRuntime().exec(command) + process.waitFor() + if (process.exitValue() != 0) + continue + val text = process.inputStream.reader().readText().trim() + if ("-" in text) { + targetTriple.set(StringValue(text)) + return@thread + } + } catch (e: Throwable) { + // ignore + } + } + + // If that doesn't work, then try to pick an appropriate triple from the + // environment information. This tries to model what Jakt does + val name = System.getProperty("os.name")?.lowercase() + + if (name == null) { + // Give up + targetTriple.set(StringValue("unknown-unknown-unknown-unknown")) + return@thread + } + + val is64Bit = + System.getProperty("os.arch")?.contains("64") == true || + System.getProperty("java.vm.name")?.contains("64") == true || + System.getProperty("sun.management.compiler")?.contains("64") == true || + System.getProperty("sun.arch.data.model")?.contains("64") == true + + val tripleGuess = when { + "win" in name -> if (is64Bit) "i686-pc-windows-msvc" else "x86_64-pc-windows-msvc" + "linux" in name -> "x86_64-pc-linux-gnu" + "bsd" in name -> "x86_64-pc-bsd-unknown" + "mac" in name || "darwin" in name -> "x86_64-apple-darwin-unknown" + "unix" in name -> "x86_64-pc-unix-unknown" + else -> "unknown-unknown-unknown-unknown" + } + + targetTriple.set(StringValue(tripleGuess)) + } + + companion object { + // The system triple (ex: "x64_64-pc-linux-gnu"). This is used in comptime + // execution to populate the ___jakt_get_target_triple_string function + val targetTriple = AtomicReference() } } diff --git a/src/main/kotlin/org/serenityos/jakt/psi/caching/JaktCache.kt b/src/main/kotlin/org/serenityos/jakt/psi/caching/JaktCache.kt index eff3fce5..0f737e6e 100644 --- a/src/main/kotlin/org/serenityos/jakt/psi/caching/JaktCache.kt +++ b/src/main/kotlin/org/serenityos/jakt/psi/caching/JaktCache.kt @@ -39,6 +39,17 @@ abstract class JaktCache(project: Project) : Disposable { return map.getOrPut(key) { resolver(key) } as V } + @Suppress("UNCHECKED_CAST") + fun resolve(key: K): V? { + ProgressManager.checkCanceled() + return getCacheFor(key)[key] as V? + } + + fun set(key: K, value: V) { + ProgressManager.checkCanceled() + getCacheFor(key)[key] = value + } + private fun getCacheFor(element: PsiElement): ConcurrentMap { val owner = element.modificationBoundary From 762905458d19e46308a6df9e267fb7af67ce7501 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sat, 1 Oct 2022 17:22:50 -0700 Subject: [PATCH 04/18] FoldingBuilder: Support folding match expressions --- .../org/serenityos/jakt/folding/JaktBlockFoldingBuilder.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/kotlin/org/serenityos/jakt/folding/JaktBlockFoldingBuilder.kt b/src/main/kotlin/org/serenityos/jakt/folding/JaktBlockFoldingBuilder.kt index 4b403b7c..1af3b659 100644 --- a/src/main/kotlin/org/serenityos/jakt/folding/JaktBlockFoldingBuilder.kt +++ b/src/main/kotlin/org/serenityos/jakt/folding/JaktBlockFoldingBuilder.kt @@ -66,6 +66,11 @@ class JaktBlockFoldingBuilder : CustomFoldingBuilder() { descriptors += FoldingDescriptor(o, o.textRange) } + override fun visitMatchExpression(o: JaktMatchExpression) { + val body = o.matchBody ?: return + descriptors += FoldingDescriptor(o, TextRange(body.curlyOpen.startOffset, body.curlyClose.endOffset)) + } + private fun FoldingDescriptor(element: PsiElement) = FoldingDescriptor(element, element.textRange) } } From f1651e30d78a527db560dc1d090b2560e45a61a7 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sat, 1 Oct 2022 19:29:43 -0700 Subject: [PATCH 05/18] Interpreter: Add all expressions/statements and put them in order --- .../serenityos/jakt/comptime/Interpreter.kt | 151 +++++++++++------- 1 file changed, 94 insertions(+), 57 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 09aa0e98..c1d5330c 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -66,6 +66,31 @@ class Interpreter(element: JaktPsiElement) { private fun evaluateImpl(element: JaktPsiElement): Value? { return when (element) { + /*** EXPRESSIONS ***/ + + is JaktMatchExpression -> TODO() + is JaktTryExpression -> TODO() + is JaktLambdaExpression -> TODO() + is JaktAssignmentBinaryExpression -> TODO() + is JaktThisExpression -> TODO() + is JaktFieldAccessExpression -> TODO() + is JaktRangeExpression -> { + val start = (element.expressionList[0]?.let { evaluate(it)!! } as? IntegerValue) ?: IntegerValue(0) + val end = evaluate(element.expressionList[1])!! as IntegerValue + RangeValue(start.value, end.value, isInclusive = false) + } + is JaktLogicalOrBinaryExpression -> TODO() + is JaktLogicalAndBinaryExpression -> TODO() + is JaktBitwiseOrBinaryExpression -> TODO() + is JaktBitwiseXorBinaryExpression -> TODO() + is JaktBitwiseAndBinaryExpression -> TODO() + is JaktRelationalBinaryExpression -> TODO() + is JaktShiftBinaryExpression -> TODO() + is JaktAddBinaryExpression -> TODO() + is JaktMultiplyBinaryExpression -> TODO() + is JaktCastExpression -> TODO() + is JaktIsExpression -> TODO() + is JaktUnaryExpression -> TODO() is JaktBooleanLiteral -> BoolValue(element.trueKeyword != null) is JaktNumericLiteral -> { element.binaryLiteral?.let { @@ -96,6 +121,23 @@ class Interpreter(element: JaktPsiElement) { return StringValue(element.stringLiteral!!.text.drop(1).dropLast(1)) } + is JaktAccessExpression -> { + val target = evaluate(element.expression)!! + if (element.dotQuestionMark != null) + TODO() + if (element.decimalLiteral != null) { + (target as TupleValue).values[element.decimalLiteral!!.text.toInt()] + } else { + target[element.identifier!!.text] + } + } + is JaktIndexedAccessExpression -> { + val target = evaluate(element.expressionList[0])!! + val value = evaluate(element.expressionList[1])!! + if (target is ArrayValue && value is RangeValue) { + ArraySlice(target, value.range) + } else target[(value as StringValue).value] + } is JaktPlainQualifierExpression -> { val qualifier = element.plainQualifier @@ -112,7 +154,15 @@ class Interpreter(element: JaktPsiElement) { value!! } - is JaktTupleExpression -> TupleValue(element.expressionList.map { evaluate(it)!! }) + is JaktCallExpression -> { + val target = evaluate(element.expression)!! + check(target is FunctionValue) + + val args = element.argumentList.argumentList.map { evaluate(it.expression)!! } + check(args.size in target.validParamCount) + + target.call(this, getThisValue(element.expression), args) + } is JaktArrayExpression -> { element.elementsArrayBody?.let { body -> return ArrayValue(body.expressionList.map { evaluate(it)!! }.toMutableList()) @@ -124,53 +174,41 @@ class Interpreter(element: JaktPsiElement) { check(size is IntegerValue) ArrayValue((0 until size.value).map { value }.toMutableList()) } - is JaktSetExpression -> SetValue(element.expressionList.map { evaluate(it)!! }.toMutableSet()) is JaktDictionaryExpression -> DictionaryValue( element.dictionaryElementList.associate { evaluate(it.expressionList[0])!! to evaluate(it.expressionList[1])!! }.toMutableMap() ) - is JaktRangeExpression -> { - val start = evaluate(element.expressionList[0])!! as IntegerValue - val end = evaluate(element.expressionList[1])!! as IntegerValue - RangeValue(start.value, end.value, isInclusive = false) - } - is JaktIndexedAccessExpression -> { - val target = evaluate(element.expressionList[0])!! - val value = evaluate(element.expressionList[1])!! - if (target is ArrayValue && value is RangeValue) { - ArraySlice(target, value.range) - } else target[(value as StringValue).value] - } - is JaktAccessExpression -> { - val target = evaluate(element.expression)!! - if (element.dotQuestionMark != null) - TODO() - if (element.decimalLiteral != null) { - (target as TupleValue).values[element.decimalLiteral!!.text.toInt()] - } else { - target[element.identifier!!.text] - } - } - is JaktFunction -> { - val parameters = element.parameterList.parameterList.map { param -> - FunctionValue.Parameter( - param.identifier.text, - param.expression?.let { evaluate(it)!! } - ) - } + is JaktSetExpression -> SetValue(element.expressionList.map { evaluate(it)!! }.toMutableSet()) + is JaktTupleExpression -> TupleValue(element.expressionList.map { evaluate(it)!! }) - UserFunctionValue(parameters, element.block ?: element.expression!!) - } - is JaktCallExpression -> { - val target = evaluate(element.expression)!! - check(target is FunctionValue) + /*** STATEMENTS ***/ - val args = element.argumentList.argumentList.map { evaluate(it.expression)!! } - check(args.size in target.validParamCount) + is JaktExpressionStatement -> { + evaluate(element.expression) + VoidValue + } + is JaktReturnStatement -> throw ReturnException(element.expression?.let { evaluate(it)!! }) + is JaktThrowStatement -> TODO() + is JaktDeferStatement -> TODO() + is JaktIfStatement -> { + val canBeExpr = element.canBeExpr + val condition = evaluate(element.expression)!! + check(condition is BoolValue) - target.call(this, getThisValue(element.expression), args) + if (condition.value) { + evaluate(element.block).let { + if (canBeExpr) it else VoidValue + } + } else { + element.ifStatement?.let(::evaluate) + ?: element.elseBlock?.let(::evaluate) + ?: VoidValue + } } + is JaktWhileStatement -> TODO() + is JaktLoopStatement -> TODO() + is JaktForStatement -> TODO() is JaktVariableDeclarationStatement -> { if (element.parenOpen != null) TODO() @@ -180,34 +218,33 @@ class Interpreter(element: JaktPsiElement) { scope[name] = rhs VoidValue } + is JaktGuardStatement -> TODO() + is JaktYieldStatement -> TODO() + is JaktBreakStatement -> TODO() + is JaktContinueStatement -> TODO() + is JaktUnsafeStatement -> TODO() + is JaktInlineCppStatement -> TODO() is JaktBlock -> { pushScope(Scope(scope)) element.statementList.forEach(::evaluate) popScope() VoidValue } - is JaktIfStatement -> { - val canBeExpr = element.canBeExpr - val condition = evaluate(element.expression)!! - check(condition is BoolValue) - if (condition.value) { - evaluate(element.block).let { - if (canBeExpr) it else VoidValue - } - } else { - element.ifStatement?.let(::evaluate) - ?: element.elseBlock?.let(::evaluate) - ?: VoidValue + /*** DECLARATIONS ***/ + + is JaktFunction -> { + val parameters = element.parameterList.parameterList.map { param -> + FunctionValue.Parameter( + param.identifier.text, + param.expression?.let { evaluate(it)!! } + ) } - } - is JaktReturnStatement -> throw ReturnException(element.expression?.let { evaluate(it)!! }) - is JaktExpressionStatement -> { - evaluate(element.expression) - VoidValue + + UserFunctionValue(parameters, element.block ?: element.expression!!) } - else -> TODO("${element::class.java} it not handled by the Interpreter") + else -> error("${element::class.simpleName} is not support at comptime") } } From 40a3170900e841c0c7ca606c2d88568c552178b2 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Oct 2022 09:13:23 -0700 Subject: [PATCH 06/18] Interpreter: Propogate errors to the caller and rely less on exceptions --- .../serenityos/jakt/comptime/Interpreter.kt | 427 +++++++++++++----- .../jakt/comptime/InterpreterException.kt | 5 + .../org/serenityos/jakt/comptime/Value.kt | 31 +- .../org/serenityos/jakt/comptime/builtins.kt | 64 ++- .../serenityos/jakt/comptime/extensions.kt | 10 +- 5 files changed, 408 insertions(+), 129 deletions(-) create mode 100644 src/main/kotlin/org/serenityos/jakt/comptime/InterpreterException.kt diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index c1d5330c..275e427e 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -1,12 +1,17 @@ package org.serenityos.jakt.comptime import com.intellij.openapi.util.Ref +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.refactoring.suggested.endOffset +import com.intellij.refactoring.suggested.startOffset import org.serenityos.jakt.JaktFile import org.serenityos.jakt.psi.JaktPsiElement import org.serenityos.jakt.psi.JaktScope import org.serenityos.jakt.psi.ancestors import org.serenityos.jakt.psi.api.* import org.serenityos.jakt.psi.caching.comptimeCache +import org.serenityos.jakt.psi.findChildOfType class Interpreter(element: JaktPsiElement) { var scope: Scope @@ -18,10 +23,12 @@ class Interpreter(element: JaktPsiElement) { val scope = Scope(null) if (it is JaktScope) { for (decl in it.getDeclarations()) { - try { - scope[decl.name ?: continue] = evaluate(decl)!! - } catch (e: Throwable) { - // Ignore and attempt to continue execution + if (decl is JaktImportBraceEntry) + continue // TODO + + scope[decl.name ?: continue] = when (val result = evaluate(decl)) { + is ExecutionResult.Normal -> result.value + else -> continue } } } @@ -50,21 +57,16 @@ class Interpreter(element: JaktPsiElement) { // A return value of null indicates the expression is not comptime. An exception being // thrown indicates the expression is comptime, but it malformed in some way and cannot // be evaluated. - fun evaluate(element: JaktPsiElement): Value? { + fun evaluate(element: JaktPsiElement): ExecutionResult { val cache = element.comptimeCache() - cache.resolve>(element)?.let { return it.get() } - - try { - val value = evaluateImpl(element) - cache.set(element, Ref(value)) - return value - } catch (e: Throwable) { - cache.set(element, Ref(null)) - throw e - } + cache.resolve>(element)?.get()?.let { return it } + + val value = evaluateImpl(element) + cache.set(element, Ref(value)) + return value } - private fun evaluateImpl(element: JaktPsiElement): Value? { + private fun evaluateImpl(element: JaktPsiElement): ExecutionResult { return when (element) { /*** EXPRESSIONS ***/ @@ -75,9 +77,37 @@ class Interpreter(element: JaktPsiElement) { is JaktThisExpression -> TODO() is JaktFieldAccessExpression -> TODO() is JaktRangeExpression -> { - val start = (element.expressionList[0]?.let { evaluate(it)!! } as? IntegerValue) ?: IntegerValue(0) - val end = evaluate(element.expressionList[1])!! as IntegerValue - RangeValue(start.value, end.value, isInclusive = false) + val (startExpr, endExpr) = when { + element.expressionList.size == 2 -> element.expressionList[0] to element.expressionList[1] + element.expressionList.isEmpty() -> null to null + element.expressionList[0].textRange.endOffset < element.dotDot.textRange.startOffset -> + element.expressionList[0] to null + else -> null to element.expressionList[0] + } + + val start = startExpr?.let { + when (val result = evaluate(it)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it) + else -> return result + } + } ?: IntegerValue(0) + + val end = startExpr?.let { + when (val result = evaluate(it)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it) + else -> return result + } + } ?: IntegerValue(Long.MAX_VALUE) + + if (start !is IntegerValue) + error("Expected range start value to be an integer", startExpr!!) + + if (end !is IntegerValue) + error("Expected range end value to be an integer", endExpr!!) + + ExecutionResult.Normal(RangeValue(start.value, end.value, isInclusive = false)) } is JaktLogicalOrBinaryExpression -> TODO() is JaktLogicalAndBinaryExpression -> TODO() @@ -91,169 +121,339 @@ class Interpreter(element: JaktPsiElement) { is JaktCastExpression -> TODO() is JaktIsExpression -> TODO() is JaktUnaryExpression -> TODO() - is JaktBooleanLiteral -> BoolValue(element.trueKeyword != null) + is JaktBooleanLiteral -> ExecutionResult.Normal(BoolValue(element.trueKeyword != null)) is JaktNumericLiteral -> { element.binaryLiteral?.let { - return IntegerValue(it.text.toLong(2)) + return ExecutionResult.Normal(IntegerValue(it.text.toLong(2))) } element.octalLiteral?.let { - return IntegerValue(it.text.toLong(8)) + return ExecutionResult.Normal(IntegerValue(it.text.toLong(8))) } element.hexLiteral?.let { - return IntegerValue(it.text.toLong(16)) + return ExecutionResult.Normal(IntegerValue(it.text.toLong(16))) } val decimalText = element.decimalLiteral!!.text - return if ("." in decimalText) { - FloatValue(decimalText.toDouble()) - } else IntegerValue(decimalText.toLong(10)) + return ExecutionResult.Normal( + if ("." in decimalText) { + FloatValue(decimalText.toDouble()) + } else IntegerValue(decimalText.toLong(10)) + ) } is JaktLiteral -> { element.byteCharLiteral?.let { - return ByteCharValue(it.text[2].code.toByte()) + return ExecutionResult.Normal(ByteCharValue(it.text[2].code.toByte())) } element.charLiteral?.let { - return CharValue(it.text.single()) + return ExecutionResult.Normal(CharValue(it.text.single())) } - return StringValue(element.stringLiteral!!.text.drop(1).dropLast(1)) + ExecutionResult.Normal(StringValue(element.stringLiteral!!.text.drop(1).dropLast(1))) } is JaktAccessExpression -> { - val target = evaluate(element.expression)!! + val target = when (val result = evaluate(element.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + } + if (element.dotQuestionMark != null) TODO() + if (element.decimalLiteral != null) { - (target as TupleValue).values[element.decimalLiteral!!.text.toInt()] + if (target !is TupleValue) + error("Invalid tuple index into non-tuple value", element.decimalLiteral!!) + + ExecutionResult.Normal(target.values[element.decimalLiteral!!.text.toInt()]) } else { - target[element.identifier!!.text] + val value = target[element.identifier!!.text] ?: error("Unknown field ${element.identifier!!.text}") + ExecutionResult.Normal(value) } } is JaktIndexedAccessExpression -> { - val target = evaluate(element.expressionList[0])!! - val value = evaluate(element.expressionList[1])!! - if (target is ArrayValue && value is RangeValue) { - ArraySlice(target, value.range) - } else target[(value as StringValue).value] + val target = when (val result = evaluate(element.expressionList[0])) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expressionList[0]) + else -> return result + } + + val value = when (val result = evaluate(element.expressionList[1])) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expressionList[1]) + else -> return result + } + + if (target !is ArrayValue) + error("Unexpected index into non-array value", element.expressionList[0]) + + when (value) { + is RangeValue -> ExecutionResult.Normal(ArraySlice(target, value.range)) + is IntegerValue -> { + val result = target.values.getOrNull(value.value.toInt()) + ?: error("Index ${value.value} out-of-range for array of length ${target.values.size}") + ExecutionResult.Normal(result) + } + else -> error("Expected integer or range in array indexing expression", element.expressionList[1]) + } } is JaktPlainQualifierExpression -> { val qualifier = element.plainQualifier val parts = generateSequence(qualifier) { it.plainQualifier } - .map { it.identifier.text } + .map { it to it.identifier.text } .toMutableList() .asReversed() - var value: Value? = scope[parts[0]] - parts.removeFirst() + var value: Value? = null + var currScope: Scope? = scope + + while (currScope != null) { + if (parts[0].second in currScope) { + value = scope[parts[0].second] + parts.removeFirst() + break + } + + currScope = currScope.outer + } + + if (value == null) { + val type = if (parts.size > 1) "qualifier" else "identifier" + error("Unknown $type \"${parts[0].second}\"", parts[0].first) + } - for (part in parts) - value = value!![part] + for (part in parts) { + if (part.second !in value!!) + error("\"${value.typeName()}\" has no member named ${part.second}", part.first) - value!! + value = value[part.second]!! + } + + ExecutionResult.Normal(value!!) } is JaktCallExpression -> { - val target = evaluate(element.expression)!! - check(target is FunctionValue) + val target = when (val result = evaluate(element.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + } - val args = element.argumentList.argumentList.map { evaluate(it.expression)!! } - check(args.size in target.validParamCount) + if (target !is FunctionValue) + error("\"${target.typeName()}\" is not callable", element.expression) + + val args = element.argumentList.argumentList.map { + when (val result = evaluate(it.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + } + } + + if (args.size !in target.validParamCount) { + error( + "Expected between ${target.validParamCount.first} and ${target.validParamCount.last} " + + "arguments, but found ${args.size} arguments", + element.argumentList + ) + } + + val thisValue = when (val expr = element.expression) { + is JaktAccessExpression -> when (val result = evaluate(expr.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", expr.expression) + else -> return result + } + is JaktFieldAccessExpression -> (scope as FunctionScope).thisBinding!! + is JaktIndexedAccessExpression -> when (val result = evaluate(expr.expressionList.first())) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + } + else -> null + } - target.call(this, getThisValue(element.expression), args) + target.call(this, thisValue, args) } is JaktArrayExpression -> { - element.elementsArrayBody?.let { body -> - return ArrayValue(body.expressionList.map { evaluate(it)!! }.toMutableList()) + element.elementsArrayBody?.let { body -> + val array = ArrayValue(body.expressionList.map { + when (val result = evaluate(it)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it) + else -> return result + } + }.toMutableList()) + + return ExecutionResult.Normal(array) } val body = element.sizedArrayBody!! - val value = evaluate(body.expressionList[0])!! - val size = evaluate(body.expressionList[1])!! - check(size is IntegerValue) - ArrayValue((0 until size.value).map { value }.toMutableList()) + + val value = when (val result = evaluate(body.expressionList[0])) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", body.expressionList[0]) + else -> return result + } + + val size = when (val result = evaluate(body.expressionList[1])) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", body.expressionList[1]) + else -> return result + } + + if (size !is IntegerValue) + error("Array size initializer must be an integer", body.expressionList[1]) + + ExecutionResult.Normal(ArrayValue((0 until size.value).map { value }.toMutableList())) } - is JaktDictionaryExpression -> DictionaryValue( + is JaktDictionaryExpression -> ExecutionResult.Normal(DictionaryValue( element.dictionaryElementList.associate { - evaluate(it.expressionList[0])!! to evaluate(it.expressionList[1])!! + val key = when (val result = evaluate(it.expressionList[0])) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it.expressionList[0]) + else -> return result + } + + val value = when (val result = evaluate(it.expressionList[1])) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it.expressionList[1]) + else -> return result + } + + key to value }.toMutableMap() - ) - is JaktSetExpression -> SetValue(element.expressionList.map { evaluate(it)!! }.toMutableSet()) - is JaktTupleExpression -> TupleValue(element.expressionList.map { evaluate(it)!! }) + )) + is JaktSetExpression -> ExecutionResult.Normal(SetValue(element.expressionList.map { + when (val result = evaluate(it)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it) + else -> return result + } + }.toMutableSet())) + is JaktTupleExpression -> ExecutionResult.Normal(TupleValue(element.expressionList.map { + when (val result = evaluate(it)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it) + else -> return result + } + })) /*** STATEMENTS ***/ - is JaktExpressionStatement -> { - evaluate(element.expression) - VoidValue + is JaktExpressionStatement -> when (val result = evaluate(element.expression)) { + is ExecutionResult.Normal -> ExecutionResult.Normal(result.value) + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result } - is JaktReturnStatement -> throw ReturnException(element.expression?.let { evaluate(it)!! }) - is JaktThrowStatement -> TODO() + is JaktReturnStatement -> ExecutionResult.Return(element.expression?.let { + when (val result = evaluate(it)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it) + else -> return result + } + } ?: VoidValue) + is JaktThrowStatement -> ExecutionResult.Throw(when (val result = evaluate(element.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + }) is JaktDeferStatement -> TODO() is JaktIfStatement -> { - val canBeExpr = element.canBeExpr - val condition = evaluate(element.expression)!! - check(condition is BoolValue) + val condition = when (val result = evaluate(element.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + } + + if (condition !is BoolValue) + error("Expected bool", element.expression) if (condition.value) { - evaluate(element.block).let { - if (canBeExpr) it else VoidValue + when (val result = evaluate(element.block)) { + is ExecutionResult.Normal -> ExecutionResult.Normal(VoidValue) + is ExecutionResult.Yield -> error("Unexpected yield", element.block.findChildOfType()!!) + else -> return result } - } else { - element.ifStatement?.let(::evaluate) - ?: element.elseBlock?.let(::evaluate) - ?: VoidValue - } + } else if (element.ifStatement != null) { + evaluate(element.ifStatement!!) + } else if (element.elseBlock != null) { + when (val result = evaluate(element.elseBlock!!)) { + is ExecutionResult.Normal -> ExecutionResult.Normal(VoidValue) + is ExecutionResult.Yield -> error("Unexpected yield", element.block.findChildOfType()!!) + else -> return result + } + } else ExecutionResult.Normal(VoidValue) } is JaktWhileStatement -> TODO() is JaktLoopStatement -> TODO() is JaktForStatement -> TODO() is JaktVariableDeclarationStatement -> { - if (element.parenOpen != null) - TODO() + if (element.parenOpen != null) { + error( + "Destructuring variable assignments are not supported", + TextRange(element.parenOpen!!.startOffset, element.parenClose!!.endOffset), + ) + } - val rhs = evaluate(element.expression)!! + val rhs = when (val result = evaluate(element.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + } + + // TODO: Ensure variable exists val name = element.variableDeclList[0].name!! scope[name] = rhs - VoidValue + + ExecutionResult.Normal(VoidValue) } is JaktGuardStatement -> TODO() - is JaktYieldStatement -> TODO() - is JaktBreakStatement -> TODO() - is JaktContinueStatement -> TODO() - is JaktUnsafeStatement -> TODO() - is JaktInlineCppStatement -> TODO() + is JaktYieldStatement -> ExecutionResult.Yield(when (val result = evaluate(element.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + }) + is JaktBreakStatement -> ExecutionResult.Break + is JaktContinueStatement -> ExecutionResult.Continue + is JaktUnsafeStatement -> error("Cannot evaluate unsafe blocks at comptime", element) + is JaktInlineCppStatement -> error("Cannot evaluate inline cpp blocks at comptime") is JaktBlock -> { pushScope(Scope(scope)) - element.statementList.forEach(::evaluate) - popScope() - VoidValue + try { + element.statementList.forEach { + val result = evaluate(it) + if (result is ExecutionResult.Yield || result is ExecutionResult.Return) + return result + } + ExecutionResult.Normal(VoidValue) + } finally { + popScope() + } } - - /*** DECLARATIONS ***/ - is JaktFunction -> { val parameters = element.parameterList.parameterList.map { param -> - FunctionValue.Parameter( - param.identifier.text, - param.expression?.let { evaluate(it)!! } - ) + val default = param.expression?.let { + when (val result = evaluate(it)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it) + else -> return result + } + } + + FunctionValue.Parameter(param.identifier.text, default) } - UserFunctionValue(parameters, element.block ?: element.expression!!) + val target = element.block ?: element.expression ?: return ExecutionResult.Normal(VoidValue) + ExecutionResult.Normal(UserFunctionValue(parameters, target)) } - else -> error("${element::class.simpleName} is not support at comptime") - } - } + // Ignored declarations (hoisted at scope initialization) + is JaktImport -> ExecutionResult.Normal(VoidValue) - private fun getThisValue(expression: JaktExpression): Value? { - return when (expression) { - is JaktAccessExpression -> evaluate(expression.expression)!! - is JaktFieldAccessExpression -> (scope as FunctionScope).thisBinding!! - is JaktIndexedAccessExpression -> evaluate(expression.expressionList.first())!! - else -> null + else -> error("${element::class.simpleName} is not support at comptime") } } @@ -266,6 +466,11 @@ class Interpreter(element: JaktPsiElement) { scope.initialize("abort", abortFunction) } + fun error(message: String, element: PsiElement): Nothing = error(message, element.textRange) + + fun error(message: String, range: TextRange): Nothing = + throw InterpreterException(message, range) + open class Scope(var outer: Scope?) { protected val bindings = mutableMapOf() @@ -296,14 +501,22 @@ class Interpreter(element: JaktPsiElement) { fun argument(name: String) = bindings[name]!! } - abstract class FlowException : Error() + sealed interface ExecutionResult { + class Return(val value: Value) : ExecutionResult + + class Yield(val value: Value) : ExecutionResult + + class Throw(val value: Value) : ExecutionResult - class ReturnException(val value: Value?) : FlowException() + class Normal(val value: Value) : ExecutionResult - class YieldException(val value: Value?) : FlowException() + object Continue : ExecutionResult + + object Break : ExecutionResult + } companion object { - fun evaluate(element: JaktPsiElement): Value? { + fun evaluate(element: JaktPsiElement): ExecutionResult { return Interpreter(element).evaluate(element) } } diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/InterpreterException.kt b/src/main/kotlin/org/serenityos/jakt/comptime/InterpreterException.kt new file mode 100644 index 00000000..aeb75284 --- /dev/null +++ b/src/main/kotlin/org/serenityos/jakt/comptime/InterpreterException.kt @@ -0,0 +1,5 @@ +package org.serenityos.jakt.comptime + +import com.intellij.openapi.util.TextRange + +class InterpreterException(message: String, val range: TextRange) : Exception(message) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt index 8a0d7a42..16e8c099 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt @@ -1,7 +1,6 @@ package org.serenityos.jakt.comptime import org.serenityos.jakt.psi.JaktPsiElement -import org.serenityos.jakt.psi.api.JaktExpression // As with everything else in the plugin, we are very lenient when it comes to types. // All integers are treated as i64, just to make my life easier. Similarly, all float @@ -10,6 +9,8 @@ import org.serenityos.jakt.psi.api.JaktExpression sealed class Value { private val fields = mutableMapOf() + abstract fun typeName(): String + open operator fun contains(name: String) = name in fields open operator fun get(name: String) = fields[name] @@ -20,30 +21,37 @@ sealed class Value { } object VoidValue : Value() { + override fun typeName() = "void" override fun toString() = "void" } data class BoolValue(val value: Boolean) : Value() { + override fun typeName() = "bool" override fun toString() = value.toString() } data class IntegerValue(val value: Long) : Value() { + override fun typeName() = "i64" override fun toString() = value.toString() } data class FloatValue(val value: Double) : Value() { + override fun typeName() = "f64" override fun toString() = value.toString() } data class CharValue(val value: Char) : Value() { + override fun typeName() = "c_char" override fun toString() = "'$value'" } data class ByteCharValue(val value: Byte) : Value() { + override fun typeName() = "u8" override fun toString() = "b'$value'" } data class TupleValue(val values: List) : Value() { + override fun typeName() = "Tuple" override fun toString() = values.joinToString(prefix = "(", postfix = ")") } @@ -55,9 +63,11 @@ abstract class FunctionValue(private val minParamCount: Int, private val maxPara require(minParamCount <= maxParamCount) } + override fun typeName() = "function" + constructor(parameters: List) : this(parameters.count { it.default == null }, parameters.size) - abstract fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value + abstract fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Interpreter.ExecutionResult data class Parameter(val name: String, val default: Value? = null) } @@ -66,7 +76,7 @@ class UserFunctionValue( private val parameters: List, val body: JaktPsiElement /* JaktBlock | JaktExpression */, // TODO: Storing PSI is bad, right? ) : FunctionValue(parameters) { - override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value { + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Interpreter.ExecutionResult { val newScope = Interpreter.FunctionScope(interpreter.scope, thisValue) for ((index, param) in parameters.withIndex()) { @@ -80,15 +90,12 @@ class UserFunctionValue( interpreter.pushScope(newScope) - return try { - interpreter.evaluate(body).let { - if (body is JaktExpression) it!! else VoidValue - } - } catch (e: Interpreter.ReturnException) { - e.value ?: VoidValue - } catch (e: Interpreter.FlowException) { - error("Unexpected FlowException in UserFunction: ${e::class.simpleName}") - } finally { + return when (val result = interpreter.evaluate(body)) { + is Interpreter.ExecutionResult.Normal -> Interpreter.ExecutionResult.Normal(VoidValue) + is Interpreter.ExecutionResult.Return -> Interpreter.ExecutionResult.Normal(result.value) + is Interpreter.ExecutionResult.Throw -> result + else -> interpreter.error("Unexpected control flow", body) + }.also { interpreter.popScope() } } diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt index 988c16b3..8992208e 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt @@ -7,8 +7,8 @@ class BuiltinFunction( parameterCount: Int, private val func: (Value?, List) -> Value, ) : FunctionValue(parameterCount, parameterCount) { - override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Value { - return func(thisValue, arguments) + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Interpreter.ExecutionResult { + return Interpreter.ExecutionResult.Normal(func(thisValue, arguments)) } } @@ -19,6 +19,8 @@ data class OptionalValue(val value: Value?) : Value() { this["value_or"] = getValueOr } + override fun typeName() = "Optional" + override fun toString() = "Optional(${value?.toString() ?: ""})" companion object { @@ -48,6 +50,10 @@ data class ArrayIterator( this["next"] = next } + override fun typeName() = "ArrayIterator" + + override fun toString() = "ArrayIterator($nextIndex..$endInclusiveIndex)" + companion object { private val next = BuiltinFunction(0) { thisValue, _ -> require(thisValue is ArrayIterator) @@ -61,8 +67,6 @@ data class ArrayIterator( } data class ArrayValue(val values: MutableList) : Value() { - override fun toString() = values.joinToString(prefix = "[", postfix = "]") - init { this["is_empty"] = isEmpty this["size"] = size @@ -74,6 +78,10 @@ data class ArrayValue(val values: MutableList) : Value() { this["last"] = last } + override fun typeName() = "Array" + + override fun toString() = values.joinToString(prefix = "[", postfix = "]") + companion object { private val isEmpty = BuiltinFunction(0) { thisValue, _ -> require(thisValue is ArrayValue) @@ -131,6 +139,10 @@ data class ArraySlice(val array: ArrayValue, val range: IntRange) : Value() { this["last"] = last } + override fun typeName() = "ArraySlice" + + override fun toString() = "ArraySlice($range)" + companion object { private val isEmpty = BuiltinFunction(0) { thisValue, _ -> require(thisValue is ArraySlice) @@ -194,6 +206,10 @@ object StringStruct : Value() { this["number"] = number this["repeated"] = repeated } + + override fun typeName() = "String" + + override fun toString() = "StringStruct" } data class StringValue(val value: String) : Value() { @@ -213,6 +229,8 @@ data class StringValue(val value: String) : Value() { this["ends_with"] = endsWith } + override fun typeName() = "String" + override fun toString() = "\"$value\"" companion object { @@ -327,6 +345,10 @@ object StringBuilderStruct : Value() { init { this["create"] = create } + + override fun typeName() = "StringBuilder" + + override fun toString() = "StringBuilderStruct" } class StringBuilderInstance : Value() { @@ -342,7 +364,9 @@ class StringBuilderInstance : Value() { this["clear"] = clear } - override fun toString() = "StringBuilder(content = \"$builder\")" + override fun typeName() = "StringBuilder" + + override fun toString() = "StringBuilder(\"$builder\")" companion object { private val append = BuiltinFunction(1) { thisValue, arguments -> @@ -403,6 +427,10 @@ data class DictionaryIterator(val dictionary: DictionaryValue) : Value() { this["next"] = next } + override fun typeName() = "DictionaryIterator" + + override fun toString() = "DictionaryIterator" + companion object { private val next = BuiltinFunction(0) { thisValue, _ -> require(thisValue is DictionaryIterator) @@ -426,6 +454,8 @@ data class DictionaryValue(val elements: MutableMap) : Value() { this["iterator"] = iterator } + override fun typeName() = "Dictionary" + override fun toString() = elements.entries.joinToString(prefix = "{", postfix = "}") { "${it.key}: ${it.value}" } @@ -487,6 +517,10 @@ data class SetIterator(val values: MutableSet) : Value() { this["next"] = next } + override fun typeName() = "SetIterator" + + override fun toString() = "SetIterator" + companion object { private val next = BuiltinFunction(0) { thisValue, _ -> require(thisValue is SetIterator) @@ -508,6 +542,8 @@ data class SetValue(val values: MutableSet) : Value() { this["iterator"] = iterator } + override fun typeName() = "Set" + override fun toString() = values.joinToString(prefix = "{", postfix = "}") companion object { @@ -563,6 +599,10 @@ data class RangeValue(val start: Long, val end: Long, val isInclusive: Boolean) this["exclusive"] = exclusive } + override fun typeName() = "Range" + + override fun toString() = "Range($start..$end, inclusive = $isInclusive)" + private fun getAndAdvance(): Long { return current.also { if (forwards) current++ else current-- @@ -609,9 +649,15 @@ object ErrorStruct : Value() { init { this["from_errno"] = fromErrno } + + override fun typeName() = "Error" + + override fun toString() = "ErrorStruct" } class ErrorInstance(private val codeValue: Long) : Value() { + override fun typeName() = "Error" + override fun toString() = "Error(code = $codeValue)" } @@ -634,6 +680,10 @@ object FileStruct : Value() { this["exists"] = exists this["open_for_reading"] = openForReading } + + override fun typeName() = "File" + + override fun toString() = "FileStruct" } class FileInstance(val file: File) : Value() { @@ -641,6 +691,10 @@ class FileInstance(val file: File) : Value() { this["read_all"] = readAll } + override fun typeName() = "File" + + override fun toString() = "File($file)" + companion object { private val readAll = BuiltinFunction(0) { thisValue, arguments -> require(thisValue is FileInstance) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt index 43c9fce0..23773d65 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt @@ -11,8 +11,11 @@ import org.serenityos.jakt.psi.findChildOfType val JaktPsiElement.comptimeValue: Value? get() = comptimeCache().resolveWithCaching(this) { try { - Interpreter.evaluate(this) - } catch (e: Throwable) { + when (val result = Interpreter.evaluate(this)) { + is Interpreter.ExecutionResult.Normal -> result.value + else -> null + } + } catch (e: InterpreterException) { null }.let(::Ref) }.get() @@ -23,6 +26,3 @@ val JaktIfStatement.ifStatement: JaktIfStatement? val JaktIfStatement.elseBlock: JaktBlock? get() = childrenOfType().getOrNull(1) - -val JaktIfStatement.canBeExpr: Boolean - get() = elseKeyword != null && (ifStatement != null || elseBlock != null) From 28912e3386a596a3c4a7d86ce40d817e30102787 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Oct 2022 09:31:26 -0700 Subject: [PATCH 07/18] Interpreter: Do not cache intermediate results This will interfer with scope assignments, as once assignment, a variable would never be able to change its value. --- .../org/serenityos/jakt/comptime/Interpreter.kt | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 275e427e..dbdf58ee 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -1,6 +1,5 @@ package org.serenityos.jakt.comptime -import com.intellij.openapi.util.Ref import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.refactoring.suggested.endOffset @@ -10,7 +9,6 @@ import org.serenityos.jakt.psi.JaktPsiElement import org.serenityos.jakt.psi.JaktScope import org.serenityos.jakt.psi.ancestors import org.serenityos.jakt.psi.api.* -import org.serenityos.jakt.psi.caching.comptimeCache import org.serenityos.jakt.psi.findChildOfType class Interpreter(element: JaktPsiElement) { @@ -57,16 +55,7 @@ class Interpreter(element: JaktPsiElement) { // A return value of null indicates the expression is not comptime. An exception being // thrown indicates the expression is comptime, but it malformed in some way and cannot // be evaluated. - fun evaluate(element: JaktPsiElement): ExecutionResult { - val cache = element.comptimeCache() - cache.resolve>(element)?.get()?.let { return it } - - val value = evaluateImpl(element) - cache.set(element, Ref(value)) - return value - } - - private fun evaluateImpl(element: JaktPsiElement): ExecutionResult { + private fun evaluate(element: JaktPsiElement): ExecutionResult { return when (element) { /*** EXPRESSIONS ***/ From caffb76e4f14dfa23553da298fe63496313f4348 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Oct 2022 11:04:34 -0700 Subject: [PATCH 08/18] Interpreter: Support assignments --- .../serenityos/jakt/comptime/Interpreter.kt | 333 ++++++++++++++++-- .../org/serenityos/jakt/comptime/Value.kt | 6 +- .../org/serenityos/jakt/comptime/builtins.kt | 2 +- 3 files changed, 309 insertions(+), 32 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index dbdf58ee..fe9b2e46 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -10,6 +10,8 @@ import org.serenityos.jakt.psi.JaktScope import org.serenityos.jakt.psi.ancestors import org.serenityos.jakt.psi.api.* import org.serenityos.jakt.psi.findChildOfType +import org.serenityos.jakt.psi.reference.hasNamespace +import org.serenityos.jakt.utils.unreachable class Interpreter(element: JaktPsiElement) { var scope: Scope @@ -55,14 +57,106 @@ class Interpreter(element: JaktPsiElement) { // A return value of null indicates the expression is not comptime. An exception being // thrown indicates the expression is comptime, but it malformed in some way and cannot // be evaluated. - private fun evaluate(element: JaktPsiElement): ExecutionResult { + fun evaluate(element: JaktPsiElement): ExecutionResult { return when (element) { /*** EXPRESSIONS ***/ is JaktMatchExpression -> TODO() is JaktTryExpression -> TODO() is JaktLambdaExpression -> TODO() - is JaktAssignmentBinaryExpression -> TODO() + is JaktAssignmentBinaryExpression -> { + val binaryOp = when { + element.plusEquals != null -> BinaryOperator.Add + element.minusEquals != null -> BinaryOperator.Subtract + element.asteriskEquals != null -> BinaryOperator.Multiply + element.slashEquals != null -> BinaryOperator.Divide + element.percentEquals != null -> BinaryOperator.Modulo + element.arithLeftShiftEquals != null -> BinaryOperator.ArithLeftShift + element.leftShiftEquals != null -> BinaryOperator.LeftShift + element.arithRightShiftEquals != null -> BinaryOperator.ArithRightShift + element.rightShiftEquals != null -> BinaryOperator.RightShift + else -> null + } + + val newValue = if (binaryOp != null) { + applyBinaryOperator(element.left, element.right!!, binaryOp).let { + when (it) { + is ExecutionResult.Normal -> it.value + is ExecutionResult.Yield -> error( + "Unexpected yield", + TextRange(element.left.startOffset, element.right!!.endOffset), + ) + else -> return it + } + } + } else { + evaluate(element.right!!).let { + when (it) { + is ExecutionResult.Normal -> it.value + is ExecutionResult.Yield -> error("Unexpected yield", element.right!!) + else -> return it + } + } + } + + when (val assignmentTarget = element.left) { + is JaktPlainQualifierExpression -> { + if (assignmentTarget.plainQualifier.hasNamespace) + error("Invalid assignment target", assignmentTarget) + + val name = assignmentTarget.plainQualifier.name!! + if (!assign(name, newValue, initialize = false)) + error("Unknown identifier \"$name\"", assignmentTarget) + } + is JaktIndexedAccessExpression -> { + val target = when (val result = evaluate(element.left)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.left) + else -> return result + } + + if (target !is ArrayValue) + error("Expected array, found ${target.typeName()}", element.left) + + val index = when (val result = evaluate(element.right!!)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.right!!) + else -> return result + } + + if (index !is IntegerValue) + error("Expected integer, found ${index.typeName()}", element.right!!) + + if (index.value.toInt() > target.values.size) + error("Out-of-bounds assignment to array of length ${target.values.size} with index ${index.value}", assignmentTarget) + + target.values[index.value.toInt()] = newValue + } + is JaktAccessExpression -> { + val target = when (val result = evaluate(element.left)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.left) + else -> return result + } + + if (assignmentTarget.decimalLiteral != null) { + if (target !is TupleValue) + error("Expected tuple, found ${target.typeName()}", element.left) + + val index = assignmentTarget.decimalLiteral!!.text.toInt() + if (index > target.values.size) + error("Cannot assign to index $index of tuple of length ${target.values.size}") + + target.values[index] = newValue + } else { + target[assignmentTarget.identifier!!.text] = newValue + } + } + else -> error("Invalid assignment target", assignmentTarget) + } + + ExecutionResult.Normal(VoidValue) + } is JaktThisExpression -> TODO() is JaktFieldAccessExpression -> TODO() is JaktRangeExpression -> { @@ -328,7 +422,7 @@ class Interpreter(element: JaktPsiElement) { is ExecutionResult.Yield -> error("Unexpected yield", it) else -> return result } - })) + }.toMutableList())) /*** STATEMENTS ***/ @@ -393,9 +487,7 @@ class Interpreter(element: JaktPsiElement) { else -> return result } - // TODO: Ensure variable exists - val name = element.variableDeclList[0].name!! - scope[name] = rhs + assign(element.variableDeclList[0].name!!, rhs, initialize = true) ExecutionResult.Normal(VoidValue) } @@ -446,19 +538,218 @@ class Interpreter(element: JaktPsiElement) { } } + private fun applyBinaryOperator(lhsExpr: JaktExpression, rhsExpr: JaktExpression, op: BinaryOperator): ExecutionResult { + if (op == BinaryOperator.LogicalOr || op == BinaryOperator.LogicalAnd) { + val shortCircuitValue = op == BinaryOperator.LogicalOr + + val lhsValue = when (val result = evaluate(lhsExpr)) { + is ExecutionResult.Normal -> result.value + else -> return result + } + if (lhsValue !is BoolValue) + error("Expected bool, found ${lhsValue.typeName()}", lhsExpr) + + if (lhsValue.value == shortCircuitValue) + return ExecutionResult.Normal(BoolValue(shortCircuitValue)) + + val rhsValue = when (val result = evaluate(rhsExpr)) { + is ExecutionResult.Normal -> result.value + else -> return result + } + if (rhsValue !is BoolValue) + error("Expected bool, found ${rhsValue.typeName()}", rhsExpr) + + ExecutionResult.Normal(rhsValue) + } + + val lhsValue = when (val result = evaluate(lhsExpr)) { + is ExecutionResult.Normal -> result.value + else -> return result + } + + val rhsValue = when (val result = evaluate(rhsExpr)) { + is ExecutionResult.Normal -> result.value + else -> return result + } + + fun incompatError(): Nothing { + error( + "Incompatible types \"${lhsValue.typeName()}\" and \"${rhsValue.typeName()}\" for operator ${op.op}", + TextRange(lhsExpr.textRange.startOffset, rhsExpr.textRange.endOffset) + ) + } + + if (lhsValue.typeName() != rhsValue.typeName()) + incompatError() + + // TODO: Separator integer types + + val value = when (op) { + BinaryOperator.BitwiseOr, + BinaryOperator.BitwiseXor, + BinaryOperator.BitwiseAnd, + BinaryOperator.LeftShift, + BinaryOperator.RightShift, + BinaryOperator.ArithLeftShift, + BinaryOperator.ArithRightShift -> { + if (lhsValue !is IntegerValue || rhsValue !is IntegerValue) + incompatError() + + val value = when (op) { + BinaryOperator.BitwiseOr -> lhsValue.value or rhsValue.value + BinaryOperator.BitwiseXor -> lhsValue.value xor rhsValue.value + BinaryOperator.BitwiseAnd -> lhsValue.value and rhsValue.value + BinaryOperator.LeftShift, BinaryOperator.ArithLeftShift -> lhsValue.value shl rhsValue.value.toInt() + BinaryOperator.RightShift, BinaryOperator.ArithRightShift -> lhsValue.value shr rhsValue.value.toInt() + else -> unreachable() + } + + IntegerValue(value) + } + BinaryOperator.Add, + BinaryOperator.Subtract, + BinaryOperator.Multiply, + BinaryOperator.Divide, + BinaryOperator.Modulo -> { + val lhsNum = when (lhsValue) { + is IntegerValue -> lhsValue.value.toDouble() + is FloatValue -> lhsValue.value + else -> incompatError() + } + + val rhsNum = when (rhsValue) { + is IntegerValue -> rhsValue.value.toDouble() + is FloatValue -> rhsValue.value + else -> incompatError() + } + + val result = when (op) { + BinaryOperator.Add -> lhsNum + rhsNum + BinaryOperator.Subtract -> lhsNum - rhsNum + BinaryOperator.Multiply -> lhsNum * rhsNum + BinaryOperator.Divide -> lhsNum / rhsNum + BinaryOperator.Modulo -> lhsNum % rhsNum + else -> unreachable() + } + + if (lhsValue is IntegerValue) { + IntegerValue(result.toLong()) + } else FloatValue(result) + } + BinaryOperator.Equals -> when (lhsValue) { + is BoolValue -> BoolValue(lhsValue.value == (rhsValue as BoolValue).value) + is IntegerValue -> BoolValue(lhsValue.value == (rhsValue as IntegerValue).value) + is FloatValue -> BoolValue(lhsValue.value == (rhsValue as FloatValue).value) + is CharValue -> BoolValue(lhsValue.value == (rhsValue as CharValue).value) + is ByteCharValue -> BoolValue(lhsValue.value == (rhsValue as ByteCharValue).value) + is StringValue -> BoolValue(lhsValue.value == (rhsValue as StringValue).value) + else -> incompatError() + } + BinaryOperator.NotEquals -> when (lhsValue) { + is BoolValue -> BoolValue(lhsValue.value != (rhsValue as BoolValue).value) + is IntegerValue -> BoolValue(lhsValue.value != (rhsValue as IntegerValue).value) + is FloatValue -> BoolValue(lhsValue.value != (rhsValue as FloatValue).value) + is CharValue -> BoolValue(lhsValue.value != (rhsValue as CharValue).value) + is ByteCharValue -> BoolValue(lhsValue.value != (rhsValue as ByteCharValue).value) + is StringValue -> BoolValue(lhsValue.value != (rhsValue as StringValue).value) + else -> incompatError() + } + BinaryOperator.GreaterThan -> when (lhsValue) { + is BoolValue -> BoolValue(lhsValue.value > (rhsValue as BoolValue).value) + is IntegerValue -> BoolValue(lhsValue.value > (rhsValue as IntegerValue).value) + is FloatValue -> BoolValue(lhsValue.value > (rhsValue as FloatValue).value) + is CharValue -> BoolValue(lhsValue.value > (rhsValue as CharValue).value) + is ByteCharValue -> BoolValue(lhsValue.value > (rhsValue as ByteCharValue).value) + is StringValue -> BoolValue(lhsValue.value > (rhsValue as StringValue).value) + else -> incompatError() + } + BinaryOperator.GreaterThanEq -> when (lhsValue) { + is BoolValue -> BoolValue(lhsValue.value >= (rhsValue as BoolValue).value) + is IntegerValue -> BoolValue(lhsValue.value >= (rhsValue as IntegerValue).value) + is FloatValue -> BoolValue(lhsValue.value >= (rhsValue as FloatValue).value) + is CharValue -> BoolValue(lhsValue.value >= (rhsValue as CharValue).value) + is ByteCharValue -> BoolValue(lhsValue.value >= (rhsValue as ByteCharValue).value) + is StringValue -> BoolValue(lhsValue.value >= (rhsValue as StringValue).value) + else -> incompatError() + } + BinaryOperator.LessThan -> when (lhsValue) { + is BoolValue -> BoolValue(lhsValue.value < (rhsValue as BoolValue).value) + is IntegerValue -> BoolValue(lhsValue.value < (rhsValue as IntegerValue).value) + is FloatValue -> BoolValue(lhsValue.value < (rhsValue as FloatValue).value) + is CharValue -> BoolValue(lhsValue.value < (rhsValue as CharValue).value) + is ByteCharValue -> BoolValue(lhsValue.value < (rhsValue as ByteCharValue).value) + is StringValue -> BoolValue(lhsValue.value < (rhsValue as StringValue).value) + else -> incompatError() + } + BinaryOperator.LessThanEq -> when (lhsValue) { + is BoolValue -> BoolValue(lhsValue.value <= (rhsValue as BoolValue).value) + is IntegerValue -> BoolValue(lhsValue.value <= (rhsValue as IntegerValue).value) + is FloatValue -> BoolValue(lhsValue.value <= (rhsValue as FloatValue).value) + is CharValue -> BoolValue(lhsValue.value <= (rhsValue as CharValue).value) + is ByteCharValue -> BoolValue(lhsValue.value <= (rhsValue as ByteCharValue).value) + is StringValue -> BoolValue(lhsValue.value <= (rhsValue as StringValue).value) + else -> incompatError() + } + else -> unreachable() + } + + return ExecutionResult.Normal(value) + } + + private fun assign(name: String, value: Value, initialize: Boolean): Boolean { + // TODO: Ensure bindings already exists in the scope + + var currScope: Scope? = scope + while (currScope != null) { + if (name in currScope) { + currScope[name] = value + return true + } + + currScope = currScope.outer + } + + return if (initialize) { + scope[name] = value + true + } else false + } + private fun initializeGlobalScope(scope: Scope) { - scope.initialize("String", StringStruct) - scope.initialize("StringBuilder", StringBuilderStruct) - scope.initialize("Error", ErrorStruct) - scope.initialize("File", FileStruct) - scope.initialize("___jakt_get_target_triple_string", jaktGetTargetTripleStringFunction) - scope.initialize("abort", abortFunction) + scope["String"] = StringStruct + scope["StringBuilder"] = StringBuilderStruct + scope["Error"] = ErrorStruct + scope["File"] = FileStruct + scope["___jakt_get_target_triple_string"] = jaktGetTargetTripleStringFunction + scope["abort"] = abortFunction } fun error(message: String, element: PsiElement): Nothing = error(message, element.textRange) - fun error(message: String, range: TextRange): Nothing = - throw InterpreterException(message, range) + fun error(message: String, range: TextRange): Nothing = throw InterpreterException(message, range) + + enum class BinaryOperator(val op: String) { + LogicalOr("or"), + LogicalAnd("and"), + BitwiseOr("|"), + BitwiseXor("^"), + BitwiseAnd("&"), + LeftShift("<<"), + RightShift(">>"), + ArithLeftShift("<<<"), + ArithRightShift(">>>"), + Equals("=="), + NotEquals("!="), + GreaterThan(">"), + GreaterThanEq(">="), + LessThan("<"), + LessThanEq("<="), + Add("+"), + Subtract("-"), + Multiply("*"), + Divide("/"), + Modulo("%"), + } open class Scope(var outer: Scope?) { protected val bindings = mutableMapOf() @@ -468,20 +759,6 @@ class Interpreter(element: JaktPsiElement) { operator fun get(name: String): Value? = bindings[name] ?: outer?.get(name) operator fun set(name: String, value: Value) { - var scope: Scope? = this - - while (scope != null) { - if (name in scope) { - scope[name] = value - return - } - scope = scope.outer - } - - initialize(name, value) - } - - fun initialize(name: String, value: Value) { bindings[name] = value } } diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt index 16e8c099..a4170d89 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt @@ -50,7 +50,7 @@ data class ByteCharValue(val value: Byte) : Value() { override fun toString() = "b'$value'" } -data class TupleValue(val values: List) : Value() { +data class TupleValue(val values: MutableList) : Value() { override fun typeName() = "Tuple" override fun toString() = values.joinToString(prefix = "(", postfix = ")") } @@ -81,10 +81,10 @@ class UserFunctionValue( for ((index, param) in parameters.withIndex()) { if (index <= arguments.lastIndex) { - newScope.initialize(param.name, arguments[index]) + newScope[param.name] = arguments[index] } else { check(param.default != null) - newScope.initialize(param.name, param.default) + newScope[param.name] = param.default } } diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt index 8992208e..94262836 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt @@ -436,7 +436,7 @@ data class DictionaryIterator(val dictionary: DictionaryValue) : Value() { require(thisValue is DictionaryIterator) val nextKey = thisValue.remainingKeys.random() thisValue.remainingKeys.remove(nextKey) - TupleValue(listOf(nextKey, thisValue.dictionary.elements[nextKey]!!)) + TupleValue(mutableListOf(nextKey, thisValue.dictionary.elements[nextKey]!!)) } } } From b92a9d428fbd860f5983d3f7e3740b54e5626485 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Oct 2022 15:07:16 -0700 Subject: [PATCH 09/18] Interpreter: Initial support for format string functions --- .../serenityos/jakt/comptime/Interpreter.kt | 25 +- .../org/serenityos/jakt/comptime/Value.kt | 14 +- .../org/serenityos/jakt/comptime/builtins.kt | 56 ++- .../serenityos/jakt/comptime/extensions.kt | 17 +- .../serenityos/jakt/comptime/formatStrings.kt | 378 ++++++++++++++++++ .../jakt/psi/declaration/JaktImportMixin.kt | 4 +- 6 files changed, 468 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/org/serenityos/jakt/comptime/formatStrings.kt diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index fe9b2e46..4b690279 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -16,6 +16,9 @@ import org.serenityos.jakt.utils.unreachable class Interpreter(element: JaktPsiElement) { var scope: Scope + val stdout = StringBuilder() + val stderr = StringBuilder() + init { val outerScopes = element.ancestors().filter { it is JaktFile || it is JaktFunction || it is JaktStructDeclaration || it is JaktBlock @@ -722,6 +725,11 @@ class Interpreter(element: JaktPsiElement) { scope["File"] = FileStruct scope["___jakt_get_target_triple_string"] = jaktGetTargetTripleStringFunction scope["abort"] = abortFunction + scope["format"] = FormatFunction + scope["print"] = PrintFunction + scope["println"] = PrintlnFunction + scope["eprint"] = EprintFunction + scope["eprintln"] = EprintlnFunction } fun error(message: String, element: PsiElement): Nothing = error(message, element.textRange) @@ -781,9 +789,22 @@ class Interpreter(element: JaktPsiElement) { object Break : ExecutionResult } + data class Result( + val value: Value?, + val stdout: String, + val stderr: String, + ) + companion object { - fun evaluate(element: JaktPsiElement): ExecutionResult { - return Interpreter(element).evaluate(element) + fun evaluate(element: JaktPsiElement): Result { + val interpreter = Interpreter(element) + + val value = when (val result = interpreter.evaluate(element)) { + is ExecutionResult.Normal -> result.value + else -> null + } + + return Result(value, interpreter.stdout.toString(), interpreter.stderr.toString()) } } } diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt index a4170d89..588a2cf9 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt @@ -20,32 +20,36 @@ sealed class Value { } } +interface PrimitiveValue { + val value: Any +} + object VoidValue : Value() { override fun typeName() = "void" override fun toString() = "void" } -data class BoolValue(val value: Boolean) : Value() { +data class BoolValue(override val value: Boolean) : Value(), PrimitiveValue { override fun typeName() = "bool" override fun toString() = value.toString() } -data class IntegerValue(val value: Long) : Value() { +data class IntegerValue(override val value: Long) : Value(), PrimitiveValue { override fun typeName() = "i64" override fun toString() = value.toString() } -data class FloatValue(val value: Double) : Value() { +data class FloatValue(override val value: Double) : Value(), PrimitiveValue { override fun typeName() = "f64" override fun toString() = value.toString() } -data class CharValue(val value: Char) : Value() { +data class CharValue(override val value: Char) : Value(), PrimitiveValue { override fun typeName() = "c_char" override fun toString() = "'$value'" } -data class ByteCharValue(val value: Byte) : Value() { +data class ByteCharValue(override val value: Byte) : Value(), PrimitiveValue { override fun typeName() = "u8" override fun toString() = "b'$value'" } diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt index 94262836..0b179c5e 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt @@ -1,5 +1,6 @@ package org.serenityos.jakt.comptime +import org.serenityos.jakt.comptime.Interpreter.ExecutionResult import org.serenityos.jakt.project.JaktProjectListener import java.io.File @@ -7,8 +8,8 @@ class BuiltinFunction( parameterCount: Int, private val func: (Value?, List) -> Value, ) : FunctionValue(parameterCount, parameterCount) { - override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): Interpreter.ExecutionResult { - return Interpreter.ExecutionResult.Normal(func(thisValue, arguments)) + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): ExecutionResult { + return ExecutionResult.Normal(func(thisValue, arguments)) } } @@ -212,7 +213,7 @@ object StringStruct : Value() { override fun toString() = "StringStruct" } -data class StringValue(val value: String) : Value() { +data class StringValue(override val value: String) : Value(), PrimitiveValue { init { this["is_empty"] = isEmpty this["length"] = length @@ -712,7 +713,52 @@ val jaktGetTargetTripleStringFunction = BuiltinFunction(0) { _, _ -> val abortFunction = BuiltinFunction(0) { _, _ -> error("aborted") } +abstract class FormatLikeFunction : FunctionValue(1, Int.MAX_VALUE) { + protected fun getFormatString(arguments: List): String { + require(arguments.isNotEmpty()) + val fmtSpecifier = arguments[0] + require(fmtSpecifier is StringValue) + + val fmtString = FormatStringParser(fmtSpecifier.value).parse() + return fmtString.apply(arguments.drop(1)) + } +} + +object FormatFunction : FormatLikeFunction() { + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): ExecutionResult { + return ExecutionResult.Normal(StringValue(getFormatString(arguments))) + } +} + +object PrintFunction : FormatLikeFunction() { + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): ExecutionResult { + interpreter.stdout.append(getFormatString(arguments)) + return ExecutionResult.Normal(VoidValue) + } +} + +object PrintlnFunction : FormatLikeFunction() { + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): ExecutionResult { + interpreter.stdout.append(getFormatString(arguments)) + interpreter.stdout.append('\n') + return ExecutionResult.Normal(VoidValue) + } +} + +object EprintFunction : FormatLikeFunction() { + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): ExecutionResult { + interpreter.stderr.append(getFormatString(arguments)) + return ExecutionResult.Normal(VoidValue) + } +} + +object EprintlnFunction : FormatLikeFunction() { + override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List): ExecutionResult { + interpreter.stderr.append(getFormatString(arguments)) + interpreter.stderr.append('\n') + return ExecutionResult.Normal(VoidValue) + } +} + // TODO: saturated/truncated functions when we have generic information -// TODO: Format functions, not trivial since Kotlin does not support the -// Serenity/Python-style format arguments diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt index 23773d65..9b6cedd1 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt @@ -1,6 +1,5 @@ package org.serenityos.jakt.comptime -import com.intellij.openapi.util.Ref import com.intellij.psi.util.childrenOfType import org.serenityos.jakt.psi.JaktPsiElement import org.serenityos.jakt.psi.api.JaktBlock @@ -8,17 +7,11 @@ import org.serenityos.jakt.psi.api.JaktIfStatement import org.serenityos.jakt.psi.caching.comptimeCache import org.serenityos.jakt.psi.findChildOfType -val JaktPsiElement.comptimeValue: Value? - get() = comptimeCache().resolveWithCaching(this) { - try { - when (val result = Interpreter.evaluate(this)) { - is Interpreter.ExecutionResult.Normal -> result.value - else -> null - } - } catch (e: InterpreterException) { - null - }.let(::Ref) - }.get() +fun JaktPsiElement.performComptimeEvaluation(): Interpreter.Result { + return comptimeCache().resolveWithCaching(this) { + Interpreter.evaluate(this) + } +} // Utility accessors val JaktIfStatement.ifStatement: JaktIfStatement? diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/formatStrings.kt b/src/main/kotlin/org/serenityos/jakt/comptime/formatStrings.kt new file mode 100644 index 00000000..6822281b --- /dev/null +++ b/src/main/kotlin/org/serenityos/jakt/comptime/formatStrings.kt @@ -0,0 +1,378 @@ +package org.serenityos.jakt.comptime + +import com.intellij.openapi.util.Ref +import org.serenityos.jakt.utils.unreachable + +// Based on https://github.com/SerenityOS/serenity/blob/master/AK/Format.cpp + +data class FormatString( + private val literals: List, + private val specifierStrings: List, +) { + init { + require(specifierStrings.size == literals.size - 1) + } + + fun apply(arguments: List): String { + val context = Context(arguments) + val specifiers = specifierStrings.map { FormatSpecifierParser(it).parse(context) } + + return buildString { + append(literals[0]) + + for (i in specifiers.indices) { + specifiers[i].apply(this, arguments) + append(literals[i + 1]) + } + } + } +} + +data class Specifier( + var index: Int = 0, + var alignment: Alignment = Alignment.Right, + var sign: Sign = Sign.OnlyIfNeeded, + var mode: Mode = Mode.Default, + var alternative: Boolean = false, + var fillChar: Char = ' ', + var zeroPad: Boolean = false, + var width: Int? = null, + var precision: Int? = null, +) { + enum class Alignment { + Left, + Center, + Right, + } + + enum class Sign { + OnlyIfNeeded, + Always, + Reserved, + } + + enum class Mode(val str: kotlin.String) { + Default(""), + Binary("b"), + BinaryUppercase("B"), + Decimal("d"), + Octal("o"), + Hexadecimal("x"), + HexadecimalUppercase("X"), + Character("c"), + String("s"), + Pointer("p"), + Float("f"), + HexFloat("a"), + HexFloatUppercase("A"), + HexDump("hex-dump"), + } + + // TODO: This needs a lot of work to be accurate + fun apply(builder: StringBuilder, arguments: List) { + val target = arguments.getOrNull(index) ?: + error("Format specifier refers to argument ${index + 1}, but only ${arguments.size} arguments were provided") + + check(target is PrimitiveValue) { "Cannot format non-primitive type ${target.typeName()} at comptime" } + + check(mode != Mode.Pointer && mode != Mode.HexDump && mode != Mode.Binary && mode != Mode.BinaryUppercase) { + "Unsupported format specifier '${mode.str}'" + } + + check(alignment != Alignment.Center) { + "Unsupported alignment '^'" + } + + check(fillChar == ' ') { + "Unsupported non-space fill character" + } + + val javaFormatSpecifier = buildString { + append('%') + + if (alignment == Alignment.Left) + append('-') + + if (alternative) + append('#') + + when (sign) { + Sign.Always -> append('+') + Sign.Reserved -> append(' ') + Sign.OnlyIfNeeded -> {} + } + + if (zeroPad) + append('0') + + if (width != null) + append(width) + + if (precision != null) { + append('.') + append(precision) + } + + if (mode == Mode.Default) { + mode = when (target) { + is StringValue -> Mode.String + is IntegerValue -> Mode.Decimal + is FloatValue -> Mode.Float + is CharValue -> Mode.Character + else -> unreachable() + } + } + + append(mode.str) + } + + builder.append(javaFormatSpecifier.format(target.value)) + } +} + +open class GenericParser(val text: String) { + var cursor = 0 + + val done: Boolean + get() = cursor > text.lastIndex + + val char: Char + get() = text[cursor] + + fun consumeNumber(): Int? { + val pos = cursor + + while (!done && char.isDigit()) + cursor++ + + if (pos == cursor) + return null + + return text.substring(pos, cursor).toIntOrNull() ?: run { + cursor = pos + null + } + } + + fun consumeIf(char: Char): Boolean { + return if (matches(char)) { + cursor++ + true + } else false + } + + fun consumeIf(str: String): Boolean { + return if (matches(str)) { + cursor += str.length + true + } else false + } + + fun peek(n: Int = 0) = text.getOrNull(cursor + n) + + fun consume() = char.also { cursor++ } + + fun matches(ch: Char): Boolean { + return !done && ch == char + } + + fun matches(string: String): Boolean { + return text.substring(cursor).startsWith(string) + } +} + +class Context(val arguments: List, private var nextValue: Int = 0) { + fun nextIndex() = nextValue++ +} + +class FormatSpecifierParser(specifier: String) : GenericParser(specifier) { + fun parse(context: Context): Specifier = with(Specifier()) { + index = consumeNumber() ?: context.nextIndex() + + if (!consumeIf(':')) + return@with this + + if ("<^>".contains(peek(1) ?: 'a')) { + check(char !in "{}") { "Malformed specifier \"$text\"" } + fillChar = consume() + } + + alignment = when { + consumeIf('<') -> Specifier.Alignment.Left + consumeIf('^') -> Specifier.Alignment.Center + consumeIf('>') -> Specifier.Alignment.Right + else -> alignment + } + + sign = when { + consumeIf('-') -> Specifier.Sign.OnlyIfNeeded + consumeIf('+') -> Specifier.Sign.Always + consumeIf(' ') -> Specifier.Sign.Reserved + else -> sign + } + + if (consumeIf('#')) + alternative = true + + if (consumeIf('0')) + zeroPad = true + + val index = Ref(null) + if (consumeReplacementField(index)) { + if (index.isNull) + index.set(context.nextIndex()) + + val widthValue = context.arguments.getOrNull(index.get()!!) + check(widthValue != null) { + "Width parameter refers to non-existent argument ${index.get()!!}" + } + check(widthValue is IntegerValue) { + "Expected integer for width argument at index ${index.get()!!}, found ${widthValue.typeName()}" + } + width = widthValue.value.toInt() + } else { + val num = consumeNumber() + if (num != null) + width = num + } + + if (consumeIf('.')) { + if (consumeReplacementField(index)) { + if (index.isNull) + index.set(context.nextIndex()) + + val precisionValue = context.arguments.getOrNull(index.get()!!) + check(precisionValue != null) { + "Precision parameter refers to non-existent argument ${index.get()!!}" + } + check(precisionValue is IntegerValue) { + "Expected integer for precision argument at index ${index.get()!!}, found ${precisionValue.typeName()}" + } + precision = precisionValue.value.toInt() + } else { + val num = consumeNumber() + if (num != null) + precision = num + } + } + + mode = when { + consumeIf('b') -> Specifier.Mode.Binary + consumeIf('B') -> Specifier.Mode.BinaryUppercase + consumeIf('d') -> Specifier.Mode.Decimal + consumeIf('o') -> Specifier.Mode.Octal + consumeIf('x') -> Specifier.Mode.Hexadecimal + consumeIf('X') -> Specifier.Mode.HexadecimalUppercase + consumeIf('c') -> Specifier.Mode.Character + consumeIf('s') -> Specifier.Mode.String + consumeIf('P') -> Specifier.Mode.Pointer + consumeIf('f') -> Specifier.Mode.Float + consumeIf('a') -> Specifier.Mode.HexFloat + consumeIf('A') -> Specifier.Mode.HexFloatUppercase + consumeIf("hex-dump") -> Specifier.Mode.HexDump + matches('}') -> Specifier.Mode.Default + !done -> error("Unknown format specifier '$char'") + else -> mode + } + + check(consumeIf('}')) + + check(done) + + this + } + + private fun consumeReplacementField(ref: Ref): Boolean { + if (!consumeIf('{')) + return false + + ref.set(consumeNumber()) + + check(consumeIf('}')) + + return true + } +} + +class FormatStringParser(formatString: String) : GenericParser(formatString) { + fun parse(): FormatString { + if (done) + return FormatString(listOf(""), emptyList()) + + val literals = mutableListOf() + val specifiers = mutableListOf() + + while (true) { + literals.add(consumeLiteral()) + val specifier = consumeSpecifier() + + if (specifier == null) { + check(done) { + "Expected specifier at offset $cursor" + } + + return FormatString(literals, specifiers) + } + + specifiers.add(specifier) + } + } + + private fun consumeLiteral(): String { + val pos = cursor + + while (!done) { + if (consumeIf("{{")) + continue + + if (consumeIf("}}")) + continue + + if (matches("{") || matches("}")) + return text.substring(pos, cursor) + + cursor++ + } + + return text.substring(pos) + } + + private fun consumeSpecifier(): String? { + require(!matches("}")) { + "Unexpected '}' at offset $cursor" + } + + if (!consumeIf("{")) + return null + + val pos = cursor + + consumeNumber() + + if (consumeIf(":")) { + var level = 1 + + while (level > 0) { + check(!done) { + "Unexpected end of string in format specifier" + } + + if (matches("{")) + level++ + + if (matches("}")) + level-- + + cursor++ + } + + return text.substring(pos, cursor) + } + + check(consumeIf("}")) { + "Expected '}' at offset $cursor" + } + + return "" + } +} diff --git a/src/main/kotlin/org/serenityos/jakt/psi/declaration/JaktImportMixin.kt b/src/main/kotlin/org/serenityos/jakt/psi/declaration/JaktImportMixin.kt index ab5cb316..c8909298 100644 --- a/src/main/kotlin/org/serenityos/jakt/psi/declaration/JaktImportMixin.kt +++ b/src/main/kotlin/org/serenityos/jakt/psi/declaration/JaktImportMixin.kt @@ -6,7 +6,7 @@ import org.serenityos.jakt.JaktFile import org.serenityos.jakt.JaktTypes import org.serenityos.jakt.comptime.ArrayValue import org.serenityos.jakt.comptime.StringValue -import org.serenityos.jakt.comptime.comptimeValue +import org.serenityos.jakt.comptime.performComptimeEvaluation import org.serenityos.jakt.project.jaktProject import org.serenityos.jakt.psi.api.JaktImport import org.serenityos.jakt.psi.caching.typeCache @@ -33,7 +33,7 @@ val JaktImport.targetString: String get() = typeCache().resolveWithCaching(this) { importTarget.identifier?.let { return@resolveWithCaching it.text } - when (val comptimeValue = importTarget.callExpression?.comptimeValue) { + when (val comptimeValue = importTarget.callExpression?.performComptimeEvaluation()?.value) { is StringValue -> comptimeValue.value is ArrayValue -> { val project = jaktProject From 147f04c108a40c0bbf5dafce127f3ab1e2c92517 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Oct 2022 15:16:31 -0700 Subject: [PATCH 10/18] Interpreter: Handle binary operator expressions --- .../serenityos/jakt/comptime/Interpreter.kt | 135 +++++++++++++----- 1 file changed, 102 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 4b690279..49ae8575 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -131,7 +131,10 @@ class Interpreter(element: JaktPsiElement) { error("Expected integer, found ${index.typeName()}", element.right!!) if (index.value.toInt() > target.values.size) - error("Out-of-bounds assignment to array of length ${target.values.size} with index ${index.value}", assignmentTarget) + error( + "Out-of-bounds assignment to array of length ${target.values.size} with index ${index.value}", + assignmentTarget + ) target.values[index.value.toInt()] = newValue } @@ -195,15 +198,67 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(RangeValue(start.value, end.value, isInclusive = false)) } - is JaktLogicalOrBinaryExpression -> TODO() - is JaktLogicalAndBinaryExpression -> TODO() - is JaktBitwiseOrBinaryExpression -> TODO() - is JaktBitwiseXorBinaryExpression -> TODO() - is JaktBitwiseAndBinaryExpression -> TODO() - is JaktRelationalBinaryExpression -> TODO() - is JaktShiftBinaryExpression -> TODO() - is JaktAddBinaryExpression -> TODO() - is JaktMultiplyBinaryExpression -> TODO() + is JaktLogicalOrBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + BinaryOperator.LogicalOr + ) + is JaktLogicalAndBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + BinaryOperator.LogicalAnd + ) + is JaktBitwiseOrBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + BinaryOperator.BitwiseOr + ) + is JaktBitwiseXorBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + BinaryOperator.BitwiseXor + ) + is JaktBitwiseAndBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + BinaryOperator.BitwiseAnd + ) + is JaktRelationalBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + when { + element.doubleEquals != null -> BinaryOperator.Equals + element.notEquals != null -> BinaryOperator.NotEquals + element.greaterThan != null -> BinaryOperator.GreaterThan + element.greaterThanEquals != null -> BinaryOperator.GreaterThanEq + element.lessThan != null -> BinaryOperator.LessThan + else -> BinaryOperator.LessThanEq + }, + ) + is JaktShiftBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + when { + element.leftShift != null -> BinaryOperator.LeftShift + element.arithLeftShift != null -> BinaryOperator.ArithLeftShift + element.rightShift != null -> BinaryOperator.RightShift + else -> BinaryOperator.ArithRightShift + }, + ) + is JaktAddBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + if (element.plus != null) BinaryOperator.Add else BinaryOperator.Subtract, + ) + is JaktMultiplyBinaryExpression -> applyBinaryOperator( + element.left, + element.right!!, + when { + element.asterisk != null -> BinaryOperator.Multiply + element.slash != null -> BinaryOperator.Divide + else -> BinaryOperator.Modulo + }, + ) is JaktCastExpression -> TODO() is JaktIsExpression -> TODO() is JaktUnaryExpression -> TODO() @@ -395,23 +450,25 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(ArrayValue((0 until size.value).map { value }.toMutableList())) } - is JaktDictionaryExpression -> ExecutionResult.Normal(DictionaryValue( - element.dictionaryElementList.associate { - val key = when (val result = evaluate(it.expressionList[0])) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it.expressionList[0]) - else -> return result - } + is JaktDictionaryExpression -> ExecutionResult.Normal( + DictionaryValue( + element.dictionaryElementList.associate { + val key = when (val result = evaluate(it.expressionList[0])) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it.expressionList[0]) + else -> return result + } - val value = when (val result = evaluate(it.expressionList[1])) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it.expressionList[1]) - else -> return result - } + val value = when (val result = evaluate(it.expressionList[1])) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", it.expressionList[1]) + else -> return result + } - key to value - }.toMutableMap() - )) + key to value + }.toMutableMap() + ) + ) is JaktSetExpression -> ExecutionResult.Normal(SetValue(element.expressionList.map { when (val result = evaluate(it)) { is ExecutionResult.Normal -> result.value @@ -460,7 +517,10 @@ class Interpreter(element: JaktPsiElement) { if (condition.value) { when (val result = evaluate(element.block)) { is ExecutionResult.Normal -> ExecutionResult.Normal(VoidValue) - is ExecutionResult.Yield -> error("Unexpected yield", element.block.findChildOfType()!!) + is ExecutionResult.Yield -> error( + "Unexpected yield", + element.block.findChildOfType()!! + ) else -> return result } } else if (element.ifStatement != null) { @@ -468,7 +528,10 @@ class Interpreter(element: JaktPsiElement) { } else if (element.elseBlock != null) { when (val result = evaluate(element.elseBlock!!)) { is ExecutionResult.Normal -> ExecutionResult.Normal(VoidValue) - is ExecutionResult.Yield -> error("Unexpected yield", element.block.findChildOfType()!!) + is ExecutionResult.Yield -> error( + "Unexpected yield", + element.block.findChildOfType()!! + ) else -> return result } } else ExecutionResult.Normal(VoidValue) @@ -495,11 +558,13 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(VoidValue) } is JaktGuardStatement -> TODO() - is JaktYieldStatement -> ExecutionResult.Yield(when (val result = evaluate(element.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - }) + is JaktYieldStatement -> ExecutionResult.Yield( + when (val result = evaluate(element.expression)) { + is ExecutionResult.Normal -> result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.expression) + else -> return result + } + ) is JaktBreakStatement -> ExecutionResult.Break is JaktContinueStatement -> ExecutionResult.Continue is JaktUnsafeStatement -> error("Cannot evaluate unsafe blocks at comptime", element) @@ -541,7 +606,11 @@ class Interpreter(element: JaktPsiElement) { } } - private fun applyBinaryOperator(lhsExpr: JaktExpression, rhsExpr: JaktExpression, op: BinaryOperator): ExecutionResult { + private fun applyBinaryOperator( + lhsExpr: JaktExpression, + rhsExpr: JaktExpression, + op: BinaryOperator + ): ExecutionResult { if (op == BinaryOperator.LogicalOr || op == BinaryOperator.LogicalAnd) { val shortCircuitValue = op == BinaryOperator.LogicalOr From a50700879c29f84160e9f9e9689fdcbae0fbf935 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Oct 2022 15:59:54 -0700 Subject: [PATCH 11/18] Interpreter: Support defer statements --- .../kotlin/org/serenityos/jakt/comptime/Interpreter.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 49ae8575..6ea72cf5 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -54,6 +54,8 @@ class Interpreter(element: JaktPsiElement) { } fun popScope() { + for (defer in scope.defers) + evaluate(defer) // TODO: Do something with the return value? scope = scope.outer!! } @@ -503,7 +505,10 @@ class Interpreter(element: JaktPsiElement) { is ExecutionResult.Yield -> error("Unexpected yield", element.expression) else -> return result }) - is JaktDeferStatement -> TODO() + is JaktDeferStatement -> { + scope.defers.add(element.statement) + ExecutionResult.Normal(VoidValue) + } is JaktIfStatement -> { val condition = when (val result = evaluate(element.expression)) { is ExecutionResult.Normal -> result.value @@ -829,6 +834,8 @@ class Interpreter(element: JaktPsiElement) { } open class Scope(var outer: Scope?) { + val defers = mutableListOf() + protected val bindings = mutableMapOf() operator fun contains(name: String) = name in bindings From c03024a26b56864a9352da2abc0fb7623b6c9db3 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Oct 2022 16:26:42 -0700 Subject: [PATCH 12/18] Interpreter: Add evaluation helper method to eliminate code repetition --- .../serenityos/jakt/comptime/Interpreter.kt | 249 ++++-------------- 1 file changed, 56 insertions(+), 193 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 6ea72cf5..21d335d9 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -9,7 +9,7 @@ import org.serenityos.jakt.psi.JaktPsiElement import org.serenityos.jakt.psi.JaktScope import org.serenityos.jakt.psi.ancestors import org.serenityos.jakt.psi.api.* -import org.serenityos.jakt.psi.findChildOfType +import org.serenityos.jakt.psi.descendantOfType import org.serenityos.jakt.psi.reference.hasNamespace import org.serenityos.jakt.utils.unreachable @@ -94,15 +94,7 @@ class Interpreter(element: JaktPsiElement) { else -> return it } } - } else { - evaluate(element.right!!).let { - when (it) { - is ExecutionResult.Normal -> it.value - is ExecutionResult.Yield -> error("Unexpected yield", element.right!!) - else -> return it - } - } - } + } else evaluateNonYield(element.right!!) { return it } when (val assignmentTarget = element.left) { is JaktPlainQualifierExpression -> { @@ -114,20 +106,12 @@ class Interpreter(element: JaktPsiElement) { error("Unknown identifier \"$name\"", assignmentTarget) } is JaktIndexedAccessExpression -> { - val target = when (val result = evaluate(element.left)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.left) - else -> return result - } + val target = evaluateNonYield(element.left) { return it } if (target !is ArrayValue) error("Expected array, found ${target.typeName()}", element.left) - val index = when (val result = evaluate(element.right!!)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.right!!) - else -> return result - } + val index = evaluateNonYield(element.right!!) { return it } if (index !is IntegerValue) error("Expected integer, found ${index.typeName()}", element.right!!) @@ -141,11 +125,7 @@ class Interpreter(element: JaktPsiElement) { target.values[index.value.toInt()] = newValue } is JaktAccessExpression -> { - val target = when (val result = evaluate(element.left)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.left) - else -> return result - } + val target = evaluateNonYield(element.left) { return it } if (assignmentTarget.decimalLiteral != null) { if (target !is TupleValue) @@ -176,21 +156,9 @@ class Interpreter(element: JaktPsiElement) { else -> null to element.expressionList[0] } - val start = startExpr?.let { - when (val result = evaluate(it)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it) - else -> return result - } - } ?: IntegerValue(0) + val start = startExpr?.let { e -> evaluateNonYield(e) { return it } } ?: IntegerValue(0) - val end = startExpr?.let { - when (val result = evaluate(it)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it) - else -> return result - } - } ?: IntegerValue(Long.MAX_VALUE) + val end = startExpr?.let { e -> evaluateNonYield(e) { return it } } ?: IntegerValue(Long.MAX_VALUE) if (start !is IntegerValue) error("Expected range start value to be an integer", startExpr!!) @@ -297,11 +265,7 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(StringValue(element.stringLiteral!!.text.drop(1).dropLast(1))) } is JaktAccessExpression -> { - val target = when (val result = evaluate(element.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - } + val target = evaluateNonYield(element.expression) { return it } if (element.dotQuestionMark != null) TODO() @@ -317,17 +281,8 @@ class Interpreter(element: JaktPsiElement) { } } is JaktIndexedAccessExpression -> { - val target = when (val result = evaluate(element.expressionList[0])) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expressionList[0]) - else -> return result - } - - val value = when (val result = evaluate(element.expressionList[1])) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expressionList[1]) - else -> return result - } + val target = evaluateNonYield(element.expressionList[0]) { return it } + val value = evaluateNonYield(element.expressionList[1]) { return it } if (target !is ArrayValue) error("Unexpected index into non-array value", element.expressionList[0]) @@ -378,21 +333,13 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(value!!) } is JaktCallExpression -> { - val target = when (val result = evaluate(element.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - } + val target = evaluateNonYield(element.expression) { return it } if (target !is FunctionValue) error("\"${target.typeName()}\" is not callable", element.expression) - val args = element.argumentList.argumentList.map { - when (val result = evaluate(it.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - } + val args = element.argumentList.argumentList.map { arg -> + evaluateNonYield(arg.expression) { return it } } if (args.size !in target.validParamCount) { @@ -404,17 +351,9 @@ class Interpreter(element: JaktPsiElement) { } val thisValue = when (val expr = element.expression) { - is JaktAccessExpression -> when (val result = evaluate(expr.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", expr.expression) - else -> return result - } + is JaktAccessExpression -> evaluateNonYield(expr.expression) { return it } is JaktFieldAccessExpression -> (scope as FunctionScope).thisBinding!! - is JaktIndexedAccessExpression -> when (val result = evaluate(expr.expressionList.first())) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - } + is JaktIndexedAccessExpression -> evaluateNonYield(expr.expressionList.first()) { return it } else -> null } @@ -422,12 +361,8 @@ class Interpreter(element: JaktPsiElement) { } is JaktArrayExpression -> { element.elementsArrayBody?.let { body -> - val array = ArrayValue(body.expressionList.map { - when (val result = evaluate(it)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it) - else -> return result - } + val array = ArrayValue(body.expressionList.map { expr -> + evaluateNonYield(expr) { return it } }.toMutableList()) return ExecutionResult.Normal(array) @@ -435,17 +370,8 @@ class Interpreter(element: JaktPsiElement) { val body = element.sizedArrayBody!! - val value = when (val result = evaluate(body.expressionList[0])) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", body.expressionList[0]) - else -> return result - } - - val size = when (val result = evaluate(body.expressionList[1])) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", body.expressionList[1]) - else -> return result - } + val value = evaluateNonYield(body.expressionList[0]) { return it } + val size = evaluateNonYield(body.expressionList[1]) { return it } if (size !is IntegerValue) error("Array size initializer must be an integer", body.expressionList[1]) @@ -454,92 +380,49 @@ class Interpreter(element: JaktPsiElement) { } is JaktDictionaryExpression -> ExecutionResult.Normal( DictionaryValue( - element.dictionaryElementList.associate { - val key = when (val result = evaluate(it.expressionList[0])) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it.expressionList[0]) - else -> return result - } - - val value = when (val result = evaluate(it.expressionList[1])) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it.expressionList[1]) - else -> return result - } - + element.dictionaryElementList.associate { pair -> + val key = evaluateNonYield(pair.expressionList[0]) { return it } + val value = evaluateNonYield(pair.expressionList[1]) { return it } key to value }.toMutableMap() ) ) - is JaktSetExpression -> ExecutionResult.Normal(SetValue(element.expressionList.map { - when (val result = evaluate(it)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it) - else -> return result - } + is JaktSetExpression -> ExecutionResult.Normal(SetValue(element.expressionList.map { e -> + evaluateNonYield(e) { return it } }.toMutableSet())) - is JaktTupleExpression -> ExecutionResult.Normal(TupleValue(element.expressionList.map { - when (val result = evaluate(it)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it) - else -> return result - } + is JaktTupleExpression -> ExecutionResult.Normal(TupleValue(element.expressionList.map { e -> + evaluateNonYield(e) { return it } }.toMutableList())) /*** STATEMENTS ***/ - is JaktExpressionStatement -> when (val result = evaluate(element.expression)) { - is ExecutionResult.Normal -> ExecutionResult.Normal(result.value) - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result + is JaktExpressionStatement -> { + evaluateNonYield(element.expression) { return it } + ExecutionResult.Normal(VoidValue) } - is JaktReturnStatement -> ExecutionResult.Return(element.expression?.let { - when (val result = evaluate(it)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it) - else -> return result - } + is JaktReturnStatement -> ExecutionResult.Return(element.expression?.let {e -> + evaluateNonYield(e) { return it } } ?: VoidValue) - is JaktThrowStatement -> ExecutionResult.Throw(when (val result = evaluate(element.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - }) + is JaktThrowStatement -> ExecutionResult.Throw(evaluateNonYield(element.expression) { return it }) is JaktDeferStatement -> { scope.defers.add(element.statement) ExecutionResult.Normal(VoidValue) } is JaktIfStatement -> { - val condition = when (val result = evaluate(element.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - } + val condition = evaluateNonYield(element.expression) { return it } if (condition !is BoolValue) error("Expected bool", element.expression) if (condition.value) { - when (val result = evaluate(element.block)) { - is ExecutionResult.Normal -> ExecutionResult.Normal(VoidValue) - is ExecutionResult.Yield -> error( - "Unexpected yield", - element.block.findChildOfType()!! - ) - else -> return result - } + evaluateNonYield(element.block) { return it } } else if (element.ifStatement != null) { - evaluate(element.ifStatement!!) + evaluateNonYield(element.ifStatement!!) { return it } } else if (element.elseBlock != null) { - when (val result = evaluate(element.elseBlock!!)) { - is ExecutionResult.Normal -> ExecutionResult.Normal(VoidValue) - is ExecutionResult.Yield -> error( - "Unexpected yield", - element.block.findChildOfType()!! - ) - else -> return result - } - } else ExecutionResult.Normal(VoidValue) + evaluateNonYield(element.elseBlock!!) { return it } + } + + ExecutionResult.Normal(VoidValue) } is JaktWhileStatement -> TODO() is JaktLoopStatement -> TODO() @@ -552,24 +435,13 @@ class Interpreter(element: JaktPsiElement) { ) } - val rhs = when (val result = evaluate(element.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - } - + val rhs = evaluateNonYield(element.expression) { return it } assign(element.variableDeclList[0].name!!, rhs, initialize = true) ExecutionResult.Normal(VoidValue) } is JaktGuardStatement -> TODO() - is JaktYieldStatement -> ExecutionResult.Yield( - when (val result = evaluate(element.expression)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", element.expression) - else -> return result - } - ) + is JaktYieldStatement -> ExecutionResult.Yield(evaluateNonYield(element.expression) { return it }) is JaktBreakStatement -> ExecutionResult.Break is JaktContinueStatement -> ExecutionResult.Continue is JaktUnsafeStatement -> error("Cannot evaluate unsafe blocks at comptime", element) @@ -589,12 +461,8 @@ class Interpreter(element: JaktPsiElement) { } is JaktFunction -> { val parameters = element.parameterList.parameterList.map { param -> - val default = param.expression?.let { - when (val result = evaluate(it)) { - is ExecutionResult.Normal -> result.value - is ExecutionResult.Yield -> error("Unexpected yield", it) - else -> return result - } + val default = param.expression?.let { e -> + evaluateNonYield(e) { return it } } FunctionValue.Parameter(param.identifier.text, default) @@ -607,7 +475,7 @@ class Interpreter(element: JaktPsiElement) { // Ignored declarations (hoisted at scope initialization) is JaktImport -> ExecutionResult.Normal(VoidValue) - else -> error("${element::class.simpleName} is not support at comptime") + else -> error("${element::class.simpleName} is not supported at comptime") } } @@ -619,35 +487,22 @@ class Interpreter(element: JaktPsiElement) { if (op == BinaryOperator.LogicalOr || op == BinaryOperator.LogicalAnd) { val shortCircuitValue = op == BinaryOperator.LogicalOr - val lhsValue = when (val result = evaluate(lhsExpr)) { - is ExecutionResult.Normal -> result.value - else -> return result - } + val lhsValue = evaluateNonYield(lhsExpr) { return it } if (lhsValue !is BoolValue) error("Expected bool, found ${lhsValue.typeName()}", lhsExpr) if (lhsValue.value == shortCircuitValue) return ExecutionResult.Normal(BoolValue(shortCircuitValue)) - val rhsValue = when (val result = evaluate(rhsExpr)) { - is ExecutionResult.Normal -> result.value - else -> return result - } + val rhsValue = evaluateNonYield(rhsExpr) { return it } if (rhsValue !is BoolValue) error("Expected bool, found ${rhsValue.typeName()}", rhsExpr) ExecutionResult.Normal(rhsValue) } - val lhsValue = when (val result = evaluate(lhsExpr)) { - is ExecutionResult.Normal -> result.value - else -> return result - } - - val rhsValue = when (val result = evaluate(rhsExpr)) { - is ExecutionResult.Normal -> result.value - else -> return result - } + val lhsValue = evaluateNonYield(lhsExpr) { return it } + val rhsValue = evaluateNonYield(rhsExpr) { return it } fun incompatError(): Nothing { error( @@ -806,6 +661,14 @@ class Interpreter(element: JaktPsiElement) { scope["eprintln"] = EprintlnFunction } + private inline fun evaluateNonYield(element: JaktPsiElement, returner: (ExecutionResult) -> Nothing): Value { + when (val result = evaluate(element)) { + is ExecutionResult.Normal -> return result.value + is ExecutionResult.Yield -> error("Unexpected yield", element.descendantOfType() ?: element) + else -> returner(result) + } + } + fun error(message: String, element: PsiElement): Nothing = error(message, element.textRange) fun error(message: String, range: TextRange): Nothing = throw InterpreterException(message, range) From 312a3edf34d8487b6e861b763f4e6e06fd7cc178 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Oct 2022 23:00:24 -0700 Subject: [PATCH 13/18] Comptime: Add an action to evaluate the element under the cursor This action will, if possible, evaluate the element and then display a popup with the element it evaluated (to make sure the plugin didn't make a mistake), the result of the evaluation, and any output sent to stdout/stderr. --- .../jakt/comptime/ShowComptimeValueAction.kt | 151 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 8 + 2 files changed, 159 insertions(+) create mode 100644 src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt b/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt new file mode 100644 index 00000000..cc34c952 --- /dev/null +++ b/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt @@ -0,0 +1,151 @@ +package org.serenityos.jakt.comptime + +import com.intellij.codeInsight.documentation.DocumentationComponent +import com.intellij.codeInsight.documentation.DocumentationHtmlUtil +import com.intellij.codeInsight.hint.HintManagerImpl +import com.intellij.lang.documentation.DocumentationMarkup +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.impl.EditorCssFontResolver +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiElement +import com.intellij.ui.LightweightHint +import com.intellij.ui.scale.JBUIScale +import com.intellij.util.ui.HTMLEditorKitBuilder +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import org.serenityos.jakt.psi.JaktPsiElement +import org.serenityos.jakt.psi.ancestors +import org.serenityos.jakt.psi.api.JaktCallExpression +import org.serenityos.jakt.psi.api.JaktVariableDeclarationStatement +import java.awt.Font +import java.awt.Point +import javax.swing.JEditorPane +import javax.swing.text.StyledDocument + +class ShowComptimeValueAction : AnAction() { + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = e.getComptimeTargetElement() != null + } + + override fun actionPerformed(e: AnActionEvent) { + val element = e.getComptimeTargetElement() ?: return + val hint = LightweightHint(ComptimePopup(element)) + val editor = e.getData(CommonDataKeys.EDITOR)!! + + HintManagerImpl.getInstanceImpl().showEditorHint( + hint, + editor, + HintManagerImpl.getHintPosition(hint, editor, editor.caretModel.logicalPosition, 0), + 0, + 0, + false, + ) + } + + private fun AnActionEvent.getComptimeTargetElement(): JaktPsiElement? { + val baseElement = element ?: return null + + return baseElement.ancestors(withSelf = true).firstOrNull { + when (it) { + is JaktCallExpression, + is JaktVariableDeclarationStatement -> true + else -> false + } + } as? JaktPsiElement + } + + private val AnActionEvent.element: PsiElement? + get() { + // TODO: CommonDataKeys.PSI_ELEMENT? + val file = dataContext.getData(CommonDataKeys.PSI_FILE) ?: return null + val editor = dataContext.getData(CommonDataKeys.EDITOR) ?: return null + return file.findElementAt(editor.caretModel.offset) + } + + private operator fun Point.plus(other: Point) = Point(x + other.x, y + other.y) + + @Suppress("UnstableApiUsage") + private class ComptimePopup(element: JaktPsiElement) : JEditorPane() { + init { + val editorKit = HTMLEditorKitBuilder() + .withFontResolver(EditorCssFontResolver.getGlobalInstance()) + .build() + + DocumentationHtmlUtil.addDocumentationPaneDefaultCssRules(editorKit) + + // Overwrite a rule added by the above call: "html { padding-bottom: 8pm }" + editorKit.styleSheet.addRule("html { padding-bottom: 0px; }") + + this.editorKit = editorKit + border = JBUI.Borders.empty() + + // DocumentationEditorPane::applyFontProps + if (document is StyledDocument) { + val fontName = if (Registry.`is`("documentation.component.editor.font")) { + EditorColorsManager.getInstance().globalScheme.editorFontName + } else font.fontName + + font = UIUtil.getFontWithFallback( + fontName, + Font.PLAIN, + JBUIScale.scale(DocumentationComponent.getQuickDocFontSize().size), + ) + } + + buildText(element) + } + + private fun buildText(element: JaktPsiElement) { + val result = try { + Result.success(element.performComptimeEvaluation()) + } catch (e: Throwable) { + Result.failure(e) + } + + val builder = StringBuilder() + + builder.append("
")
+            // TODO: Render the text
+            builder.append(StringUtil.escapeXmlEntities(element.text))
+            builder.append("
") + if (result.isSuccess) { + val output = result.getOrThrow() + + builder.append(DocumentationMarkup.SECTIONS_START) + builder.append(DocumentationMarkup.SECTION_HEADER_START) + builder.append("Output") + builder.append(DocumentationMarkup.SECTION_SEPARATOR) + if (output.value == null) { + builder.append("Unable to evaluate element") + } else { + builder.append(StringUtil.escapeXmlEntities(output.value.toString())) + } + builder.append(DocumentationMarkup.SECTION_END) + + if (output.stdout.isNotEmpty()) { + builder.append(DocumentationMarkup.SECTION_HEADER_START) + builder.append("stdout") + builder.append(DocumentationMarkup.SECTION_SEPARATOR) + builder.append(StringUtil.escapeXmlEntities(output.stdout).replace("\n", "
")) + builder.append(DocumentationMarkup.SECTION_END) + } + + if (output.stderr.isNotEmpty()) { + builder.append(DocumentationMarkup.SECTION_HEADER_START) + builder.append("stderr") + builder.append(DocumentationMarkup.SECTION_SEPARATOR) + builder.append(StringUtil.escapeXmlEntities(output.stderr).replace("\n", "
")) + builder.append(DocumentationMarkup.SECTION_END) + } + } else { + builder.append(StringUtil.escapeXmlEntities("Internal error: ${result.exceptionOrNull()!!.message}")) + } + + text = builder.toString() + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 8f8bc8f0..24883eaa 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -221,4 +221,12 @@ + + + + + From ec8b5f99da331001e4689604b0b80f62522401a3 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Tue, 4 Oct 2022 18:39:56 -0700 Subject: [PATCH 14/18] Interpreter: Add more detail for unsupported expressions --- .../serenityos/jakt/comptime/Interpreter.kt | 24 +++++++++---------- .../jakt/comptime/ShowComptimeValueAction.kt | 1 - 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 21d335d9..56218c6d 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -66,9 +66,9 @@ class Interpreter(element: JaktPsiElement) { return when (element) { /*** EXPRESSIONS ***/ - is JaktMatchExpression -> TODO() - is JaktTryExpression -> TODO() - is JaktLambdaExpression -> TODO() + is JaktMatchExpression -> TODO("comptime match expressions") + is JaktTryExpression -> TODO("comptime try expressions") + is JaktLambdaExpression -> TODO("comptime lambdas") is JaktAssignmentBinaryExpression -> { val binaryOp = when { element.plusEquals != null -> BinaryOperator.Add @@ -145,8 +145,8 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(VoidValue) } - is JaktThisExpression -> TODO() - is JaktFieldAccessExpression -> TODO() + is JaktThisExpression -> TODO("comptime this expressions") + is JaktFieldAccessExpression -> TODO("comptime field access") is JaktRangeExpression -> { val (startExpr, endExpr) = when { element.expressionList.size == 2 -> element.expressionList[0] to element.expressionList[1] @@ -229,9 +229,9 @@ class Interpreter(element: JaktPsiElement) { else -> BinaryOperator.Modulo }, ) - is JaktCastExpression -> TODO() - is JaktIsExpression -> TODO() - is JaktUnaryExpression -> TODO() + is JaktCastExpression -> TODO("comptime casts") + is JaktIsExpression -> TODO("comptime is expressions") + is JaktUnaryExpression -> TODO("comptime unary expressions") is JaktBooleanLiteral -> ExecutionResult.Normal(BoolValue(element.trueKeyword != null)) is JaktNumericLiteral -> { element.binaryLiteral?.let { @@ -424,9 +424,9 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(VoidValue) } - is JaktWhileStatement -> TODO() - is JaktLoopStatement -> TODO() - is JaktForStatement -> TODO() + is JaktWhileStatement -> TODO("comptime while statements") + is JaktLoopStatement -> TODO("comptime loop statements") + is JaktForStatement -> TODO("comptime for statements") is JaktVariableDeclarationStatement -> { if (element.parenOpen != null) { error( @@ -440,7 +440,7 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(VoidValue) } - is JaktGuardStatement -> TODO() + is JaktGuardStatement -> TODO("comptime guard statements") is JaktYieldStatement -> ExecutionResult.Yield(evaluateNonYield(element.expression) { return it }) is JaktBreakStatement -> ExecutionResult.Break is JaktContinueStatement -> ExecutionResult.Continue diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt b/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt index cc34c952..addff86f 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt @@ -60,7 +60,6 @@ class ShowComptimeValueAction : AnAction() { private val AnActionEvent.element: PsiElement? get() { - // TODO: CommonDataKeys.PSI_ELEMENT? val file = dataContext.getData(CommonDataKeys.PSI_FILE) ?: return null val editor = dataContext.getData(CommonDataKeys.EDITOR) ?: return null return file.findElementAt(editor.caretModel.offset) From 5be67310964edb217d6747bf089d0aba2c37267a Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Tue, 4 Oct 2022 19:20:04 -0700 Subject: [PATCH 15/18] Interpreter: Support more expressions and statements --- .../serenityos/jakt/comptime/Interpreter.kt | 281 ++++++++++++++---- 1 file changed, 225 insertions(+), 56 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 56218c6d..726a8256 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -5,11 +5,9 @@ import com.intellij.psi.PsiElement import com.intellij.refactoring.suggested.endOffset import com.intellij.refactoring.suggested.startOffset import org.serenityos.jakt.JaktFile -import org.serenityos.jakt.psi.JaktPsiElement -import org.serenityos.jakt.psi.JaktScope -import org.serenityos.jakt.psi.ancestors +import org.serenityos.jakt.JaktTypes +import org.serenityos.jakt.psi.* import org.serenityos.jakt.psi.api.* -import org.serenityos.jakt.psi.descendantOfType import org.serenityos.jakt.psi.reference.hasNamespace import org.serenityos.jakt.utils.unreachable @@ -67,7 +65,28 @@ class Interpreter(element: JaktPsiElement) { /*** EXPRESSIONS ***/ is JaktMatchExpression -> TODO("comptime match expressions") - is JaktTryExpression -> TODO("comptime try expressions") + is JaktTryExpression -> { + when (val result = evaluate(element.expression ?: element.blockList[0])) { + is ExecutionResult.Throw -> if (element.catchKeyword != null) { + val pushedScope = if (element.catchDecl != null) { + val newScope = Scope(scope) + newScope[element.catchDecl!!.identifier.text] = result.value + pushScope(newScope) + true + } else false + + evaluate(element.blockList[1]) + + if (pushedScope) + popScope() + + } + !is ExecutionResult.Normal -> return result + else -> {} + } + + ExecutionResult.Normal(VoidValue) + } is JaktLambdaExpression -> TODO("comptime lambdas") is JaktAssignmentBinaryExpression -> { val binaryOp = when { @@ -96,52 +115,7 @@ class Interpreter(element: JaktPsiElement) { } } else evaluateNonYield(element.right!!) { return it } - when (val assignmentTarget = element.left) { - is JaktPlainQualifierExpression -> { - if (assignmentTarget.plainQualifier.hasNamespace) - error("Invalid assignment target", assignmentTarget) - - val name = assignmentTarget.plainQualifier.name!! - if (!assign(name, newValue, initialize = false)) - error("Unknown identifier \"$name\"", assignmentTarget) - } - is JaktIndexedAccessExpression -> { - val target = evaluateNonYield(element.left) { return it } - - if (target !is ArrayValue) - error("Expected array, found ${target.typeName()}", element.left) - - val index = evaluateNonYield(element.right!!) { return it } - - if (index !is IntegerValue) - error("Expected integer, found ${index.typeName()}", element.right!!) - - if (index.value.toInt() > target.values.size) - error( - "Out-of-bounds assignment to array of length ${target.values.size} with index ${index.value}", - assignmentTarget - ) - - target.values[index.value.toInt()] = newValue - } - is JaktAccessExpression -> { - val target = evaluateNonYield(element.left) { return it } - - if (assignmentTarget.decimalLiteral != null) { - if (target !is TupleValue) - error("Expected tuple, found ${target.typeName()}", element.left) - - val index = assignmentTarget.decimalLiteral!!.text.toInt() - if (index > target.values.size) - error("Cannot assign to index $index of tuple of length ${target.values.size}") - - target.values[index] = newValue - } else { - target[assignmentTarget.identifier!!.text] = newValue - } - } - else -> error("Invalid assignment target", assignmentTarget) - } + assign(element.left, newValue) ExecutionResult.Normal(VoidValue) } @@ -231,7 +205,33 @@ class Interpreter(element: JaktPsiElement) { ) is JaktCastExpression -> TODO("comptime casts") is JaktIsExpression -> TODO("comptime is expressions") - is JaktUnaryExpression -> TODO("comptime unary expressions") + is JaktUnaryExpression -> applyUnaryOperator( + element.expression, + when { + element.minus != null -> UnaryOperator.Minus + element.keywordNot != null -> UnaryOperator.Not + element.tilde != null -> UnaryOperator.BitwiseNot + element.ampersand != null -> when { + element.rawKeyword != null -> UnaryOperator.RawReference + element.mutKeyword != null -> UnaryOperator.MutReference + else -> UnaryOperator.Reference + } + element.asterisk != null -> UnaryOperator.Dereference + else -> { + val plusPlus = element.findChildOfType(JaktTypes.PLUS_PLUS) + if (plusPlus != null) { + if (plusPlus.startOffset < element.expression.startOffset) { + UnaryOperator.PrefixIncrement + } else UnaryOperator.PostfixIncrement + } else { + val minusMinus = element.findChildOfType(JaktTypes.MINUS_MINUS)!! + if (minusMinus.startOffset < element.expression.startOffset) { + UnaryOperator.PrefixDecrement + } else UnaryOperator.PostfixDecrement + } + } + } + ) is JaktBooleanLiteral -> ExecutionResult.Normal(BoolValue(element.trueKeyword != null)) is JaktNumericLiteral -> { element.binaryLiteral?.let { @@ -424,8 +424,35 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(VoidValue) } - is JaktWhileStatement -> TODO("comptime while statements") - is JaktLoopStatement -> TODO("comptime loop statements") + is JaktWhileStatement -> { + while (true) { + val exprResult = evaluateNonYield(element.expression) { return it } + if (exprResult !is BoolValue) + error("Expected bool value, found ${exprResult.typeName()}", element.expression) + + if (!exprResult.value) + break + + when (val blockResult = evaluate(element.block)) { + is ExecutionResult.Break -> break + is ExecutionResult.Continue, is ExecutionResult.Normal -> {} + else -> return blockResult + } + } + + ExecutionResult.Normal(VoidValue) + } + is JaktLoopStatement -> { + while (true) { + when (val blockResult = evaluate(element.block)) { + is ExecutionResult.Break -> break + is ExecutionResult.Continue, is ExecutionResult.Normal -> {} + else -> return blockResult + } + } + + ExecutionResult.Normal(VoidValue) + } is JaktForStatement -> TODO("comptime for statements") is JaktVariableDeclarationStatement -> { if (element.parenOpen != null) { @@ -440,7 +467,20 @@ class Interpreter(element: JaktPsiElement) { ExecutionResult.Normal(VoidValue) } - is JaktGuardStatement -> TODO("comptime guard statements") + is JaktGuardStatement -> { + val condition = evaluateNonYield(element.expression) { return it } + if (condition !is BoolValue) + error("Expected bool value, found ${condition.typeName()}", element.expression) + + if (condition.value) { + when (val result = evaluate(element.block)) { + is ExecutionResult.Normal -> error("Unexpected fallthrough from guard block", element.block) + else -> return result + } + } + + ExecutionResult.Normal(VoidValue) + } is JaktYieldStatement -> ExecutionResult.Yield(evaluateNonYield(element.expression) { return it }) is JaktBreakStatement -> ExecutionResult.Break is JaktContinueStatement -> ExecutionResult.Continue @@ -451,7 +491,7 @@ class Interpreter(element: JaktPsiElement) { try { element.statementList.forEach { val result = evaluate(it) - if (result is ExecutionResult.Yield || result is ExecutionResult.Return) + if (result !is ExecutionResult.Normal) return result } ExecutionResult.Normal(VoidValue) @@ -628,6 +668,120 @@ class Interpreter(element: JaktPsiElement) { return ExecutionResult.Normal(value) } + private fun applyUnaryOperator(expr: JaktExpression, op: UnaryOperator): ExecutionResult { + return when (op) { + UnaryOperator.PostfixIncrement, UnaryOperator.PostfixDecrement, + UnaryOperator.PrefixIncrement, UnaryOperator.PrefixDecrement -> { + val isPrefix = op == UnaryOperator.PrefixIncrement || op == UnaryOperator.PrefixDecrement + val isIncrement = op == UnaryOperator.PrefixIncrement || op == UnaryOperator.PostfixIncrement + + val delta = if (isIncrement) 1 else -1 + + val currValue = evaluateNonYield(expr) { return it } + val newValue = when (currValue) { + is IntegerValue -> IntegerValue(currValue.value + delta) + is FloatValue -> FloatValue(currValue.value + delta) + else -> error("Invalid type ${currValue.typeName()} for numeric prefix operator '${op.op}'", expr) + } + + assign(expr, newValue)?.let { return it } + + ExecutionResult.Normal(if (isPrefix) newValue else currValue) + } + UnaryOperator.Minus -> { + val value = evaluateNonYield(expr) { return it } + when (value) { + is IntegerValue -> IntegerValue(-value.value) + is FloatValue -> FloatValue(-value.value) + else -> error("Invalid type ${value.typeName()} for unary integer '-' operator", expr) + }.let(ExecutionResult::Normal) + } + UnaryOperator.Not -> { + val value = evaluateNonYield(expr) { return it } + if (value is BoolValue) { + ExecutionResult.Normal(BoolValue(!value.value)) + } else { + error("Invalid type ${value.typeName()} for boolean 'not' operator", expr) + } + } + UnaryOperator.BitwiseNot -> { + val value = evaluateNonYield(expr) { return it } + if (value is IntegerValue) { + ExecutionResult.Normal(IntegerValue(value.value.inv())) + } else { + error("Invalid type ${value.typeName()} for integer '~' operator", expr) + } + } + UnaryOperator.Reference -> TODO("comptime unary reference operator") + UnaryOperator.RawReference -> TODO("comptime unary reference operator") + UnaryOperator.MutReference -> TODO("comptime unary reference operator") + UnaryOperator.Dereference -> TODO("comptime unary dereference operator") + UnaryOperator.Unwrap -> { + val value = evaluateNonYield(expr) { return it } + + when (value) { + is OptionalValue -> if (value.value == null) { + error("Attempt to unwrap empty optional value", expr) + } else { + ExecutionResult.Normal(value.value) + } + else -> error("Invalid type ${value.typeName()} for optional unwrap operator '!'", expr) + } + } + } + } + + private fun assign(expr: JaktExpression, value: Value): ExecutionResult? { + when (expr) { + is JaktPlainQualifierExpression -> { + if (expr.plainQualifier.hasNamespace) + error("Invalid assignment target", expr) + + val name = expr.plainQualifier.name!! + if (!assign(name, value, initialize = false)) + error("Unknown identifier \"$name\"", expr) + } + is JaktIndexedAccessExpression -> { + val target = evaluateNonYield(expr.expressionList[0]) { return it } + + if (target !is ArrayValue) + error("Expected array, found ${target.typeName()}", expr.expressionList[0]) + + val index = evaluateNonYield(expr.expressionList[1]!!) { return it } + + if (index !is IntegerValue) + error("Expected integer, found ${index.typeName()}", expr.expressionList[1]!!) + + if (index.value.toInt() > target.values.size) + error( + "Out-of-bounds assignment to array of length ${target.values.size} with index ${index.value}", + expr + ) + + target.values[index.value.toInt()] = value + } + is JaktAccessExpression -> { + val target = evaluateNonYield(expr.expression) { return it } + + if (expr.decimalLiteral != null) { + if (target !is TupleValue) + error("Expected tuple, found ${target.typeName()}", expr.expression) + + val index = expr.decimalLiteral!!.text.toInt() + if (index > target.values.size) + error("Cannot assign to index $index of tuple of length ${target.values.size}") + + target.values[index] = value + } else { + target[expr.identifier!!.text] = value + } + } + else -> error("Invalid assignment target", expr) + } + + return null + } + private fun assign(name: String, value: Value, initialize: Boolean): Boolean { // TODO: Ensure bindings already exists in the scope @@ -673,6 +827,21 @@ class Interpreter(element: JaktPsiElement) { fun error(message: String, range: TextRange): Nothing = throw InterpreterException(message, range) + enum class UnaryOperator(val op: String) { + PrefixIncrement("++"), + PrefixDecrement("--"), + PostfixIncrement("++"), + PostfixDecrement("--"), + Minus("-"), + Not("not"), + BitwiseNot("~"), + Reference("&"), + RawReference("&raw"), + MutReference("&mut"), + Dereference("*"), + Unwrap("!"), + } + enum class BinaryOperator(val op: String) { LogicalOr("or"), LogicalAnd("and"), From f64ae673fb66eeeeba2d5235f30acfa591380105 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Wed, 5 Oct 2022 03:42:06 -0700 Subject: [PATCH 16/18] Interpreter: Fix a bunch of small bugs --- .../serenityos/jakt/comptime/Interpreter.kt | 21 +++++++++++-------- .../org/serenityos/jakt/comptime/Value.kt | 5 ++++- .../org/serenityos/jakt/comptime/builtins.kt | 8 +++---- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 726a8256..848d9a62 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -217,6 +217,7 @@ class Interpreter(element: JaktPsiElement) { else -> UnaryOperator.Reference } element.asterisk != null -> UnaryOperator.Dereference + element.exclamationPoint != null -> UnaryOperator.Unwrap else -> { val plusPlus = element.findChildOfType(JaktTypes.PLUS_PLUS) if (plusPlus != null) { @@ -235,15 +236,15 @@ class Interpreter(element: JaktPsiElement) { is JaktBooleanLiteral -> ExecutionResult.Normal(BoolValue(element.trueKeyword != null)) is JaktNumericLiteral -> { element.binaryLiteral?.let { - return ExecutionResult.Normal(IntegerValue(it.text.toLong(2))) + return ExecutionResult.Normal(IntegerValue(it.text.drop(2).toLong(2))) } element.octalLiteral?.let { - return ExecutionResult.Normal(IntegerValue(it.text.toLong(8))) + return ExecutionResult.Normal(IntegerValue(it.text.drop(2).toLong(8))) } element.hexLiteral?.let { - return ExecutionResult.Normal(IntegerValue(it.text.toLong(16))) + return ExecutionResult.Normal(IntegerValue(it.text.drop(2).toLong(16))) } val decimalText = element.decimalLiteral!!.text @@ -259,7 +260,7 @@ class Interpreter(element: JaktPsiElement) { } element.charLiteral?.let { - return ExecutionResult.Normal(CharValue(it.text.single())) + return ExecutionResult.Normal(CharValue(it.text[1].code.toChar())) } ExecutionResult.Normal(StringValue(element.stringLiteral!!.text.drop(1).dropLast(1))) @@ -538,7 +539,7 @@ class Interpreter(element: JaktPsiElement) { if (rhsValue !is BoolValue) error("Expected bool, found ${rhsValue.typeName()}", rhsExpr) - ExecutionResult.Normal(rhsValue) + return ExecutionResult.Normal(rhsValue) } val lhsValue = evaluateNonYield(lhsExpr) { return it } @@ -785,6 +786,11 @@ class Interpreter(element: JaktPsiElement) { private fun assign(name: String, value: Value, initialize: Boolean): Boolean { // TODO: Ensure bindings already exists in the scope + if (initialize) { + scope[name] = value + return true + } + var currScope: Scope? = scope while (currScope != null) { if (name in currScope) { @@ -795,10 +801,7 @@ class Interpreter(element: JaktPsiElement) { currScope = currScope.outer } - return if (initialize) { - scope[name] = value - true - } else false + return false } private fun initializeGlobalScope(scope: Scope) { diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt index 588a2cf9..c9a6a8a8 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Value.kt @@ -1,6 +1,7 @@ package org.serenityos.jakt.comptime import org.serenityos.jakt.psi.JaktPsiElement +import org.serenityos.jakt.psi.api.JaktExpression // As with everything else in the plugin, we are very lenient when it comes to types. // All integers are treated as i64, just to make my life easier. Similarly, all float @@ -95,7 +96,9 @@ class UserFunctionValue( interpreter.pushScope(newScope) return when (val result = interpreter.evaluate(body)) { - is Interpreter.ExecutionResult.Normal -> Interpreter.ExecutionResult.Normal(VoidValue) + is Interpreter.ExecutionResult.Normal -> if (body is JaktExpression) { + Interpreter.ExecutionResult.Normal(result.value) + } else Interpreter.ExecutionResult.Normal(VoidValue) is Interpreter.ExecutionResult.Return -> Interpreter.ExecutionResult.Normal(result.value) is Interpreter.ExecutionResult.Throw -> result else -> interpreter.error("Unexpected control flow", body) diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt index 0b179c5e..1c4133d1 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/builtins.kt @@ -192,7 +192,7 @@ object StringStruct : Value() { val (char, count) = arguments require(char is CharValue && count is IntegerValue) StringValue(buildString { - repeat(count.value.toInt()) { append(char) } + repeat(count.value.toInt()) { append(char.value) } }) } @@ -274,17 +274,17 @@ data class StringValue(override val value: String) : Value(), PrimitiveValue { require(thisValue is StringValue) val (start, end) = arguments require(start is IntegerValue && end is IntegerValue) - StringValue(thisValue.value.substring(start.value.toInt(), end.value.toInt())) + StringValue(thisValue.value.substring(start.value.toInt(), (start.value + end.value).toInt())) } private val toUInt = BuiltinFunction(0) { thisValue, _ -> require(thisValue is StringValue) - IntegerValue(thisValue.value.toLong()) + OptionalValue(thisValue.value.toLongOrNull()?.let(::IntegerValue)) } private val toInt = BuiltinFunction(0) { thisValue, _ -> require(thisValue is StringValue) - IntegerValue(thisValue.value.toLong()) + OptionalValue(thisValue.value.toLongOrNull()?.let(::IntegerValue)) } private val isWhitespace = BuiltinFunction(0) { thisValue, _ -> From 10fa59d0e5b8b535649a01879a8e384f03542cac Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Wed, 5 Oct 2022 03:42:30 -0700 Subject: [PATCH 17/18] Tests: Add comptime interpreter tests --- .../jakt/comptime/ShowComptimeValueAction.kt | 19 +------ .../serenityos/jakt/comptime/extensions.kt | 16 ++++++ .../org/serenityos/jakt/JaktBaseTest.kt | 9 ++-- .../jakt/comptime/JaktComptimeTest.kt | 41 ++++++++++++++ .../jakt/comptime/JaktStringComptimeTest.kt | 53 +++++++++++++++++++ 5 files changed, 118 insertions(+), 20 deletions(-) create mode 100644 src/test/kotlin/org/serenityos/jakt/comptime/JaktComptimeTest.kt create mode 100644 src/test/kotlin/org/serenityos/jakt/comptime/JaktStringComptimeTest.kt diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt b/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt index addff86f..b3ca8908 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/ShowComptimeValueAction.kt @@ -18,9 +18,6 @@ import com.intellij.util.ui.HTMLEditorKitBuilder import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import org.serenityos.jakt.psi.JaktPsiElement -import org.serenityos.jakt.psi.ancestors -import org.serenityos.jakt.psi.api.JaktCallExpression -import org.serenityos.jakt.psi.api.JaktVariableDeclarationStatement import java.awt.Font import java.awt.Point import javax.swing.JEditorPane @@ -28,11 +25,11 @@ import javax.swing.text.StyledDocument class ShowComptimeValueAction : AnAction() { override fun update(e: AnActionEvent) { - e.presentation.isEnabledAndVisible = e.getComptimeTargetElement() != null + e.presentation.isEnabledAndVisible = e.element?.getComptimeTargetElement() != null } override fun actionPerformed(e: AnActionEvent) { - val element = e.getComptimeTargetElement() ?: return + val element = e.element?.getComptimeTargetElement() ?: return val hint = LightweightHint(ComptimePopup(element)) val editor = e.getData(CommonDataKeys.EDITOR)!! @@ -46,18 +43,6 @@ class ShowComptimeValueAction : AnAction() { ) } - private fun AnActionEvent.getComptimeTargetElement(): JaktPsiElement? { - val baseElement = element ?: return null - - return baseElement.ancestors(withSelf = true).firstOrNull { - when (it) { - is JaktCallExpression, - is JaktVariableDeclarationStatement -> true - else -> false - } - } as? JaktPsiElement - } - private val AnActionEvent.element: PsiElement? get() { val file = dataContext.getData(CommonDataKeys.PSI_FILE) ?: return null diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt index 9b6cedd1..e1343727 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/extensions.kt @@ -1,9 +1,13 @@ package org.serenityos.jakt.comptime +import com.intellij.psi.PsiElement import com.intellij.psi.util.childrenOfType import org.serenityos.jakt.psi.JaktPsiElement +import org.serenityos.jakt.psi.ancestors import org.serenityos.jakt.psi.api.JaktBlock +import org.serenityos.jakt.psi.api.JaktCallExpression import org.serenityos.jakt.psi.api.JaktIfStatement +import org.serenityos.jakt.psi.api.JaktVariableDeclarationStatement import org.serenityos.jakt.psi.caching.comptimeCache import org.serenityos.jakt.psi.findChildOfType @@ -13,6 +17,18 @@ fun JaktPsiElement.performComptimeEvaluation(): Interpreter.Result { } } +fun PsiElement.getComptimeTargetElement(): JaktPsiElement? { + val baseElement = this + + return baseElement.ancestors(withSelf = true).firstOrNull { + when (it) { + is JaktCallExpression, + is JaktVariableDeclarationStatement -> true + else -> false + } + } as? JaktPsiElement +} + // Utility accessors val JaktIfStatement.ifStatement: JaktIfStatement? get() = findChildOfType() diff --git a/src/test/kotlin/org/serenityos/jakt/JaktBaseTest.kt b/src/test/kotlin/org/serenityos/jakt/JaktBaseTest.kt index 8abbcedd..4ddc75ff 100644 --- a/src/test/kotlin/org/serenityos/jakt/JaktBaseTest.kt +++ b/src/test/kotlin/org/serenityos/jakt/JaktBaseTest.kt @@ -27,9 +27,8 @@ abstract class JaktBaseTest : BasePlatformTestCase() { tagElements.forEach { comment -> val matches = tagRegex.findAll(comment.text).toList() - check(matches.isNotEmpty()) { - "Comment in test with no tags" - } + if (matches.isEmpty()) + return@forEach for (match in matches) { val group = match.groups[1] ?: error("Invalid tag comment") @@ -52,6 +51,10 @@ abstract class JaktBaseTest : BasePlatformTestCase() { } } + check(taggedElements.isNotEmpty()) { + "No tagged elements found" + } + return taggedElements } diff --git a/src/test/kotlin/org/serenityos/jakt/comptime/JaktComptimeTest.kt b/src/test/kotlin/org/serenityos/jakt/comptime/JaktComptimeTest.kt new file mode 100644 index 00000000..567cd6ad --- /dev/null +++ b/src/test/kotlin/org/serenityos/jakt/comptime/JaktComptimeTest.kt @@ -0,0 +1,41 @@ +package org.serenityos.jakt.comptime + +import org.intellij.lang.annotations.Language +import org.serenityos.jakt.JaktBaseTest + +abstract class JaktComptimeTest : JaktBaseTest() { + protected fun doTest(@Language("Jakt") text: String, test: (Interpreter.Result) -> Unit) { + setupFor(text) + + val taggedElements = extractTaggedElements() + val elements = taggedElements["T"] + check(!elements.isNullOrEmpty()) { + "No tagged elements found" + } + + check(elements.size == 1) { + "More than one tagged element found" + } + + val targetElement = elements.single().getComptimeTargetElement() + check(targetElement != null) { + "Element cannot be evaluated at comptime" + } + + test(Interpreter.evaluate(targetElement)) + } + + protected fun doStdoutTest(@Language("Jakt") text: String, expectedStdout: String) = doTest(text) { + check(expectedStdout == it.stdout) + } + + protected fun doStderrTest(@Language("Jakt") text: String, expectedStderr: String) = doTest(text) { + check(expectedStderr == it.stderr) + } + + protected fun doValueTest(@Language("Jakt") text: String, expectedValue: Value) = doTest(text) { + check(it.value == expectedValue) { + "Expected $expectedValue, found ${it.value}" + } + } +} diff --git a/src/test/kotlin/org/serenityos/jakt/comptime/JaktStringComptimeTest.kt b/src/test/kotlin/org/serenityos/jakt/comptime/JaktStringComptimeTest.kt new file mode 100644 index 00000000..d8e9bdd7 --- /dev/null +++ b/src/test/kotlin/org/serenityos/jakt/comptime/JaktStringComptimeTest.kt @@ -0,0 +1,53 @@ +package org.serenityos.jakt.comptime + +class JaktStringComptimeTest : JaktComptimeTest() { + fun `test string methods`() = doStdoutTest(""" + comptime empty() throws => "".is_empty() + comptime length() throws => "a string of length 21".length() + comptime substring() throws => "abcdef".substring(start: 1, length: 3) + comptime hash() throws => "well, hello friends".hash() + comptime number() throws => String::number(123) + comptime to_uint() throws => "123".to_uint() + comptime to_int() throws => "-456".to_int() + comptime is_whitespace() throws => " ".is_whitespace() and not "abc".is_whitespace() + comptime contains() throws => "abcdef".contains("bcd") + comptime replace() throws => "well, hiya friends".replace(replace: "hiya", with: "hello") + comptime byte_at() throws => "AAAA".byte_at(3) + comptime starts_with() throws => "abcdef".starts_with("abc") + comptime ends_with() throws => "abcdef".ends_with("def") + comptime repeated() throws => String::repeated(character: 'A', count: 5) + comptime split() throws => "a;b;c".split(';') + + comptime test() throws { + mut success = empty() + success = success and length() == 21 + success = success and substring() == "bcd" + success = success and hash() == "well, hello friends".hash() + success = success and number() == "123" + success = success and to_uint()! == 123u32 + success = success and to_int()! == -456i32 + success = success and is_whitespace() + success = success and contains() + success = success and replace() == "well, hello friends" + success = success and byte_at() == 0x41 + success = success and starts_with() + success = success and ends_with() + success = success and repeated() == "AAAAA" + let parts = split() + success = success and parts[0] == "a" + success = success and parts[1] == "b" + success = success and parts[2] == "c" + + if success { + print("PASS") + } else { + print("FAIL") + } + } + + function main() { + test() + //^T + } + """, "PASS") +} From 22ddb06a4cdfbe3a3c4edd5ae10fec6bcb6127f3 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Wed, 5 Oct 2022 03:45:38 -0700 Subject: [PATCH 18/18] Tests: Add comptime bitwise operator test --- .../serenityos/jakt/comptime/Interpreter.kt | 1 + .../jakt/comptime/JaktBasicComptimeTest.kt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/test/kotlin/org/serenityos/jakt/comptime/JaktBasicComptimeTest.kt diff --git a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt index 848d9a62..1e20f032 100644 --- a/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt +++ b/src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt @@ -394,6 +394,7 @@ class Interpreter(element: JaktPsiElement) { is JaktTupleExpression -> ExecutionResult.Normal(TupleValue(element.expressionList.map { e -> evaluateNonYield(e) { return it } }.toMutableList())) + is JaktParenExpression -> evaluate(element.expression!!) /*** STATEMENTS ***/ diff --git a/src/test/kotlin/org/serenityos/jakt/comptime/JaktBasicComptimeTest.kt b/src/test/kotlin/org/serenityos/jakt/comptime/JaktBasicComptimeTest.kt new file mode 100644 index 00000000..cdff0a18 --- /dev/null +++ b/src/test/kotlin/org/serenityos/jakt/comptime/JaktBasicComptimeTest.kt @@ -0,0 +1,18 @@ +package org.serenityos.jakt.comptime + +class JaktBasicComptimeTest : JaktComptimeTest() { + fun `test bitwise operators`() = doStdoutTest(""" + comptime bitwise() { + if (((0x123 ^ 0x456) << 12) | (0x789 & 0xabc)) == 0x575288 { + print("PASS") + } else { + print("FAIL") + } + } + + function main() { + bitwise() + //^T + } + """.trimIndent(), "PASS") +}