Skip to content

Commit 23689b1

Browse files
authored
Merge pull request #106 from onion-lang/codex/bcelからasmへの置き換え
Introduce experimental ASM backend
2 parents 6931176 + 4874efe commit 23689b1

File tree

7 files changed

+224
-9
lines changed

7 files changed

+224
-9
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package onion.compiler
2+
3+
import org.objectweb.asm.{ClassWriter, Opcodes, Type}
4+
import org.objectweb.asm.commons.GeneratorAdapter
5+
import org.objectweb.asm.commons.Method
6+
7+
/**
8+
* Experimental ASM-based bytecode generator. Only supports a very small
9+
* subset of language features. Unsupported constructs are ignored or
10+
* generate empty stubs.
11+
*/
12+
class AsmCodeGeneration(config: CompilerConfig) extends BytecodeGenerator:
13+
private def toAsmModifier(mod: Int): Int =
14+
var access = 0
15+
if Modifier.isPublic(mod) then access |= Opcodes.ACC_PUBLIC
16+
if Modifier.isProtected(mod) then access |= Opcodes.ACC_PROTECTED
17+
if Modifier.isPrivate(mod) then access |= Opcodes.ACC_PRIVATE
18+
if Modifier.isStatic(mod) then access |= Opcodes.ACC_STATIC
19+
if Modifier.isFinal(mod) then access |= Opcodes.ACC_FINAL
20+
if Modifier.isAbstract(mod) then access |= Opcodes.ACC_ABSTRACT
21+
access
22+
23+
private def asmType(tp: TypedAST.Type): Type = tp match
24+
case TypedAST.BasicType.VOID => Type.VOID_TYPE
25+
case TypedAST.BasicType.BOOLEAN => Type.BOOLEAN_TYPE
26+
case TypedAST.BasicType.BYTE => Type.BYTE_TYPE
27+
case TypedAST.BasicType.SHORT => Type.SHORT_TYPE
28+
case TypedAST.BasicType.CHAR => Type.CHAR_TYPE
29+
case TypedAST.BasicType.INT => Type.INT_TYPE
30+
case TypedAST.BasicType.LONG => Type.LONG_TYPE
31+
case TypedAST.BasicType.FLOAT => Type.FLOAT_TYPE
32+
case TypedAST.BasicType.DOUBLE => Type.DOUBLE_TYPE
33+
case c: TypedAST.ClassType => Type.getObjectType(c.name.replace('.', '/'))
34+
case a: TypedAST.ArrayType => Type.getType("[" * a.dimension + asmType(a.component).getDescriptor)
35+
case _ => Type.VOID_TYPE
36+
37+
def process(classes: Seq[TypedAST.ClassDefinition]): Seq[CompiledClass] =
38+
classes.map(codeClass)
39+
40+
private def codeClass(node: TypedAST.ClassDefinition): CompiledClass =
41+
val cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS)
42+
val superName =
43+
if node.superClass != null then node.superClass.name.replace('.', '/')
44+
else "java/lang/Object"
45+
val interfaces =
46+
if node.interfaces != null then node.interfaces.map(_.name.replace('.', '/')).toArray
47+
else Array.empty[String]
48+
cw.visit(Opcodes.V21, toAsmModifier(node.modifier) | (if node.isInterface then Opcodes.ACC_INTERFACE else 0),
49+
node.name.replace('.', '/'), null, superName, interfaces)
50+
51+
for m <- node.methods do m match
52+
case md: TypedAST.MethodDefinition => codeMethod(cw, md)
53+
case _ =>
54+
cw.visitEnd()
55+
val bytes = cw.toByteArray
56+
val dir = if config.outputDirectory != null then config.outputDirectory else ""
57+
CompiledClass(node.name, dir, bytes)
58+
59+
private def codeMethod(cw: ClassWriter, node: TypedAST.MethodDefinition): Unit =
60+
val access = toAsmModifier(node.modifier)
61+
val desc = Method(node.name, asmType(node.returnType), node.arguments.map(asmType)).getDescriptor
62+
val mv = cw.visitMethod(access, node.name, desc, null, null)
63+
val gen = new GeneratorAdapter(mv, access, node.name, desc)
64+
mv.visitCode()
65+
node.block.statements match
66+
case Array(ret: TypedAST.Return) =>
67+
emitReturn(gen, ret.term, node.returnType)
68+
case _ =>
69+
gen.visitInsn(Opcodes.RETURN)
70+
gen.endMethod()
71+
72+
private def emitReturn(gen: GeneratorAdapter, term: TypedAST.Term, tp: TypedAST.Type): Unit = term match
73+
case v: TypedAST.IntValue =>
74+
gen.push(v.value)
75+
gen.returnValue()
76+
case v: TypedAST.StringValue =>
77+
gen.push(v.value)
78+
gen.returnValue()
79+
case _ =>
80+
gen.visitInsn(Opcodes.RETURN)
81+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package onion.compiler
2+
3+
trait BytecodeGenerator:
4+
def process(classes: Seq[TypedAST.ClassDefinition]): Seq[CompiledClass]

src/main/scala/onion/compiler/ClassTable.scala

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package onion.compiler
99

1010
import java.util.{HashMap => JHashMap}
1111
import onion.compiler.environment.BcelRefs.BcelClassType
12+
import onion.compiler.environment.AsmRefs.AsmClassType
1213
import onion.compiler.environment.ClassFileTable
1314
import onion.compiler.environment.ReflectionRefs.ReflectClassType
1415

@@ -39,12 +40,18 @@ class ClassTable(classPath: String) {
3940
clazz = new BcelClassType(javaClass, this)
4041
classFiles.put(clazz.name, clazz)
4142
} else {
42-
try {
43-
clazz = new ReflectClassType(Class.forName(className, true, Thread.currentThread.getContextClassLoader), this)
43+
val bytes = table.loadBytes(className)
44+
if (bytes != null) {
45+
clazz = new AsmClassType(bytes, this)
4446
classFiles.put(clazz.name, clazz)
45-
}
46-
catch {
47-
case e: ClassNotFoundException => {}
47+
} else {
48+
try {
49+
clazz = new ReflectClassType(Class.forName(className, true, Thread.currentThread.getContextClassLoader), this)
50+
classFiles.put(clazz.name, clazz)
51+
}
52+
catch {
53+
case e: ClassNotFoundException => {}
54+
}
4855
}
4956
}
5057
}

src/main/scala/onion/compiler/CodeGeneration.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,10 +307,10 @@ object CodeGeneration {
307307
}
308308
}
309309

310-
class CodeGeneration(config: CompilerConfig) {
310+
class CodeGeneration(config: CompilerConfig) extends BytecodeGenerator {
311311
import CodeGeneration._
312312

313-
def process(classes: Seq[IRT.ClassDefinition]): Seq[CompiledClass] = {
313+
def process(classes: Seq[TypedAST.ClassDefinition]): Seq[CompiledClass] = {
314314
compiledClasses.clear
315315
val base = (if (config.outputDirectory != null) config.outputDirectory else ".") + Systems.fileSeparator
316316
for (klass <- classes) codeClass(klass)

src/main/scala/onion/compiler/TypedGenerating.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ class TypedGenerating(config: CompilerConfig)
1313
def newEnvironment(source: Seq[TypedAST.ClassDefinition]): TypedGeneratingEnvironment =
1414
new TypedGeneratingEnvironment
1515

16-
private val generator = new CodeGeneration(config)
16+
private val generator: BytecodeGenerator =
17+
if sys.props.get("onion.asm").exists(_.toBoolean) then
18+
new AsmCodeGeneration(config)
19+
else
20+
new CodeGeneration(config)
1721

1822
def processBody(source: Seq[TypedAST.ClassDefinition], environment: TypedGeneratingEnvironment): Seq[CompiledClass] =
19-
generator.process(source.asInstanceOf[Seq[IRT.ClassDefinition]])
23+
generator.process(source)
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package onion.compiler.environment
2+
3+
import onion.compiler.{IRT, Modifier, OnionTypeConversion, MultiTable, OrderedTable, ClassTable}
4+
import org.objectweb.asm.{ClassReader, Opcodes}
5+
import org.objectweb.asm.tree.{ClassNode, MethodNode, FieldNode}
6+
import org.apache.bcel.generic.{Type => BType}
7+
8+
object AsmRefs {
9+
private def toOnionModifier(access: Int): Int = {
10+
var mod = 0
11+
if ((access & Opcodes.ACC_PRIVATE) != 0) mod |= Modifier.PRIVATE
12+
if ((access & Opcodes.ACC_PROTECTED) != 0) mod |= Modifier.PROTECTED
13+
if ((access & Opcodes.ACC_PUBLIC) != 0) mod |= Modifier.PUBLIC
14+
if ((access & Opcodes.ACC_STATIC) != 0) mod |= Modifier.STATIC
15+
if ((access & Opcodes.ACC_SYNCHRONIZED) != 0) mod |= Modifier.SYNCHRONIZED
16+
if ((access & Opcodes.ACC_ABSTRACT) != 0) mod |= Modifier.ABSTRACT
17+
if ((access & Opcodes.ACC_FINAL) != 0) mod |= Modifier.FINAL
18+
mod
19+
}
20+
21+
final val CONSTRUCTOR_NAME = "<init>"
22+
23+
class AsmMethodRef(method: MethodNode, override val affiliation: IRT.ClassType, bridge: OnionTypeConversion) extends IRT.Method {
24+
override val modifier: Int = toOnionModifier(method.access)
25+
override val name: String = method.name
26+
private val argTypes = org.objectweb.asm.Type.getArgumentTypes(method.desc).map(t => bridge.toOnionType(BType.getType(t.getDescriptor)))
27+
override def arguments: Array[IRT.Type] = argTypes.clone()
28+
override val returnType: IRT.Type = bridge.toOnionType(BType.getType(org.objectweb.asm.Type.getReturnType(method.desc).getDescriptor))
29+
val underlying: MethodNode = method
30+
}
31+
32+
class AsmFieldRef(field: FieldNode, override val affiliation: IRT.ClassType, bridge: OnionTypeConversion) extends IRT.FieldRef {
33+
override val modifier: Int = toOnionModifier(field.access)
34+
override val name: String = field.name
35+
override val `type`: IRT.Type = bridge.toOnionType(BType.getType(field.desc))
36+
val underlying: FieldNode = field
37+
}
38+
39+
class AsmConstructorRef(method: MethodNode, override val affiliation: IRT.ClassType, bridge: OnionTypeConversion) extends IRT.ConstructorRef {
40+
override val modifier: Int = toOnionModifier(method.access)
41+
override val name: String = CONSTRUCTOR_NAME
42+
private val args0 = org.objectweb.asm.Type.getArgumentTypes(method.desc).map(t => bridge.toOnionType(BType.getType(t.getDescriptor)))
43+
override def getArgs: Array[IRT.Type] = args0.clone()
44+
val underlying: MethodNode = method
45+
}
46+
47+
class AsmClassType(classBytes: Array[Byte], table: ClassTable) extends IRT.AbstractClassType {
48+
private val node = {
49+
val cr = new ClassReader(classBytes)
50+
val n = new ClassNode()
51+
cr.accept(n, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES)
52+
n
53+
}
54+
55+
private val bridge = new OnionTypeConversion(table)
56+
private val modifier_ = toOnionModifier(node.access)
57+
58+
private lazy val methods_ : MultiTable[IRT.Method] = {
59+
val m = new MultiTable[IRT.Method]
60+
import scala.jdk.CollectionConverters._
61+
for (method <- node.methods.asInstanceOf[java.util.List[MethodNode]].asScala if method.name != CONSTRUCTOR_NAME) {
62+
m.add(new AsmMethodRef(method, this, bridge))
63+
}
64+
m
65+
}
66+
67+
private lazy val fields_ : OrderedTable[IRT.FieldRef] = {
68+
val f = new OrderedTable[IRT.FieldRef]
69+
import scala.jdk.CollectionConverters._
70+
for (field <- node.fields.asInstanceOf[java.util.List[FieldNode]].asScala) {
71+
f.add(new AsmFieldRef(field, this, bridge))
72+
}
73+
f
74+
}
75+
76+
private lazy val constructors_ : Seq[IRT.ConstructorRef] = {
77+
import scala.jdk.CollectionConverters._
78+
node.methods.asInstanceOf[java.util.List[MethodNode]].asScala.collect {
79+
case m if m.name == CONSTRUCTOR_NAME => new AsmConstructorRef(m, this, bridge)
80+
}.toSeq
81+
}
82+
83+
def isInterface: Boolean = (node.access & Opcodes.ACC_INTERFACE) != 0
84+
def modifier: Int = modifier_
85+
def name: String = node.name.replace('/', '.')
86+
def superClass: IRT.ClassType = {
87+
if (node.superName == null) null else table.load(node.superName.replace('/', '.'))
88+
}
89+
def interfaces: Seq[IRT.ClassType] = {
90+
import scala.jdk.CollectionConverters._
91+
node.interfaces.asInstanceOf[java.util.List[String]].asScala.map(n => table.load(n.replace('/', '.'))).toIndexedSeq
92+
}
93+
94+
def methods: Seq[IRT.Method] = methods_.values
95+
def methods(name: String): Array[IRT.Method] = methods_.get(name).toArray
96+
def fields: Array[IRT.FieldRef] = fields_.values.toArray
97+
def field(name: String): IRT.FieldRef = fields_.get(name).orNull
98+
def constructors: Array[IRT.ConstructorRef] = constructors_.toArray
99+
}
100+
}
101+

src/main/scala/onion/compiler/environment/ClassFileTable.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ class ClassFileTable(classPathString: String) {
3636
}
3737
}
3838

39+
def loadBytes(className: String): Array[Byte] = {
40+
try {
41+
val classFile: ClassPath.ClassFile = classPath.getClassFile(className)
42+
val in = classFile.getInputStream
43+
val out = new ByteArrayOutputStream()
44+
val buf = new Array[Byte](8192)
45+
var len = in.read(buf)
46+
while (len != -1) {
47+
out.write(buf, 0, len)
48+
len = in.read(buf)
49+
}
50+
in.close()
51+
out.toByteArray
52+
} catch {
53+
case _: IOException => null
54+
}
55+
}
56+
3957
private def add(className: String): JavaClass = {
4058
try {
4159
val classFile: ClassPath.ClassFile = classPath.getClassFile(className)

0 commit comments

Comments
 (0)