Skip to content

Commit c88fde9

Browse files
ChronarakchengluyuLPTK
authored
Record patterns, conjunction, disjunction added to ucs (#306)
Co-authored-by: Florent Ferrari <[email protected]> Co-authored-by: Luyu Cheng <[email protected]> Co-authored-by: Lionel Parreaux <[email protected]>
1 parent c7ad33c commit c88fde9

25 files changed

+568
-174
lines changed

hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,16 +393,22 @@ enum Case:
393393
case Lit(lit: Literal)
394394
case Cls(cls: ClassLikeSymbol, path: Path)
395395
case Tup(len: Int, inf: Bool)
396+
/** checks field existence
397+
* @param safe true will omit the instanceof Object check
398+
*/
399+
case Field(name: Tree.Ident, safe: Bool)
396400

397401
lazy val freeVars: Set[Local] = this match
398402
case Lit(_) => Set.empty
399403
case Cls(_, path) => path.freeVars
400404
case Tup(_, _) => Set.empty
405+
case Field(_, _) => Set.empty
401406

402407
lazy val freeVarsLLIR: Set[Local] = this match
403408
case Lit(_) => Set.empty
404409
case Cls(_, path) => path.freeVarsLLIR
405410
case Tup(_, _) => Set.empty
411+
case Field(_, _) => Set.empty
406412

407413
sealed trait TrivialResult extends Result
408414

hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ class BlockTransformer(subst: SymbolSubst):
202202
val path2 = applyPath(path)
203203
if (cls2 is cls) && (path2 is path) then cse else Case.Cls(cls2, path2)
204204
case Case.Tup(len, inf) => cse
205+
case Case.Field(name, safe) => cse
205206

206207
def applyHandler(hdr: Handler): Handler =
207208
val sym2 = hdr.sym.subst

hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,17 @@ class Lowering()(using Config, TL, Raise, State, Ctx):
490490
(cse, Assign(arg, Select(sr, param.id/*FIXME incorrect Ident?*/)(S(param)), blk))
491491
mkMatch(mkArgs(clsParams.iterator.zip(args).collect { case (s1, S(s2)) => (s1, s2) }.toList))
492492
case Pattern.Tuple(len, inf) => mkMatch(Case.Tup(len, inf) -> go(tail, topLevel = false))
493+
case Pattern.Record(entries) =>
494+
val objectSym = ctx.builtins.Object
495+
mkMatch( // checking that we have an object
496+
Case.Cls(objectSym, Value.Ref(BuiltinSymbol(objectSym.nme, false, false, true, false))),
497+
entries.foldRight(go(tail, topLevel = false)):
498+
case ((fieldName, fieldSymbol), blk) =>
499+
mkMatch(
500+
Case.Field(fieldName, safe = true), // we know we have an object, no need to check again
501+
Assign(fieldSymbol, Select(sr, fieldName)(N), blk)
502+
)
503+
)
493504
case Split.Else(els) =>
494505
if k.isInstanceOf[TailOp] && isIf then term_nonTail(els)(k)
495506
else

hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,10 @@ class JSBuilder(using TL, State, Ctx) extends CodeBuilder:
375375
case Elaborator.ctx.builtins.Int => doc"globalThis.Number.isInteger($sd)"
376376
case _ => doc"$sd instanceof ${result(pth)}"
377377
case Case.Tup(len, inf) => doc"globalThis.Array.isArray($sd) && $sd.length ${if inf then ">=" else "==="} ${len}"
378+
case Case.Field(n, safe = false) =>
379+
doc"""typeof $sd === "object" && $sd !== null && "${n.name}" in $sd"""
380+
case Case.Field(n, safe = true) =>
381+
doc""""${n.name}" in $sd"""
378382
val h = doc" # if (${ cond(hd._1) }) ${ braced(returningTerm(hd._2, endSemi = false)) }"
379383
val t = tl.foldLeft(h)((acc, arm) =>
380384
acc :: doc" else if (${ cond(arm._1) }) ${ braced(returningTerm(arm._2, endSemi = false)) }")

hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,8 @@ enum Pattern extends AutoLocated:
2626
case Record(entries: List[(Ident -> BlockLocalSymbol)])
2727

2828
def subTerms: Ls[Term] = this match
29-
case Lit(_) => Nil
3029
case ClassLike(_, t, _, _) => t :: Nil
31-
case Synonym(_, _) => Nil
32-
case Tuple(_, _) => Nil
33-
case Record(_) => Nil
30+
case _: (Lit | Synonym | Tuple | Record) => Nil
3431

3532
def children: Ls[Located] = this match
3633
case Lit(literal) => literal :: Nil

hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Desugarer.scala

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import mlscript.utils.*, shorthands.*
77
import Message.MessageContext
88
import utils.TraceLogger
99
import syntax.Literal
10-
import Keyword.{as, and, `do`, `else`, is, let, `then`}
10+
import Keyword.{as, and, `do`, `else`, is, let, `then`, where}
1111
import collection.mutable.{HashMap, SortedSet}
1212
import Elaborator.{ctx, Ctxl}
1313
import scala.annotation.targetName
@@ -21,6 +21,7 @@ object Desugarer:
2121

2222
class ScrutineeData:
2323
val classes: HashMap[ClassSymbol, List[BlockLocalSymbol]] = HashMap.empty
24+
val fields: HashMap[Ident, BlockLocalSymbol] = HashMap.empty
2425
val tupleLead: HashMap[Int, BlockLocalSymbol] = HashMap.empty
2526
val tupleLast: HashMap[Int, BlockLocalSymbol] = HashMap.empty
2627
end Desugarer
@@ -89,18 +90,13 @@ class Desugarer(val elaborator: Elaborator)
8990
def ++(fallback: Split): Split =
9091
if fallback == Split.End then
9192
split
92-
else if split.isFull then
93-
raise:
94-
ErrorReport:
95-
msg"The following branches are unreachable." -> fallback.toLoc ::
96-
msg"Because the previous split is full." -> split.toLoc :: Nil
97-
split
9893
else (split match
9994
case Split.Cons(head, tail) => Split.Cons(head, tail ++ fallback)
10095
case Split.Let(name, term, tail) => Split.Let(name, term, tail ++ fallback)
101-
case Split.Else(_) /* impossible */ | Split.End => fallback)
96+
case Split.Else(_) | Split.End => fallback)
10297

10398
private val subScrutineeMap = HashMap.empty[BlockLocalSymbol, ScrutineeData]
99+
private val fieldScrutineeMap = HashMap.empty[BlockLocalSymbol, ScrutineeData]
104100

105101
extension (symbol: BlockLocalSymbol)
106102
def getSubScrutinees(cls: ClassSymbol): List[BlockLocalSymbol] =
@@ -113,6 +109,11 @@ class Desugarer(val elaborator: Elaborator)
113109
def getTupleLastSubScrutinee(index: Int): BlockLocalSymbol =
114110
val data = subScrutineeMap.getOrElseUpdate(symbol, new ScrutineeData)
115111
data.tupleLast.getOrElseUpdate(index, TempSymbol(N, s"last$index"))
112+
def getFieldScrutinee(fieldName: Ident): BlockLocalSymbol =
113+
subScrutineeMap
114+
.getOrElseUpdate(symbol, new ScrutineeData)
115+
.fields
116+
.getOrElseUpdate(fieldName, TempSymbol(N, s"field${fieldName.name}"))
116117

117118

118119
def default: Split => Sequel = split => _ => split
@@ -459,9 +460,7 @@ class Desugarer(val elaborator: Elaborator)
459460
if pat.patternParams.size > 0 then
460461
error(
461462
msg"Pattern `${pat.nme}` expects ${"pattern argument".pluralize(pat.patternParams.size, true)}" ->
462-
pat.patternParams.foldLeft[Opt[Loc]](N):
463-
case (N, param) => param.sym.toLoc
464-
case (S(loc), param) => S(loc ++ param.sym.toLoc),
463+
Loc(pat.patternParams.iterator.map(_.sym)),
465464
msg"But no arguments were given" -> ctor.toLoc)
466465
fallback
467466
else
@@ -508,12 +507,8 @@ class Desugarer(val elaborator: Elaborator)
508507
if pat.patternParams.size != patArgs.size then
509508
error(
510509
msg"Pattern `${pat.nme}` expects ${"pattern argument".pluralize(pat.patternParams.size, true)}" ->
511-
pat.patternParams.foldLeft[Opt[Loc]](N):
512-
case (N, param) => param.sym.toLoc
513-
case (S(loc), param) => S(loc ++ param.sym.toLoc),
514-
msg"But ${"pattern argument".pluralize(patArgs.size, true)} were given" -> args.foldLeft[Opt[Loc]](N):
515-
case (N, arg) => arg.toLoc
516-
case (S(loc), arg) => S(loc ++ arg.toLoc))
510+
Loc(pat.patternParams.iterator.map(_.sym)),
511+
msg"But ${"pattern argument".pluralize(patArgs.size, true)} were given" -> Loc(args))
517512
fallback
518513
else
519514
Branch(ref, Pattern.Synonym(pat, patArgs.zip(args)), sequel(ctx)) ~: fallback
@@ -589,6 +584,12 @@ class Desugarer(val elaborator: Elaborator)
589584
Branch(ref, Pattern.Lit(IntLit(-value)), sequel(ctx)) ~: fallback
590585
case App(Ident("-"), Tup(DecLit(value) :: Nil)) => fallback => ctx =>
591586
Branch(ref, Pattern.Lit(DecLit(-value)), sequel(ctx)) ~: fallback
587+
case App(Ident("&"), Tree.Tup(lhs :: rhs :: Nil)) => fallback => ctx =>
588+
val newSequel = expandMatch(scrutSymbol, rhs, sequel)(fallback)
589+
expandMatch(scrutSymbol, lhs, newSequel)(fallback)(ctx)
590+
case App(Ident("|"), Tree.Tup(lhs :: rhs :: Nil)) => fallback => ctx =>
591+
val newFallback = expandMatch(scrutSymbol, rhs, sequel)(fallback)(ctx)
592+
expandMatch(scrutSymbol, lhs, sequel)(newFallback)(ctx)
592593
// A single constructor pattern.
593594
case Annotated(Ident("compile"), app @ App(ctor: Ctor, Tup(args))) =>
594595
dealWithAppCtorCase(app, ctor, args, true)
@@ -607,18 +608,50 @@ class Desugarer(val elaborator: Elaborator)
607608
case pattern and consequent => fallback => ctx =>
608609
val innerSplit = termSplit(consequent, identity)(Split.End)
609610
expandMatch(scrutSymbol, pattern, innerSplit)(fallback)(ctx)
611+
case pattern where condition => fallback => ctx =>
612+
val sym = TempSymbol(N, "conditionTemp")
613+
val newSequel = expandMatch(sym, Tree.BoolLit(true), sequel)(fallback)
614+
val newNewSequel = (ctx: Ctx) => Split.Let(sym, term(condition)(using ctx), newSequel(ctx))
615+
expandMatch(scrutSymbol, pattern, newNewSequel)(fallback)(ctx)
610616
case Jux(Ident(".."), Ident(_)) => fallback => _ =>
611617
raise(ErrorReport(msg"Illegal rest pattern." -> pattern.toLoc :: Nil))
612618
fallback
613-
case InfixApp(id: Ident, Keyword.`:`, pat) => fallback => ctx =>
614-
val sym = VarSymbol(id)
615-
val ctx2 = ctx
616-
// + (id.name -> sym) // * This binds the field's name in the context; probably surprising
617-
Split.Let(sym, ref.sel(id, N),
618-
expandMatch(sym, pat, sequel)(fallback)(ctx2))
619+
case InfixApp(fieldName: Ident, Keyword.`:`, pat) => fallback => ctx =>
620+
val symbol = scrutSymbol.getFieldScrutinee(fieldName)
621+
Branch(
622+
ref,
623+
Pattern.Record((fieldName, symbol) :: Nil),
624+
subMatches((R(symbol), pat) :: Nil, sequel)(Split.End)(ctx)
625+
) ~: fallback
626+
case Pun(false, fieldName) => fallback => ctx =>
627+
val symbol = scrutSymbol.getFieldScrutinee(fieldName)
628+
Branch(
629+
ref,
630+
Pattern.Record((fieldName, symbol) :: Nil),
631+
subMatches((R(symbol), fieldName) :: Nil, sequel)(Split.End)(ctx)
632+
) ~: fallback
619633
case Block(st :: Nil) => fallback => ctx =>
620634
expandMatch(scrutSymbol, st, sequel)(fallback)(ctx)
621-
// case Block(sts) => fallback => ctx => // TODO
635+
case Block(sts) => fallback => ctx => // we assume this is a record
636+
sts.foldRight[Option[List[(Tree.Ident, BlockLocalSymbol, Tree)]]](S(Nil)){
637+
// this collects the record parts, or fails if some statement does not correspond
638+
// to a record field
639+
case (_, N) => N // we only need to fail once to return N
640+
case (p, S(tl)) => p match
641+
case InfixApp(fieldName: Ident, Keyword.`:`, pat) =>
642+
S((fieldName, scrutSymbol.getFieldScrutinee(fieldName), pat) :: tl)
643+
case Pun(false, fieldName) =>
644+
S((fieldName, scrutSymbol.getFieldScrutinee(fieldName), fieldName) :: tl)
645+
case p =>
646+
raise(ErrorReport(msg"invalid record field pattern" -> p.toLoc :: Nil))
647+
None
648+
}.fold(fallback)(recordContent =>
649+
Branch(
650+
ref,
651+
Pattern.Record(recordContent.map((fieldName, symbol, _) => (fieldName, symbol))),
652+
subMatches(recordContent.map((_, symbol, pat) => (R(symbol), pat)), sequel)(Split.End)(ctx)
653+
) ~: fallback
654+
)
622655
case Bra(BracketKind.Curly | BracketKind.Round, inner) => fallback => ctx =>
623656
expandMatch(scrutSymbol, inner, sequel)(fallback)(ctx)
624657
case pattern => fallback => _ =>

hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/HelperExtractors.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ object HelperExtractors:
1313
case App(Ident("|"), Tup(lhs :: rhs :: Nil)) => S(lhs, rhs)
1414
case _ => N
1515

16+
/** A helper extractor for matching the tree of `x a y`.*/
17+
object and:
18+
infix def unapply(tree: App): Opt[(Tree, Tree)] = tree match
19+
case App(Ident("&"), Tup(lhs :: rhs :: Nil)) => S(lhs, rhs)
20+
case _ => N
21+
1622
/** A helper extractor for matching the tree of `x ..= y` and `x ..< y`.
1723
* The Boolean value indicates whether the range is inclusive.
1824
*/

hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import mlscript.utils.*, shorthands.*
66
import syntax.{Literal, Tree}, utils.TraceLogger
77
import Message.MessageContext
88
import Elaborator.Ctx
9+
import hkmc2.semantics.ClassDef.Parameterized
910

1011
class Normalization(elaborator: Elaborator)(using raise: Raise, ctx: Ctx):
1112
import Normalization.*, Mode.*
@@ -47,7 +48,14 @@ class Normalization(elaborator: Elaborator)(using raise: Raise, ctx: Ctx):
4748
case (c1: Pattern.ClassLike, c2: Pattern.ClassLike) => c1.sym === c2.sym
4849
case (Pattern.Lit(l1), Pattern.Lit(l2)) => l1 === l2
4950
case (Pattern.Tuple(n1, b1), Pattern.Tuple(n2, b2)) => n1 === n2 && b1 === b2
50-
case (_, _) => false
51+
case (Pattern.Record(ls1), Pattern.Record(ls2)) =>
52+
ls1.lazyZip(ls2).forall:
53+
case ((fieldName1, p1), (fieldName2, p2)) =>
54+
fieldName1 === fieldName2 && p1 === p2
55+
case (Pattern.Synonym(sym1, args1), Pattern.Synonym(sym2, args2)) =>
56+
args1 === args2 && sym1.params === sym2.params && sym1.body === sym2.body
57+
case (_: Pattern.ClassLike, _) | (_: Pattern.Lit, _) |
58+
(_: Pattern.Tuple, _) | (_: Pattern.Synonym, _) | (_: Pattern.Record, _) => false
5159
/** Checks if `lhs` can be subsumed under `rhs`. */
5260
def <:<(rhs: Pattern): Bool = compareCasePattern(lhs, rhs)
5361
/**
@@ -68,6 +76,23 @@ class Normalization(elaborator: Elaborator)(using raise: Raise, ctx: Ctx):
6876
case lhs: Pattern.ClassLike => lhs.refined = true
6977
case _ => ()
7078

79+
extension (lhs: Pattern.Record)
80+
/** reduces the record pattern `lhs` assuming we have matched `rhs`.
81+
* It removes field matches that may now be unnecessary
82+
*/
83+
infix def assuming(rhs: Pattern): Pattern.Record = rhs match
84+
case Pattern.Record(rhsEntries) =>
85+
val filteredEntries = lhs.entries.filter:
86+
(fieldName1, _) => rhsEntries.forall { (fieldName2, _) => !(fieldName1 === fieldName2)}
87+
Pattern.Record(filteredEntries)
88+
case Pattern.ClassLike(sym = cls : ClassSymbol) => cls.defn match
89+
case S(ClassDef.Parameterized(params = paramList)) =>
90+
val filteredEntries = lhs.entries.filter:
91+
(fieldName1, _) => paramList.params.forall { (param:Param) => !(fieldName1 === param.sym.id)}
92+
Pattern.Record(filteredEntries)
93+
case S(_) | N => lhs
94+
case _ => lhs
95+
7196
inline def apply(split: Split): Split = normalize(split)(using VarSet())
7297

7398
/**
@@ -82,7 +107,7 @@ class Normalization(elaborator: Elaborator)(using raise: Raise, ctx: Ctx):
82107
):
83108
def rec(split: Split)(using vs: VarSet): Split = split match
84109
case Split.Cons(Branch(scrutinee, pattern, consequent), alternative) => pattern match
85-
case pattern: (Pattern.Lit | Pattern.ClassLike | Pattern.Tuple) =>
110+
case pattern: (Pattern.Lit | Pattern.ClassLike | Pattern.Tuple | Pattern.Record) =>
86111
log(s"MATCH: ${scrutinee.showDbg} is ${pattern.showDbg}")
87112
val whenTrue = normalize(specialize(consequent ++ alternative, +, scrutinee, pattern))
88113
val whenFalse = rec(specialize(alternative, -, scrutinee, pattern).clearFallback)
@@ -154,8 +179,9 @@ class Normalization(elaborator: Elaborator)(using raise: Raise, ctx: Ctx):
154179

155180
/**
156181
* Specialize `split` with the assumption that `scrutinee` matches `pattern`.
157-
* If `matchOrNot` is `true`, the function _keeps_ branches that agree on
158-
* `scrutinee` matches `pattern`. Otherwise, the function _removes_ branches
182+
* If `mode` is `+`, the function _keeps_ branches that agree on
183+
* `scrutinee` matching `pattern` and simplifies the record patterns it sees if the fields were already matched.
184+
* Otherwise (if `mode` is `-`), the function _removes_ branches
159185
* that agree on `scrutinee` matches `pattern`.
160186
*/
161187
private def specialize(
@@ -188,23 +214,35 @@ class Normalization(elaborator: Elaborator)(using raise: Raise, ctx: Ctx):
188214
else if split.isFallback then
189215
log(s"Case 1.1.3: $pattern is unrelated with $thatPattern")
190216
rec(tail)
191-
else if pattern <:< thatPattern then
192-
// TODO: the warning will be useful when we have inheritance information
193-
// raiseDesugaringWarning(
194-
// msg"the pattern always matches" -> thatPattern.toLoc,
195-
// msg"the scrutinee was matched against ${pattern.toString}" -> pattern.toLoc,
196-
// msg"which is a subtype of ${thatPattern.toString}" -> (pattern match {
197-
// case Pattern.Class(cls, _, _) => cls.toLoc
198-
// case _ => thatPattern.toLoc
199-
// }))
200-
rec(continuation) ++ rec(tail)
201-
else
202-
// TODO: the warning will be useful when we have inheritance information
203-
// raiseDesugaringWarning(
204-
// msg"possibly conflicting patterns for this scrutinee" -> scrutinee.toLoc,
205-
// msg"the scrutinee was matched against ${pattern.toString}" -> pattern.toLoc,
206-
// msg"which is unrelated with ${thatPattern.toString}" -> thatPattern.toLoc)
207-
rec(tail)
217+
else thatPattern match
218+
case thatPattern :Pattern.Record =>
219+
log(s"Case 1.1.4: $thatPattern is a record")
220+
// we can use information if pattern is itself a record, or if it is a constructor with arguments
221+
val simplifiedRecord = thatPattern assuming pattern
222+
if simplifiedRecord.entries.isEmpty then
223+
tail
224+
else
225+
Split.Cons(Branch(thatScrutinee, simplifiedRecord, continuation), tail)
226+
case _ =>
227+
if pattern <:< thatPattern then
228+
// TODO: the warning will be useful when we have inheritance information
229+
// raiseDesugaringWarning(
230+
// msg"the pattern always matches" -> thatPattern.toLoc,
231+
// msg"the scrutinee was matched against ${pattern.toString}" -> pattern.toLoc,
232+
// msg"which is a subtype of ${thatPattern.toString}" -> (pattern match {
233+
// case Pattern.Class(cls, _, _) => cls.toLoc
234+
// case _ => thatPattern.toLoc
235+
// }))
236+
log(s"case 1.1.5: $pattern <:< $thatPattern")
237+
split
238+
else
239+
// TODO: the warning will be useful when we have inheritance information
240+
// raiseDesugaringWarning(
241+
// msg"possibly conflicting patterns for this scrutinee" -> scrutinee.toLoc,
242+
// msg"the scrutinee was matched against ${pattern.toString}" -> pattern.toLoc,
243+
// msg"which is unrelated with ${thatPattern.toString}" -> thatPattern.toLoc)
244+
log(s"Case 1.1._ else : ${tail}")
245+
rec(tail)
208246
case - =>
209247
log(s"Case 1.2: $scrutinee === $thatScrutinee")
210248
thatPattern reportInconsistentRefinedWith pattern
@@ -249,7 +287,17 @@ object Normalization:
249287
case (Lit(Tree.StrLit(_)), ClassLike(blt.`Str`, _, _, _)) => true
250288
case (Lit(Tree.DecLit(_)), ClassLike(blt.`Num`, _, _, _)) => true
251289
case (Lit(Tree.BoolLit(_)), ClassLike(blt.`Bool`, _, _, _)) => true
252-
case (_, _) => false
290+
case (_: Synonym, _: Synonym) => lhs =:= rhs
291+
case (Record(entries1), Record(entries2)) =>
292+
entries1.forall { (fieldName1, _) => entries2.exists { (fieldName2, _) => fieldName1 === fieldName2 } }
293+
case (Record(entries), ClassLike(sym = cs: ClassSymbol)) =>
294+
val clsParams = cs.defn match
295+
case S(Parameterized(params = paramList)) => paramList.params
296+
case (S(_) | N) => Nil
297+
entries.forall { (fieldName, _) => clsParams.exists {
298+
case Param(flags = FldFlags(value = value), sym = sym) => value && fieldName === sym.id
299+
}}
300+
case (_: (ClassLike | Tuple | Lit | Record | Synonym), _) => false
253301

254302
final case class VarSet(declared: Set[BlockLocalSymbol]):
255303
def +(nme: BlockLocalSymbol): VarSet = copy(declared + nme)

0 commit comments

Comments
 (0)