Skip to content

Add support for units of measure #1454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 84 commits into
base: horizon
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
f429a4d
Add grammar
BenMusch May 28, 2019
05b3a9c
Get a number syntax that parses
BenMusch May 28, 2019
cb2f411
Update AST, currently getting parse error
BenMusch May 28, 2019
2907a0e
Update s-num matches to work (ignoring units)
BenMusch May 29, 2019
1988ae2
Add some labels/tosource
BenMusch May 29, 2019
4c48e62
Add parse tests
BenMusch May 29, 2019
e908e27
Add wf-checks for units
BenMusch May 29, 2019
e0da7e7
Fix bugs, add well-formedness tests
BenMusch May 29, 2019
8b1f148
Add units to a-num
BenMusch May 30, 2019
a39fe86
Add logic to normalize the units
BenMusch May 30, 2019
f8a0548
WIP: Why won't this fail?
BenMusch May 30, 2019
25d0aa7
Some stuff is working, code needs cleanup and more thorough tests
BenMusch May 31, 2019
274d27f
Remove a print
BenMusch May 31, 2019
1ad0ef8
Remove an is-operation that wasn't being used
BenMusch May 31, 2019
0e9bc03
Delegate more responsibility to the Unitnums
BenMusch Jun 1, 2019
6ce0ce0
Fix a bug
BenMusch Jun 1, 2019
bdde186
Add some basic tests, fix a bug
BenMusch Jun 3, 2019
685c160
Fix grammar ambiguity, more tests
BenMusch Jun 3, 2019
0ea5f33
WIP
BenMusch Jun 3, 2019
c81f57a
Unsupported operation tests
BenMusch Jun 3, 2019
6efe7cb
Add units to fracs, test some numeric operations
BenMusch Jun 3, 2019
27a7d00
Add some tests
BenMusch Jun 3, 2019
da92df1
Add units to rfracs, fix bug, run all the tests
BenMusch Jun 3, 2019
6aaa314
Fix a test
BenMusch Jun 3, 2019
1720bcc
Update grammar for predicates
BenMusch Jun 4, 2019
0629341
Add a-unit to Ann ast
BenMusch Jun 4, 2019
6ee5828
Add parsing tests
BenMusch Jun 4, 2019
bc08458
Add wf-checks
BenMusch Jun 4, 2019
e468a90
Get rid of AUnits, normalize unit expressions at compile-time
BenMusch Jun 4, 2019
ca760e6
Hack to default units to 1
BenMusch Jun 5, 2019
05fd792
Change from a runtime approach to a desugar approach
BenMusch Jun 5, 2019
927c4c3
Fix JS bug
BenMusch Jun 5, 2019
4d478ab
Add support for underscore annotations
BenMusch Jun 5, 2019
ca16198
Tests for contracts
BenMusch Jun 5, 2019
020f04e
Add wf checks for underscores in units
BenMusch Jun 5, 2019
bf0689d
Enforce unit quantity and ordering via grammar, not wf
BenMusch Jun 5, 2019
43eada1
Switch numbers to always have a unit, start adding a visitor for units
BenMusch Jun 6, 2019
ff2434c
Refactor compile-unit to be faster
BenMusch Jun 6, 2019
05384be
Refactor units to be visited
BenMusch Jun 6, 2019
7adc2f5
Use fold-keys
BenMusch Jun 6, 2019
5244510
Sort units when making strings
BenMusch Jun 6, 2019
b641650
Refactor the grammar to resemble binops
BenMusch Jun 6, 2019
6f14aaa
Refactor unitnum handling in makeInteger*op() and throw errors on fra…
BenMusch Jun 6, 2019
e861205
Some housekeeping
BenMusch Jun 6, 2019
22e8088
Don't re-use mixed-binops
BenMusch Jun 7, 2019
880c7ab
Update annotations on num-* builtins
BenMusch Jun 7, 2019
9f1561f
Fix-up tests, now failing due to error messages. Need to test every n…
BenMusch Jun 7, 2019
ca86032
Test all num functions, fix some edge cases
BenMusch Jun 7, 2019
c9f8ea3
Add withUnits pattern with some intermediate hacks and test failures
BenMusch Jun 13, 2019
dcc35ea
Slightly better errors
BenMusch Jun 14, 2019
d470bd6
deploy
BenMusch Jun 14, 2019
f1ae2b8
Some WIP
BenMusch Jun 14, 2019
535c6e7
Add a test
BenMusch Jun 15, 2019
33339f6
Fix-up render-fancy-reason for error
BenMusch Jun 15, 2019
c45a0db
Remove wf-check on powers
BenMusch Jun 15, 2019
ee8026f
???
BenMusch Jun 15, 2019
bb56566
Add support for 1 as a unit
BenMusch Jun 17, 2019
32e84d1
sqrt/expt support for unitnums
BenMusch Jun 17, 2019
f917679
Add some more unit functions used by the UI
BenMusch Jun 17, 2019
0217179
Always reset implicit on withUnit() calls
BenMusch Jun 17, 2019
0ea223d
Add test for un-implicit-ing a <1> annot
BenMusch Jun 18, 2019
7761c60
Pyret error for incompatible units
BenMusch Jun 18, 2019
b833d0c
More pyret errors + better contract failures
BenMusch Jun 18, 2019
9cb5c1c
Better tests for unit errors
BenMusch Jun 18, 2019
1f40427
Dont support polymorphic 0, improve jsnums performance on units
BenMusch Jun 18, 2019
ac3cb8b
Reset pitometer files for now
BenMusch Jun 18, 2019
8fe17b2
Reset build dir
BenMusch Jun 18, 2019
c7a5dbd
Final pre-PR cleanup
BenMusch Jun 18, 2019
56be3de
Fix regression tests
BenMusch Jun 18, 2019
84e2dec
Use bigint when necessary on unit powers
BenMusch Jun 19, 2019
e7cd223
Fix division bug
BenMusch Jun 19, 2019
780780e
Improve units-on-unsupport-ann message
BenMusch Jun 21, 2019
9701324
Improve incompatible-units error message
BenMusch Jun 21, 2019
0a032ad
Better invalid-unit-state messages
BenMusch Jun 21, 2019
6d1de41
Remove TODO in grammar
BenMusch Jun 23, 2019
e76f4d3
Fix bug that drops srcloc on withUnit calls
BenMusch Jun 23, 2019
7ddfc67
Some progress, still need to test all error messages
BenMusch Jun 26, 2019
7854c19
Remove error message TODOs
BenMusch Jul 8, 2019
75a1dc9
Fix identical equality
BenMusch Jul 9, 2019
4348552
slight changes in error messages
BenMusch Jul 15, 2019
a788536
Fix equality bug
BenMusch Jul 16, 2019
7c66302
Fix bug in equal3 for within() checks on strings
BenMusch Jul 16, 2019
8637982
Add tests for within() comparisons of strings, fix error message grammar
BenMusch Jul 17, 2019
b849f93
Add comment explaining logic of equalHelp()
BenMusch Jul 17, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 73 additions & 11 deletions src/arr/compiler/anf-loop-compiler.arr
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,57 @@ fun is-function-flat(flatness-env :: FL.FEnv, fun-name :: String) -> Boolean:
is-flat-enough(flatness-opt)
end

fun normalize-unit-help(u :: A.Unit, factor :: NumInteger, acc :: D.MutableStringDict<NumInteger>) -> D.MutableStringDict<NumInteger>:
cases (A.Unit) u block:
| u-one(_) => acc
| u-base(l, id) =>
acc.set-now(tostring(id), acc.get-now(tostring(id)).or-else(0) + factor)
acc
| u-mul(_, _, lhs, rhs) => normalize-unit-help(lhs, factor, normalize-unit-help(rhs, factor, acc))
| u-div(_, _, lhs, rhs) => normalize-unit-help(lhs, factor, normalize-unit-help(rhs, factor * -1, acc))
| u-pow(_, _, shadow u, n) => normalize-unit-help(u, n * factor, acc)
| u-paren(_, shadow u) => normalize-unit-help(u, factor, acc)
end
end
fun normalize-unit(u :: A.Unit) -> D.StringDict<NumInteger>:
normalize-unit-help(u, 1, [mutable-string-dict: ]).freeze()
end

fun compile-unit-help(u :: A.Unit) -> J.JExpr:
normalized = normalize-unit(u)
fields = normalized.fold-keys(
lam(key, acc):
power = normalized.get-value(key)
if power == 0:
acc
else:
val = if num-is-fixnum(power):
j-num(power)
else:
rt-method("makeNumberFromString", [clist: j-str(tostring(power))])
end
CL.concat-cons(j-field(key, val), acc)
end
end,
CL.concat-empty)

if fields.length() == 0:
rt-field("UNIT_ONE")
else:
j-obj(CL.concat-cons(j-field("$count", j-num(fields.length())), fields))
end
end
fun compile-unit(u :: A.Unit) -> J.JExpr:
cases (A.Unit) u block:
| u-base(l, id) =>
if A.is-s-underscore(id):
rt-field("UNIT_ANY")
else:
compile-unit-help(u)
end
| else => compile-unit-help(u)
end
end

fun compile-ann(ann :: A.Ann, visitor) -> DAG.CaseResults%(is-c-exp):
cases(A.Ann) ann:
Expand Down Expand Up @@ -395,6 +445,12 @@ fun compile-ann(ann :: A.Ann, visitor) -> DAG.CaseResults%(is-c-exp):
rt-method(pred-maker, [clist: compiled-base.exp, compiled-exp.exp, j-str(name)]),
cl-append(compiled-base.other-stmts, compiled-exp.other-stmts)
)
| a-unit(l, base, u) =>
compiled-base = compile-ann(base, visitor)
compiled-unit = compile-unit(u)
c-exp(
rt-method("makeUnitAnn", [clist: compiled-base.exp, compiled-unit, visitor.get-loc(l)]),
cl-empty)
| a-dot(l, m, field) =>
c-exp(
rt-method("getDotAnn", [clist:
Expand Down Expand Up @@ -1667,11 +1723,16 @@ compiler-visitor = {
method a-srcloc(self, l, loc):
c-exp(self.get-loc(loc), cl-empty)
end,
method a-num(self, l :: Loc, n :: Number):
if num-is-fixnum(n):
method a-num(self, l :: Loc, n :: Number, u :: A.Unit) block:
if num-is-fixnum(n) and A.is-u-one(u):
c-exp(j-parens(j-num(n)), cl-empty)
else:
c-exp(rt-method("makeNumberFromString", [clist: j-str(tostring(n))]), cl-empty)
make-num-call = rt-method("makeNumberFromString", [clist: j-str(tostring(n))])
if A.is-u-one(u):
c-exp(make-num-call, cl-empty)
else:
c-exp(rt-method("addUnit", [clist: make-num-call, compile-unit(u)]), cl-empty)
end
end
end,
method a-str(self, l :: Loc, s :: String):
Expand Down Expand Up @@ -1907,21 +1968,22 @@ remove-useless-if-visitor = N.default-map-visitor.{

check:
d = N.dummy-loc
u = A.u-one(d)
true1 = N.a-if(d, N.a-bool(d, true),
N.a-lettable(d, N.a-val(d, N.a-num(d, 1))),
N.a-lettable(d, N.a-val(d, N.a-num(d, 2))))
true1.visit(remove-useless-if-visitor) is N.a-val(d, N.a-num(d, 1))
N.a-lettable(d, N.a-val(d, N.a-num(d, 1, u))),
N.a-lettable(d, N.a-val(d, N.a-num(d, 2, u))))
true1.visit(remove-useless-if-visitor) is N.a-val(d, N.a-num(d, 1, u))

false4 = N.a-if(d, N.a-bool(d, false),
N.a-lettable(d, N.a-val(d, N.a-num(d, 3))),
N.a-lettable(d, N.a-val(d, N.a-num(d, 4))))
false4.visit(remove-useless-if-visitor) is N.a-val(d, N.a-num(d, 4))
N.a-lettable(d, N.a-val(d, N.a-num(d, 3, u))),
N.a-lettable(d, N.a-val(d, N.a-num(d, 4, u))))
false4.visit(remove-useless-if-visitor) is N.a-val(d, N.a-num(d, 4, u))

N.a-if(d, N.a-id(d, A.s-name(d, "x")), N.a-lettable(d, true1), N.a-lettable(d, false4)
).visit(remove-useless-if-visitor)
is N.a-if(d, N.a-id(d, A.s-name(d, "x")),
N.a-lettable(d, N.a-val(d, N.a-num(d, 1))),
N.a-lettable(d, N.a-val(d, N.a-num(d, 4))))
N.a-lettable(d, N.a-val(d, N.a-num(d, 1, u))),
N.a-lettable(d, N.a-val(d, N.a-num(d, 4, u))))

end
|#
Expand Down
12 changes: 8 additions & 4 deletions src/arr/compiler/anf.arr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ provide-types *
import ast as A
import srcloc as SL
import file("ast-anf.arr") as N
import string-dict as SD

type Loc = SL.Srcloc

Expand Down Expand Up @@ -166,11 +167,14 @@ fun anf(e :: A.Expr, k :: ANFCont) -> N.AExpr:
end)

end)
| s-num(l, n) => k(N.a-val(l, N.a-num(l, n)))
| s-num(l, n, u) =>
k(N.a-val(l, N.a-num(l, n, u)))
# num, den are exact ints, and s-frac desugars to the exact rational num/den
| s-frac(l, num, den) => k(N.a-val(l, N.a-num(l, num / den))) # Possibly unneeded if removed by desugar?
| s-frac(l, num, den, u) =>
k(N.a-val(l, N.a-num(l, num / den, u))) # Possibly unneeded if removed by desugar?
# num, den are exact ints, and s-rfrac desugars to the roughnum fraction corresponding to num/den
| s-rfrac(l, num, den) => k(N.a-val(l, N.a-num(l, num-to-roughnum(num / den)))) # Possibly unneeded if removed by desugar?
| s-rfrac(l, num, den, u) =>
k(N.a-val(l, N.a-num(l, num-to-roughnum(num / den), u))) # Possibly unneeded if removed by desugar?
| s-str(l, s) => k(N.a-val(l, N.a-str(l, s)))
| s-undefined(l) => k(N.a-val(l, N.a-undefined(l)))
| s-bool(l, b) => k(N.a-val(l, N.a-bool(l, b)))
Expand Down Expand Up @@ -342,7 +346,7 @@ fun anf(e :: A.Expr, k :: ANFCont) -> N.AExpr:
N.a-let(
l,
bind(l, array-id),
N.a-prim-app(l, "makeArrayN", [list: N.a-num(l, values.length())], flat-prim-app),
N.a-prim-app(l, "makeArrayN", [list: N.a-num(l, values.length(), A.u-one(l))], flat-prim-app),
anf-name-arr-rec(values, array-id, 0, lam():
k(N.a-val(l, N.a-id(l, array-id)))
end))
Expand Down
25 changes: 18 additions & 7 deletions src/arr/compiler/ast-anf.arr
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ str-provide = PP.str("provide")
str-as = PP.str("as")
str-from = PP.str("from")
str-newtype = PP.str("newtype ")
str-percent = PP.str("%")
str-caret = PP.str("^")
str-space = PP.str(" ")

dummy-loc = SL.builtin("dummy-location")
is-s-provide-complete = A.is-s-provide-complete
Expand Down Expand Up @@ -461,9 +464,16 @@ data AVal:
| a-srcloc(l :: Loc, loc :: Loc) with:
method label(self): "a-srcloc" end,
method tosource(self): PP.str(torepr(self.loc)) end
| a-num(l :: Loc, n :: Number) with:
| a-num(l :: Loc, n :: Number, u :: A.Unit) with:
method label(self): "a-num" end,
method tosource(self): PP.number(self.n) end
method tosource(self):
if A.is-u-one(self.u):
PP.number(self.n)
else:
PP.separate(str-percent,
[list: PP.number(self.n), PP.surround(INDENT, 0, PP.langle, self.u.tosource(), PP.rangle)])
end
end
| a-str(l :: Loc, s :: String) with:
method label(self): "a-str" end,
method tosource(self): PP.str(torepr(self.s)) end
Expand Down Expand Up @@ -577,7 +587,7 @@ end
fun strip-loc-val(val :: AVal):
cases(AVal) val:
| a-srcloc(_, l) => a-srcloc(dummy-loc, l)
| a-num(_, n) => a-num(dummy-loc, n)
| a-num(_, n, u) => a-num(dummy-loc, n, u)
| a-str(_, s) => a-str(dummy-loc, s)
| a-bool(_, b) => a-bool(dummy-loc, b)
| a-undefined(_) => a-undefined(dummy-loc)
Expand Down Expand Up @@ -705,8 +715,8 @@ default-map-visitor = {
method a-srcloc(self, l, loc):
a-srcloc(l, loc)
end,
method a-num(self, l :: Loc, n :: Number):
a-num(l, n)
method a-num(self, l :: Loc, n :: Number, u :: A.Unit):
a-num(l, n, u)
end,
method a-str(self, l :: Loc, s :: String):
a-str(l, s)
Expand Down Expand Up @@ -761,6 +771,7 @@ fun freevars-ann-acc(ann :: A.Ann, seen-so-far :: NameDict<A.Name>) -> NameDict<
| a-tuple(l, fields) => freevars-list-acc(fields, seen-so-far)
| a-app(l, a, args) => freevars-list-acc(args, freevars-ann-acc(a, seen-so-far))
| a-method-app(l, a, _, args) => freevars-list-acc(args, freevars-ann-acc(a, seen-so-far))
| a-unit(l, a, u) => freevars-ann-acc(a, seen-so-far)
| a-pred(l, a, pred) =>
name = cases(A.Expr) pred:
| s-id(_, n) => n
Expand Down Expand Up @@ -811,7 +822,7 @@ where:
x = n("x")
y = n("y")
freevars-e(
a-let(d, a-bind(d, x, A.a-blank), a-val(d, a-num(d, 4)),
a-let(d, a-bind(d, x, A.a-blank), a-val(d, a-num(d, 4, A.u-one(d))),
a-lettable(d, a-val(d, a-id(d, y))))).keys-list() is [list: y.key()]
end

Expand Down Expand Up @@ -968,7 +979,7 @@ fun freevars-v-acc(v :: AVal, seen-so-far :: NameDict<A.Name>) -> NameDict<A.Nam
seen-so-far.set-now(id.key(), id)
seen-so-far
| a-srcloc(_, _) => seen-so-far
| a-num(_, _) => seen-so-far
| a-num(_, _, _) => seen-so-far
| a-str(_, _) => seen-so-far
| a-bool(_, _) => seen-so-far
| a-undefined(_) => seen-so-far
Expand Down
4 changes: 4 additions & 0 deletions src/arr/compiler/ast-util.arr
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ fun is-stateful-ann(ann :: A.Ann) -> Boolean:
| a-record(_, fields) => fields.map(_.ann).all(is-stateful-ann)
| a-tuple(_, fields) => fields.all(is-stateful-ann)
| a-app(_, inner, args) => is-stateful-ann(inner)
| a-unit(_, _, _) => true # TODO(Benmusch, 4 Jun 2019): true for now. Could refine later
| a-pred(_, _, _) => true # TODO(Oak, 21 Jan 2016): true for now. Could refine later
| a-dot(_, _, _) => true # TODO(Oak, 7 Feb 2016): true for now. Could refine later
| a-checked(_, _) => raise("NYI")
Expand Down Expand Up @@ -1041,6 +1042,9 @@ fun get-named-provides(resolved :: CS.NameResolution, uri :: URI, compile-env ::
| a-pred(l, ann, exp) =>
# TODO(joe): give more info than this to type checker? only needed dynamically, right?
ann-to-typ(ann)
| a-unit(l, ann, u) =>
# TODO(benmusch): needs to change if/when units are added to the type checker
ann-to-typ(ann)
| a-dot(l, obj, field) =>
maybe-b = resolved.type-bindings.get-now(obj.key())
cases(Option) maybe-b:
Expand Down
102 changes: 102 additions & 0 deletions src/arr/compiler/compile-structs.arr
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,57 @@ data CompileError:
ED.loc(self.loc),
ED.text(" because its denominator is zero.")]]
end
| invalid-unit-power(loc, power) with:
method render-fancy-reason(self):
[ED.error:
[ED.para:
ED.text("Reading a "),
ED.highlight(ED.text("unit annotation"), [ED.locs: self.loc], 0),
ED.text(" errored:")],
ED.cmcode(self.loc),
[ED.para:
ED.text("The exponent "),
ED.embed(self.power),
ED.text(" is not allowed in unit expressions. "),
ED.text("Make sure to use a non-zero integer value.")]]
end,
method render-reason(self):
[ED.error:
[ED.para:
ED.text("Pyret disallows units with the exponent")],
[ED.para:
ED.embed(self.power)],
[ED.para:
ED.text("at "),
ED.loc(self.loc),
ED.text(". Make sure to use a non-zero integer value.")]]
end
| one-as-power-base(loc, power) with:
method render-fancy-reason(self):
[ED.error:
[ED.para:
ED.text("Reading a "),
ED.highlight(ED.text("unit annotation"), [ED.locs: self.loc], 0),
ED.text(" errored:")],
ED.cmcode(self.loc),
[ED.para:
ED.code(ED.text("1")),
ED.text(" is raised to the power "),
ED.embed(self.power),
ED.text(". One cannot be raised to a power")]]
end,
method render-reason(self):
[ED.error:
[ED.para:
ED.code(ED.text("1")),
ED.text(" is raised to the power ")],
[ED.para:
ED.embed(self.power)],
[ED.para:
ED.text("at "),
ED.loc(self.loc),
ED.text(". One cannot be raised to a power")]]
end
| mixed-binops(exp-loc, op-a-name, op-a-loc, op-b-name, op-b-loc) with:
method render-fancy-reason(self):
[ED.error:
Expand Down Expand Up @@ -700,6 +751,36 @@ data CompileError:
ED.loc(self.op-b-loc),
ED.text(". Use parentheses to group the operations and to make the order of operations clear.")]]
end
| mixed-unit-ops(exp-loc, op-a-name, op-a-loc, op-b-name, op-b-loc) with:
method render-fancy-reason(self):
[ED.error:
[ED.para:
ED.text("Reading this "),
ED.highlight(ED.text("unit"), [ED.locs: self.exp-loc], -1),
ED.text(" errored:")],
ED.cmcode(self.exp-loc),
[ED.para:
ED.text("The "),
ED.code(ED.highlight(ED.text(self.op-a-name),[list: self.op-a-loc], 0)),
ED.text(" operation is at the same level as the "),
ED.code(ED.highlight(ED.text(self.op-b-name),[list: self.op-b-loc], 1)),
ED.text(" operation.")],
[ED.para:
ED.text("Use parentheses to group the operations and to make the order of operations clear.")]]
end,
method render-reason(self):
[ED.error:
[ED.para:
ED.text("Unit operators of different kinds cannot be mixed at the same level, but "),
ED.code(ED.text(self.op-a-name)),
ED.text(" is at "),
ED.loc(self.op-a-loc),
ED.text(" at the same level as "),
ED.code(ED.text(self.op-b-name)),
ED.text(" at "),
ED.loc(self.op-b-loc),
ED.text(". Use parentheses to group the operations and to make the order of operations clear.")]]
end
| block-ending(l :: Loc, block-loc :: Loc, kind) with:
method render-fancy-reason(self):
[ED.error:
Expand Down Expand Up @@ -972,6 +1053,27 @@ data CompileError:
ED.text(self.kind),
ED.text(".")]]
end
| underscore-as-unit(l :: Loc) with:
method render-fancy-reason(self):
[ED.error:
[ED.para:
ED.text("The underscore "),
ED.code(ED.highlight(ED.text("_"), [ED.locs: self.l], 0)),
ED.text(" is invalid."),
ED.text(" Underscores can only be used in units when"),
ED.text(" they are an annotation and there is nothing else in the unit expression")]]
end,
method render-reason(self):
[ED.error:
[ED.para:
ED.text("The underscore "),
ED.code(ED.text("_")),
ED.text(" at "),
ED.loc(self.l),
ED.text(" is invalid."),
ED.text(" Underscores can only be used in units when"),
ED.text(" they are an annotation and there is nothing else in the unit expression")]]
end
| underscore-as-pattern(l :: Loc) with:
method render-fancy-reason(self):
[ED.error:
Expand Down
Loading