feat(compiler): Allow omitting field name in struct creation [LNG-261] (#943)

This commit is contained in:
InversionSpaces 2023-10-25 11:12:09 +02:00 committed by GitHub
parent 2f11a0649f
commit fcdb5b0fef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 400 additions and 119 deletions

View File

@ -0,0 +1,59 @@
package aqua.parser.lexer
import aqua.parser.lift.Span.S
import aqua.parser.lexer.Token.*
import cats.data.NonEmptyList
import cats.parse.Parser as P
import cats.syntax.functor.*
import cats.Comonad
import cats.arrow.FunctionK
enum NamedArg[F[_]] extends Token[F] {
// for `name = value`
case Full(name: Name[F], value: ValueToken[F])
// for just `name` (short for `name = name`)
case Short(variable: VarToken[F])
lazy val argName: Name[F] = this match {
case Full(name, _) => name
case Short(variable) => variable.name
}
lazy val argValue: ValueToken[F] = this match {
case Full(_, value) => value
case Short(variable) => variable
}
override def as[T](v: T): F[T] = this match {
case Full(name, value) => name.as(v)
case Short(variable) => variable.as(v)
}
override def mapK[K[_]: Comonad](fk: FunctionK[F, K]): NamedArg[K] =
this match {
case Full(name, value) => Full(name.mapK(fk), value.mapK(fk))
case Short(variable) => Short(variable.mapK(fk))
}
}
object NamedArg {
private val namedArgFull: P[NamedArg.Full[S]] =
P.defer(
(Name.p.between(` *`, `/s*`) <*
`=`.between(` *`, `/s*`)) ~
ValueToken.`value`.between(` *`, `/s*`)
).map(NamedArg.Full.apply)
private val namedArgShort: P[NamedArg.Short[S]] =
P.defer(
VarToken.variable.between(` *`, `/s*`)
).map(NamedArg.Short.apply)
val namedArg: P[NamedArg[S]] =
namedArgFull.backtrack | namedArgShort
val namedArgs: P[NonEmptyList[NamedArg[S]]] =
P.defer(` `.?.with1 ~ `(` ~ `/s*` *> comma(namedArg) <* `/s*` *> `)`)
}

View File

@ -3,18 +3,19 @@ package aqua.parser.lexer
import aqua.parser.lexer.Token.*
import aqua.parser.lift.LiftParser
import aqua.parser.lift.LiftParser.*
import aqua.parser.lift.Span
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
import aqua.types.LiteralType
import aqua.parser.lexer.CallArrowToken.CallBraces
import aqua.parser.lexer.NamedArg.namedArgs
import cats.~>
import cats.data.{NonEmptyList, NonEmptyMap}
import cats.parse.{Numbers, Parser as P, Parser0 as P0}
import cats.syntax.comonad.*
import cats.syntax.functor.*
import cats.{Comonad, Functor}
import scala.language.postfixOps
import cats.~>
import aqua.parser.lift.Span
import aqua.parser.lift.Span.{P0ToSpan, PToSpan}
import aqua.types.LiteralType
import aqua.parser.lexer.CallArrowToken.CallBraces
sealed trait PropertyOp[F[_]] extends Token[F] {
def mapK[K[_]: Comonad](fk: F ~> K): PropertyOp[K]
@ -47,12 +48,14 @@ case class IntoIndex[F[_]: Comonad](point: F[Unit], idx: Option[ValueToken[F]])
override def mapK[K[_]: Comonad](fk: F ~> K): IntoIndex[K] = copy(fk(point), idx.map(_.mapK(fk)))
}
case class IntoCopy[F[_]: Comonad](point: F[Unit], fields: NonEmptyMap[String, ValueToken[F]])
extends PropertyOp[F] {
case class IntoCopy[F[_]: Comonad](
point: F[Unit],
args: NonEmptyList[NamedArg[F]]
) extends PropertyOp[F] {
override def as[T](v: T): F[T] = point.as(v)
override def mapK[K[_]: Comonad](fk: F ~> K): IntoCopy[K] =
copy(fk(point), fields.map(_.mapK(fk)))
copy(fk(point), args.map(_.mapK(fk)))
}
object PropertyOp {
@ -66,9 +69,7 @@ object PropertyOp {
}
val parseCopy: P[PropertyOp[Span.S]] =
(`.` *> (`copy`.lift ~ namedArgs)).map { case (point, fields) =>
IntoCopy(point, NonEmptyMap.of(fields.head, fields.tail: _*))
}
(`.` *> (`copy`.lift ~ namedArgs)).map(IntoCopy.apply)
private val parseIdx: P[PropertyOp[Span.S]] =
(P.defer(

View File

@ -1,6 +1,7 @@
package aqua.parser.lexer
import aqua.parser.lift.Span.S
import cats.data.NonEmptyList
import cats.parse.{Accumulator0, Parser as P, Parser0 as P0}
import cats.{~>, Comonad, Functor}
@ -120,18 +121,6 @@ object Token {
val `<-` : P[Unit] = P.string("<-")
val `/s*` : P0[Unit] = ` \n+`.backtrack | ` *`.void
val namedArg: P[(String, ValueToken[S])] =
P.defer(
`name`.between(` *`, `/s*`) ~
`=`.between(` *`, `/s*`).void ~
ValueToken.`value`.between(` *`, `/s*`)
).map { case ((name, _), vt) =>
(name, vt)
}
val namedArgs: P[NonEmptyList[(String, ValueToken[S])]] =
P.defer(` `.?.with1 ~ `(` ~ `/s*` *> comma(namedArg) <* `/s*` *> `)`)
case class LiftToken[F[_]: Functor, A](point: F[A]) extends Token[F] {
override def as[T](v: T): F[T] = Functor[F].as(point, v)

View File

@ -3,6 +3,7 @@ package aqua.parser.lexer
import aqua.parser.Expr
import aqua.parser.head.FilenameExpr
import aqua.parser.lexer.Token.*
import aqua.parser.lexer.NamedArg.namedArgs
import aqua.parser.lift.LiftParser
import aqua.parser.lift.LiftParser.*
import aqua.types.LiteralType
@ -260,7 +261,7 @@ object CallArrowToken {
case class NamedValueToken[F[_]: Comonad](
typeName: NamedTypeToken[F],
fields: NonEmptyMap[String, ValueToken[F]]
fields: NonEmptyList[NamedArg[F]]
) extends ValueToken[F] {
override def mapK[K[_]: Comonad](fk: F ~> K): NamedValueToken[K] =
@ -277,7 +278,7 @@ object NamedValueToken {
"Missing braces '()' after the struct type"
)
.map { case (dn, args) =>
NamedValueToken(NamedTypeToken(dn), args.toNem)
NamedValueToken(NamedTypeToken(dn), args)
}
}

View File

@ -19,15 +19,18 @@ class AbilityValueExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
parseData(str) should be(
NamedValueToken(
NamedTypeToken[Id]("AbilityA"),
NonEmptyMap.of(
"v1" -> toNumber(1),
"f1" -> PropertyToken[Id](
NonEmptyList.of(
NamedArg.Full(toName("v1"), toNumber(1)),
NamedArg.Full(
toName("f1"),
PropertyToken(
VarToken(toName("input")),
NonEmptyList.one(IntoField("arrow"))
)
)
)
)
)
}
"one line struct value" should "be parsed" in {

View File

@ -4,21 +4,10 @@ import aqua.AquaSpec
import aqua.AquaSpec.{toNumber, toStr, toVar}
import aqua.parser.expr.ConstantExpr
import aqua.parser.expr.func.AssignmentExpr
import aqua.parser.lexer.{
CallArrowToken,
CollectionToken,
IntoArrow,
LiteralToken,
Name,
NamedTypeToken,
NamedValueToken,
PropertyToken,
Token,
ValueToken,
VarToken
}
import aqua.parser.lexer.*
import aqua.parser.lexer.CollectionToken.Mode.ArrayMode
import aqua.types.LiteralType
import cats.Id
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
@ -32,52 +21,67 @@ class StructValueExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
val one = toNumber(1)
val two = toNumber(2)
val three = toNumber(3)
val a = LiteralToken[Id]("\"a\"", LiteralType.string)
val b = LiteralToken[Id]("\"b\"", LiteralType.string)
val c = LiteralToken[Id]("\"c\"", LiteralType.string)
val a = toStr("a")
val b = toStr("b")
val c = toStr("c")
parseData(
str
) should be(
NamedValueToken(
NamedTypeToken[Id]("Obj"),
NonEmptyMap.of(
"f1" -> one,
"f2" -> a,
"f3" -> CollectionToken[Id](ArrayMode, List(one, two, three)),
"f4" -> CollectionToken[Id](ArrayMode, List(b, c)),
"f5" -> NamedValueToken(
NonEmptyList.of(
NamedArg.Full(toName("f1"), one),
NamedArg.Full(toName("f2"), a),
NamedArg.Full(toName("f3"), CollectionToken[Id](ArrayMode, List(one, two, three))),
NamedArg.Full(toName("f4"), CollectionToken[Id](ArrayMode, List(b, c))),
NamedArg.Full(
toName("f5"),
NamedValueToken(
NamedTypeToken[Id]("NestedObj"),
NonEmptyMap.of(
"i1" -> two,
"i2" -> b,
"i3" -> CallArrowToken(Name[Id]("funcCall"), List(three)),
"i4" -> VarToken[Id](Name[Id]("value"))
NonEmptyList.of(
NamedArg.Full(toName("i1"), two),
NamedArg.Full(toName("i2"), b),
NamedArg.Full(toName("i3"), CallArrowToken(toName("funcCall"), List(three))),
NamedArg.Full(toName("i4"), VarToken(toName("value")))
)
)
),
"f6" -> CallArrowToken(Name[Id]("funcCall"), List(one)),
"f7" -> PropertyToken[Id](
NamedArg.Full(toName("f6"), CallArrowToken(Name[Id]("funcCall"), List(one))),
NamedArg.Full(
toName("f7"),
PropertyToken[Id](
VarToken[Id](Name[Id]("Serv")),
NonEmptyList.one(IntoArrow[Id](Name[Id]("call"), List(two)))
)
)
)
)
)
}
"one named arg" should "be parsed" in {
val result = aqua.parser.lexer.Token.namedArg
"named args" should "parse one full named arg" in {
val result = NamedArg.namedArg
.parseAll(""" a
| =
| 3""".stripMargin)
.map(v => (v._1, v._2.mapK(spanToId)))
.map(_.mapK(spanToId))
.value
result should be(("a", toNumber(3)))
result should be(NamedArg.Full(toName("a"), toNumber(3)))
}
"named args" should "be parsed" in {
val result = Token.namedArgs
it should "parse one short named arg" in {
val result = NamedArg.namedArg
.parseAll(" b ")
.map(_.mapK(spanToId))
.value
result should be(NamedArg.Short(toVar("b")))
}
it should "parse a few full named args" in {
val result = NamedArg.namedArgs
.parseAll("""(
|a = "str",
|b = 3,
@ -86,16 +90,61 @@ class StructValueExprSpec extends AnyFlatSpec with Matchers with AquaSpec {
| 5
|)""".stripMargin)
.value
.map { case (str, vt) => (str, vt.mapK(spanToId)) }
.map(_.mapK(spanToId))
result should be(
NonEmptyList[(String, ValueToken[Id])](
("a", toStr("str")),
("b", toNumber(3)) :: ("c", toNumber(5)) :: Nil
NonEmptyList.of(
NamedArg.Full(toName("a"), toStr("str")),
NamedArg.Full(toName("b"), toNumber(3)),
NamedArg.Full(toName("c"), toNumber(5))
)
)
}
it should "parse a few short named args" in {
val result = NamedArg.namedArgs
.parseAll("""(
|a,
| b ,
|c
|
|)""".stripMargin)
.value
.map(_.mapK(spanToId))
result should be(
NonEmptyList.of(
NamedArg.Short(toVar("a")),
NamedArg.Short(toVar("b")),
NamedArg.Short(toVar("c"))
)
)
}
it should "parse mixed named args" in {
val args = List(
"meaning = 42" -> NamedArg.Full(toName("meaning"), toNumber(42)),
"variable" -> NamedArg.Short(toVar("variable")),
"col = [1,2,3]" -> NamedArg.Full(
toName("col"),
CollectionToken[Id](ArrayMode, List(toNumber(1), toNumber(2), toNumber(3)))
),
"arrow" -> NamedArg.Short(toVar("arrow"))
)
args.permutations.foreach(perm =>
val str = perm.map(_._1).mkString("(", ", ", ")")
val expected = NonEmptyList.fromListUnsafe(perm.map(_._2))
val result = NamedArg.namedArgs
.parseAll(str)
.value
.map(_.mapK(spanToId))
result should be(expected)
)
}
"one line struct value" should "be parsed" in {
parseAndCheckStruct(
"""Obj(f1 = 1, f2 = "a", f3 = [1,2,3], f4=["b", "c"], f5 =NestedObj(i1 = 2, i2 = "b", i3= funcCall(3), i4 = value), f6=funcCall(1), f7 = Serv.call(2))"""

View File

@ -42,39 +42,59 @@ class PropertyOpSpec extends AnyFlatSpec with Matchers with EitherValues {
PropertyOp.ops.parseAll("!-1").isLeft shouldBe true
}
"copy ops" should "parse" in {
val opsP = (s: String) => PropertyOp.ops.parseAll(s).value.map(_.mapK(spanToId))
def copyOpsP(s: String) = PropertyOp.ops.parseAll(s).value.map(_.mapK(spanToId))
opsP(".copy(a = \"str\", b = 12)") should be(
"copy ops" should "parse one copy" in {
copyOpsP(".copy(a = \"str\", b = 12)") should be(
NonEmptyList.of(
IntoCopy[Id](
(),
NonEmptyMap.of(
"a" -> LiteralToken("\"str\"", LiteralType.string),
"b" -> toNumber(12)
)
)
)
)
opsP(".copy(a = \"str\", b = 12).copy(c = 54, d = someVar)") should be(
NonEmptyList.of(
IntoCopy[Id](
(),
NonEmptyMap.of(
"a" -> LiteralToken("\"str\"", LiteralType.string),
"b" -> toNumber(12)
)
),
IntoCopy[Id](
(),
NonEmptyMap.of(
"c" -> toNumber(54),
"d" -> VarToken("someVar")
NamedArg.Full(toName("a"), toStr("str")),
NamedArg.Full(toName("b"), toNumber(12))
)
)
)
)
}
it should "parse sequential copy" in {
copyOpsP(".copy(a = \"str\", b = 12).copy(c = 54, d = someVar)") should be(
NonEmptyList.of(
IntoCopy[Id](
(),
NonEmptyList.of(
NamedArg.Full(toName("a"), toStr("str")),
NamedArg.Full(toName("b"), toNumber(12))
)
),
IntoCopy[Id](
(),
NonEmptyList.of(
NamedArg.Full(toName("c"), toNumber(54)),
NamedArg.Full(toName("d"), toVar("someVar"))
)
)
)
)
}
it should "parse mixed args in copy" in {
val args = List(
"a = \"str\"" -> NamedArg.Full(toName("a"), toStr("str")),
"b = 12" -> NamedArg.Full(toName("b"), toNumber(12)),
"c" -> NamedArg.Short(toVar("c")),
"d" -> NamedArg.Short(toVar("d"))
)
args.toSet.subsets().filter(_.nonEmpty).flatMap(_.toList.permutations).foreach { args =>
val str = args.map(_._1).mkString(".copy(", ", ", ")")
val expected = NonEmptyList.of(
IntoCopy[Id]((), NonEmptyList.fromListUnsafe(args.map(_._2)))
)
copyOpsP(str) should be(expected)
}
}
}

View File

@ -34,6 +34,23 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
report: ReportAlgebra[S, Alg]
) extends Logging {
private def reportNamedArgsDuplicates(
args: NonEmptyList[NamedArg[S]]
): Alg[Unit] = args
.groupBy(_.argName.value)
.filter { case (_, group) =>
group.size > 1
}
.toList
.traverse_ { case (name, group) =>
group.traverse_ { arg =>
report.error(
arg.argName,
s"Duplicate argument `$name`"
)
}
}
private def resolveSingleProperty(rootType: Type, op: PropertyOp[S]): Alg[Option[PropertyRaw]] =
op match {
case op: IntoField[S] =>
@ -46,12 +63,13 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
)
} yield arrowProp
case op: IntoCopy[S] =>
for {
maybeFields <- op.fields.traverse(valueToRaw)
copyProp <- maybeFields.sequence.flatTraverse(
T.resolveCopy(rootType, op, _)
(for {
_ <- OptionT.liftF(
reportNamedArgsDuplicates(op.args)
)
} yield copyProp
fields <- op.args.traverse(arg => OptionT(valueToRaw(arg.argValue)).map(arg -> _))
prop <- OptionT(T.resolveCopy(op, rootType, fields))
} yield prop).value
case op: IntoIndex[S] =>
for {
maybeIdx <- op.idx.fold(LiteralRaw.Zero.some.pure)(valueToRaw)
@ -102,7 +120,13 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
case dvt @ NamedValueToken(typeName, fields) =>
(for {
resolvedType <- OptionT(T.resolveType(typeName))
fieldsGiven <- fields.traverse(value => OptionT(valueToRaw(value)))
// Report duplicate fields
_ <- OptionT.liftF(
reportNamedArgsDuplicates(fields)
)
fieldsGiven <- fields
.traverse(arg => OptionT(valueToRaw(arg.argValue)).map(arg.argName.value -> _))
.map(_.toNem) // Take only last value for a field
fieldsGivenTypes = fieldsGiven.map(_.`type`)
generated <- OptionT.fromOption(
resolvedType match {
@ -297,7 +321,8 @@ class ValuesAlgebra[S[_], Alg[_]: Monad](using
valueToRaw(v).flatMap(
_.flatTraverse {
case ca: CallArrowRaw => (ca, ca.baseType).some.pure[Alg]
case apr@ApplyPropertyRaw(_, IntoArrowRaw(_, arrowType, _)) => (apr, arrowType).some.pure[Alg]
case apr @ ApplyPropertyRaw(_, IntoArrowRaw(_, arrowType, _)) =>
(apr, arrowType).some.pure[Alg]
// TODO: better error message (`raw` formatting)
case raw => report.error(v, s"Expected arrow call, got $raw").as(none)
}

View File

@ -37,10 +37,11 @@ trait TypesAlgebra[S[_], Alg[_]] {
def resolveIndex(rootT: Type, op: IntoIndex[S], idx: ValueRaw): Alg[Option[PropertyRaw]]
def resolveCopy(
token: IntoCopy[S],
rootT: Type,
op: IntoCopy[S],
fields: NonEmptyMap[String, ValueRaw]
fields: NonEmptyList[(NamedArg[S], ValueRaw)]
): Alg[Option[PropertyRaw]]
def resolveField(rootT: Type, op: IntoField[S]): Alg[Option[PropertyRaw]]
def resolveArrow(

View File

@ -216,7 +216,7 @@ class TypesInterpreter[S[_], X](using
)
.as(None)
) {
case at@ArrowType(_, _) =>
case at @ ArrowType(_, _) =>
locations
.pointFieldLocation(name, op.name.value, op)
.as(Some(IntoArrowRaw(op.name.value, at, arguments)))
@ -245,22 +245,33 @@ class TypesInterpreter[S[_], X](using
// TODO actually it's stateless, exists there just for reporting needs
override def resolveCopy(
token: IntoCopy[S],
rootT: Type,
op: IntoCopy[S],
fields: NonEmptyMap[String, ValueRaw]
args: NonEmptyList[(NamedArg[S], ValueRaw)]
): State[X, Option[PropertyRaw]] =
rootT match {
case st: StructType =>
fields.toSortedMap.toList.traverse { case (fieldName, value) =>
args.forallM { case (arg, value) =>
val fieldName = arg.argName.value
st.fields.lookup(fieldName) match {
case Some(t) =>
ensureTypeMatches(op.fields.lookup(fieldName).getOrElse(op), t, value.`type`)
case None => report.error(op, s"No field with name '$fieldName' in $rootT").as(false)
ensureTypeMatches(arg.argValue, t, value.`type`)
case None =>
report.error(arg.argName, s"No field with name '$fieldName' in $rootT").as(false)
}
}.map(res => if (res.forall(identity)) Some(IntoCopyRaw(st, fields)) else None)
}.map(
Option.when(_)(
IntoCopyRaw(
st,
args.map { case (arg, value) =>
arg.argName.value -> value
}.toNem
)
)
)
case _ =>
report.error(op, s"Expected $rootT to be a data type").as(None)
report.error(token, s"Expected $rootT to be a data type").as(None)
}
// TODO actually it's stateless, exists there just for reporting needs
@ -339,12 +350,12 @@ class TypesInterpreter[S[_], X](using
)
.as(false)
} else {
valueFields.toSortedMap.toList.traverse { (name, `type`) =>
valueFields.toSortedMap.toList.forallM { (name, `type`) =>
typeFields.lookup(name) match {
case Some(t) =>
val nextToken = token match {
case NamedValueToken(_, fields) =>
fields.lookup(name).getOrElse(token)
fields.find(_.argName.value == name).getOrElse(token)
// TODO: Is it needed?
case PropertyToken(_, properties) =>
properties.last
@ -359,7 +370,7 @@ class TypesInterpreter[S[_], X](using
)
.as(false)
}
}.map(_.forall(identity))
}
}
case _ =>
val notes =

View File

@ -5,7 +5,7 @@ import aqua.parser.Ast
import aqua.raw.ops.{Call, CallArrowRawTag, FuncOp, OnTag, ParTag, RawTag, SeqGroupTag, SeqTag}
import aqua.parser.Parser
import aqua.parser.lift.{LiftParser, Span}
import aqua.raw.value.{ApplyBinaryOpRaw, LiteralRaw, ValueRaw, VarRaw}
import aqua.raw.value.*
import aqua.types.*
import aqua.raw.ops.*
@ -674,4 +674,126 @@ class SemanticsSpec extends AnyFlatSpec with Matchers with Inside {
warnings.exists(_.hints.exists(_.contains("used"))) should be(true)
}
}
{
val fieldCases = List(
"field = 42" -> "field = field",
"field = 42" -> "field",
"integer = 42" -> "field = integer",
"" -> "field = 42"
)
val strCases = List(
"str = \"str\"" -> "str = str",
"str = \"str\"" -> "str",
"string = \"str\"" -> "str = string",
"" -> "str = \"str\""
)
it should "handle struct creation" in {
for {
fieldCase <- fieldCases
(fieldDef, fieldArg) = fieldCase
strCase <- strCases
(strDef, strArg) = strCase
} {
val defs = List(fieldDef, strDef).filter(_.nonEmpty).mkString("\n ")
val args = List(fieldArg, strArg).filter(_.nonEmpty).mkString(", ")
val script = s"""|data Struct:
| field: i8
| str: string
|
|func main() -> Struct:
| $defs
| <- Struct($args)
|""".stripMargin
insideBody(script) { body =>
matchSubtree(body) { case (ReturnTag(vals), _) =>
inside(vals.head) { case MakeStructRaw(fields, _) =>
fields.contains("field") should be(true)
fields.contains("str") should be(true)
}
}
}
}
}
it should "handle ability creation" in {
def arrow(name: String) =
s"""|$name = (x: i8) -> bool:
| <- x > 0
|""".stripMargin
val arrowCases = List(
arrow("arrow") -> "arrow = arrow",
arrow("arrow") -> "arrow",
arrow("closure") -> "arrow = closure"
)
for {
arrowCase <- arrowCases
(arrowDef, arrowArg) = arrowCase
fieldCase <- fieldCases
(fieldDef, fieldArg) = fieldCase
strCase <- strCases
(strDef, strArg) = strCase
} {
val defs = List(arrowDef, fieldDef, strDef).filter(_.nonEmpty).mkString("\n ")
val args = List(arrowArg, fieldArg, strArg).filter(_.nonEmpty).mkString(", ")
val script = s"""|ability Ab:
| field: i8
| str: string
| arrow(x: i8) -> bool
|
|func main() -> Ab:
| $defs
| <- Ab($args)
|""".stripMargin
insideBody(script) { body =>
matchSubtree(body) { case (ReturnTag(vals), _) =>
inside(vals.head) { case AbilityRaw(fields, _) =>
fields.contains("arrow") should be(true)
fields.contains("field") should be(true)
fields.contains("str") should be(true)
}
}
}
}
}
}
it should "forbid duplicate fields in data or ability creation" in {
List("data", "ability").foreach { form =>
val script = s"""|$form StructOrAb:
| field: i8
|
|func main() -> StructOrAb:
| field = 24
| <- StructOrAb(field = 42, field)
|""".stripMargin
insideSemErrors(script) { errors =>
atLeast(1, errors.toChain.toList) shouldBe a[RulesViolated[Span.S]]
}
}
}
it should "forbid duplicate fields in data copy" in {
val script = """|data Struct:
| field: i8
|
|func main() -> Struct:
| st = Struct(field = 24)
| field = 37
| <- st.copy(field = 42, field)
|""".stripMargin
insideSemErrors(script) { errors =>
atLeast(1, errors.toChain.toList) shouldBe a[RulesViolated[Span.S]]
}
}
}