Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add a comptime interpreter #9

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 -> {
Expand Down
922 changes: 922 additions & 0 deletions src/main/kotlin/org/serenityos/jakt/comptime/Interpreter.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.serenityos.jakt.comptime

import com.intellij.openapi.util.TextRange

class InterpreterException(message: String, val range: TextRange) : Exception(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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 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.element?.getComptimeTargetElement() != null
}

override fun actionPerformed(e: AnActionEvent) {
val element = e.element?.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 val AnActionEvent.element: PsiElement?
get() {
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("<pre>")
// TODO: Render the text
builder.append(StringUtil.escapeXmlEntities(element.text))
builder.append("</pre>")
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", "<br />"))
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", "<br />"))
builder.append(DocumentationMarkup.SECTION_END)
}
} else {
builder.append(StringUtil.escapeXmlEntities("Internal error: ${result.exceptionOrNull()!!.message}"))
}

text = builder.toString()
}
}
}
109 changes: 109 additions & 0 deletions src/main/kotlin/org/serenityos/jakt/comptime/Value.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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 class Value {
private val fields = mutableMapOf<String, Value>()

abstract fun typeName(): String

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
}
}

interface PrimitiveValue {
val value: Any
}

object VoidValue : Value() {
override fun typeName() = "void"
override fun toString() = "void"
}

data class BoolValue(override val value: Boolean) : Value(), PrimitiveValue {
override fun typeName() = "bool"
override fun toString() = value.toString()
}

data class IntegerValue(override val value: Long) : Value(), PrimitiveValue {
override fun typeName() = "i64"
override fun toString() = value.toString()
}

data class FloatValue(override val value: Double) : Value(), PrimitiveValue {
override fun typeName() = "f64"
override fun toString() = value.toString()
}

data class CharValue(override val value: Char) : Value(), PrimitiveValue {
override fun typeName() = "c_char"
override fun toString() = "'$value'"
}

data class ByteCharValue(override val value: Byte) : Value(), PrimitiveValue {
override fun typeName() = "u8"
override fun toString() = "b'$value'"
}

data class TupleValue(val values: MutableList<Value>) : Value() {
override fun typeName() = "Tuple"
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

init {
require(minParamCount <= maxParamCount)
}

override fun typeName() = "function"

constructor(parameters: List<Parameter>) : this(parameters.count { it.default == null }, parameters.size)

abstract fun call(interpreter: Interpreter, thisValue: Value?, arguments: List<Value>): Interpreter.ExecutionResult

data class Parameter(val name: String, val default: Value? = null)
}

class UserFunctionValue(
private val parameters: List<Parameter>,
val body: JaktPsiElement /* JaktBlock | JaktExpression */, // TODO: Storing PSI is bad, right?
) : FunctionValue(parameters) {
override fun call(interpreter: Interpreter, thisValue: Value?, arguments: List<Value>): Interpreter.ExecutionResult {
val newScope = Interpreter.FunctionScope(interpreter.scope, thisValue)

for ((index, param) in parameters.withIndex()) {
if (index <= arguments.lastIndex) {
newScope[param.name] = arguments[index]
} else {
check(param.default != null)
newScope[param.name] = param.default
}
}

interpreter.pushScope(newScope)

return when (val result = interpreter.evaluate(body)) {
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)
}.also {
interpreter.popScope()
}
}
}
Loading