Skip to content

Commit

Permalink
Merge pull request #23 from KacperFKorban/sum-types
Browse files Browse the repository at this point in the history
Sum types
  • Loading branch information
KacperFKorban authored Mar 13, 2024
2 parents cb655b9 + d0ebc39 commit 6852b59
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 63 deletions.
10 changes: 10 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,13 @@ lazy val web = projectMatrix
)
.dependsOn(guinep)
.jvmPlatform(scalaVersions = List(scala3))

lazy val testcases = projectMatrix
.in(file("testcases"))
.settings(commonSettings)
.settings(
name := "GUInep-testcases",
publish / skip := true
)
.dependsOn(web)
.jvmPlatform(scalaVersions = List(scala3))
95 changes: 73 additions & 22 deletions guinep/src/main/scala/macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ private[guinep] object macros {
def wrongParamsListError(f: Expr[Any]): Nothing =
report.errorAndAbort(s"Wrong params list, expected a function reference, got: ${f.show}", f.asTerm.pos)

private def unsupportedFunctionParamType(t: TypeRepr, pos: Position): Nothing =
report.errorAndAbort(s"Unsupported function param type: ${t.show}", pos)
private def unsupportedFunctionParamType(t: TypeRepr, pos: Option[Position] = None): Nothing = pos match {
case Some(p) => report.errorAndAbort(s"Unsupported function param type: ${t.show}", p)
case None => report.errorAndAbort(s"Unsupported function param type: ${t.show}")
}

extension (t: Term)
private def select(s: Term): Term = Select(t, s.symbol)
Expand All @@ -52,35 +54,66 @@ private[guinep] object macros {
wrongParamsListError(f)
}

private def functionFormElementFromTree(tree: Tree): FormElement = tree match {
case ValDef(name, tpt, _) =>
val paramType = tpt.tpe
val paramName = name
paramType match {
case ntpe: NamedType if ntpe.name == "String" => FormElement.TextInput(paramName)
case ntpe: NamedType if ntpe.name == "Int" => FormElement.NumberInput(paramName)
case ntpe: NamedType if ntpe.name == "Boolean" => FormElement.CheckboxInput(paramName)
case ntpe: NamedType =>
val classSymbol = ntpe.classSymbol.getOrElse(unsupportedFunctionParamType(paramType, tree.pos))
val fields = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isValDef).map(_.tree)
FormElement.FieldSet(paramName, fields.map(functionFormElementFromTree))
case _ => unsupportedFunctionParamType(paramType, tree.pos)
}
private def isProductTpe(tpe: TypeRepr): Boolean =
val typeSymbol = tpe.typeSymbol
val typeIsSingleton = tpe.isSingleton
val typeIsCaseClass = typeSymbol.flags.is(Flags.Case)
val typeIsAnyVal = tpe.baseClasses.contains(defn.AnyValClass)
typeIsSingleton || typeIsCaseClass || typeIsAnyVal

private def isSumTpe(tpe: TypeRepr): Boolean =
val typeSymbol = tpe.typeSymbol
val typeIsSingleton = tpe.isSingleton
val typeIsEnum = typeSymbol.flags.is(Flags.Enum)
val typeIsSealedTraitOrAbstractClass = typeSymbol.flags.is(Flags.Sealed) && (typeSymbol.flags.is(Flags.Trait) || typeSymbol.flags.is(Flags.Abstract))
val typeIsNonCaseClassWithChildren = !typeSymbol.flags.is(Flags.Case) && typeSymbol.children.nonEmpty
!typeIsSingleton && (typeIsEnum || typeIsSealedTraitOrAbstractClass || typeIsNonCaseClassWithChildren)

private def isCaseObjectTpe(tpe: TypeRepr): Boolean =
val typeSymbol = tpe.typeSymbol
val isModule = typeSymbol.flags.is(Flags.Module)
val isEnumCaseNonClassDef = typeSymbol.flags.is(Flags.Enum) && typeSymbol.flags.is(Flags.Case) && !typeSymbol.isClassDef
isModule || isEnumCaseNonClassDef

private def tpeArguments(tpe: TypeRepr): List[TypeRepr] = tpe match {
case AppliedType(tpe, args) => args
case _ => Nil
}

private def functionFormElementFromTree(paramName: String, paramType: TypeRepr): FormElement = paramType match {
case ntpe: NamedType if ntpe.name == "String" => FormElement.TextInput(paramName)
case ntpe: NamedType if ntpe.name == "Int" => FormElement.NumberInput(paramName)
case ntpe: NamedType if ntpe.name == "Boolean" => FormElement.CheckboxInput(paramName)
case ntpe if isProductTpe(ntpe) =>
val classSymbol = ntpe.typeSymbol
val fields = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isValDef).map(_.tree).collect { case v: ValDef => v }
FormElement.FieldSet(paramName, fields.map(v => functionFormElementFromTree(v.name, v.tpt.tpe)))
case ntpe if isSumTpe(ntpe) =>
val classSymbol = ntpe.typeSymbol
val typeParamSyms = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isType)
val tpeArgs = tpeArguments(ntpe)
val childrenAppliedTpes = classSymbol.children.map(_.typeRef)
val childrenFormElements = childrenAppliedTpes.map(t => functionFormElementFromTree("value", t))
val options = classSymbol.children.map(_.name).zip(childrenFormElements)
FormElement.Dropdown(paramName, options)
case _ =>
unsupportedFunctionParamType(paramType)
}

private def functionFormElementsImpl(f: Expr[Any]): Expr[Seq[FormElement]] = {
private def functionFormElementsImpl(f: Expr[Any]): Expr[Seq[FormElement]] =
Expr.ofSeq(
functionParams(f).map(functionFormElementFromTree).map(Expr(_))
functionParams(f).map { case ValDef(name, tpt, _) => functionFormElementFromTree(name, tpt.tpe) } .map(Expr(_))
)
}

private def constructArg(paramTpe: TypeRepr, param: Term): Term = {
paramTpe match {
case ntpe: NamedType if ntpe.name == "String" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType if ntpe.name == "Int" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType if ntpe.name == "Boolean" => param.select("asInstanceOf").appliedToType(ntpe)
case ntpe: NamedType =>
val classSymbol = ntpe.classSymbol.getOrElse(unsupportedFunctionParamType(paramTpe, param.pos))
case ntpe if isCaseObjectTpe(ntpe) =>
Ident(ntpe.typeSymbol.termRef)
case ntpe if isProductTpe(ntpe) =>
val classSymbol = ntpe.classSymbol.getOrElse(unsupportedFunctionParamType(paramTpe, Some(param.pos)))
val fields = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isValDef).map(_.tree)
val paramValue = '{ ${param.asExpr}.asInstanceOf[Map[String, Any]] }.asTerm
val args = fields.collect { case field: ValDef =>
Expand All @@ -89,7 +122,25 @@ private[guinep] object macros {
constructArg(field.tpt.tpe, fieldValue)
}
New(Inferred(ntpe)).select(classSymbol.primaryConstructor).appliedToArgs(args)
case _ => unsupportedFunctionParamType(paramTpe, param.pos)
case ntpe if isSumTpe(ntpe) =>
val classSymbol = ntpe.classSymbol.getOrElse(unsupportedFunctionParamType(paramTpe, Some(param.pos)))
val className = classSymbol.name
val children = classSymbol.children
val paramMap = '{ ${param.asExpr}.asInstanceOf[Map[String, Any]] }.asTerm
val paramName = paramMap.select("apply").appliedTo(Literal(StringConstant("name")))
val paramValue = paramMap.select("apply").appliedTo(Literal(StringConstant("value")))
children.foldRight[Term]{
'{ throw new RuntimeException(s"Class ${${paramName.asExpr}} is not a child of ${${Expr(className)}}") }.asTerm
} { (child, acc) =>
val childName = Literal(StringConstant(child.name))
If(
paramName.select("equals").appliedTo(childName),
constructArg(child.typeRef, paramValue),
acc
)
}
case _ =>
unsupportedFunctionParamType(paramTpe, Some(param.pos))
}
}

Expand Down
4 changes: 2 additions & 2 deletions guinep/src/main/scala/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ private[guinep] object model {
case TextInput(override val name: String) extends FormElement(name)
case NumberInput(override val name: String) extends FormElement(name)
case CheckboxInput(override val name: String) extends FormElement(name)
case Dropdown(override val name: String, options: List[(String, String)]) extends FormElement(name)
case Dropdown(override val name: String, options: List[(String, FormElement)]) extends FormElement(name)
case TextArea(override val name: String, rows: Option[Int] = None, cols: Option[Int] = None) extends FormElement(name)
case DateInput(override val name: String) extends FormElement(name)
case EmailInput(override val name: String) extends FormElement(name)
Expand All @@ -26,7 +26,7 @@ private[guinep] object model {
case FormElement.CheckboxInput(name) =>
s"""{ "name": '$name', "type": 'checkbox' }"""
case FormElement.Dropdown(name, options) =>
s"""{ "name": '$name', "type": 'dropdown', "options": [${options.map { case (k, v) => s"""{"key": "$k", "value": "$v"}""" }.mkString(",")}] }"""
s"""{ "name": '$name', "type": 'dropdown', "options": [${options.map { case (k, v) => s"""{"name": "$k", "value": ${v.toJSONRepr}}""" }.mkString(",")}] }"""
case FormElement.TextArea(name, rows, cols) =>
s"""{ "name": '$name', "type": 'textarea', "rows": ${rows.getOrElse("")}, "cols": ${cols.getOrElse("")} }"""
case FormElement.DateInput(name) =>
Expand Down
67 changes: 67 additions & 0 deletions testcases/src/main/scala/main.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package guinep.compiletest

def personsAge(name: String): Int = name match {
case "Bartek" => 20
case _ => 0
}

def upperCaseText(text: String): String =
text.toUpperCase

def add(a: Int, b: Int) =
a + b

def concat(a: String, b: String) =
a + b

def giveALongText(b: Boolean): String =
if b then "XXXXXXXXXXXXXXXXXXXXXX" else "-"

case class Add(a: Int, b: Int)

def addObj(add: Add) =
add.a + add.b

def greetMaybeName(maybeName: Option[String]): String =
maybeName.fold("Hello!")(name => s"Hello, $name!")

enum Language:
case English, Polish

def greetInLanguage(name: String, language: Language): String =
language match
case Language.English => s"Hello, $name!"
case Language.Polish => s"Cześć, $name!"

sealed trait MaybeString
case class JustString(value: String) extends MaybeString
case object NoString extends MaybeString

def nameWithPossiblePrefix(name: String, maybePrefix: MaybeString): String =
maybePrefix match
case JustString(value) => s"$value $name"
case NoString => name

enum MaybeString1:
case JustString(value: String)
case NoString

def nameWithPossiblePrefix1(name: String, maybePrefix: MaybeString1): String =
maybePrefix match
case MaybeString1.JustString(value) => s"$value $name"
case MaybeString1.NoString => name

@main
def run: Unit =
guinep.web(
personsAge,
upperCaseText,
add,
concat,
giveALongText,
addObj,
// greetMaybeName,
greetInLanguage,
nameWithPossiblePrefix,
nameWithPossiblePrefix1
)
42 changes: 40 additions & 2 deletions web/src/main/scala/htmlgen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ private[guinep] trait HtmlGen {
.sidebar a:hover { background-color: #0056b3; }
.main-content { margin-left: 232px; padding: 40px; display: flex; justify-content: center; padding-top: 20px; }
.form-container { width: 300px; }
.form-input { margin-bottom: 10px; }
label { display: inline-block; margin-right: 10px; vertical-align: middle; }
input:not([type=submit]) { display: inline-block; padding: 8px; margin-bottom: 10px; box-sizing: border-box; }
input[type=submit] { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
input[type=submit]:hover { background-color: #45a049; }
select { display: inline-block; margin-bottom: 10px; padding: 8px; box-sizing: border-box; }
.result { margin-top: 20px; font-weight: bold; }
"""
),
Expand Down Expand Up @@ -76,9 +76,20 @@ private[guinep] trait HtmlGen {
const funs = ${funsToJsonArray};
const selectedFun = funs.find(fun => fun[0] === funName);

function setSubFormOnSelect(subForm, formElement, selectedOption) {
const selectedOptionForm = formElement.options.find(option => option.name === selectedOption);
subForm.innerHTML = '';
if (selectedOptionForm.value.elements.length > 0) {
subForm.style.visibility = 'visible';
selectedOptionForm.value.elements.forEach(subFormElem => addFormElement(subForm, subFormElem));
} else {
subForm.style.visibility = 'hidden';
}
}

function addFormElement(form, formElem) {
const br = document.createElement('br');
if (formElem.type == 'fieldset') {
if (formElem.type === 'fieldset') {
const fieldset = document.createElement('fieldset');
fieldset.name = formElem.name;
const legend = document.createElement('legend');
Expand All @@ -87,6 +98,29 @@ private[guinep] trait HtmlGen {
formElem.elements.forEach(subElem => addFormElement(fieldset, subElem));
form.appendChild(fieldset);
form.appendChild(br.cloneNode());
} else if (formElem.type === 'dropdown') {
const fieldset = document.createElement('fieldset');
fieldset.name = formElem.name;
const legend = document.createElement('legend');
legend.innerText = formElem.name;
fieldset.appendChild(legend);
const nameSelect = document.createElement('select');
nameSelect.name = 'name';
formElem.options.forEach(option => {
const optionElem = document.createElement('option');
optionElem.value = option.name;
optionElem.innerText = option.name;
nameSelect.appendChild(optionElem);
});
fieldset.appendChild(nameSelect);
fieldset.appendChild(br.cloneNode());
const valueFieldSet = document.createElement('fieldset');
valueFieldSet.name = 'value';
fieldset.appendChild(valueFieldSet);
nameSelect.onchange = (selection) => setSubFormOnSelect(valueFieldSet, formElem, selection.target.value);
setSubFormOnSelect(valueFieldSet, formElem, formElem.options[0].name);
form.appendChild(fieldset);
form.appendChild(br.cloneNode());
} else {
const label = document.createElement('label');
label.innerText = formElem.name + ': ';
Expand Down Expand Up @@ -146,6 +180,10 @@ private[guinep] trait HtmlGen {
} else {
parentObj[name] = value;
}
} else if (element.tagName === 'SELECT') {
const name = element.name;
const value = element.value;
parentObj[name] = value;
}
};

Expand Down
13 changes: 10 additions & 3 deletions web/src/main/scala/serialization.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,15 @@ private[guinep] object serialization:
case FormElement.CheckboxInput(_) => value.asBoolean.toRight(s"Invalid boolean: $value")
case FormElement.Dropdown(_, options) =>
for {
v <- value.asString.toRight(s"Invalid string: $value")
res <- options.find(_._1 == v).map(_._2).toRight(s"Invalid option: $value")
} yield res
v <- value.asObject.toRight(s"Invalid object: $value")
ddName <- v.get("name").flatMap(_.asString).toRight(s"Invalid name: $value")
ddValue = v.get("value").getOrElse(Obj())
ddValueObj <- ddValue.asObject.toRight(s"Invalid object: $value")
foundOption <- options.find(_._1 == ddName).toRight(s"Invalid option: $value")
res <- foundOption._2.parseJSONValue(ddValueObj)
} yield Map(
"name" -> ddName,
"value" -> res
)
case FormElement.TextArea(_, _, _) => Right(value)
case _ => Left(s"Unsupported form element: $formElement")
34 changes: 0 additions & 34 deletions web/src/test/scala/compiletest.scala

This file was deleted.

0 comments on commit 6852b59

Please sign in to comment.