Skip to content
Open
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ compiles into JVM class files as in-memory or real files.
Originally, Onion was written in Java. It has been rewritten in Scala completely except Parser,
using JavaCC.

## Architecture
The compiler parses source code into an untyped AST and then performs type
checking to produce a **typed AST**. The old intermediate representation (IRT)
has been folded into this typed tree. Code generation now runs on the typed
AST using a new ASM-based backend.

## Tools

### onionc
Expand Down
3 changes: 2 additions & 1 deletion bin/onion
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ then
exit
fi

$JAVA_HOME/bin/java -classpath $CLASSPATH:$ONION_HOME/onion.jar:$ONION_HOME/onion-library.jar:$ONION_HOME/lib/bcel.jar onion.tools.ScriptRunner $@
$JAVA_HOME/bin/java -classpath $CLASSPATH:$ONION_HOME/onion.jar:$ONION_HOME/onion-library.jar onion.tools.ScriptRunner "$@"

4 changes: 2 additions & 2 deletions bin/onion.bat
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ echo Please set the environment variable ONION_HOME
goto END

:START
%JAVA_HOME%\bin\java -classpath %CLASSPATH%;%ONION_HOME%\onion.jar;%ONION_HOME%\lib\bcel.jar;%ONION_HOME%\onion-library.jar onion.tools.ScriptRunner %*
%JAVA_HOME%\bin\java -classpath %CLASSPATH%;%ONION_HOME%\onion.jar;%ONION_HOME%\onion-library.jar onion.tools.ScriptRunner %*

:END
:END
8 changes: 5 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ lazy val onionSettings = Seq(
scalacOptions ++= Seq("-encoding", "utf8", "-unchecked", "-deprecation", "-feature", "-language:implicitConversions", "-language:existentials"),
javacOptions ++= Seq("-sourcepath", "src.lib", "-Xlint:unchecked", "-source", "21"),
libraryDependencies ++= Seq(
"org.apache.bcel" % "bcel" % "6.0",
"org.ow2.asm" % "asm" % "5.0.2",
"org.ow2.asm" % "asm" % "9.8",
"org.ow2.asm" % "asm-tree" % "9.8",
"org.ow2.asm" % "asm-commons" % "9.8",
"org.ow2.asm" % "asm-util" % "9.8",
"net.java.dev.javacc" % "javacc" % "5.0",
"junit" % "junit" % "4.7" % "test",
"org.scalatest" %% "scalatest" % "3.2.19" % "test"
Expand Down Expand Up @@ -123,4 +125,4 @@ lazy val onionSettings = Seq(
assembly / assemblyJarName := "onion.jar"
)

fork in run := true
fork in run := true
2 changes: 1 addition & 1 deletion src/main/resources/META-INF/Manifest.mf
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
Main-Class: onion.tools.CompilerFrontend
Class-Path: lib/bcel.jar lib/scala-compiler.jar lib/scala-library.jar lib/onion-library.jar
Class-Path: lib/scala-compiler.jar lib/scala-library.jar lib/onion-library.jar
118 changes: 118 additions & 0 deletions src/main/scala/onion/compiler/AsmCodeGeneration.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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]
val classAccess =
if node.isInterface then
toAsmModifier(node.modifier) | Opcodes.ACC_INTERFACE | Opcodes.ACC_ABSTRACT
else
toAsmModifier(node.modifier)
cw.visit(Opcodes.V21, classAccess,
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 isAbstract = Modifier.isAbstract(node.modifier) || node.block == null
if isAbstract then
cw.visitMethod(access | Opcodes.ACC_ABSTRACT, node.name, desc, null, null).visitEnd()
else
val mv = cw.visitMethod(access, node.name, desc, null, null)
val gen = new GeneratorAdapter(mv, access, node.name, desc)
mv.visitCode()
findReturn(node.block) match
case Some(ret) =>
emitReturn(gen, ret.term, node.returnType)
case None =>
emitDefaultReturn(gen, node.returnType)
gen.endMethod()

private def findReturn(stmt: TypedAST.ActionStatement): Option[TypedAST.Return] = stmt match
case r: TypedAST.Return => Some(r)
case sb: TypedAST.StatementBlock =>
sb.statements.iterator.map(findReturn).collectFirst { case Some(r) => r }
case _ => None

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 _ =>
emitDefaultReturn(gen, tp)

private def emitDefaultReturn(gen: GeneratorAdapter, tp: TypedAST.Type): Unit =
tp match
case TypedAST.BasicType.VOID =>
gen.visitInsn(Opcodes.RETURN)
case TypedAST.BasicType.BOOLEAN | TypedAST.BasicType.BYTE |
TypedAST.BasicType.SHORT | TypedAST.BasicType.CHAR |
TypedAST.BasicType.INT =>
gen.push(0)
gen.returnValue()
case TypedAST.BasicType.LONG =>
gen.push(0L)
gen.returnValue()
case TypedAST.BasicType.FLOAT =>
gen.push(0f)
gen.returnValue()
case TypedAST.BasicType.DOUBLE =>
gen.push(0d)
gen.returnValue()
case _ =>
gen.visitInsn(Opcodes.ACONST_NULL)
gen.returnValue()

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]
60 changes: 20 additions & 40 deletions src/main/scala/onion/compiler/ClassTable.scala
Original file line number Diff line number Diff line change
@@ -1,63 +1,43 @@
/* ************************************************************** *
* *
* Copyright (c) 2016-, Kota Mizushima, All rights reserved. *
* *
* *
* This software is distributed under the modified BSD License. *
* ************************************************************** */
package onion.compiler

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

/**
* @author Kota Mizushima
*
*/
class ClassTable(classPath: String) {
class ClassTable(classPath: String):
val classes = new OrderedTable[IRT.ClassDefinition]
private val classFiles = new JHashMap[String, IRT.ClassType]
private val arrayClasses = new JHashMap[String, IRT.ArrayType]
private val table = new ClassFileTable(classPath)

def loadArray(component: IRT.Type, dimension: Int): IRT.ArrayType = {
def loadArray(component: IRT.Type, dimension: Int): IRT.ArrayType =
val arrayName = "[" * dimension + component.name
var array: IRT.ArrayType = arrayClasses.get(arrayName)
if (array != null) return array
array = new IRT.ArrayType(component, dimension, this)
var array = arrayClasses.get(arrayName)
if array != null then return array
array = IRT.ArrayType(component, dimension, this)
arrayClasses.put(arrayName, array)
array
}

def load(className: String): IRT.ClassType = {
var clazz: IRT.ClassType = lookup(className)
if (clazz == null) {
val javaClass = table.load(className)
if (javaClass != null) {
clazz = new ClassFileClassTypeRef(javaClass, this)
def load(className: String): IRT.ClassType =
var clazz = lookup(className)
if clazz == null then
val bytes = table.loadBytes(className)
if bytes != null then
clazz = new AsmClassType(bytes, this)
classFiles.put(clazz.name, clazz)
} else {
try {
clazz = new ReflectionalClassTypeRef(Class.forName(className, true, Thread.currentThread.getContextClassLoader), this)
else
try
clazz = new ReflectClassType(Class.forName(className, true, Thread.currentThread.getContextClassLoader), this)
classFiles.put(clazz.name, clazz)
}
catch {
case e: ClassNotFoundException => {}
}
}
}
catch
case _: ClassNotFoundException => ()
clazz
}

def rootClass: IRT.ClassType = load("java.lang.Object")

def lookup(className: String): IRT.ClassType = {
classes.get(className) match {
def lookup(className: String): IRT.ClassType =
classes.get(className) match
case Some(ref) => ref
case None => classFiles.get(className)
}
}

}
Loading
Loading