Commit b80efcce authored by Kunshan Wang's avatar Kunshan Wang

CALL, RET and some fac and fib programs.

parent 952d4eea
......@@ -57,8 +57,6 @@ class InterpreterThread(val id: Int, microVM: MicroVM, initialStack: Interpreter
}
private def interpretCurrentInstruction(): Unit = try {
val curInst = this.curInst
logger.debug(ctx + "Executing instruction...")
curInst match {
......@@ -345,13 +343,50 @@ class InterpreterThread(val id: Int, microVM: MicroVM, initialStack: Interpreter
val dest = cases.find(pair => boxOf(pair._1).asInstanceOf[BoxInt].value == ov).map(_._2).getOrElse(defDest)
branchAndMovePC(dest)
}
case _ => throw new UvmRefImplException("Operand type must be integer. %s found.".format(opndTy))
case _ => throw new UvmRefImplException(ctx + "Operand type must be integer. %s found.".format(opndTy))
}
}
case i @ InstPhi(_, _) => throw new UvmRefImplException("PHI instructions reached in normal execution, " +
case i @ InstPhi(_, _) => throw new UvmRefImplException(ctx + "PHI instructions reached in normal execution, " +
"but PHI must only appear in the beginning of basic blocks and not in the entry block.")
case i @ InstCall(sig, callee, argList, excClause, keepAlives) => {
val calleeFunc = boxOf(callee).asInstanceOf[BoxFunc].func.getOrElse {
throw new UvmRuntimeException(ctx + "Callee must not be NULL")
}
val funcVer = getFuncDefOrTriggerCallback(calleeFunc)
val argBoxes = argList.map(boxOf)
curStack.pushFrame(funcVer, argBoxes)
}
case i @ InstTailCall(sig, callee, argList) => {
val calleeFunc = boxOf(callee).asInstanceOf[BoxFunc].func.getOrElse {
throw new UvmRuntimeException(ctx + "Callee must not be NULL")
}
val funcVer = getFuncDefOrTriggerCallback(calleeFunc)
val argBoxes = argList.map(boxOf)
curStack.replaceTop(funcVer, argBoxes)
}
case i @ InstRet(retTy, retVal) => {
val rvb = boxOf(retVal)
curStack.popFrame()
val newCurInst = curInst // in the parent frame of the RET
boxOf(newCurInst).copyFrom(rvb)
continueNormally()
}
case i @ InstRetVoid() => {
curStack.popFrame()
continueNormally()
}
// Indentation guide: Insert more instructions here.
case i @ InstTrap(retTy, excClause, keepAlives) => {
......@@ -406,7 +441,7 @@ class InterpreterThread(val id: Int, microVM: MicroVM, initialStack: Interpreter
}
} catch {
case e: Exception => {
logger.debug(ctx + "Exception thrown while interpreting instruction.")
logger.error(ctx + "Exception thrown while interpreting instruction.")
throw e
}
}
......@@ -531,8 +566,20 @@ class InterpreterThread(val id: Int, microVM: MicroVM, initialStack: Interpreter
case i: InstWatchPoint => i.exc
case i: InstSwapStack => i.excClause.map(_.exc)
case _ => {
throw new UvmRefImplException("Instruction %s (%s) is in a stack frame when an exception is thrown.".format(inst.repr, inst.getClass.getName))
throw new UvmRefImplException(ctx + "Instruction %s (%s) is in a stack frame when an exception is thrown.".format(inst.repr, inst.getClass.getName))
}
}
}
@tailrec
private def getFuncDefOrTriggerCallback(f: Function): FuncVer = {
f.versions.headOption match {
case Some(v) => v
case None =>
logger.debug(ctx + "Function %s is undefined. Trigger undefined function event.".format(f.repr))
microVM.trapManager.undefinedFunctionHandler.handleUndefinedFunction(f.id)
getFuncDefOrTriggerCallback(f)
}
}
}
......@@ -30,6 +30,22 @@ class InterpreterStack(val id: Int, val stackMemory: StackMemory, stackBottomFun
res
}
}
def pushFrame(funcVer: FuncVer, args: Seq[ValueBox]): Unit = {
val newFrame = InterpreterFrame.frameForCall(funcVer, args, Some(top))
top = newFrame
}
def replaceTop(funcVer: FuncVer, args: Seq[ValueBox]): Unit = {
val newFrame = InterpreterFrame.frameForCall(funcVer, args, top.prev)
top = newFrame
}
def popFrame(): Unit = {
top = top.prev.getOrElse {
throw new UvmRuntimeException("Attemting to pop the last frame of a stack. Stack ID: %d.".format(id))
}
}
}
class InterpreterFrame(val funcVer: FuncVer, val prev: Option[InterpreterFrame]) {
......
......@@ -4,7 +4,9 @@ import uvm._
import uvm.comminsts._
import uvm.types._
abstract class SSAVariable extends IdentifiedSettable
abstract class SSAVariable extends IdentifiedSettable {
override def hashCode(): Int = id
}
// Global variables: Constants, Global Cells and Functions (Function is defined in controlFlow.scala)
......
package uvm.refimpl.itpr
import org.scalatest._
import java.io.FileReader
import uvm._
import uvm.types._
import uvm.ssavariables._
import uvm.refimpl._
import uvm.refimpl.itpr._
import MemoryOrder._
import AtomicRMWOptr._
import uvm.refimpl.mem.TypeSizes.Word
class UvmInterpreterSimpleTests extends FlatSpec with Matchers {
{ // Configure logger
import org.slf4j.LoggerFactory
import org.slf4j.{ Logger => SLogger }
import ch.qos.logback.classic.{ Logger => LLogger, Level }
import ch.qos.logback.classic.Level._
def setLevel(name: String, level: Level): Unit = {
LoggerFactory.getLogger(name).asInstanceOf[LLogger].setLevel(level)
}
setLevel(SLogger.ROOT_LOGGER_NAME, INFO)
setLevel("uvm.refimpl.itpr", DEBUG)
}
val microVM = new MicroVM();
implicit def idOf(name: String): Int = microVM.globalBundle.allNs(name).id
implicit def nameOf(id: Int): String = microVM.globalBundle.allNs(id).name.get
{
val ca = microVM.newClientAgent()
val r = new FileReader("tests/uvm-refimpl-test/primitives.uir")
ca.loadBundle(r)
r.close()
val r2 = new FileReader("tests/uvm-refimpl-test/simple-tests.uir")
ca.loadBundle(r2)
r2.close()
ca.close()
}
type TrapHandlerFunction = (ClientAgent, Handle, Handle, Int) => TrapHandlerResult
class MockTrapHandler(thf: TrapHandlerFunction) extends TrapHandler {
def handleTrap(ca: ClientAgent, thread: Handle, stack: Handle, watchPointID: Int): TrapHandlerResult = {
thf(ca, thread, stack, watchPointID)
}
}
def testFunc(ca: ClientAgent, func: Handle, args: Seq[Handle])(handler: TrapHandlerFunction): Unit = {
microVM.trapManager.trapHandler = new MockTrapHandler(handler)
val hStack = ca.newStack(func, args)
val hThread = ca.newThread(hStack)
microVM.threadStackManager.joinAll()
}
implicit class MagicalBox(vb: ValueBox) {
def asInt: BigInt = vb.asInstanceOf[BoxInt].value
def asSInt(l: Int): BigInt = OpHelper.prepareSigned(vb.asInstanceOf[BoxInt].value, l)
def asUInt(l: Int): BigInt = OpHelper.prepareUnsigned(vb.asInstanceOf[BoxInt].value, l)
def asFloat: Float = vb.asInstanceOf[BoxFloat].value
def asDouble: Double = vb.asInstanceOf[BoxDouble].value
def asRef: Word = vb.asInstanceOf[BoxRef].objRef
def asIRef: (Word, Word) = { val b = vb.asInstanceOf[BoxIRef]; (b.objRef, b.offset) }
def asIRefAddr: Word = { val b = vb.asInstanceOf[BoxIRef]; b.objRef + b.offset }
def asStruct: Seq[ValueBox] = vb.asInstanceOf[BoxStruct].values
def asFunc: Option[Function] = vb.asInstanceOf[BoxFunc].func
def asThread: Option[InterpreterThread] = vb.asInstanceOf[BoxThread].thread
def asStack: Option[InterpreterStack] = vb.asInstanceOf[BoxStack].stack
def asTR64Box: BoxTagRef64 = vb.asInstanceOf[BoxTagRef64]
def asTR64Raw: Long = vb.asInstanceOf[BoxTagRef64].raw
def asVec: Seq[ValueBox] = vb.asInstanceOf[BoxVector].values
}
"Factorial functions" should "work" in {
val ca = microVM.newClientAgent()
val func = ca.putFunction("@test_fac")
testFunc(ca, func, Seq()) { (ca, th, st, wp) =>
val Seq(r1, r2, r3) = ca.dumpKeepalives(st, 0)
r1.vb.asInt shouldEqual 3628800
r2.vb.asInt shouldEqual 3628800
r3.vb.asInt shouldEqual 3628800
TrapRebindPassVoid(st)
}
ca.close()
}
"Fibonacci functions" should "work" in {
val ca = microVM.newClientAgent()
val func = ca.putFunction("@test_fib")
val watch = true
testFunc(ca, func, Seq()) { (ca, th, st, wp) =>
val trapName = nameOf(ca.currentInstruction(st, 0))
trapName match {
case "@fibonacci_mat_v1.watch" => {
if (watch) {
val vhs = ca.dumpKeepalives(st, 0)
val vs = vhs.map(_.vb.asInt)
println("watch " + vs)
}
TrapRebindPassVoid(st)
}
case "@test_fib_v1.checktrap" => {
val Seq(r1, r2) = ca.dumpKeepalives(st, 0)
r1.vb.asInt shouldEqual 55
r2.vb.asInt shouldEqual 55
TrapRebindPassVoid(st)
}
case _ => fail("Should not hit " + trapName)
}
}
ca.close()
}
}
\ No newline at end of file
......@@ -757,4 +757,22 @@ class UvmInterpreterSpec extends FlatSpec with Matchers {
ca.close()
}
"CALL and RET" should "work for normal returns" in {
val ca = microVM.newClientAgent()
val func = ca.putFunction("@call_ret")
val a0 = ca.putInt("@i64", 3)
val a1 = ca.putInt("@i64", 4)
testFunc(ca, func, Seq(a0, a1)) { (ca, th, st, wp) =>
val Seq(ss) = ca.dumpKeepalives(st, 0)
ss.vb.asInt shouldEqual 25
TrapRebindPassVoid(st)
}
ca.close()
}
}
\ No newline at end of file
......@@ -405,24 +405,21 @@
COMMINST @uvm.thread_exit
}
// .funcsig @square_sum_sig = @i_ii
// .funcdef @square_sum VERSION @square_sum_v1 <@square_sum_sig> (%a %b) {
// %entry:
// %a2 = MUL <@i64> %a %a
// %b2 = MUL <@i64> %b %b
// %s = ADD <@i64> %a2 %b2
// RET <@i64> %s
// }
//
// .funcsig @call_ret_sig = @i_ii
// .funcdef @call_ret VERSION @call_ret_v1 <@call_ret_sig> (%a %b) {
// %entry:
// %ss = CALL <@i_ii> @square_sum (%a %b)
// %trap = TRAP <@void> KEEPALIVE (%ss)
// %exit:
// COMMINST @uvm.thread_exit
// }
//
.funcdef @square_sum VERSION @square_sum_v1 <@i_ii> (%a %b) {
%entry:
%a2 = MUL <@i64> %a %a
%b2 = MUL <@i64> %b %b
%s = ADD <@i64> %a2 %b2
RET <@i64> %s
}
.funcdef @call_ret VERSION @call_ret_v1 <@i_ii> (%a %b) {
%entry:
%ss = CALL <@i_ii> @square_sum (%a %b)
%trap = TRAP <@void> KEEPALIVE (%ss)
COMMINST @uvm.thread_exit
}
// .funcsig @thrower_sig = @noparamsnoret
// .funcdef @thrower VERSION @thrower_v1 <@thrower_sig> () {
// %entry:
......
......@@ -12,25 +12,17 @@
.const @TRUE <@i64> = 1
.const @FALSE <@i64> = 0
.funcsig @i_i = @i64 (@i64)
.funcsig @i_ii = @i64 (@i64 @i64)
.typedef @refvoid = ref<@void>
.const @NULLREF <@refvoid> = NULL
.typedef @StructFoo = struct <@i32 @i64 @float @double>
.const @STRUCT_FOO <@StructFoo> = {1 2 3.0f 4.0d}
.typedef @refi64 = ref<@i64>
.typedef @irefi64 = iref<@i64>
.typedef @weakrefi64 = weakref<@i64>
.typedef @StructBar = struct <
@i64 @i32 @i16 @i8 @float @double
@refi64 @irefi64 @weakrefi64
>
.typedef @refBar = ref<@StructBar>
.typedef @irefBar = iref<@StructBar>
.typedef @hCharArray = hybrid<@i64 @i8>
.const @I64_0 <@i64> = 0
.const @I64_1 <@i64> = 1
.const @I64_2 <@i64> = 2
.const @I64_10 <@i64> = 10
// require primitives.uir
.funcdef @factorial_rec <@i64 (@i64)> (%n) {
%zero = EQ <@i64> %n 0
BRANCH2 %zero %iszero %notzero
%iszero:
RET <@i64> 1
%notzero:
%nm1 = SUB <@i64> %n 1
%rec = CALL <@i64 (@i64)> @factorial_rec (%nm1)
%result = MUL <@i64> %n %rec
RET <@i64> %result
.funcdef @factorial_rec VERSION @factorial_rec_v1 <@i_i> (%n) {
%entry:
%zero = EQ <@i64> %n @I64_0
BRANCH2 %zero %iszero %notzero
%iszero:
RET <@i64> @I64_1
%notzero:
%nm1 = SUB <@i64> %n @I64_1
%rec = CALL <@i_i> @factorial_rec (%nm1)
%result = MUL <@i64> %n %rec
RET <@i64> %result
}
.funcdef @factorial_iter <@i64 (@i64)> (%n) {
%entry:
BRANCH %head
.funcdef @factorial_iter VERSION @factorial_iter_v1 <@i_i> (%n) {
%entry:
BRANCH %head
%head:
%i = PHI <@i64> { %entry: 1; %next: %i2; }
%prod = PHI <@i64> { %entry: 1; %next: %prod2; }
%cmp = SLE <@i64> %i %n
BRANCH2 %cmp %body %exit
%head:
%i = PHI <@i64> { %entry: @I64_1; %next: %i2; }
%prod = PHI <@i64> { %entry: @I64_1; %next: %prod2; }
%cmp = SLE <@i64> %i %n
BRANCH2 %cmp %body %exit
%body:
%prod2 = MUL <@i64> %prod %i
%i2 = ADD <@i64> %i 1
BRANCH %next
%body:
%prod2 = MUL <@i64> %prod %i
%i2 = ADD <@i64> %i @I64_1
BRANCH %next
%next:
BRANCH %head
%next:
BRANCH %head
%exit:
RET <@i64> %prod
%exit:
RET <@i64> %prod
}
.funcdef @factorial_tailrec <@i64 (@i64 @i64)> (%n %prod) {
%zero = EQ <@i64> %n 0
BRANCH2 %zero %iszero %notzero
%iszero:
RET <@i64> %prod
%notzero:
%nm1 = SUB <@i64> %n 1
%mul = MUL <@i64> %n %prod
TAILCALL <@i64 (@i64 @i64)> @factorial_tailrec (%nm1 %mul)
.funcdef @factorial_tailrec VERSION @factorial_tailrec_v1 <@i_ii> (%n %prod) {
%entry:
%zero = EQ <@i64> %n @I64_1
BRANCH2 %zero %iszero %notzero
%iszero:
RET <@i64> %prod
%notzero:
%nm1 = SUB <@i64> %n @I64_1
%mul = MUL <@i64> %n %prod
TAILCALL <@i_ii> @factorial_tailrec (%nm1 %mul)
}
.funcdef @test_fac <@noparamsnoret> () {
%r1 = CALL <@i64 (@i64)> @factorial_rec (10)
%r2 = CALL <@i64 (@i64)> @factorial_iter (10)
%r3 = CALL <@i64 (@i64 @i64)> @factorial_tailrec (10 1)
%checktrap = TRAP <@void> %exit %exit KEEPALIVE (%r1 %r2 %r3)
%exit:
ICALL @uvm.thread_exit()
THROW @NULLREF
.funcdef @test_fac VERSION @test_fac_v1 <@noparamsnoret> () {
%entry:
%r1 = CALL <@i_i> @factorial_rec (@I64_10)
%r2 = CALL <@i_i> @factorial_iter (@I64_10)
%r3 = CALL <@i_ii> @factorial_tailrec (@I64_10 @I64_1)
%checktrap = TRAP <@void> KEEPALIVE (%r1 %r2 %r3)
COMMINST @uvm.thread_exit
}
.funcdef @fibonacci_rec <@i64 (@i64)> (%n) {
%zero = EQ <@i64> %n 0
BRANCH2 %zero %iszero %notzero
%iszero:
RET <@i64> 0
%notzero:
%one = EQ <@i64> %n 1
BRANCH2 %one %isone %notone
%isone:
RET <@i64> 1
%notone:
%nm1 = SUB <@i64> %n 1
%nm2 = SUB <@i64> %n 2
%rec1 = CALL <@i64 (@i64)> @fibonacci_rec (%nm1)
%rec2 = CALL <@i64 (@i64)> @fibonacci_rec (%nm2)
%result = ADD <@i64> %rec1 %rec2
RET <@i64> %result
.funcdef @fibonacci_rec VERSION @fibonacci_rec_v1 <@i_i> (%n) {
%entry:
%zero = EQ <@i64> %n @I64_0
BRANCH2 %zero %iszero %notzero
%iszero:
RET <@i64> @I64_0
%notzero:
%one = EQ <@i64> %n @I64_1
BRANCH2 %one %isone %notone
%isone:
RET <@i64> @I64_1
%notone:
%nm1 = SUB <@i64> %n @I64_1
%nm2 = SUB <@i64> %n @I64_2
%rec1 = CALL <@i_i> @fibonacci_rec (%nm1)
%rec2 = CALL <@i_i> @fibonacci_rec (%nm2)
%result = ADD <@i64> %rec1 %rec2
RET <@i64> %result
}
// M(n) = [F_{n+1} F_n ;
......@@ -89,77 +91,79 @@
// 1 0]
// M(0) = [1 0;
// 0 1]
.funcdef @fibonacci_mat <@i64 (@i64)> (%n) {
%entry:
BRANCH %head
%head:
%a = PHI <@i64> { %entry: 1; %next: %a2; }
%b = PHI <@i64> { %entry: 1; %next: %b2; }
%c = PHI <@i64> { %entry: 1; %next: %c2; }
%d = PHI <@i64> { %entry: 0; %next: %d2; }
%aa = PHI <@i64> { %entry: 1; %next: %aa2; }
%bb = PHI <@i64> { %entry: 0; %next: %bb2; }
%cc = PHI <@i64> { %entry: 0; %next: %cc2; }
%dd = PHI <@i64> { %entry: 1; %next: %dd2; }
%nn = PHI <@i64> { %entry: %n; %next: %nn2; }
%watch = TRAP <@void> %head2 %head2 KEEPALIVE (%a %b %c %d %aa %bb %cc %dd %nn)
%head2:
%nn0 = EQ <@i64> %nn 0
BRANCH2 %nn0 %exit %body
%body:
%nodd = AND <@i64> %nn 1
BRANCH2 %nodd %odd %even
%odd:
%aa_a = MUL <@i64> %aa %a
%bb_c = MUL <@i64> %bb %c
%aa_b = MUL <@i64> %aa %b
%bb_d = MUL <@i64> %bb %d
%cc_a = MUL <@i64> %cc %a
%dd_c = MUL <@i64> %dd %c
%cc_b = MUL <@i64> %cc %b
%dd_d = MUL <@i64> %dd %d
%aa3 = ADD <@i64> %aa_a %bb_c
%bb3 = ADD <@i64> %aa_b %bb_d
%cc3 = ADD <@i64> %cc_a %dd_c
%dd3 = ADD <@i64> %cc_b %dd_d
BRANCH %next
%even:
BRANCH %next
%next:
%aa2 = PHI <@i64> { %odd: %aa3; %even: %aa; }
%bb2 = PHI <@i64> { %odd: %bb3; %even: %bb; }
%cc2 = PHI <@i64> { %odd: %cc3; %even: %cc; }
%dd2 = PHI <@i64> { %odd: %dd3; %even: %dd; }
%a_a = MUL <@i64> %a %a
%a_b = MUL <@i64> %a %b
%a_c = MUL <@i64> %a %c
%b_c = MUL <@i64> %b %c
%b_d = MUL <@i64> %b %d
%c_d = MUL <@i64> %c %d
%d_d = MUL <@i64> %d %d
%a2 = ADD <@i64> %a_a %b_c
%b2 = ADD <@i64> %a_b %b_d
%c2 = ADD <@i64> %a_c %c_d
%d2 = ADD <@i64> %b_c %d_d
%nn2 = ASHR <@i64> %nn 1
BRANCH %head
%exit:
RET <@i64> %bb
.funcdef @fibonacci_mat VERSION @fibonacci_mat_v1 <@i_i> (%n) {
%entry:
BRANCH %head
%head:
%a = PHI <@i64> { %entry: @I64_1; %next: %a2; }
%b = PHI <@i64> { %entry: @I64_1; %next: %b2; }
%c = PHI <@i64> { %entry: @I64_1; %next: %c2; }
%d = PHI <@i64> { %entry: @I64_0; %next: %d2; }
%aa = PHI <@i64> { %entry: @I64_1; %next: %aa2; }
%bb = PHI <@i64> { %entry: @I64_0; %next: %bb2; }
%cc = PHI <@i64> { %entry: @I64_0; %next: %cc2; }
%dd = PHI <@i64> { %entry: @I64_1; %next: %dd2; }
%nn = PHI <@i64> { %entry: %n; %next: %nn2; }
%watch = TRAP <@void> KEEPALIVE (%a %b %c %d %aa %bb %cc %dd %nn)
%nn0 = EQ <@i64> %nn @I64_0
BRANCH2 %nn0 %exit %body
%body:
%nnm2 = AND <@i64> %nn @I64_1
%nodd = EQ <@i64> %nnm2 @I64_1
BRANCH2 %nodd %odd %even
%odd:
%aa_a = MUL <@i64> %aa %a
%bb_c = MUL <@i64> %bb %c
%aa_b = MUL <@i64> %aa %b
%bb_d = MUL <@i64> %bb %d
%cc_a = MUL <@i64> %cc %a
%dd_c = MUL <@i64> %dd %c
%cc_b = MUL <@i64> %cc %b
%dd_d = MUL <@i64> %dd %d
%aa3 = ADD <@i64> %aa_a %bb_c
%bb3 = ADD <@i64> %aa_b %bb_d
%cc3 = ADD <@i64> %cc_a %dd_c
%dd3 = ADD <@i64> %cc_b %dd_d
BRANCH %next
%even:
BRANCH %next
%next:
%aa2 = PHI <@i64> { %odd: %aa3; %even: %aa; }
%bb2 = PHI <@i64> { %odd: %bb3; %even: %bb; }
%cc2 = PHI <@i64> { %odd: %cc3; %even: %cc; }
%dd2 = PHI <@i64> { %odd: %dd3; %even: %dd; }
%a_a = MUL <@i64> %a %a
%a_b = MUL <@i64> %a %b
%a_c = MUL <@i64> %a %c
%b_c = MUL <@i64> %b %c
%b_d = MUL <@i64> %b %d
%c_d = MUL <@i64> %c %d
%d_d = MUL <@i64> %d %d
%a2 = ADD <@i64> %a_a %b_c
%b2 = ADD <@i64> %a_b %b_d
%c2 = ADD <@i64> %a_c %c_d
%d2 = ADD <@i64> %b_c %d_d
%nn2 = ASHR <@i64> %nn @I64_1
BRANCH %head
%exit:
RET <@i64> %bb
}
.funcdef @test_fib <@noparamsnoret> () {
%r1 = CALL <@i64 (@i64)> @fibonacci_rec (10)
%r2 = CALL <@i64 (@i64)> @fibonacci_mat (10)
%checktrap = TRAP <@void> %exit %exit KEEPALIVE (%r1 %r2)
%exit:
ICALL @uvm.thread_exit()
THROW @NULLREF
}
\ No newline at end of file
.funcdef @test_fib VERSION @test_fib_v1 <@noparamsnoret> () {
%entry:
%r1 = CALL <@i_i> @fibonacci_rec (@I64_10)
%r2 = CALL <@i_i> @fibonacci_mat (@I64_10)
%checktrap = TRAP <@void> KEEPALIVE (
%r1
%r2
)
COMMINST @uvm.thread_exit
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment