Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
81 changes: 81 additions & 0 deletions src/main/scala/onion/compiler/AsmCodeGeneration.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package onion.compiler

import org.objectweb.asm.{ClassWriter, Opcodes, Type}
import org.objectweb.asm.commons.GeneratorAdapter
import org.objectweb.asm.commons.Method

/**
* Experimental ASM-based bytecode generator. Only supports a very small
* subset of language features. Unsupported constructs are ignored or
* generate empty stubs.
*/
class AsmCodeGeneration(config: CompilerConfig) extends BytecodeGenerator:
private def toAsmModifier(mod: Int): Int =
var access = 0
if Modifier.isPublic(mod) then access |= Opcodes.ACC_PUBLIC
if Modifier.isProtected(mod) then access |= Opcodes.ACC_PROTECTED
if Modifier.isPrivate(mod) then access |= Opcodes.ACC_PRIVATE
if Modifier.isStatic(mod) then access |= Opcodes.ACC_STATIC
if Modifier.isFinal(mod) then access |= Opcodes.ACC_FINAL
if Modifier.isAbstract(mod) then access |= Opcodes.ACC_ABSTRACT
access

private def asmType(tp: TypedAST.Type): Type = tp match
case TypedAST.BasicType.VOID => Type.VOID_TYPE
case TypedAST.BasicType.BOOLEAN => Type.BOOLEAN_TYPE
case TypedAST.BasicType.BYTE => Type.BYTE_TYPE
case TypedAST.BasicType.SHORT => Type.SHORT_TYPE
case TypedAST.BasicType.CHAR => Type.CHAR_TYPE
case TypedAST.BasicType.INT => Type.INT_TYPE
case TypedAST.BasicType.LONG => Type.LONG_TYPE
case TypedAST.BasicType.FLOAT => Type.FLOAT_TYPE
case TypedAST.BasicType.DOUBLE => Type.DOUBLE_TYPE
case c: TypedAST.ClassType => Type.getObjectType(c.name.replace('.', '/'))
case a: TypedAST.ArrayType => Type.getType("[" * a.dimension + asmType(a.component).getDescriptor)
case _ => Type.VOID_TYPE

def process(classes: Seq[TypedAST.ClassDefinition]): Seq[CompiledClass] =
classes.map(codeClass)

private def codeClass(node: TypedAST.ClassDefinition): CompiledClass =
val cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS)
val superName =
if node.superClass != null then node.superClass.name.replace('.', '/')
else "java/lang/Object"
val interfaces =
if node.interfaces != null then node.interfaces.map(_.name.replace('.', '/')).toArray
else Array.empty[String]
cw.visit(Opcodes.V21, toAsmModifier(node.modifier) | (if node.isInterface then Opcodes.ACC_INTERFACE else 0),
node.name.replace('.', '/'), null, superName, interfaces)

for m <- node.methods do m match
case md: TypedAST.MethodDefinition => codeMethod(cw, md)
case _ =>
cw.visitEnd()
val bytes = cw.toByteArray
val dir = if config.outputDirectory != null then config.outputDirectory else ""
CompiledClass(node.name, dir, bytes)

private def codeMethod(cw: ClassWriter, node: TypedAST.MethodDefinition): Unit =
val access = toAsmModifier(node.modifier)
val desc = Method(node.name, asmType(node.returnType), node.arguments.map(asmType)).getDescriptor
val mv = cw.visitMethod(access, node.name, desc, null, null)
val gen = new GeneratorAdapter(mv, access, node.name, desc)
mv.visitCode()
node.block.statements match
case Array(ret: TypedAST.Return) =>
emitReturn(gen, ret.term, node.returnType)
case _ =>
gen.visitInsn(Opcodes.RETURN)
gen.endMethod()

private def emitReturn(gen: GeneratorAdapter, term: TypedAST.Term, tp: TypedAST.Type): Unit = term match
case v: TypedAST.IntValue =>
gen.push(v.value)
gen.returnValue()
case v: TypedAST.StringValue =>
gen.push(v.value)
gen.returnValue()
case _ =>
gen.visitInsn(Opcodes.RETURN)

4 changes: 4 additions & 0 deletions src/main/scala/onion/compiler/BytecodeGenerator.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package onion.compiler

trait BytecodeGenerator:
def process(classes: Seq[TypedAST.ClassDefinition]): Seq[CompiledClass]
17 changes: 12 additions & 5 deletions src/main/scala/onion/compiler/ClassTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package onion.compiler

import java.util.{HashMap => JHashMap}
import onion.compiler.environment.BcelRefs.BcelClassType
import onion.compiler.environment.AsmRefs.AsmClassType
import onion.compiler.environment.ClassFileTable
import onion.compiler.environment.ReflectionRefs.ReflectClassType

Expand Down Expand Up @@ -39,12 +40,18 @@ class ClassTable(classPath: String) {
clazz = new BcelClassType(javaClass, this)
classFiles.put(clazz.name, clazz)
} else {
try {
clazz = new ReflectClassType(Class.forName(className, true, Thread.currentThread.getContextClassLoader), this)
val bytes = table.loadBytes(className)
if (bytes != null) {
clazz = new AsmClassType(bytes, this)
classFiles.put(clazz.name, clazz)
}
catch {
case e: ClassNotFoundException => {}
} else {
try {
clazz = new ReflectClassType(Class.forName(className, true, Thread.currentThread.getContextClassLoader), this)
classFiles.put(clazz.name, clazz)
}
catch {
case e: ClassNotFoundException => {}
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/scala/onion/compiler/CodeGeneration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,10 @@ object CodeGeneration {
}
}

class CodeGeneration(config: CompilerConfig) {
class CodeGeneration(config: CompilerConfig) extends BytecodeGenerator {
import CodeGeneration._

def process(classes: Seq[IRT.ClassDefinition]): Seq[CompiledClass] = {
def process(classes: Seq[TypedAST.ClassDefinition]): Seq[CompiledClass] = {
compiledClasses.clear
val base = (if (config.outputDirectory != null) config.outputDirectory else ".") + Systems.fileSeparator
for (klass <- classes) codeClass(klass)
Expand Down
8 changes: 6 additions & 2 deletions src/main/scala/onion/compiler/TypedGenerating.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ class TypedGenerating(config: CompilerConfig)
def newEnvironment(source: Seq[TypedAST.ClassDefinition]): TypedGeneratingEnvironment =
new TypedGeneratingEnvironment

private val generator = new CodeGeneration(config)
private val generator: BytecodeGenerator =
if sys.props.get("onion.asm").exists(_.toBoolean) then
new AsmCodeGeneration(config)
else
new CodeGeneration(config)

def processBody(source: Seq[TypedAST.ClassDefinition], environment: TypedGeneratingEnvironment): Seq[CompiledClass] =
generator.process(source.asInstanceOf[Seq[IRT.ClassDefinition]])
generator.process(source)
101 changes: 101 additions & 0 deletions src/main/scala/onion/compiler/environment/AsmRefs.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package onion.compiler.environment

import onion.compiler.{IRT, Modifier, OnionTypeConversion, MultiTable, OrderedTable, ClassTable}
import org.objectweb.asm.{ClassReader, Opcodes}
import org.objectweb.asm.tree.{ClassNode, MethodNode, FieldNode}
import org.apache.bcel.generic.{Type => BType}

object AsmRefs {
private def toOnionModifier(access: Int): Int = {
var mod = 0
if ((access & Opcodes.ACC_PRIVATE) != 0) mod |= Modifier.PRIVATE
if ((access & Opcodes.ACC_PROTECTED) != 0) mod |= Modifier.PROTECTED
if ((access & Opcodes.ACC_PUBLIC) != 0) mod |= Modifier.PUBLIC
if ((access & Opcodes.ACC_STATIC) != 0) mod |= Modifier.STATIC
if ((access & Opcodes.ACC_SYNCHRONIZED) != 0) mod |= Modifier.SYNCHRONIZED
if ((access & Opcodes.ACC_ABSTRACT) != 0) mod |= Modifier.ABSTRACT
if ((access & Opcodes.ACC_FINAL) != 0) mod |= Modifier.FINAL
mod
}

final val CONSTRUCTOR_NAME = "<init>"

class AsmMethodRef(method: MethodNode, override val affiliation: IRT.ClassType, bridge: OnionTypeConversion) extends IRT.Method {
override val modifier: Int = toOnionModifier(method.access)
override val name: String = method.name
private val argTypes = org.objectweb.asm.Type.getArgumentTypes(method.desc).map(t => bridge.toOnionType(BType.getType(t.getDescriptor)))
override def arguments: Array[IRT.Type] = argTypes.clone()
override val returnType: IRT.Type = bridge.toOnionType(BType.getType(org.objectweb.asm.Type.getReturnType(method.desc).getDescriptor))
val underlying: MethodNode = method
}

class AsmFieldRef(field: FieldNode, override val affiliation: IRT.ClassType, bridge: OnionTypeConversion) extends IRT.FieldRef {
override val modifier: Int = toOnionModifier(field.access)
override val name: String = field.name
override val `type`: IRT.Type = bridge.toOnionType(BType.getType(field.desc))
val underlying: FieldNode = field
}

class AsmConstructorRef(method: MethodNode, override val affiliation: IRT.ClassType, bridge: OnionTypeConversion) extends IRT.ConstructorRef {
override val modifier: Int = toOnionModifier(method.access)
override val name: String = CONSTRUCTOR_NAME
private val args0 = org.objectweb.asm.Type.getArgumentTypes(method.desc).map(t => bridge.toOnionType(BType.getType(t.getDescriptor)))
override def getArgs: Array[IRT.Type] = args0.clone()
val underlying: MethodNode = method
}

class AsmClassType(classBytes: Array[Byte], table: ClassTable) extends IRT.AbstractClassType {
private val node = {
val cr = new ClassReader(classBytes)
val n = new ClassNode()
cr.accept(n, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES)
n
}

private val bridge = new OnionTypeConversion(table)
private val modifier_ = toOnionModifier(node.access)

private lazy val methods_ : MultiTable[IRT.Method] = {
val m = new MultiTable[IRT.Method]
import scala.jdk.CollectionConverters._
for (method <- node.methods.asInstanceOf[java.util.List[MethodNode]].asScala if method.name != CONSTRUCTOR_NAME) {
m.add(new AsmMethodRef(method, this, bridge))
}
m
}

private lazy val fields_ : OrderedTable[IRT.FieldRef] = {
val f = new OrderedTable[IRT.FieldRef]
import scala.jdk.CollectionConverters._
for (field <- node.fields.asInstanceOf[java.util.List[FieldNode]].asScala) {
f.add(new AsmFieldRef(field, this, bridge))
}
f
}

private lazy val constructors_ : Seq[IRT.ConstructorRef] = {
import scala.jdk.CollectionConverters._
node.methods.asInstanceOf[java.util.List[MethodNode]].asScala.collect {
case m if m.name == CONSTRUCTOR_NAME => new AsmConstructorRef(m, this, bridge)
}.toSeq
}

def isInterface: Boolean = (node.access & Opcodes.ACC_INTERFACE) != 0
def modifier: Int = modifier_
def name: String = node.name.replace('/', '.')
def superClass: IRT.ClassType = {
if (node.superName == null) null else table.load(node.superName.replace('/', '.'))
}
def interfaces: Seq[IRT.ClassType] = {
import scala.jdk.CollectionConverters._
node.interfaces.asInstanceOf[java.util.List[String]].asScala.map(n => table.load(n.replace('/', '.'))).toIndexedSeq
}

def methods: Seq[IRT.Method] = methods_.values
def methods(name: String): Array[IRT.Method] = methods_.get(name).toArray
def fields: Array[IRT.FieldRef] = fields_.values.toArray
def field(name: String): IRT.FieldRef = fields_.get(name).orNull
def constructors: Array[IRT.ConstructorRef] = constructors_.toArray
}
}

18 changes: 18 additions & 0 deletions src/main/scala/onion/compiler/environment/ClassFileTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,24 @@ class ClassFileTable(classPathString: String) {
}
}

def loadBytes(className: String): Array[Byte] = {
try {
val classFile: ClassPath.ClassFile = classPath.getClassFile(className)
val in = classFile.getInputStream
val out = new ByteArrayOutputStream()
val buf = new Array[Byte](8192)
var len = in.read(buf)
while (len != -1) {
out.write(buf, 0, len)
len = in.read(buf)
}
in.close()
out.toByteArray
} catch {
case _: IOException => null
}
}

private def add(className: String): JavaClass = {
try {
val classFile: ClassPath.ClassFile = classPath.getClassFile(className)
Expand Down