pork: it's got it all, ffi, state machine tokenizer, and better IDE support

This commit is contained in:
Alex Zenla 2023-10-13 01:04:35 -07:00
parent d355fb3914
commit 5078f38f61
Signed by: alex
GPG Key ID: C0780728420EBFE5
58 changed files with 939 additions and 293 deletions

View File

@ -178,13 +178,20 @@ types:
type: Block
- name: elseBlock
type: Block?
ImportPath:
parent: Node
referencedElementValue: components
referencedElementType: CompilationUnit
values:
- name: components
type: List<Symbol>
ImportDeclaration:
parent: Declaration
values:
- name: form
type: Symbol
- name: components
type: List<Symbol>
- name: path
type: ImportPath
IntegerLiteral:
parent: Expression
values:

View File

@ -18,6 +18,7 @@ digraph A {
type_FunctionDefinition [shape=box,label="FunctionDefinition"]
type_LetDefinition [shape=box,label="LetDefinition"]
type_If [shape=box,label="If"]
type_ImportPath [shape=box,label="ImportPath"]
type_ImportDeclaration [shape=box,label="ImportDeclaration"]
type_IntegerLiteral [shape=box,label="IntegerLiteral"]
type_LongLiteral [shape=box,label="LongLiteral"]
@ -45,6 +46,7 @@ digraph A {
type_Node -> type_Block
type_Node -> type_CompilationUnit
type_Node -> type_ArgumentSpec
type_Node -> type_ImportPath
type_Node -> type_ForInItem
type_Node -> type_Native
type_Expression -> type_LetAssignment
@ -98,7 +100,9 @@ digraph A {
type_LetDefinition -> type_Expression [style=dotted]
type_If -> type_Expression [style=dotted]
type_If -> type_Block [style=dotted]
type_ImportPath -> type_Symbol [style=dotted]
type_ImportDeclaration -> type_Symbol [style=dotted]
type_ImportDeclaration -> type_ImportPath [style=dotted]
type_ListLiteral -> type_Expression [style=dotted]
type_Parentheses -> type_Expression [style=dotted]
type_PrefixOperation -> type_PrefixOperator [style=dotted]

View File

@ -6,23 +6,23 @@ import kotlinx.serialization.Serializable
@Serializable
@SerialName("importDeclaration")
class ImportDeclaration(val form: Symbol, val components: List<Symbol>) : Declaration() {
class ImportDeclaration(val form: Symbol, val path: ImportPath) : Declaration() {
override val type: NodeType = NodeType.ImportDeclaration
override fun <T> visitChildren(visitor: NodeVisitor<T>): List<T> =
visitor.visitAll(listOf(form), components)
visitor.visitNodes(form, path)
override fun <T> visit(visitor: NodeVisitor<T>): T =
visitor.visitImportDeclaration(this)
override fun equals(other: Any?): Boolean {
if (other !is ImportDeclaration) return false
return other.form == form && other.components == components
return other.form == form && other.path == path
}
override fun hashCode(): Int {
var result = form.hashCode()
result = 31 * result + components.hashCode()
result = 31 * result + path.hashCode()
result = 31 * result + type.hashCode()
return result
}

View File

@ -0,0 +1,28 @@
// GENERATED CODE FROM PORK AST CODEGEN
package gay.pizza.pork.ast.gen
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@SerialName("importPath")
class ImportPath(val components: List<Symbol>) : Node() {
override val type: NodeType = NodeType.ImportPath
override fun <T> visitChildren(visitor: NodeVisitor<T>): List<T> =
visitor.visitAll(components)
override fun <T> visit(visitor: NodeVisitor<T>): T =
visitor.visitImportPath(this)
override fun equals(other: Any?): Boolean {
if (other !is ImportPath) return false
return other.components == components
}
override fun hashCode(): Int {
var result = components.hashCode()
result = 31 * result + type.hashCode()
return result
}
}

View File

@ -41,6 +41,9 @@ class NodeCoalescer(val followChildren: Boolean = true, val handler: (Node) -> U
override fun visitImportDeclaration(node: ImportDeclaration): Unit =
handle(node)
override fun visitImportPath(node: ImportPath): Unit =
handle(node)
override fun visitIndexedBy(node: IndexedBy): Unit =
handle(node)

View File

@ -34,6 +34,8 @@ interface NodeParser {
fun parseImportDeclaration(): ImportDeclaration
fun parseImportPath(): ImportPath
fun parseIndexedBy(): IndexedBy
fun parseInfixOperation(): InfixOperation

View File

@ -19,6 +19,7 @@ fun NodeParser.parse(type: NodeType): Node =
NodeType.FunctionDefinition -> parseFunctionDefinition()
NodeType.LetDefinition -> parseLetDefinition()
NodeType.If -> parseIf()
NodeType.ImportPath -> parseImportPath()
NodeType.ImportDeclaration -> parseImportDeclaration()
NodeType.IntegerLiteral -> parseIntegerLiteral()
NodeType.LongLiteral -> parseLongLiteral()

View File

@ -19,6 +19,7 @@ enum class NodeType(val parent: NodeType? = null) {
FunctionDefinition(Definition),
If(Expression),
ImportDeclaration(Declaration),
ImportPath(Node),
IndexedBy(Expression),
InfixOperation(Expression),
IntegerLiteral(Expression),

View File

@ -28,6 +28,8 @@ interface NodeVisitor<T> {
fun visitImportDeclaration(node: ImportDeclaration): T
fun visitImportPath(node: ImportPath): T
fun visitIndexedBy(node: IndexedBy): T
fun visitInfixOperation(node: InfixOperation): T

View File

@ -16,6 +16,7 @@ fun <T> NodeVisitor<T>.visit(node: Node): T =
is FunctionDefinition -> visitFunctionDefinition(node)
is LetDefinition -> visitLetDefinition(node)
is If -> visitIf(node)
is ImportPath -> visitImportPath(node)
is ImportDeclaration -> visitImportDeclaration(node)
is IntegerLiteral -> visitIntegerLiteral(node)
is LongLiteral -> visitLongLiteral(node)

View File

@ -54,7 +54,7 @@ class CompilationUnitContext(
}
private fun processImport(import: ImportDeclaration) {
val importPath = import.components.joinToString("/") { it.id } + ".pork"
val importPath = import.path.components.joinToString("/") { it.id } + ".pork"
val importLocator = ImportLocator(import.form.id, importPath)
val evaluationContext = evaluator.context(importLocator)
internalScope.inherit(evaluationContext.externalScope)
@ -67,9 +67,11 @@ class CompilationUnitContext(
companion object {
private val preludeImport = ImportDeclaration(
Symbol("std"),
listOf(
Symbol("lang"),
Symbol("prelude")
ImportPath(
listOf(
Symbol("lang"),
Symbol("prelude")
)
)
)
}

View File

@ -391,6 +391,10 @@ class EvaluationVisitor(root: Scope, val stack: CallStack) : NodeVisitor<Any> {
topLevelUsedError("ImportDeclaration", "CompilationUnitContext")
}
override fun visitImportPath(node: ImportPath): Any {
topLevelUsedError("ImportPath", "CompilationUnitContext")
}
override fun visitIndexedBy(node: IndexedBy): Any {
val value = node.expression.visit(this)
val index = node.index.visit(this)

View File

@ -11,7 +11,7 @@ class FunctionContext(val compilationUnitContext: CompilationUnitContext, val no
val native = node.native!!
val nativeFunctionProvider =
compilationUnitContext.evaluator.nativeFunctionProvider(native.form.id)
nativeFunctionProvider.provideNativeFunction(native.definitions.map { it.text }, node.arguments)
nativeFunctionProvider.provideNativeFunction(native.definitions.map { it.text }, node.arguments, compilationUnitContext)
}
private val nativeCached by lazy { resolveMaybeNative() }

View File

@ -11,7 +11,15 @@ class InternalNativeProvider(val quiet: Boolean = false) : NativeProvider {
"listInitWith" to CallableFunction(::listInitWith)
)
override fun provideNativeFunction(definitions: List<String>, arguments: List<ArgumentSpec>): CallableFunction {
fun add(name: String, function: CallableFunction) {
functions[name] = function
}
override fun provideNativeFunction(
definitions: List<String>,
arguments: List<ArgumentSpec>,
inside: CompilationUnitContext
): CallableFunction {
val definition = definitions[0]
return functions[definition] ?: throw RuntimeException("Unknown Internal Function: $definition")
}

View File

@ -3,5 +3,5 @@ package gay.pizza.pork.evaluator
import gay.pizza.pork.ast.gen.ArgumentSpec
interface NativeProvider {
fun provideNativeFunction(definitions: List<String>, arguments: List<ArgumentSpec>): CallableFunction
fun provideNativeFunction(definitions: List<String>, arguments: List<ArgumentSpec>, inside: CompilationUnitContext): CallableFunction
}

View File

@ -1,7 +1,23 @@
import std ffi.malloc
import std ffi.struct
export let timeval = ffiStructDefine(
"long", "seconds",
"unsigned int", "microseconds"
)
export let timezone = ffiStructDefine(
"int", "minutes_greenwich",
"int", "dst_time"
)
func gettimeofday(value, tz)
native ffi "c" "int gettimeofday(struct timeval*, struct timezone*)"
export func main() {
let pointer = malloc(8192)
println(pointer)
free(pointer)
let time = ffiStructAllocate(timeval)
let zone = ffiStructAllocate(timezone)
let result = gettimeofday(time, zone)
let seconds = ffiStructValue(timeval, "seconds", time)
println("Result:", result)
println("Seconds:", seconds)
}

View File

@ -60,6 +60,9 @@ export func SDL_GetModState()
export let SDL_RENDERER_PRESENTVSYNC = 4
export func SDL_PollEvent(event)
native ffi "SDL2" "int SDL_PollEvent(struct SDL_Event*)"
export func SDL_CreateRenderer(window, index, flags)
native ffi "SDL2" "void* SDL_CreateRenderer(void*, int, unsigned int)"

View File

@ -1,6 +1,5 @@
package gay.pizza.pork.ffi
import java.nio.file.Path
import kotlin.io.path.*
object FfiMacPlatform : FfiPlatform {
@ -8,18 +7,23 @@ object FfiMacPlatform : FfiPlatform {
"/Library/Frameworks"
)
override fun findLibrary(name: String): Path? {
override fun findLibrary(name: String): String? {
val frameworksToCheck = frameworksDirectories.map { frameworkDirectory ->
Path("$frameworkDirectory/$name.framework/$name")
}
for (framework in frameworksToCheck) {
if (!framework.exists()) continue
return if (framework.isSymbolicLink()) {
return framework.parent.resolve(framework.readSymbolicLink()).absolute()
return framework.parent.resolve(framework.readSymbolicLink()).absolutePathString()
} else {
framework.absolute()
framework.absolutePathString()
}
}
if (name == "c") {
return "libSystem.dylib"
}
return null
}
}

View File

@ -3,104 +3,153 @@ package gay.pizza.pork.ffi
import com.kenai.jffi.*
import com.kenai.jffi.Function
import gay.pizza.pork.ast.gen.ArgumentSpec
import gay.pizza.pork.evaluator.CallableFunction
import gay.pizza.pork.evaluator.NativeProvider
import gay.pizza.pork.evaluator.None
import java.nio.file.Path
import gay.pizza.pork.evaluator.*
import kotlin.io.path.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
class FfiNativeProvider : NativeProvider {
private val ffiTypeRegistry = FfiTypeRegistry()
private val internalFunctions = mutableMapOf<String, (ArgumentList) -> Any>(
"ffiStructDefine" to ::ffiStructDefine,
"ffiStructAllocate" to ::ffiStructAllocate,
"ffiStructValue" to ::ffiStructValue,
"ffiStructBytes" to ::ffiStructBytes
)
private val rootTypeRegistry = FfiTypeRegistry()
init {
rootTypeRegistry.registerPrimitiveTypes()
}
override fun provideNativeFunction(
definitions: List<String>,
arguments: List<ArgumentSpec>,
inside: CompilationUnitContext
): CallableFunction {
if (definitions[0] == "internal") {
val internal = internalFunctions[definitions[1]] ?:
throw RuntimeException("Unknown internal function: ${definitions[1]}")
return CallableFunction { functionArguments, _ ->
internal(functionArguments)
}
}
override fun provideNativeFunction(definitions: List<String>, arguments: List<ArgumentSpec>): CallableFunction {
val functionDefinition = FfiFunctionDefinition.parse(definitions[0], definitions[1])
val functionAddress = lookupSymbol(functionDefinition)
val ffiTypeRegistry = rootTypeRegistry.fork()
addStructDefs(ffiTypeRegistry, functionDefinition.parameters, inside)
val parameters = functionDefinition.parameters.map { id ->
ffiTypeRegistry.lookup(id) ?: throw RuntimeException("Unknown ffi type: $id")
ffiTypeRegistry.required(id)
}
val returnTypeId = functionDefinition.returnType
val returnType = ffiTypeRegistry.lookup(returnTypeId) ?:
throw RuntimeException("Unknown ffi return type: $returnTypeId")
val returnType = ffiTypeRegistry.required(returnTypeId)
val returnTypeFfi = typeConversion(returnType)
val parameterArray = parameters.map { typeConversion(it) }.toTypedArray()
val function = Function(functionAddress, returnTypeFfi, *parameterArray)
val context = function.callContext
val invoker = Invoker.getInstance()
return CallableFunction { functionArguments, _ ->
val buffer = HeapInvocationBuffer(context)
val freeStringList = mutableListOf<FfiString>()
for ((index, spec) in arguments.withIndex()) {
val ffiType = ffiTypeRegistry.lookup(functionDefinition.parameters[index]) ?:
throw RuntimeException("Unknown ffi type: ${functionDefinition.parameters[index]}")
if (spec.multiple) {
val variableArguments = functionArguments
.subList(index, functionArguments.size)
variableArguments.forEach {
var value = it
if (value is String) {
value = FfiString.allocate(value)
freeStringList.add(value)
}
FfiPrimitiveType.push(buffer, value)
}
break
} else {
var argumentValue = functionArguments[index]
if (argumentValue is String) {
argumentValue = FfiString.allocate(argumentValue)
freeStringList.add(argumentValue)
}
ffiType.put(buffer, argumentValue)
}
}
try {
val buffer = buildArgumentList(
context,
arguments,
functionArguments,
ffiTypeRegistry,
functionDefinition,
freeStringList
)
return@CallableFunction invoke(invoker, function, buffer, returnType)
} finally {
freeStringList.forEach { it.free() }
for (ffiString in freeStringList) {
ffiString.free()
}
}
}
}
private fun addStructDefs(ffiTypeRegistry: FfiTypeRegistry, types: List<String>, inside: CompilationUnitContext) {
for (parameter in types) {
if (!parameter.startsWith("struct ")) {
continue
}
var structureName = parameter.substring(7)
if (structureName.endsWith("*")) {
structureName = structureName.substring(0, structureName.length - 1)
}
val structureDefinitionValue = inside.internalScope.value(structureName)
if (structureDefinitionValue !is FfiStructDefinition) {
throw RuntimeException("Structure '${structureName}' was not an FfiStructDefinition.")
}
val struct = FfiStruct(ffiTypeRegistry)
for ((name, type) in structureDefinitionValue.values) {
struct.add(name, type)
}
ffiTypeRegistry.add("struct $structureName", struct)
}
}
private fun buildArgumentList(
context: CallContext,
functionArgumentSpecs: List<ArgumentSpec>,
functionArguments: List<Any>,
ffiTypeRegistry: FfiTypeRegistry,
functionDefinition: FfiFunctionDefinition,
freeStringList: MutableList<FfiString>
): HeapInvocationBuffer {
val buffer = HeapInvocationBuffer(context)
for ((index, spec) in functionArgumentSpecs.withIndex()) {
val ffiType = ffiTypeRegistry.lookup(functionDefinition.parameters[index]) ?:
throw RuntimeException("Unknown ffi type: ${functionDefinition.parameters[index]}")
if (spec.multiple) {
val variableArguments = functionArguments
.subList(index, functionArguments.size)
for (variableArgument in variableArguments) {
var value = variableArgument
if (value is String) {
value = FfiString.allocate(value)
freeStringList.add(value)
}
FfiPrimitiveType.push(buffer, value)
}
break
} else {
var argumentValue = functionArguments[index]
if (argumentValue is String) {
argumentValue = FfiString.allocate(argumentValue)
freeStringList.add(argumentValue)
}
ffiType.put(buffer, argumentValue)
}
}
return buffer
}
private fun lookupSymbol(functionDefinition: FfiFunctionDefinition): Long {
val actualLibraryPath = findLibraryPath(functionDefinition.library)
val library = Library.getCachedInstance(actualLibraryPath.absolutePathString(), Library.NOW)
val library = Library.getCachedInstance(actualLibraryPath, Library.NOW)
?: throw RuntimeException("Failed to load library $actualLibraryPath")
val functionAddress = library.getSymbolAddress(functionDefinition.function)
if (functionAddress == 0L) {
throw RuntimeException(
"Failed to find symbol ${functionDefinition.function} in " +
"library ${actualLibraryPath.absolutePathString()}")
"library $actualLibraryPath")
}
return functionAddress
}
private fun typeConversion(type: FfiType): Type = when (type) {
FfiPrimitiveType.UnsignedByte -> Type.UINT8
FfiPrimitiveType.Byte -> Type.SINT8
FfiPrimitiveType.UnsignedInt -> Type.UINT32
FfiPrimitiveType.Int -> Type.SINT32
FfiPrimitiveType.UnsignedShort -> Type.UINT16
FfiPrimitiveType.Short -> Type.SINT16
FfiPrimitiveType.UnsignedLong -> Type.UINT64
FfiPrimitiveType.Long -> Type.SINT64
FfiPrimitiveType.String -> Type.POINTER
FfiPrimitiveType.Pointer -> Type.POINTER
FfiPrimitiveType.Void -> Type.VOID
else -> throw RuntimeException("Unknown ffi type: $type")
}
private fun findLibraryPath(name: String): Path {
private fun findLibraryPath(name: String): String {
val initialPath = Path(name)
if (initialPath.exists()) {
return initialPath
return initialPath.absolutePathString()
}
return FfiPlatforms.current.platform.findLibrary(name)
?: throw RuntimeException("Unable to find library: $name")
return FfiPlatforms.current.platform.findLibrary(name) ?: name
}
private fun invoke(invoker: Invoker, function: Function, buffer: HeapInvocationBuffer, type: FfiType): Any = when (type) {
@ -113,4 +162,65 @@ class FfiNativeProvider : NativeProvider {
FfiPrimitiveType.String -> invoker.invokeAddress(function, buffer)
else -> throw RuntimeException("Unsupported ffi return type: $type")
} ?: None
private fun ffiStructDefine(arguments: ArgumentList): Any {
val copy = arguments.toMutableList()
val fields = LinkedHashMap<String, String>()
while (copy.isNotEmpty()) {
val type = copy.removeAt(0)
fields[copy.removeAt(0).toString()] = type.toString()
}
return FfiStructDefinition(fields)
}
private fun ffiStructAllocate(arguments: ArgumentList): Any {
val ffiTypeRegistry = rootTypeRegistry.fork()
val structDefinition = arguments[0] as FfiStructDefinition
val structType = FfiStruct(ffiTypeRegistry)
for ((name, type) in structDefinition.values) {
structType.add(name, type)
}
return FfiAddress(MemoryIO.getInstance().allocateMemory(structType.size, true))
}
private fun ffiStructValue(arguments: ArgumentList): Any {
val structDefinition = arguments[0] as FfiStructDefinition
val field = arguments[1] as String
val value = arguments[2] as FfiAddress
val ffiTypeRegistry = rootTypeRegistry.fork()
val structType = FfiStruct(ffiTypeRegistry)
for ((name, type) in structDefinition.values) {
structType.add(name, type)
}
return structType.get(field, value)
}
private fun ffiStructBytes(arguments: ArgumentList): Any {
val structDefinition = arguments[0] as FfiStructDefinition
val address = arguments[1] as FfiAddress
val ffiTypeRegistry = rootTypeRegistry.fork()
val structType = FfiStruct(ffiTypeRegistry)
for ((name, type) in structDefinition.values) {
structType.add(name, type)
}
return structType.read(address, 0)
}
companion object {
fun typeConversion(type: FfiType): Type = when (type) {
FfiPrimitiveType.UnsignedByte -> Type.UINT8
FfiPrimitiveType.Byte -> Type.SINT8
FfiPrimitiveType.UnsignedInt -> Type.UINT32
FfiPrimitiveType.Int -> Type.SINT32
FfiPrimitiveType.UnsignedShort -> Type.UINT16
FfiPrimitiveType.Short -> Type.SINT16
FfiPrimitiveType.UnsignedLong -> Type.UINT64
FfiPrimitiveType.Long -> Type.SINT64
FfiPrimitiveType.String -> Type.POINTER
FfiPrimitiveType.Pointer -> Type.POINTER
FfiPrimitiveType.Void -> Type.VOID
is FfiStruct -> type.ffiStruct
else -> throw RuntimeException("Unknown ffi type: $type")
}
}
}

View File

@ -1,7 +1,5 @@
package gay.pizza.pork.ffi
import java.nio.file.Path
enum class FfiPlatforms(val id: String, val platform: FfiPlatform) {
Mac("macOS", FfiMacPlatform),
Windows("Windows", FfiWindowsPlatform),
@ -20,5 +18,5 @@ enum class FfiPlatforms(val id: String, val platform: FfiPlatform) {
}
interface FfiPlatform {
fun findLibrary(name: String): Path?
fun findLibrary(name: String): String?
}

View File

@ -1,6 +1,7 @@
package gay.pizza.pork.ffi
import com.kenai.jffi.InvocationBuffer
import com.kenai.jffi.MemoryIO
import gay.pizza.pork.evaluator.None
enum class FfiPrimitiveType(
@ -89,6 +90,21 @@ enum class FfiPrimitiveType(
return ffi
}
override fun read(address: FfiAddress, offset: kotlin.Int): Any {
val actual = address.location + offset
return when (this) {
UnsignedByte, Byte -> MemoryIO.getInstance().getByte(actual)
UnsignedShort, Short -> MemoryIO.getInstance().getShort(actual)
UnsignedInt, Int -> MemoryIO.getInstance().getInt(actual)
UnsignedLong, Long -> MemoryIO.getInstance().getLong(actual)
Float -> MemoryIO.getInstance().getFloat(actual)
Double -> MemoryIO.getInstance().getDouble(actual)
Pointer -> MemoryIO.getInstance().getAddress(actual)
String -> FfiString(FfiAddress(actual))
Void -> None
}
}
companion object {
fun push(buffer: InvocationBuffer, value: Any): Unit = when (value) {
is kotlin.Byte -> buffer.putByte(value.toInt())

View File

@ -1,36 +1,61 @@
package gay.pizza.pork.ffi
import com.kenai.jffi.InvocationBuffer
import com.kenai.jffi.MemoryIO
import com.kenai.jffi.Struct
import gay.pizza.pork.evaluator.None
import java.util.*
class FfiStruct : FfiType {
private val fields = TreeMap<String, FfiStructField>()
class FfiStruct(val ffiTypeRegistry: FfiTypeRegistry) : FfiType {
private val fields = LinkedHashMap<String, FfiStructField>()
private var internalStructType: Struct? = null
val ffiStruct: Struct
get() {
if (internalStructType == null) {
internalStructType = Struct.newStruct(*ffiTypes.map {
FfiNativeProvider.typeConversion(it)
}.toTypedArray())
}
return internalStructType!!
}
private var internalTypes: List<FfiType>? = null
val ffiTypes: List<FfiType>
get() {
if (internalTypes == null) {
internalTypes = fields.values.map { ffiTypeRegistry.required(it.type) }
}
return internalTypes!!
}
data class FfiStructField(val name: String, val type: FfiType)
data class FfiStructField(val name: String, val type: String)
fun add(field: String, type: FfiType) {
fun add(field: String, type: String) {
fields[field] = FfiStructField(field, type)
internalStructType = null
internalTypes = null
}
override val size: Long
get() = fields.values.sumOf { it.type.size }
get() = ffiStruct.size().toLong()
override fun put(buffer: InvocationBuffer, value: Any?) {
when (value) {
is Map<*, *> -> {
for (field in fields.values) {
val item = value[field.name] ?: None
field.type.put(buffer, item)
val itemType = ffiTypeRegistry.required(field.type)
itemType.put(buffer, item)
}
}
is List<*> -> {
for ((index, field) in fields.values.withIndex()) {
field.type.put(buffer, value[index])
val itemType = ffiTypeRegistry.required(field.type)
itemType.put(buffer, value[index])
}
}
is None -> {}
else -> {
throw RuntimeException("Unknown value type: $value")
}
@ -40,4 +65,28 @@ class FfiStruct : FfiType {
override fun value(ffi: Any?): Any {
return None
}
override fun read(address: FfiAddress, offset: Int): Any {
val bytes = ByteArray(size.toInt()) { 0 }
MemoryIO.getInstance().getByteArray(address.location, bytes, offset, size.toInt())
return bytes
}
fun get(field: String, address: FfiAddress): Any {
var indexWithoutAlignment = 0L
var type: FfiType? = null
for ((index, key) in fields.keys.withIndex()) {
if (key == field) {
type = ffiTypes[index]
break
}
indexWithoutAlignment += ffiTypes[index].size
}
if (type == null) {
throw RuntimeException("Unable to read unknown field $field from struct.")
}
return type.read(address, indexWithoutAlignment.toInt())
}
}

View File

@ -0,0 +1,3 @@
package gay.pizza.pork.ffi
data class FfiStructDefinition(val values: LinkedHashMap<String, String>)

View File

@ -7,4 +7,5 @@ interface FfiType {
fun put(buffer: InvocationBuffer, value: Any?)
fun value(ffi: Any?): Any
fun read(address: FfiAddress, offset: Int): Any
}

View File

@ -1,9 +1,9 @@
package gay.pizza.pork.ffi
class FfiTypeRegistry {
class FfiTypeRegistry(val parent: FfiTypeRegistry? = null) {
private val types = mutableMapOf<String, FfiType>()
init {
fun registerPrimitiveTypes() {
for (type in FfiPrimitiveType.entries) {
add(type.id, type)
}
@ -12,7 +12,14 @@ class FfiTypeRegistry {
fun add(name: String, type: FfiType) {
types[name] = type
if (type is FfiStruct) {
types["${name}*"] = FfiPrimitiveType.Pointer
}
}
fun lookup(name: String): FfiType? = types[name]
fun lookup(name: String): FfiType? = types[name] ?: parent?.lookup(name)
fun required(name: String): FfiType = lookup(name) ?: throw RuntimeException("Unknown ffi type: $name")
fun fork(): FfiTypeRegistry = FfiTypeRegistry(this)
}

View File

@ -3,5 +3,5 @@ package gay.pizza.pork.ffi
import java.nio.file.Path
object FfiUnixPlatform : FfiPlatform {
override fun findLibrary(name: String): Path? = null
override fun findLibrary(name: String): String? = null
}

View File

@ -3,5 +3,5 @@ package gay.pizza.pork.ffi
import java.nio.file.Path
object FfiWindowsPlatform : FfiPlatform {
override fun findLibrary(name: String): Path? = null
override fun findLibrary(name: String): String? = null
}

View File

@ -2,6 +2,7 @@ package gay.pizza.pork.ffi
import gay.pizza.pork.ast.gen.ArgumentSpec
import gay.pizza.pork.evaluator.CallableFunction
import gay.pizza.pork.evaluator.CompilationUnitContext
import gay.pizza.pork.evaluator.NativeProvider
import gay.pizza.pork.evaluator.None
import java.lang.invoke.MethodHandles
@ -10,7 +11,11 @@ import java.lang.invoke.MethodType
class JavaNativeProvider : NativeProvider {
private val lookup = MethodHandles.lookup()
override fun provideNativeFunction(definitions: List<String>, arguments: List<ArgumentSpec>): CallableFunction {
override fun provideNativeFunction(
definitions: List<String>,
arguments: List<ArgumentSpec>,
inside: CompilationUnitContext
): CallableFunction {
val functionDefinition = JavaFunctionDefinition.parse(definitions)
val javaClass = lookupClass(functionDefinition.type)
val returnTypeClass = lookupClass(functionDefinition.returnType)

View File

@ -23,7 +23,7 @@ class World(val importSource: ImportSource) {
}
val charSource = contentSource.loadAsCharSource(importLocator.path)
val tokenizer = Tokenizer(charSource)
val tokenStream = tokenizer.tokenize()
val tokenStream = tokenizer.stream()
val parser = Parser(TokenStreamSource(tokenStream), DiscardNodeAttribution)
val unit = parser.parseCompilationUnit()
internalUnits[stableKey] = unit
@ -33,7 +33,7 @@ class World(val importSource: ImportSource) {
private fun resolveAllImports(unit: CompilationUnit): Set<CompilationUnit> {
val units = mutableSetOf<CompilationUnit>()
for (declaration in unit.declarations.filterIsInstance<ImportDeclaration>()) {
val importPath = declaration.components.joinToString("/") { it.id } + ".pork"
val importPath = declaration.path.components.joinToString("/") { it.id } + ".pork"
val importLocator = ImportLocator(declaration.form.id, importPath)
val importedUnit = loadOneUnit(importLocator)
units.add(importedUnit)

View File

@ -23,7 +23,7 @@ abstract class Tool {
get() = ImportLocator("local", rootFilePath())
fun tokenize(): TokenStream =
Tokenizer(createCharSource()).tokenize()
Tokenizer(createCharSource()).stream()
fun parse(attribution: NodeAttribution = DiscardNodeAttribution): CompilationUnit =
Parser(TokenStreamSource(tokenize()), attribution).parseCompilationUnit()

View File

@ -1,5 +1,5 @@
package gay.pizza.pork.parser
class BadCharacterError(val char: Char, sourceIndex: SourceIndex) : ParseError(
"Failed to produce token for '${char}' at $sourceIndex"
class BadCharacterError(val char: Char, sourceIndex: SourceIndex, state: TokenizerState) : ParseError(
"Failed to produce token for '${char}' at $sourceIndex in state $state"
)

View File

@ -2,5 +2,5 @@ package gay.pizza.pork.parser
open class ParseError(val error: String) : RuntimeException() {
override val message: String
get() = "${error}\nDescent path: ${ParserStackAnalysis(this).findDescentPath().joinToString(", ")}"
get() = "${error}${ParserStackAnalysis(this).buildDescentPathAddendum()}"
}

View File

@ -21,7 +21,7 @@ class Parser(source: TokenSource, attribution: NodeAttribution) :
val token = peek()
var expression = when (token.type) {
TokenType.NumberLiteral -> parseNumberLiteral()
TokenType.StringLiteral -> parseStringLiteral()
TokenType.Quote -> parseStringLiteral()
TokenType.True, TokenType.False -> parseBooleanLiteral()
TokenType.LeftBracket -> parseListLiteral()
TokenType.Let -> parseLetAssignment()
@ -233,10 +233,14 @@ class Parser(source: TokenSource, attribution: NodeAttribution) :
override fun parseImportDeclaration(): ImportDeclaration = expect(NodeType.ImportDeclaration, TokenType.Import) {
val form = parseSymbol()
ImportDeclaration(form, parseImportPath())
}
override fun parseImportPath(): ImportPath = guarded(NodeType.ImportPath) {
val components = oneAndContinuedBy(TokenType.Dot) {
parseSymbol()
}
ImportDeclaration(form, components)
ImportPath(components)
}
override fun parseIndexedBy(): IndexedBy = guarded(NodeType.IndexedBy) {
@ -302,7 +306,7 @@ class Parser(source: TokenSource, attribution: NodeAttribution) :
override fun parseNative(): Native = expect(NodeType.Native, TokenType.Native) {
val form = parseSymbol()
val definitions = mutableListOf<StringLiteral>()
while (peek(TokenType.StringLiteral)) {
while (peek(TokenType.Quote)) {
definitions.add(parseStringLiteral())
}
Native(form, definitions)
@ -333,8 +337,11 @@ class Parser(source: TokenSource, attribution: NodeAttribution) :
SetAssignment(symbol, value)
}
override fun parseStringLiteral(): StringLiteral = expect(NodeType.StringLiteral, TokenType.StringLiteral) {
val content = StringEscape.unescape(StringEscape.unquote(it.text))
override fun parseStringLiteral(): StringLiteral = guarded(NodeType.StringLiteral) {
expect(TokenType.Quote)
val stringLiteralToken = expect(TokenType.StringLiteral)
expect(TokenType.Quote)
val content = StringEscape.unescape(stringLiteralToken.text)
StringLiteral(content)
}

View File

@ -24,4 +24,13 @@ class ParserStackAnalysis(private val stack: Array<StackTraceElement>) {
}
return parseDescentPaths.reversed()
}
fun buildDescentPathAddendum(): String {
val descentPath = findDescentPath()
if (descentPath.isEmpty()) {
return ""
}
return "\nParser descent path: ${descentPath.joinToString(", ")}"
}
}

View File

@ -238,6 +238,10 @@ class Printer(buffer: StringBuilder) : NodeVisitor<Unit> {
append("import ")
visit(node.form)
append(" ")
visit(node.path)
}
override fun visitImportPath(node: ImportPath) {
for ((index, component) in node.components.withIndex()) {
visit(component)
if (index != node.components.size - 1) {

View File

@ -2,12 +2,7 @@ package gay.pizza.pork.parser
object StringCharConsumer : CharConsumer {
override fun consume(type: TokenType, tokenizer: Tokenizer): String? {
if (!tokenizer.peek("\"")) {
return null
}
val buffer = StringBuilder()
buffer.append(tokenizer.source.next())
var escape = false
while (true) {
val char = tokenizer.source.peek()
@ -16,12 +11,14 @@ object StringCharConsumer : CharConsumer {
throw UnterminatedTokenError("String", tokenizer.source.currentSourceIndex())
}
if (char == '"' && !escape) {
break
}
buffer.append(tokenizer.source.next())
if (char == '\\') {
escape = true
} else if (char == '"' && !escape) {
break
}
}
return buffer.toString()

View File

@ -3,5 +3,4 @@ package gay.pizza.pork.parser
object StringEscape {
fun escape(input: String): String = input.replace("\n", "\\n")
fun unescape(input: String): String = input.replace("\\n", "\n")
fun unquote(input: String): String = input.substring(1, input.length - 1)
}

View File

@ -6,7 +6,7 @@ import gay.pizza.pork.parser.TokenTypeProperty.*
import gay.pizza.pork.parser.TokenFamily.*
import gay.pizza.pork.parser.TokenTypeProperty.AnyOf
enum class TokenType(vararg properties: TokenTypeProperty) {
enum class TokenType(vararg val properties: TokenTypeProperty) {
NumberLiteral(NumericLiteralFamily, CharMatch(CharMatcher.AnyOf(
MatchRange('0'..'9'),
NotAtIndex(0, MatchSingle('.'))
@ -17,7 +17,8 @@ enum class TokenType(vararg properties: TokenTypeProperty) {
MatchRange('0' .. '9'),
MatchSingle('_')
)), KeywordUpgrader),
StringLiteral(StringLiteralFamily, CharConsume(StringCharConsumer)),
Quote(StringLiteralFamily, SingleChar('"'), InsideStates(TokenizerState.Normal, TokenizerState.StringLiteralEnd)),
StringLiteral(StringLiteralFamily, CharConsume(StringCharConsumer), InsideStates(TokenizerState.StringLiteralStart)),
Equality(OperatorFamily),
Inequality(ManyChars("!="), OperatorFamily),
ExclamationPoint(SingleChar('!'), Promotion('=', Inequality)),
@ -91,6 +92,11 @@ enum class TokenType(vararg properties: TokenTypeProperty) {
val charConsume: CharConsume? = properties.filterIsInstance<CharConsume>().singleOrNull()
val tokenUpgrader: TokenUpgrader? =
properties.filterIsInstance<TokenUpgrader>().singleOrNull()
val validStates: List<TokenizerState> by lazy {
properties
.filterIsInstance<InsideStates>()
.singleOrNull()?.states?.toList() ?: listOf(TokenizerState.Normal)
}
val simpleWantString: String? = manyChars?.text ?: singleChar?.char?.toString()

View File

@ -5,6 +5,7 @@ interface TokenTypeProperty {
class Promotion(val nextChar: Char, val type: TokenType) : TokenTypeProperty
class ManyChars(val text: String) : TokenTypeProperty
class AnyOf(vararg val strings: String): TokenTypeProperty
class InsideStates(vararg val states: TokenizerState) : TokenTypeProperty
open class CharMatch(val matcher: CharMatcher) : TokenTypeProperty
open class CharConsume(val consumer: CharConsumer) : TokenTypeProperty
open class TokenUpgrader(val maybeUpgrade: (Token) -> Token?) : TokenTypeProperty

View File

@ -2,73 +2,100 @@ package gay.pizza.pork.parser
class Tokenizer(source: CharSource) {
val source: SourceIndexCharSource = SourceIndexCharSource(source)
private var startIndex: SourceIndex = SourceIndex.zero()
private var state = TokenizerState.Normal
fun next(): Token {
while (source.peek() != CharSource.EndOfFile) {
startIndex = source.currentSourceIndex()
for (item in TokenType.CharConsumes) {
val text = item.charConsume!!.consumer.consume(item, this)
if (text != null) {
return produceToken(item, text)
}
}
val char = source.next()
for (item in TokenType.SingleChars) {
val itemChar = item.singleChar!!.char
if (itemChar != char) {
continue
}
var type = item
var text = itemChar.toString()
var promoted = true
while (promoted) {
promoted = false
for (promotion in type.promotions) {
if (source.peek() != promotion.nextChar) {
continue
}
val nextChar = source.next()
type = promotion.type
text += nextChar
promoted = true
}
}
return produceToken(type, text)
}
var index = 0
for (item in TokenType.CharMatches) {
if (!item.charMatch!!.matcher.valid(char, index)) {
continue
}
val text = buildString {
append(char)
while (item.charMatch.matcher.valid(source.peek(), ++index)) {
append(source.next())
}
}
var token = produceToken(item, text)
val tokenUpgrader = item.tokenUpgrader
if (tokenUpgrader != null) {
token = tokenUpgrader.maybeUpgrade(token) ?: token
}
return token
}
throw BadCharacterError(char, startIndex)
private fun nextTokenOrNull(): Token? {
if (source.peek() == CharSource.EndOfFile) {
source.next()
return Token.endOfFile(source.currentSourceIndex())
}
return Token.endOfFile(startIndex.copy(index = source.currentIndex))
startIndex = source.currentSourceIndex()
for (item in TokenType.CharConsumes) {
if (!item.validStates.contains(state)) {
continue
}
val text = item.charConsume!!.consumer.consume(item, this)
if (text != null) {
return produceToken(item, text)
}
}
val char = source.next()
for (item in TokenType.SingleChars) {
if (!item.validStates.contains(state)) {
continue
}
val itemChar = item.singleChar!!.char
if (itemChar != char) {
continue
}
var type = item
var text = itemChar.toString()
var promoted = true
while (promoted) {
promoted = false
for (promotion in type.promotions) {
if (source.peek() != promotion.nextChar) {
continue
}
val nextChar = source.next()
type = promotion.type
text += nextChar
promoted = true
}
}
return produceToken(type, text)
}
var index = 0
for (item in TokenType.CharMatches) {
if (!item.validStates.contains(state)) {
continue
}
if (!item.charMatch!!.matcher.valid(char, index)) {
continue
}
val text = buildString {
append(char)
while (item.charMatch.matcher.valid(source.peek(), ++index)) {
append(source.next())
}
}
var token = produceToken(item, text)
val tokenUpgrader = item.tokenUpgrader
if (tokenUpgrader != null) {
token = tokenUpgrader.maybeUpgrade(token) ?: token
}
return token
}
return null
}
fun tokenize(): TokenStream {
fun next(): Token {
val what = source.peek()
val token = nextTokenOrNull()
if (token != null) {
for (transition in state.transitions) {
if (transition.produced == token.type) {
state = transition.enter
break
}
}
return token
}
throw BadCharacterError(what, source.currentSourceIndex(), state)
}
fun stream(): TokenStream {
val tokens = mutableListOf<Token>()
while (true) {
val token = next()

View File

@ -0,0 +1,12 @@
package gay.pizza.pork.parser
enum class TokenizerState(vararg val transitions: Transition) {
Normal(Transition({ TokenType.Quote }) { StringLiteralStart }),
StringLiteralStart(Transition({ TokenType.StringLiteral }) { StringLiteralEnd }),
StringLiteralEnd(Transition({ TokenType.Quote }) { Normal });
data class Transition(private val producedToken: () -> TokenType, private val nextState: () -> TokenizerState) {
val produced by lazy { producedToken() }
val enter by lazy { nextState() }
}
}

View File

@ -0,0 +1,11 @@
export func ffiStructDefine(items...)
native ffi "internal" "ffiStructDefine"
export func ffiStructAllocate(struct)
native ffi "internal" "ffiStructAllocate"
export func ffiStructValue(struct, field, value)
native ffi "internal" "ffiStructValue"
export func ffiStructBytes(struct, value)
native ffi "internal" "ffiStructBytes"

View File

@ -8,13 +8,14 @@ import com.intellij.platform.backend.navigation.NavigationRequest
import com.intellij.platform.backend.navigation.NavigationTarget
import com.intellij.platform.backend.presentation.TargetPresentation
import gay.pizza.pork.idea.psi.gen.PorkElement
import gay.pizza.pork.idea.resolution.PorkReferenceResolution
@Suppress("UnstableApiUsage")
data class PorkDeclarationSymbol(val module: String, val name: String) : Symbol, NavigatableSymbol {
override fun createPointer(): Pointer<out Symbol> = Pointer { this }
override fun getNavigationTargets(project: Project): MutableCollection<out NavigationTarget> {
return PorkReferenceResolution.getAllProjectPorkFiles(project)
.flatMap { PorkReferenceResolution.findAnyDefinitions(it) }
.flatMap { PorkReferenceResolution.findAnyDefinitions(it.file) }
.map { PorkNavigationTarget(it) }
.toMutableList()
}

View File

@ -35,6 +35,10 @@ object PorkElementTypes {
elementTypeFor(TokenType.StringLiteral)
)
val QuoteSet = TokenSet.create(
elementTypeFor(TokenType.Quote)
)
fun tokenTypeFor(elementType: IElementType): TokenType? =
elementTypeToTokenType[elementType]

View File

@ -10,4 +10,8 @@ class PorkFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, PorkL
}
override fun toString(): String = "Pork"
override fun isPhysical(): Boolean {
return super.isPhysical()
}
}

View File

@ -0,0 +1,34 @@
package gay.pizza.pork.idea
import com.intellij.codeInsight.hints.InlayInfo
import com.intellij.codeInsight.hints.InlayParameterHintsProvider
import com.intellij.psi.PsiElement
import com.intellij.psi.util.childrenOfType
import gay.pizza.pork.idea.psi.gen.ArgumentSpecElement
import gay.pizza.pork.idea.psi.gen.FunctionDefinitionElement
@Suppress("UnstableApiUsage")
class PorkInlayParameterHintsProvider : InlayParameterHintsProvider {
override fun getParameterHints(element: PsiElement): MutableList<InlayInfo> {
val inlays = mutableListOf<InlayInfo>()
val resolved = element.reference?.resolve()
if (resolved !is FunctionDefinitionElement) {
return inlays
}
val argumentSpecs = resolved.childrenOfType<ArgumentSpecElement>()
val arguments = if (element.children.isNotEmpty()) {
element.children.drop(1)
} else emptyList()
for ((argument, spec) in arguments.zip(argumentSpecs)) {
val name = spec.name
if (name != null) {
inlays.add(InlayInfo(name, argument.textOffset))
}
}
return inlays
}
override fun getDefaultBlackList(): MutableSet<String> =
mutableSetOf()
}

View File

@ -0,0 +1,90 @@
package gay.pizza.pork.idea
import com.intellij.lang.parameterInfo.CreateParameterInfoContext
import com.intellij.lang.parameterInfo.ParameterInfoHandler
import com.intellij.lang.parameterInfo.ParameterInfoUIContext
import com.intellij.lang.parameterInfo.ParameterInfoUtils
import com.intellij.lang.parameterInfo.UpdateParameterInfoContext
import com.intellij.openapi.util.TextRange
import com.intellij.psi.util.childrenOfType
import com.intellij.psi.util.elementsAtOffsetUp
import gay.pizza.pork.idea.psi.gen.ArgumentSpecElement
import gay.pizza.pork.idea.psi.gen.FunctionCallElement
import gay.pizza.pork.idea.psi.gen.FunctionDefinitionElement
import gay.pizza.pork.parser.TokenType
@Suppress("UnstableApiUsage")
class PorkParameterInfoHandler : ParameterInfoHandler<FunctionCallElement, FunctionDefinitionElement> {
override fun findElementForParameterInfo(context: CreateParameterInfoContext): FunctionCallElement? {
return context.file.elementsAtOffsetUp(context.offset).asSequence()
.map { it.first }
.filterIsInstance<FunctionCallElement>()
.firstOrNull()
}
override fun findElementForUpdatingParameterInfo(context: UpdateParameterInfoContext): FunctionCallElement? {
return context.file.elementsAtOffsetUp(context.offset).asSequence()
.map { it.first }
.filterIsInstance<FunctionCallElement>()
.firstOrNull()
}
override fun updateUI(p: FunctionDefinitionElement, context: ParameterInfoUIContext) {
val argumentSpecs = p.childrenOfType<ArgumentSpecElement>()
val signature = argumentSpecs.mapNotNull { it.name }.joinToString(", ")
if (argumentSpecs.isEmpty()) {
context.setupUIComponentPresentation(
"<no parameters>",
-1,
-1,
false,
false,
false,
context.defaultParameterColor
)
return
}
if (context.currentParameterIndex >= argumentSpecs.size) {
context.setupUIComponentPresentation(
signature,
-1,
-1,
false,
false,
false,
context.defaultParameterColor
)
} else {
var range: TextRange? = null
var start = 0
for ((index, item) in signature.split(", ").withIndex()) {
if (index == context.currentParameterIndex) {
range = TextRange(index, index + item.length)
}
start += item.length + 2
}
context.setupUIComponentPresentation(
signature,
range?.startOffset ?: 0,
range?.endOffset ?: (signature.length - 1),
false,
false,
false,
context.defaultParameterColor
)
}
}
override fun updateParameterInfo(parameterOwner: FunctionCallElement, context: UpdateParameterInfoContext) {
val offset = ParameterInfoUtils.getCurrentParameterIndex(
parameterOwner.node,
context.offset,
PorkElementTypes.elementTypeFor(TokenType.Comma)
)
context.setCurrentParameter(offset)
}
override fun showParameterInfo(element: FunctionCallElement, context: CreateParameterInfoContext) {
context.showHint(element, element.textOffset, this)
}
}

View File

@ -0,0 +1,5 @@
package gay.pizza.pork.idea
import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler
class PorkQuoteHandler : SimpleTokenSetQuoteHandler(PorkElementTypes.QuoteSet)

View File

@ -1,109 +0,0 @@
package gay.pizza.pork.idea
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.childrenOfType
import gay.pizza.pork.idea.psi.gen.*
object PorkReferenceResolution {
fun getRelevantFiles(containingFile: PsiFile): List<PsiFile> {
if (containingFile.virtualFile == null) {
return getAllProjectPorkFiles(containingFile.project)
}
val importDeclarationElements = PsiTreeUtil.collectElementsOfType(containingFile, ImportDeclarationElement::class.java)
val files = mutableListOf(containingFile)
for (importDeclaration in importDeclarationElements) {
val symbolElements = importDeclaration.childrenOfType<SymbolElement>()
val importType = importDeclaration.childrenOfType<SymbolElement>().first().text
if (importType != "local") {
continue
}
val basicImportPath = symbolElements.drop(1).joinToString("/") { it.text.trim() }
val actualImportPath = "../${basicImportPath}.pork"
val virtualFile = containingFile.virtualFile?.findFileByRelativePath(actualImportPath) ?: continue
val psiFile = PsiManager.getInstance(containingFile.project).findFile(virtualFile) ?: continue
files.add(psiFile)
}
return files
}
fun getAllProjectPorkFiles(project: Project): List<PsiFile> {
val porkVirtualFiles = FilenameIndex.getAllFilesByExt(project, "pork")
return porkVirtualFiles.mapNotNull { virtualFile ->
PsiManager.getInstance(project).findFile(virtualFile)
}
}
fun findAllCandidates(internalPorkElement: PorkElement, name: String? = null): List<PorkElement> =
listOf(findAnyLocals(internalPorkElement, name), findAnyDefinitions(internalPorkElement.containingFile, name)).flatten()
fun findAnyLocals(internalPorkElement: PorkElement, name: String? = null): List<PorkElement> {
val functionDefinitionElement = PsiTreeUtil.getParentOfType(internalPorkElement, FunctionDefinitionElement::class.java)
?: return emptyList()
val locals = mutableListOf<PorkElement>()
fun check(localCandidate: PsiElement, upward: Boolean) {
if (localCandidate is BlockElement && !upward) {
return
}
if (localCandidate is ArgumentSpecElement ||
localCandidate is LetAssignmentElement ||
localCandidate is VarAssignmentElement) {
locals.add(localCandidate as PorkElement)
}
if (localCandidate is ForInElement) {
val forInItem = localCandidate.childrenOfType<ForInItemElement>().firstOrNull()
if (forInItem != null) {
locals.add(forInItem)
}
}
localCandidate.children.forEach { check(it, false) }
}
PsiTreeUtil.treeWalkUp(internalPorkElement, functionDefinitionElement) { _, localCandidate ->
if (localCandidate != null) {
if (internalPorkElement == functionDefinitionElement) {
return@treeWalkUp true
}
check(localCandidate, true)
}
true
}
val argumentSpecElements = functionDefinitionElement.childrenOfType<ArgumentSpecElement>()
locals.addAll(argumentSpecElements)
val finalLocals = locals.distinctBy { it.textRange }
return finalLocals.filter { if (name != null) it.name == name else true }
}
fun findAnyDefinitions(containingFile: PsiFile, name: String? = null): List<PorkElement> {
val foundDefinitions = mutableListOf<PorkNamedElement>()
for (file in getRelevantFiles(containingFile)) {
val definitions = PsiTreeUtil.collectElements(file) { element ->
element is FunctionDefinitionElement ||
element is LetDefinitionElement
}.filterIsInstance<PorkNamedElement>()
if (name != null) {
val fileFoundDefinition = definitions.firstOrNull {
it.name == name
}
if (fileFoundDefinition != null) {
foundDefinitions.add(fileFoundDefinition)
return foundDefinitions
}
} else {
foundDefinitions.addAll(definitions)
}
}
return foundDefinitions
}
}

View File

@ -49,7 +49,13 @@ object PorkElementHelpers {
fun referenceOfElement(element: PorkElement, type: NodeType): PsiReference? {
unused(type)
val textRangeOfSymbolInElement = element.childrenOfType<SymbolElement>().firstOrNull()?.textRangeInParent ?: return null
if (element is ImportPathElement) {
return PorkFileReference(element, element.textRange)
}
val symbols = element.childrenOfType<SymbolElement>()
val textRangeOfSymbolInElement = symbols.firstOrNull()?.textRangeInParent ?: return null
return PorkIdentifierReference(element, textRangeOfSymbolInElement)
}

View File

@ -0,0 +1,22 @@
package gay.pizza.pork.idea.psi
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.util.parentOfType
import gay.pizza.pork.idea.psi.gen.ImportDeclarationElement
import gay.pizza.pork.idea.psi.gen.PorkElement
import gay.pizza.pork.idea.resolution.PorkReferenceResolution
class PorkFileReference(element: PorkElement, textRange: TextRange) : PorkReference(element, textRange) {
override fun resolve(): PsiElement? {
val importDeclarationElement = element.parentOfType<ImportDeclarationElement>() ?: return null
val resolved = PorkReferenceResolution.resolveImportFile(
element.containingFile,
PorkReferenceResolution.findPorkStdDirectory(element.project),
importDeclarationElement
)
return resolved?.file
}
override fun getVariants(): Array<Any> = arrayOf()
}

View File

@ -1,15 +1,17 @@
package gay.pizza.pork.idea.psi
import com.intellij.psi.PsiFile
import gay.pizza.pork.idea.PorkReferenceResolution
import gay.pizza.pork.idea.resolution.PorkReferenceResolution
import gay.pizza.pork.idea.psi.gen.PorkElement
import gay.pizza.pork.idea.resolution.PorkReferenceRelevantFile
interface PorkReferencable {
val internalPorkElement: PorkElement
fun getRelevantFiles(): List<PsiFile> = PorkReferenceResolution.getRelevantFiles(internalPorkElement.containingFile)
fun getRelevantFiles(): List<PorkReferenceRelevantFile> =
PorkReferenceResolution.getRelevantFiles(internalPorkElement.containingFile)
fun findAllCandidates(name: String? = null): List<PorkElement> =
listOf(findAnyLocals(name), findAnyDefinitions(name)).flatten()
PorkReferenceResolution.findAllCandidates(internalPorkElement, name)
fun findAnyLocals(name: String? = null): List<PorkElement> =
PorkReferenceResolution.findAnyLocals(internalPorkElement, name)

View File

@ -0,0 +1,20 @@
// GENERATED CODE FROM PORK AST CODEGEN
package gay.pizza.pork.idea.psi.gen
import com.intellij.lang.ASTNode
import com.intellij.navigation.ItemPresentation
import com.intellij.psi.PsiReference
import gay.pizza.pork.ast.gen.NodeType
import gay.pizza.pork.idea.psi.PorkElementHelpers
import javax.swing.Icon
class ImportPathElement(node: ASTNode) : PorkElement(node) {
override fun getReference(): PsiReference? =
PorkElementHelpers.referenceOfElement(this, NodeType.CompilationUnit)
override fun getIcon(flags: Int): Icon? =
PorkElementHelpers.iconOf(this)
override fun getPresentation(): ItemPresentation? =
PorkElementHelpers.presentationOf(this)
}

View File

@ -23,6 +23,7 @@ object PorkElementFactory {
NodeType.FunctionDefinition -> FunctionDefinitionElement(node)
NodeType.LetDefinition -> LetDefinitionElement(node)
NodeType.If -> IfElement(node)
NodeType.ImportPath -> ImportPathElement(node)
NodeType.ImportDeclaration -> ImportDeclarationElement(node)
NodeType.IntegerLiteral -> IntegerLiteralElement(node)
NodeType.LongLiteral -> LongLiteralElement(node)

View File

@ -0,0 +1,5 @@
package gay.pizza.pork.idea.resolution
import com.intellij.psi.PsiFile
class PorkReferenceRelevantFile(val file: PsiFile, val type: PorkRelevantFileType)

View File

@ -0,0 +1,197 @@
package gay.pizza.pork.idea.resolution
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.vfs.*
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.search.FilenameIndex
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.childrenOfType
import gay.pizza.pork.idea.psi.gen.*
object PorkReferenceResolution {
fun getRelevantFiles(containingFile: PsiFile): List<PorkReferenceRelevantFile> {
if (containingFile.virtualFile == null) {
return listOf(
getAllProjectPorkFiles(containingFile.project),
getAllPorkStdFiles(containingFile.project)
).flatten()
}
val importDeclarationElements = PsiTreeUtil.collectElementsOfType(
containingFile,
ImportDeclarationElement::class.java
)
val files = mutableListOf(PorkReferenceRelevantFile(containingFile, PorkRelevantFileType.Self))
val stdDirectory = findPorkStdDirectory(containingFile.project)
val prelude = resolveStdImport(containingFile, stdDirectory, "lang/prelude")
if (prelude != null) {
files.add(prelude)
}
for (importDeclaration in importDeclarationElements) {
val resolved = resolveImportFile(containingFile, stdDirectory, importDeclaration)
if (resolved != null) {
files.add(resolved)
}
}
return files
}
fun resolveImportFile(
containingFile: PsiFile,
stdDirectory: VirtualFile?,
importDeclarationElement: ImportDeclarationElement
): PorkReferenceRelevantFile? {
val importType = importDeclarationElement.childrenOfType<SymbolElement>().firstOrNull()?.text ?: return null
val importPathElement = importDeclarationElement.childrenOfType<ImportPathElement>().firstOrNull() ?: return null
val basicImportPath = importPathElement.children.joinToString("/") { it.text.trim() }
return when (importType) {
"local" -> {
val actualImportPath = "../${basicImportPath}.pork"
val actualVirtualFile = containingFile.virtualFile?.findFileByRelativePath(actualImportPath) ?: return null
referenceRelevantFile(containingFile.project, actualVirtualFile, PorkRelevantFileType.Local)
}
"std" -> {
resolveStdImport(containingFile, stdDirectory, basicImportPath)
}
else -> null
}
}
private fun resolveStdImport(containingFile: PsiFile, stdDirectory: VirtualFile?, basicImportPath: String): PorkReferenceRelevantFile? {
if (stdDirectory == null) {
return null
}
val actualVirtualFile = stdDirectory.findFile("${basicImportPath}.pork") ?: return null
return referenceRelevantFile(containingFile.project, actualVirtualFile, PorkRelevantFileType.Std)
}
private fun referenceRelevantFile(
project: Project,
virtualFile: VirtualFile,
type: PorkRelevantFileType
): PorkReferenceRelevantFile? {
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) ?: return null
return PorkReferenceRelevantFile(psiFile, type)
}
fun getAllProjectPorkFiles(project: Project): List<PorkReferenceRelevantFile> {
val psiManager = PsiManager.getInstance(project)
val porkVirtualFiles = FilenameIndex.getAllFilesByExt(project, "pork")
return porkVirtualFiles.mapNotNull { virtualFile ->
psiManager.findFile(virtualFile)
}.map { PorkReferenceRelevantFile(it, PorkRelevantFileType.Local) }
}
fun findPorkStdDirectory(project: Project): VirtualFile? = if (isPorkItself(project)) {
project.guessProjectDir()?.findDirectory("stdlib/src/main/pork")
} else {
project.guessProjectDir()?.fileSystem?.findFileByPath(
"/opt/pork/std"
)
}
fun getAllPorkStdFiles(project: Project): List<PorkReferenceRelevantFile> {
val stdDirectoryPath = findPorkStdDirectory(project) ?: return emptyList()
val psiManager = PsiManager.getInstance(project)
val stdPorkFiles = mutableListOf<PorkReferenceRelevantFile>()
VfsUtilCore.iterateChildrenRecursively(stdDirectoryPath, VirtualFileFilter.ALL) { file ->
if (file.extension == "pork") {
val psiFile = psiManager.findFile(file)
if (psiFile != null) {
stdPorkFiles.add(PorkReferenceRelevantFile(psiFile, PorkRelevantFileType.Std))
}
}
true
}
return stdPorkFiles
}
private fun isPorkItself(project: Project): Boolean {
if (project.name != "pork") {
return false
}
val projectDirectory = project.guessProjectDir() ?: return false
val prelude = projectDirectory.findFileOrDirectory(
"stdlib/src/main/pork/lang/prelude.pork"
)
return prelude != null && prelude.isFile
}
fun findAllCandidates(internalPorkElement: PorkElement, name: String? = null): List<PorkElement> =
listOf(
findAnyLocals(internalPorkElement, name),
findAnyDefinitions(internalPorkElement.containingFile, name)
).flatten()
fun findAnyLocals(internalPorkElement: PorkElement, name: String? = null): List<PorkElement> {
val functionDefinitionElement = PsiTreeUtil.getParentOfType(
internalPorkElement,
FunctionDefinitionElement::class.java
) ?: return emptyList()
val locals = mutableListOf<PorkElement>()
fun check(localCandidate: PsiElement, upward: Boolean) {
if (localCandidate is BlockElement && !upward) {
return
}
if (localCandidate is ArgumentSpecElement ||
localCandidate is LetAssignmentElement ||
localCandidate is VarAssignmentElement) {
locals.add(localCandidate as PorkElement)
}
if (localCandidate is ForInElement) {
val forInItem = localCandidate.childrenOfType<ForInItemElement>().firstOrNull()
if (forInItem != null) {
locals.add(forInItem)
}
}
localCandidate.children.forEach { check(it, false) }
}
PsiTreeUtil.treeWalkUp(internalPorkElement, functionDefinitionElement) { _, localCandidate ->
if (localCandidate != null) {
if (internalPorkElement == functionDefinitionElement) {
return@treeWalkUp true
}
check(localCandidate, true)
}
true
}
val argumentSpecElements = functionDefinitionElement.childrenOfType<ArgumentSpecElement>()
locals.addAll(argumentSpecElements)
val finalLocals = locals.distinctBy { it.textRange }
return finalLocals.filter { if (name != null) it.name == name else true }
}
fun findAnyDefinitions(containingFile: PsiFile, name: String? = null): List<PorkElement> {
val foundDefinitions = mutableListOf<PorkNamedElement>()
for (file in getRelevantFiles(containingFile)) {
val definitions = PsiTreeUtil.collectElements(file.file) { element ->
element is FunctionDefinitionElement ||
element is LetDefinitionElement
}.filterIsInstance<PorkNamedElement>()
if (name != null) {
val fileFoundDefinition = definitions.firstOrNull {
it.name == name
}
if (fileFoundDefinition != null) {
foundDefinitions.add(fileFoundDefinition)
return foundDefinitions
}
} else {
foundDefinitions.addAll(definitions)
}
}
return foundDefinitions
}
}

View File

@ -0,0 +1,7 @@
package gay.pizza.pork.idea.resolution
enum class PorkRelevantFileType {
Self,
Local,
Std
}

View File

@ -26,6 +26,15 @@
<lang.elementManipulator
implementationClass="gay.pizza.pork.idea.PorkElementManipulator"
forClass="gay.pizza.pork.idea.psi.gen.PorkElement"/>
<codeInsight.parameterNameHints
language="Pork"
implementationClass="gay.pizza.pork.idea.PorkInlayParameterHintsProvider"/>
<lang.quoteHandler
language="Pork"
implementationClass="gay.pizza.pork.idea.PorkQuoteHandler"/>
<!-- <codeInsight.parameterInfo
language="Pork"
implementationClass="gay.pizza.pork.idea.PorkParameterInfoHandler"/>-->
<psi.declarationProvider implementation="gay.pizza.pork.idea.PorkSymbolDeclarationProvider"/>
</extensions>